mirror of
https://github.com/ghndrx/prowler.git
synced 2026-02-10 14:55:00 +00:00
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:
1238
Pipfile.lock
generated
1238
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,")
|
||||
|
||||
@@ -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
prowler
7
prowler
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user