feat(organizations): Extract Metadata from Management Account ID (-O) (#1248)

* feat(organizations): add organizations funtion to provider

* feat(organizations): add organizations -O option

* fix(comments): Resolve comments.

* feat(test): add test

* fix(pipfile): update pipfile

Co-authored-by: sergargar <sergio@verica.io>
This commit is contained in:
Sergio Garcia
2022-07-05 12:00:14 +02:00
committed by GitHub
parent b2899bda69
commit d47bb09b2a
10 changed files with 840 additions and 623 deletions
Generated
+669 -569
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -8,9 +8,8 @@ prowler_version = "3.0-alfa"
groups_file = "groups.json"
# AWS services-regions matrix json
aws_services_json_file = "providers/aws/aws_regions_services.json"
aws_services_json_file = "providers/aws/aws_regions_by_service.json"
default_output_directory = getcwd() + "/output"
csv_file_suffix = timestamp.strftime("%Y%m%d%H%M%S") + ".csv"
+2 -7
View File
@@ -9,7 +9,7 @@ from colorama import Fore, Style
from config.config import groups_file
from lib.check.models import Output_From_Options, load_check_metadata
from lib.logger import logger
from lib.outputs.outputs import get_orgs_info, report
from lib.outputs.outputs import report
from lib.utils.utils import open_file, parse_json_file
@@ -98,7 +98,6 @@ def print_services(service_list: set):
print(f"- {service}")
def print_checks(provider: str, check_list: set, bulk_checks_metadata: dict):
for check in check_list:
try:
@@ -111,7 +110,6 @@ def print_checks(provider: str, check_list: set, bulk_checks_metadata: dict):
)
# List available groups
def list_groups(provider: str):
groups = parse_groups_from_file(groups_file)
@@ -193,10 +191,7 @@ def run_check(check, audit_info, output_options):
logger.debug(f"Executing check: {check.checkName}")
findings = check.execute()
# Call to get orgs, need to check if input option is passed in output options
# right now it is not checked and is called straight to generate the fields to be passed to the csv
organizations_info = get_orgs_info()
report(findings, output_options, audit_info, organizations_info)
report(findings, output_options, audit_info)
def import_check(check_path: str) -> ModuleType:
-9
View File
@@ -192,12 +192,3 @@ class Check_Report:
self.resource_tags = []
self.resource_id = ""
self.resource_arn = ""
@dataclass
class Organizations_Info:
account_details_email: str
account_details_name: str
account_details_arn: str
account_details_org: str
account_details_tags: str
+3 -8
View File
@@ -1,7 +1,8 @@
from dataclasses import asdict, dataclass
from config.config import timestamp
from lib.check.models import Check_Report, Organizations_Info
from lib.check.models import Check_Report
from providers.aws.models import AWS_Organizations_Info
@dataclass
@@ -65,7 +66,7 @@ class Check_Output_CSV:
account: str,
profile: str,
report: Check_Report,
organizations: Organizations_Info,
organizations: AWS_Organizations_Info,
):
self.assessment_start_time = timestamp.isoformat()
self.finding_unique_id = ""
@@ -181,9 +182,3 @@ class Check_Output_CSV:
groups = ""
return unrolled_compliance
def get_csv_header(self):
csv_header = []
for key in asdict(self):
csv_header = csv_header.append(key)
return csv_header
+2 -15
View File
@@ -3,12 +3,11 @@ from csv import DictWriter
from colorama import Fore, Style
from config.config import csv_file_suffix
from lib.check.models import Organizations_Info
from lib.outputs.models import Check_Output_CSV
from lib.utils.utils import file_exists, open_file
def report(check_findings, output_options, audit_info, organizations_info):
def report(check_findings, output_options, audit_info):
check_findings.sort(key=lambda x: x.region)
csv_fields = []
@@ -45,9 +44,8 @@ def report(check_findings, output_options, audit_info, organizations_info):
audit_info.audited_account,
audit_info.profile,
finding,
organizations_info,
audit_info.organizations_metadata,
)
csv_writer = DictWriter(
file_descriptors["csv"], fieldnames=csv_fields, delimiter=";"
)
@@ -106,14 +104,3 @@ def generate_csv_fields():
for field in Check_Output_CSV.__dict__["__annotations__"].keys():
csv_fields.append(field)
return csv_fields
def get_orgs_info():
organizations_info = Organizations_Info(
account_details_email="",
account_details_name="",
account_details_arn="",
account_details_org="",
account_details_tags="",
)
return organizations_info
+82 -11
View File
@@ -1,13 +1,18 @@
import sys
from arnparse import arnparse
from boto3 import session
from boto3 import client, session
from botocore.credentials import RefreshableCredentials
from botocore.session import get_session
from lib.arn.arn import arn_parsing
from lib.logger import logger
from providers.aws.models import AWS_Assume_Role, AWS_Audit_Info, AWS_Credentials
from providers.aws.models import (
AWS_Assume_Role,
AWS_Audit_Info,
AWS_Credentials,
AWS_Organizations_Info,
)
################## AWS PROVIDER
@@ -76,7 +81,12 @@ class AWS_Provider:
def provider_set_session(
input_profile, input_role, input_session_duration, input_external_id, input_regions
input_profile,
input_role,
input_session_duration,
input_external_id,
input_regions,
organizations_role_arn,
):
# Mark variable that stores all the info about the audit as global
@@ -94,11 +104,12 @@ def provider_set_session(
profile_region=None,
credentials=None,
assumed_role_info=AWS_Assume_Role(
role_arn=input_role,
session_duration=input_session_duration,
external_id=input_external_id,
role_arn=None,
session_duration=None,
external_id=None,
),
audited_regions=input_regions,
organizations_metadata=None,
)
logger.info("Generating original session ...")
@@ -115,8 +126,36 @@ def provider_set_session(
current_audit_info.audited_account = caller_identity["Account"]
current_audit_info.audited_partition = arnparse(caller_identity["Arn"]).partition
logger.info("Checking if organizations role assumption is needed ...")
if organizations_role_arn:
current_audit_info.assumed_role_info.role_arn = organizations_role_arn
current_audit_info.assumed_role_info.session_duration = input_session_duration
# Check if role arn is valid
try:
# this returns the arn already parsed, calls arnparse, into a dict to be used when it is needed to access its fields
role_arn_parsed = arn_parsing(current_audit_info.assumed_role_info.role_arn)
except Exception as error:
logger.critical(f"{error.__class__.__name__} -- {error}")
sys.exit()
else:
logger.info(
f"Getting organizations metadata for account {organizations_role_arn}"
)
assumed_credentials = assume_role(current_audit_info)
current_audit_info.organizations_metadata = get_organizations_metadata(
current_audit_info.audited_account, assumed_credentials
)
logger.info(f"Organizations metadata retrieved")
logger.info("Checking if role assumption is needed ...")
if current_audit_info.assumed_role_info.role_arn:
if input_role:
current_audit_info.assumed_role_info.role_arn = input_role
current_audit_info.assumed_role_info.session_duration = input_session_duration
current_audit_info.assumed_role_info.external_id = input_external_id
# Check if role arn is valid
try:
# this returns the arn already parsed, calls arnparse, into a dict to be used when it is needed to access its fields
@@ -153,18 +192,16 @@ def provider_set_session(
logger.info("Audit session is the original one")
current_audit_info.audit_session = current_audit_info.original_session
# Setting default region of session
if current_audit_info.audit_session.region_name:
current_audit_info.profile_region = current_audit_info.audit_session.region_name
else:
current_audit_info.profile_region = "us-east-1"
return current_audit_info
def validate_credentials(validate_session):
def validate_credentials(validate_session: session) -> dict:
try:
validate_credentials_client = validate_session.client("sts")
caller_identity = validate_credentials_client.get_caller_identity()
@@ -200,3 +237,37 @@ def assume_role(audit_info: AWS_Audit_Info) -> dict:
else:
return assumed_credentials
def get_organizations_metadata(
metadata_account: str, assumed_credentials: dict
) -> AWS_Organizations_Info:
try:
organizations_client = client(
"organizations",
aws_access_key_id=assumed_credentials["Credentials"]["AccessKeyId"],
aws_secret_access_key=assumed_credentials["Credentials"]["SecretAccessKey"],
aws_session_token=assumed_credentials["Credentials"]["SessionToken"],
)
organizations_metadata = organizations_client.describe_account(
AccountId=metadata_account
)
list_tags_for_resource = organizations_client.list_tags_for_resource(
ResourceId=metadata_account
)
except Exception as error:
logger.critical(f"{error.__class__.__name__} -- {error}")
sys.exit()
else:
# Convert Tags dictionary to String
account_details_tags = ""
for tag in list_tags_for_resource["Tags"]:
account_details_tags += tag["Key"] + ":" + tag["Value"] + ","
organizations_info = AWS_Organizations_Info(
account_details_email=organizations_metadata["Account"]["Email"],
account_details_name=organizations_metadata["Account"]["Name"],
account_details_arn=organizations_metadata["Account"]["Arn"],
account_details_org=organizations_metadata["Account"]["Arn"].split("/")[1],
account_details_tags=account_details_tags,
)
return organizations_info
+64 -2
View File
@@ -1,8 +1,14 @@
import json
import boto3
import sure # noqa
from moto import mock_iam, mock_sts
from moto import mock_iam, mock_organizations, mock_sts
from providers.aws.aws_provider import assume_role, validate_credentials
from providers.aws.aws_provider import (
assume_role,
get_organizations_metadata,
validate_credentials,
)
from providers.aws.models import AWS_Assume_Role, AWS_Audit_Info
ACCOUNT_ID = 123456789012
@@ -66,6 +72,7 @@ class Test_AWS_Provider:
audited_account=None,
audited_partition=None,
profile=None,
profile_region=None,
credentials=None,
assumed_role_info=AWS_Assume_Role(
role_arn=role_arn,
@@ -73,6 +80,7 @@ class Test_AWS_Provider:
external_id=None,
),
audited_regions=audited_regions,
organizations_metadata=None,
)
# Call assume_role
@@ -102,3 +110,57 @@ class Test_AWS_Provider:
assume_role_response["AssumedRoleUser"]["AssumedRoleId"].should.have.length_of(
21 + 1 + len(sessionName)
)
@mock_organizations
@mock_sts
@mock_iam
def test_organizations(self):
client = boto3.client("organizations", region_name="us-east-1")
iam_client = boto3.client("iam", region_name="us-east-1")
sts_client = boto3.client("sts", region_name="us-east-1")
mockname = "mock-account"
mockdomain = "moto-example.org"
mockemail = "@".join([mockname, mockdomain])
org_id = client.create_organization(FeatureSet="ALL")["Organization"]["Id"]
account_id = client.create_account(AccountName=mockname, Email=mockemail)[
"CreateAccountStatus"
]["AccountId"]
client.tag_resource(
ResourceId=account_id, Tags=[{"Key": "key", "Value": "value"}]
)
trust_policy_document = {
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::{account_id}:root".format(
account_id=ACCOUNT_ID
)
},
"Action": "sts:AssumeRole",
},
}
iam_role_arn = iam_client.role_arn = iam_client.create_role(
RoleName="test-role",
AssumeRolePolicyDocument=json.dumps(trust_policy_document),
)["Role"]["Arn"]
session_name = "new-session"
assumed_role = sts_client.assume_role(
RoleArn=iam_role_arn, RoleSessionName=session_name
)
org = get_organizations_metadata(account_id, assumed_role)
org.account_details_email.should.equal(mockemail)
org.account_details_name.should.equal(mockname)
org.account_details_arn.should.equal(
"arn:aws:organizations::{0}:account/{1}/{2}".format(
ACCOUNT_ID, org_id, account_id
)
)
org.account_details_org.should.equal(org_id)
org.account_details_tags.should.equal("key:value,")
+10
View File
@@ -19,6 +19,15 @@ class AWS_Assume_Role:
external_id: str
@dataclass
class AWS_Organizations_Info:
account_details_email: str
account_details_name: str
account_details_arn: str
account_details_org: str
account_details_tags: str
@dataclass
class AWS_Audit_Info:
original_session: session.Session
@@ -30,3 +39,4 @@ class AWS_Audit_Info:
credentials: AWS_Credentials
assumed_role_info: AWS_Assume_Role
audited_regions: list
organizations_metadata: AWS_Organizations_Info
+7
View File
@@ -133,6 +133,12 @@ if __name__ == "__main__":
help="Custom output directory, by default the folder where Prowler is stored",
default=default_output_directory,
)
parser.add_argument(
"-O",
"--organizations-role",
nargs="?",
help="Specify AWS Organizations management role ARN to be assumed, to get Organization metadata",
)
# Parse Arguments
args = parser.parse_args()
@@ -234,6 +240,7 @@ if __name__ == "__main__":
args.session_duration,
args.external_id,
args.filter_region,
args.organizations_role,
)
# Execute checks