Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AwsEC2SyncAccounts script #31680

Merged
merged 25 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Packs/AWS-EC2/.secrets-ignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
::7940
ec2::2222
ec2::2222
user@xsoar.com
6 changes: 6 additions & 0 deletions Packs/AWS-EC2/ReleaseNotes/1_4_0.md
Original file line number Diff line number Diff line change
@@ -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).
118 changes: 118 additions & 0 deletions Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/AwsEC2SyncAccounts.py
Original file line number Diff line number Diff line change
@@ -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()
22 changes: 22 additions & 0 deletions Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/AwsEC2SyncAccounts.yml
Original file line number Diff line number Diff line change
@@ -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
158 changes: 158 additions & 0 deletions Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/AwsEC2SyncAccounts_test.py
Original file line number Diff line number Diff line change
@@ -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('')
47 changes: 47 additions & 0 deletions Packs/AWS-EC2/Scripts/AwsEC2SyncAccounts/README.md
Original file line number Diff line number Diff line change
@@ -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.
jlevypaloalto marked this conversation as resolved.
Show resolved Hide resolved
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 |
2 changes: 1 addition & 1 deletion Packs/AWS-EC2/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand Down
Loading