AwsEC2SyncAccounts

Update an AWS - EC2 instance with a list of accounts in an AWS organization, which will allow EC2 commands to run in all of them.

python · AWS - EC2

Source

import demistomock as demisto
from CommonServerPython import *

ACCOUNT_LIST_COMMAND = "aws-org-account-list"
EC2_ACCOUNTS_PARAM = "accounts_to_access"


def internal_request(method: str, uri: str, body: dict = {}) -> dict:
    """A wrapper for demisto.internalHttpRequest.

    Args:
        method (str): HTTP method such as: GET or POST
        uri (str): Server uri to request. For example: "/contentpacks/marketplace/HelloWorld".
        body (dict, optional): Optional body for a POST request. Defaults to {}.

    Returns:
        dict: The body of request response.
    """
    return demisto.executeCommand(f"core-api-{method.lower()}", {"uri": uri, "body": json.dumps(body)})[0]["Contents"]["response"]  # type: ignore


def get_account_ids(ec2_instance_name: str | None, limit: int | None) -> tuple[list[str], str]:
    """Get the AWS organization accounts using the `aws-org-account-list` command.

    Returns:
        list[str]: A list of AWS account IDs.
    """
    try:
        command_args = assign_params(limit=limit, using=ec2_instance_name)
        account_list_result: list[dict] = demisto.executeCommand(ACCOUNT_LIST_COMMAND, command_args)  # type: ignore
        accounts = dict_safe_get(
            account_list_result, (0, "EntryContext", "AWS.Organizations.Account(val.Id && val.Id == obj.Id)"), []
        )
        return [account["Id"] for account in accounts], str(dict_safe_get(account_list_result, (0, "HumanReadable"), ""))
    except ValueError as e:
        raise DemistoException(f"The command {ACCOUNT_LIST_COMMAND!r} must be operational to run this script.\nServer error: {e}")
    except StopIteration:
        raise DemistoException(f"AWS - Organizations instance {ec2_instance_name!r} was not found.")
    except (KeyError, TypeError):
        account_list_result = locals().get("account_list_result")  # type: ignore # catch unbound variable error
        raise DemistoException(f"Unexpected output from {ACCOUNT_LIST_COMMAND!r}:\n{account_list_result}")
    except Exception as e:
        raise DemistoException(f"Unexpected error while fetching accounts:\n{e}")


def get_instance(ec2_instance_name: str) -> dict:
    """Get the object of the instance with the name provided.

    Args:
        instance_name (str): The name of the instance to get.

    Returns:
        dict: The instance object.
    """
    integrations = internal_request("post", "/settings/integration/search")
    return next(inst for inst in integrations["instances"] if inst["name"] == ec2_instance_name)


def set_instance(instance: dict, accounts: str) -> dict:
    """Set an instance configuration with the accounts.

    Args:
        instance (dict): The instance object to configure.
        accounts (str): The accounts to add to the body.

    Returns:
        dict: The server response from the configuration call.
    """
    accounts_param: dict = next(param for param in instance["data"] if param["name"] == EC2_ACCOUNTS_PARAM)
    accounts_param.update({"hasvalue": True, "value": accounts})
    return internal_request("put", "/settings/integration", instance)


def remove_excluded_accounts(account_ids: list, accounts_to_exclude: str | None) -> list[str]:
    return list(set(account_ids) - set(argToList(accounts_to_exclude))) if accounts_to_exclude else account_ids


def update_ec2_instance(account_ids: list[str], ec2_instance_name: str) -> str:
    """Update an AWS - EC2 instance with AWS Organization accounts.

    Args:
        account_ids (list[str]): The accounts to configure the instance with.
        instance_name (str): The name of the instance to configure.

    Returns:
        str: A message regarding the outcome of the script run.
    """
    accounts_as_str = ",".join(account_ids)
    try:
        instance = get_instance(ec2_instance_name)
        if instance["configvalues"][EC2_ACCOUNTS_PARAM] == accounts_as_str:
            return f"Account list in ***{ec2_instance_name}*** is up to date."
        response = set_instance(instance, accounts_as_str)
        if response["configvalues"][EC2_ACCOUNTS_PARAM] != accounts_as_str:
            demisto.debug(f"{response=}")
            raise DemistoException(f"Attempt to update {ec2_instance_name!r} with accounts {accounts_as_str} has failed.")
        return f"Successfully updated ***{ec2_instance_name}*** with accounts:"
    except StopIteration:
        raise DemistoException(f"AWS - EC2 instance {ec2_instance_name!r} was not found or is not an AWS - EC2 instance.")
    except (TypeError, KeyError) as e:
        raise DemistoException(f'Please make sure a "Core REST API" instance is enabled.\nError: {e}')
    except Exception as e:
        raise DemistoException(f"Unexpected error while configuring AWS - EC2 instance with accounts {accounts_as_str!r}:\n{e}")


def main():
    try:
        args: dict = demisto.args()
        account_ids, readable_output = get_account_ids(args.get("org_instance_name"), arg_to_number(args.get("max_accounts")))
        account_ids = remove_excluded_accounts(account_ids, args.get("exclude_accounts"))
        result = update_ec2_instance(account_ids, args["ec2_instance_name"])
        return_results(CommandResults(readable_output=f"## {result}  \n---  \n{readable_output}"))
    except Exception as e:
        return_error(f"Error in AwsEC2SyncAccounts: {e}")


if __name__ in ("__main__", "builtins", "__builtin__"):
    main()

README

Update an AWS - EC2 instance with a list of accounts in an AWS organization, which will allow EC2 commands to run in all of them.
This script can be run on a schedule to keep an AWS - EC2 instance in sync with the created, deleted or removed accounts of the organization.

Prerequisites

  • An AWS - EC2 instance.
  • An AWS - Organizations instance with a working aws-org-account-list command.
  • A Core REST API instance.

Script Data


Name Description
Script Type python3
Tags Amazon Web Services
Cortex XSOAR Version 6.10.0

Inputs


Argument Name Description
ec2_instance_name The name of the AWS - EC2 instance integration to update.
org_instance_name The name of the AWS - Organizations instance to collect account from. If not provided, the primary instance will be used.
exclude_accounts A comma-separated list of accounts to exclude.
max_accounts The maximum number of accounts to retrieve. Default is 50.

Outputs


There are no outputs for this script.

Script Examples

Example command

!AwsEC2SyncAccounts ec2_instance_name="AWS_EC2_Instance" org_instance_name="AWS_Organizations_Instance"

Human Readable Output

Successfully updated AWS_EC2_Instance with accounts


AWS Organization Accounts

Id Arn Name Email JoinedMethod JoinedTimestamp Status
111222333444 arn:aws:organizations::111222333444:account/o-abcde12345/111222333444 Name user@xsoar.com CREATED 2023-09-04 09:17:14.299000+00:00 ACTIVE
111222333444 arn:aws:organizations::111222333444:account/o-abcde12345/111222333444 John Doe user@xsoar.com INVITED 2022-07-25 09:11:23.528000+00:00 SUSPENDED