diff --git a/Packs/AWS-EC2/.secrets-ignore b/Packs/AWS-EC2/.secrets-ignore index 2c32491cbefd..e324ec3268c8 100644 --- a/Packs/AWS-EC2/.secrets-ignore +++ b/Packs/AWS-EC2/.secrets-ignore @@ -1,2 +1,3 @@ ::7940 -ec2::2222 \ No newline at end of file +ec2::2222 +user@xsoar.com \ No newline at end of file diff --git a/Packs/AWS-EC2/ReleaseNotes/1_4_0.md b/Packs/AWS-EC2/ReleaseNotes/1_4_0.md new file mode 100644 index 000000000000..17c497c32cb8 --- /dev/null +++ b/Packs/AWS-EC2/ReleaseNotes/1_4_0.md @@ -0,0 +1,6 @@ + +#### Scripts + +##### New: AwsEC2SyncAccounts + +New: 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. (Available from Cortex XSOAR 6.10.0). diff --git a/Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/AwsEC2SyncAccounts.py b/Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/AwsEC2SyncAccounts.py new file mode 100644 index 000000000000..8cf78c993ae6 --- /dev/null +++ b/Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/AwsEC2SyncAccounts.py @@ -0,0 +1,118 @@ +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) -> 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 = {'using': ec2_instance_name} if ec2_instance_name else {} + 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 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 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')) + 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() diff --git a/Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/AwsEC2SyncAccounts.yml b/Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/AwsEC2SyncAccounts.yml new file mode 100644 index 000000000000..f0205ced3ea8 --- /dev/null +++ b/Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/AwsEC2SyncAccounts.yml @@ -0,0 +1,22 @@ +args: +- description: The name of the AWS - EC2 instance integration to update. + name: ec2_instance_name + required: true +- description: The name of the AWS - Organizations instance to collect account from. If not provided, the primary instance will be used. + name: org_instance_name +comment: 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. +commonfields: + id: AwsEC2SyncAccounts + version: -1 +enabled: true +name: AwsEC2SyncAccounts +outputs: [] +script: '-' +tags: +- Amazon Web Services +timeout: '0' +type: python +subtype: python3 +dockerimage: demisto/python3:3.10.13.83255 +runas: DBotWeakRole +fromversion: 6.10.0 diff --git a/Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/AwsEC2SyncAccounts_test.py b/Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/AwsEC2SyncAccounts_test.py new file mode 100644 index 000000000000..6b2acc581121 --- /dev/null +++ b/Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/AwsEC2SyncAccounts_test.py @@ -0,0 +1,158 @@ +import demistomock as demisto +from CommonServerPython import * +from unittest.mock import MagicMock +import pytest + + +def test_internal_request(mocker): + from AwsEC2SyncAccounts import internal_request + + mock_execute_command = mocker.patch.object( + demisto, "executeCommand", return_value=[{'Contents': {'response': 'result'}}] + ) + + result = internal_request('POST', '/path/', {'body': 'data'}) + + assert result == 'result' + mock_execute_command.assert_called_with( + 'core-api-post', {'uri': '/path/', 'body': '{"body": "data"}'} + ) + + +def test_get_account_ids(mocker): + from AwsEC2SyncAccounts import get_account_ids + + mock_execute_command = mocker.patch.object(demisto, "executeCommand") + mock_execute_command.return_value = [ + { + "EntryContext": { + "AWS.Organizations.Account(val.Id && val.Id == obj.Id)": [ + {"Id": "1234"}, + {"Id": "5678"}, + ] + }, + "HumanReadable": "human_readable" + } + ] + + account_ids = get_account_ids('instance_name') + + assert account_ids == (["1234", "5678"], "human_readable") + mock_execute_command.assert_called_with("aws-org-account-list", {'using': 'instance_name'}) + + +def test_set_instance(mocker): + import AwsEC2SyncAccounts + + internal_request: MagicMock = mocker.patch.object( + AwsEC2SyncAccounts, "internal_request" + ) + AwsEC2SyncAccounts.set_instance( + { + "data": [ + { + "name": "accounts_to_access", + "hasvalue": False, + "value": "", + }, + { + "name": "sessionDuration" + }, + ], + }, + 'accounts' + ) + internal_request.assert_called_with( + 'put', '/settings/integration', { + "data": [ + { + "name": "accounts_to_access", + "hasvalue": True, + "value": "accounts", + }, + { + "name": "sessionDuration" + }, + ], + } + ) + + +def test_update_ec2_instance(mocker): + import AwsEC2SyncAccounts + + internal_request: MagicMock = mocker.patch.object( + AwsEC2SyncAccounts, + "internal_request", + side_effect=lambda *args: { + ("post", "/settings/integration/search"): { + "instances": [ + { + "id": "2fa1071e-af66-4668-8f79-8c57a3e4851d", + "name": "AWS - EC2", + "configvalues": { + "accounts_to_access": "", + "sessionDuration": None, + }, + "configtypes": {"accounts_to_access": 0, "sessionDuration": 0}, + "data": [ + { + "name": "accounts_to_access", + "hasvalue": False, + "value": "", + }, + { + "name": "sessionDuration" + }, + ], + }, + { + "name": "wrong name", + }, + ] + }, + ("put", "/settings/integration"): { + "configvalues": {"accounts_to_access": "1234,5678"} + }, + }.get(args[:2]), + ) + + result = AwsEC2SyncAccounts.update_ec2_instance(["1234", "5678"], "AWS - EC2") + + assert internal_request.mock_calls[0].args == ('post', '/settings/integration/search') + assert internal_request.mock_calls[1].args == ( + 'put', + '/settings/integration', + { + "id": "2fa1071e-af66-4668-8f79-8c57a3e4851d", + "name": "AWS - EC2", + "configvalues": { + "accounts_to_access": "", + "sessionDuration": None, + }, + "configtypes": {"accounts_to_access": 0, "sessionDuration": 0}, + "data": [ + { + "name": "accounts_to_access", + "hasvalue": True, + "value": "1234,5678", + }, + { + "name": "sessionDuration" + }, + ], + } + ) + assert result == "Successfully updated ***AWS - EC2*** with accounts:" + + +def test_errors(mocker): + import AwsEC2SyncAccounts as sync + + with pytest.raises(DemistoException, match='Unexpected error while configuring AWS - EC2 instance with accounts'): + sync.get_instance = lambda _: 1 / 0 + sync.update_ec2_instance([], '') + + with pytest.raises(DemistoException, match="Unexpected output from 'aws-org-account-list':\nNone"): + sync.demisto.executeCommand = lambda *_: {}['key'] + sync.get_account_ids('') diff --git a/Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/README.md b/Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/README.md new file mode 100644 index 000000000000..d4312f1d55d1 --- /dev/null +++ b/Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/README.md @@ -0,0 +1,47 @@ +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. | + +## 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 | ferrum-techs | user@xsoar.com | INVITED | 2022-07-25 09:11:23.528000+00:00 | SUSPENDED | diff --git a/Packs/AWS-EC2/pack_metadata.json b/Packs/AWS-EC2/pack_metadata.json index fc7791b35005..b8393cd9466c 100644 --- a/Packs/AWS-EC2/pack_metadata.json +++ b/Packs/AWS-EC2/pack_metadata.json @@ -2,7 +2,7 @@ "name": "AWS - EC2", "description": "Amazon Web Services Elastic Compute Cloud (EC2)", "support": "xsoar", - "currentVersion": "1.3.0", + "currentVersion": "1.4.0", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "",