Skip to content

Commit

Permalink
AwsEC2SyncAccounts script (demisto#31680)
Browse files Browse the repository at this point in the history
* init

* finished unit-tests

* docs complete

* update release notes

* docs

* name change

* CR changes

* Bump pack from version AWS-EC2 to 1.3.0.

* Demo changes

* bug fixes

* small changes

* refactor

* fix unit-tests

* added use-case

* Update docker

* build wars: round 1

* Update Packs/AWS-EC2/ReleaseNotes/1_3_0.md

Co-authored-by: ShirleyDenkberg <62508050+ShirleyDenkberg@users.noreply.github.com>

* Bump pack from version AWS-EC2 to 1.4.0.

* more tests

* more tests

* ignore secrets

---------

Co-authored-by: Content Bot <bot@demisto.com>
Co-authored-by: ShirleyDenkberg <62508050+ShirleyDenkberg@users.noreply.github.com>
  • Loading branch information
3 people authored Jan 9, 2024
1 parent fc4bc1f commit 8cc4e0b
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 2 deletions.
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.
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

0 comments on commit 8cc4e0b

Please sign in to comment.