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

EXPANDR-5776 - AWS Hierarchy Information #31951

Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
643 changes: 572 additions & 71 deletions Packs/AWS-Enrichment-Remediation/Playbooks/AWS_-_Enrichment.yml

Large diffs are not rendered by default.

Expand Up @@ -14,11 +14,15 @@ This playbook does not use any sub-playbooks.

### Scripts

This playbook does not use any scripts.
* AWSAccountHierarchy
* Set

### Commands

* aws-ec2-describe-ipam-resource-discoveries
* aws-ec2-describe-security-groups
* aws-ec2-get-ipam-discovered-public-addresses
* aws-ec2-describe-regions
* aws-ec2-describe-instances

## Playbook Inputs
Expand All @@ -29,6 +33,7 @@ This playbook does not use any scripts.
| --- | --- | --- | --- |
| Indicator Query | Indicators matching the indicator query will be used as playbook input | | Optional |
| AwsIP | AWS IP in alert | alert.remoteip | Required |
| AWSAssumeRoleName | If assuming roles for AWS, this is the name of the role to assume \(should be the same for all organizations\). | | Optional |

## Playbook Outputs

Expand All @@ -38,6 +43,7 @@ This playbook does not use any scripts.
| --- | --- | --- |
| AWS.EC2.Instances | AWS EC2 information. | unknown |
| AWS.EC2.SecurityGroups | AWS Security group information. | unknown |
| AWSHierarchy | AWS account hierarchy information. | unknown |

## Playbook Image

Expand Down
8 changes: 7 additions & 1 deletion Packs/AWS-Enrichment-Remediation/README.md
Expand Up @@ -50,4 +50,10 @@ AWS - Unclaimed S3 Bucket Remediation playbook creates the unclaimed S3 bucket s

Automation to determine which interface on an EC2 instance has an over-permissive security group, determine which security groups have over-permissive rules, and replace them with a copy of the security group that has only the over-permissive portion removed. Over-permissive is defined as sensitive ports (SSH, RDP, etc) being exposed to the internet via IPv4.

![AWSRecreateSG](https://raw.githubusercontent.com/demisto/content/575733d345d562597711727c4f8f4b603ef49096/Packs/AWS-Enrichment-Remediation/doc_files/AWSRecreateSG.png)
![AWSRecreateSG](https://raw.githubusercontent.com/demisto/content/master/Packs/AWS-Enrichment-Remediation/doc_files/AWSRecreateSG.png)

#### AWSAccountHierarchy

Automation to determine AWS Account hierarchy by looking up parent objects until the organization level is reached.
johnnywilkes marked this conversation as resolved.
Show resolved Hide resolved

![AWSAccountHierarchy](https://raw.githubusercontent.com/demisto/content/2651e6ea5f37c64e3b3e9b18e4d815f5094d6fb2/Packs/AWS-Enrichment-Remediation/doc_files/AWS_-_Enrichment.png)
12 changes: 12 additions & 0 deletions Packs/AWS-Enrichment-Remediation/ReleaseNotes/1_1_12.md
@@ -0,0 +1,12 @@

#### Playbooks

##### AWS - Enrichment

Updated the playbook to include the new *AWSAccountHierarchy* script for pulling AWS Hierarchy information.

#### Scripts

##### New: AWSAccountHierarchy

Added the **AWSAccountHierarchy** script to determine AWS Account hierarchy by looking up parent objects until the organization level is reached.
johnnywilkes marked this conversation as resolved.
Show resolved Hide resolved
@@ -0,0 +1,151 @@
import demistomock as demisto # noqa: F401
from CommonServerPython import * # noqa: F401
from typing import Dict, Any
import traceback


""" STANDALONE FUNCTION """


def lookup(parent_obj: str, level: int, instance_to_use: str) -> tuple[str, dict]:
YuvHayun marked this conversation as resolved.
Show resolved Hide resolved
"""
Lookup information on a organization unit (ou) or root object. Unless the current lookup
is a root object, it returns parent object to lookup and results of current lookup.
Args:
parent_obj (str): root/ou object in number format.
level (int): the current level that the object is (ascending).
instance_to_use (str): what integration instance to use.

Returns:
str: parent object to look up next or "NONE" for error handling and "stop" if no parent.
dict: dictionary of id, name and level of the lookup object.

"""
temp: Dict[str, str] = {}
try:
if "ou-" in parent_obj:
ou_info = execute_command(
"aws-org-organization-unit-get",
{"organization_unit_id": parent_obj, "using": instance_to_use},
)
if not ou_info:
return "NONE", temp
temp["level"] = str(level)
temp["id"] = ou_info.get("Id", "")
temp["name"] = ou_info.get("Name", "")
temp["arn"] = ou_info.get("Arn", "")
ou_parent = execute_command(
"aws-org-parent-list",
{"child_id": parent_obj, "using": instance_to_use},
)
if not ou_parent:
return "NONE", temp
next_one = ou_parent[0].get("Id", "")
elif "r-" in parent_obj:
next_one = "stop"
root_info = execute_command("aws-org-root-list", {"using": instance_to_use})
if not root_info:
return "NONE", temp
root = root_info.get("AWS.Organizations.Root(val.Id && val.Id == obj.Id)")[
0
]
temp["level"] = str(level)
temp["id"] = root.get("Id", "")
temp["name"] = root.get("Name", "")
temp["arn"] = root.get("Arn", "")
else:
raise ValueError("unexpected object type")
except TypeError:
return "NONE", temp
YuvHayun marked this conversation as resolved.
Show resolved Hide resolved
else:
return next_one, temp


""" COMMAND FUNCTION """


def aws_account_heirarchy(args: Dict[str, Any]) -> CommandResults:
"""
Determine AWS account hierarchy by looking up parent objects until the root level is reached.
Args:
args (dict): Command arguments from XSOAR.

Returns:
list[CommandResults]: outputs, readable outputs and raw response for XSOAR.

"""

account_id = args.get("account_id")

if not account_id:
raise ValueError("account_id not specified")
YuvHayun marked this conversation as resolved.
Show resolved Hide resolved
# Using `demisto.executeCommand` instead of `execute_command` because for
# multiple integration instances we can expect one too error out.
account_info = demisto.executeCommand(
"aws-org-account-list", {"account_id": account_id}
)
account_returned = [
account
for account in account_info
if (not isError(account) and account.get("Contents").get("Arn"))
]
if not account_returned:
return CommandResults(readable_output="could not find specified account info")
else:
match_account = account_returned[0].get("Contents")
instance_to_use = account_returned[0]["Metadata"]["instance"]
level = 1
hierarchy = [
{
"level": "account",
"id": match_account.get("Id", ""),
"name": match_account.get("Name", ""),
"arn": match_account.get("Arn", ""),
}
]
account_parent = demisto.executeCommand(
"aws-org-parent-list", {"child_id": account_id, "using": instance_to_use}
)
if isError(account_parent):
return CommandResults(readable_output="could not find specified account parent")
next_one, to_append = lookup(
account_parent[0].get("Contents", {})[0].get("Id", ""), level, instance_to_use
)
if next_one == "NONE":
return CommandResults(readable_output="could not find specified ou/root info")
hierarchy.append(to_append)
try:
while "stop" not in next_one:
level += 1
next_one, to_append = lookup(next_one, level, instance_to_use)
if next_one == "NONE" or next_one is None:
return CommandResults(
readable_output="could not find specified ou/root info"
)
hierarchy.append(to_append)
except TypeError:
return CommandResults(readable_output="could not find specified ou/root info")

YuvHayun marked this conversation as resolved.
Show resolved Hide resolved
return CommandResults(
outputs_prefix="AWSHierarchy",
outputs_key_field="level",
outputs=hierarchy,
)


""" MAIN FUNCTION """


def main():
try:
return_results(aws_account_heirarchy(demisto.args()))
except Exception as ex:
demisto.error(traceback.format_exc()) # print the traceback
return_error(f"Failed to execute AWSAccountHierarchy. Error: {str(ex)}")


""" ENTRY POINT """


if __name__ in ("__main__", "__builtin__", "builtins"):
main()
@@ -0,0 +1,34 @@
args:
- description: 'The unique identifier (ID) of the Amazon Web Services account that you want information about.'
name: account_id
required: true
comment: Determine AWS Account hierarchy by looking up parent objects until the organization level is reached.
johnnywilkes marked this conversation as resolved.
Show resolved Hide resolved
commonfields:
id: AWSAccountHierarchy
version: -1
dockerimage: demisto/python3:3.10.13.83255
enabled: true
engineinfo: {}
name: AWSAccountHierarchy
outputs:
- contextPath: AWSHierarchy.id
description: ID of the account/OU/root object such as `111111111111`.
type: string
- contextPath: AWSHierarchy.level
description: Level in relation to the original AWS account such as account, 1, 2, etc.
type: string
- contextPath: AWSHierarchy.arn
description: Arn of the account/OU/root object such as `arn:aws:organizations::111111111111:root/o-2222222222/r-3333`.
johnnywilkes marked this conversation as resolved.
Show resolved Hide resolved
type: string
- contextPath: AWSHierarchy.name
description: Human readable name of the account/OU/root object such as `aws-account-n`.
runas: DBotWeakRole
runonce: false
script: ''
scripttarget: 0
subtype: python3
tags: []
type: python
fromversion: 6.10.0
tests:
- No tests (auto formatted)
@@ -0,0 +1,137 @@
import demistomock as demisto # noqa: F401
from CommonServerPython import CommandResults


def test_lookup_func(mocker):
"""Tests lookup helper function.

Given:
- Mocked arguments
When:
- Sending args to lookup helper function.
Then:
- Checks the output of the helper function with the expected output.
"""
from AWSAccountHierarchy import lookup

folder_lookup = [
{
"Type": 1,
"Contents": {
"AWS.Organizations.Root(val.Id && val.Id == obj.Id)": [
{
"Arn": "arn:aws:organizations::111111111111:root/o-2222222222/r-3333",
"Id": "r-3333",
"Name": "Root",
}
]
},
}
]

mocker.patch.object(demisto, "executeCommand", return_value=folder_lookup)
args = {"parent_obj": "r-3333", "level": 2, "instance_to_use": "fake-instance-name"}
result = lookup(**args)
assert result == (
"stop",
{
"id": "r-3333",
"level": "2",
"name": "Root",
"arn": "arn:aws:organizations::111111111111:root/o-2222222222/r-3333",
},
)


def test_aws_account_heirarchy_command(mocker):
"""Tests aws_account_heirarchy function.

Given:
- Mocked arguments
When:
- Sending args to aws_account_heirarchy function.
Then:
- Checks the output of the function with the expected output.
"""
from AWSAccountHierarchy import aws_account_heirarchy

def executeCommand(name, args):
if name == "aws-org-account-list":
return [
{
"Type": 1,
"Contents": {
"Name": "master-account",
"Id": "111111111111",
"Arn": "arn:aws:organizations::111111111111:root/o-2222222222/111111111111",
},
"Metadata": {"instance": "fake-instance-name"},
}
]
elif name == "aws-org-parent-list":
return [{"Type": 1, "Contents": [{"Id": "r-3333"}]}]
elif name == "aws-org-root-list":
return [
{
"Type": 1,
"Contents": {
"AWS.Organizations.Root(val.Id && val.Id == obj.Id)": [
{
"Arn": "arn:aws:organizations::111111111111:root/o-2222222222/r-3333",
"Id": "r-3333",
"Name": "Root",
}
]
},
}
]

mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand)
args = {"account_id": "111111111111"}
result = aws_account_heirarchy(args)
expected_hierachy = [
{
"id": "111111111111",
"level": "account",
"name": "master-account",
"arn": "arn:aws:organizations::111111111111:root/o-2222222222/111111111111",
},
{
"id": "r-3333",
"level": "1",
"name": "Root",
"arn": "arn:aws:organizations::111111111111:root/o-2222222222/r-3333",
},
]
expected_result = CommandResults(
outputs_prefix="AWSHierarchy",
outputs_key_field="level",
outputs=expected_hierachy,
)
assert result.outputs == expected_result.outputs


def test_aws_account_heirarchy_command_no_account(mocker):
"""Tests aws_account_heirarchy function.

Given:
- Null mocked arguments
When:
- Sending args to aws_account_heirarchy function.
Then:
- Checks the output of the function with the expected output.
"""
from AWSAccountHierarchy import aws_account_heirarchy

def executeCommand(name, args):
if name == "aws-org-account-list":
return [{"Type": 4, "Contents": {"bad command"}}]

mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand)
args = {"account_id": "111111111111"}
result = aws_account_heirarchy(args)
expected_result = CommandResults(
readable_output="could not find specified account info"
)

assert result.readable_output == expected_result.readable_output