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

1238
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

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"

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:

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

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

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

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

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,")

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

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