From fa99ee9d5b7af5dbb990140515f6a37759e33144 Mon Sep 17 00:00:00 2001 From: Sergio Garcia <38561120+sergargar@users.noreply.github.com> Date: Tue, 27 Jun 2023 12:57:18 +0200 Subject: [PATCH] feat(allowlist): add exceptions to allowlist (#2527) --- docs/tutorials/allowlist.md | 28 ++++ prowler/config/allowlist.yaml | 29 ++++ .../providers/aws/lib/allowlist/allowlist.py | 144 +++++++++++++----- .../aws/lib/allowlist/allowlist_test.py | 107 ++++++++++++- 4 files changed, 264 insertions(+), 44 deletions(-) diff --git a/docs/tutorials/allowlist.md b/docs/tutorials/allowlist.md index 846db8fd..ed04758c 100644 --- a/docs/tutorials/allowlist.md +++ b/docs/tutorials/allowlist.md @@ -11,6 +11,7 @@ You can use `-w`/`--allowlist-file` with the path of your allowlist yaml file, b ### Resources and tags are lists that can have either Regex or Keywords. ### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together. ### Use an alternation Regex to match one of multiple tags with "ORed" logic. + ### For each check you can except Accounts, Regions, Resources and/or Tags. ########################### ALLOWLIST EXAMPLE ########################### Allowlist: Accounts: @@ -54,6 +55,33 @@ You can use `-w`/`--allowlist-file` with the path of your allowlist yaml file, b Tags: - "environment=dev" # Will ignore every resource containing the tag 'environment=dev' in every account and region + "*": + Checks: + "ecs_task_definitions_no_environment_secrets": + Regions: + - "*" + Resources: + - "*" + Exceptions: + Accounts: + - "0123456789012" + Regions: + - "eu-west-1" + - "eu-south-2" # Will ignore every resource in check ecs_task_definitions_no_environment_secrets except the ones in account 0123456789012 located in eu-south-2 or eu-west-1 + + "123456789012": + Checks: + "*": + Regions: + - "*" + Resources: + - "*" + Exceptions: + Resources: + - "test" + Tags: + - "environment=prod" # Will ignore every resource except in account 123456789012 except the ones containing the string "test" and tag environment=prod + ## Supported Allowlist Locations diff --git a/prowler/config/allowlist.yaml b/prowler/config/allowlist.yaml index e6feee71..79329989 100644 --- a/prowler/config/allowlist.yaml +++ b/prowler/config/allowlist.yaml @@ -2,6 +2,7 @@ ### Resources and tags are lists that can have either Regex or Keywords. ### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together. ### Use an alternation Regex to match one of multiple tags with "ORed" logic. +### For each check you can except Accounts, Regions, Resources and/or Tags. ########################### ALLOWLIST EXAMPLE ########################### Allowlist: Accounts: @@ -45,6 +46,34 @@ Allowlist: Tags: - "environment=dev" # Will ignore every resource containing the tag 'environment=dev' in every account and region + "*": + Checks: + "ecs_task_definitions_no_environment_secrets": + Regions: + - "*" + Resources: + - "*" + Exceptions: + Accounts: + - "0123456789012" + Regions: + - "eu-west-1" + - "eu-south-2" # Will ignore every resource in check ecs_task_definitions_no_environment_secrets except the ones in account 0123456789012 located in eu-south-2 or eu-west-1 + + "123456789012": + Checks: + "*": + Regions: + - "*" + Resources: + - "*" + Exceptions: + Resources: + - "test" + Tags: + - "environment=prod" # Will ignore every resource except in account 123456789012 except the ones containing the string "test" and tag environment=prod + + # EXAMPLE: CONTROL TOWER (to migrate) # When using Control Tower, guardrails prevent access to certain protected resources. The allowlist diff --git a/prowler/providers/aws/lib/allowlist/allowlist.py b/prowler/providers/aws/lib/allowlist/allowlist.py index 3532df21..4ff06bc0 100644 --- a/prowler/providers/aws/lib/allowlist/allowlist.py +++ b/prowler/providers/aws/lib/allowlist/allowlist.py @@ -12,7 +12,17 @@ allowlist_schema = Schema( "Accounts": { str: { "Checks": { - str: {"Regions": list, "Resources": list, Optional("Tags"): list} + str: { + "Regions": list, + "Resources": list, + Optional("Tags"): list, + Optional("Exceptions"): { + Optional("Accounts"): list, + Optional("Regions"): list, + Optional("Resources"): list, + Optional("Tags"): list, + }, + } } } } @@ -69,25 +79,22 @@ def parse_allowlist_file(audit_info, allowlist_file): dynamodb_items.update(response["Items"]) for item in dynamodb_items: # Create allowlist for every item + allowlist["Accounts"][item["Accounts"]] = { + "Checks": { + item["Checks"]: { + "Regions": item["Regions"], + "Resources": item["Resources"], + } + } + } if "Tags" in item: - allowlist["Accounts"][item["Accounts"]] = { - "Checks": { - item["Checks"]: { - "Regions": item["Regions"], - "Resources": item["Resources"], - "Tags": item["Tags"], - } - } - } - else: - allowlist["Accounts"][item["Accounts"]] = { - "Checks": { - item["Checks"]: { - "Regions": item["Regions"], - "Resources": item["Resources"], - } - } - } + allowlist["Accounts"][item["Accounts"]]["Checks"][item["Checks"]][ + "Tags" + ] = item["Tags"] + if "Exceptions" in item: + allowlist["Accounts"][item["Accounts"]]["Checks"][item["Checks"]][ + "Exceptions" + ] = item["Exceptions"] else: with open(allowlist_file) as f: allowlist = yaml.safe_load(f)["Allowlist"] @@ -109,15 +116,16 @@ def parse_allowlist_file(audit_info, allowlist_file): def is_allowlisted(allowlist, audited_account, check, region, resource, tags): try: if audited_account in allowlist["Accounts"]: + account = audited_account if is_allowlisted_in_check( - allowlist, audited_account, check, region, resource, tags + allowlist, audited_account, account, check, region, resource, tags ): return True # If there is a *, it affects to all accounts if "*" in allowlist["Accounts"]: - audited_account = "*" + account = "*" if is_allowlisted_in_check( - allowlist, audited_account, check, region, resource, tags + allowlist, audited_account, account, check, region, resource, tags ): return True return False @@ -128,29 +136,40 @@ def is_allowlisted(allowlist, audited_account, check, region, resource, tags): sys.exit(1) -def is_allowlisted_in_check(allowlist, audited_account, check, region, resource, tags): +def is_allowlisted_in_check( + allowlist, audited_account, account, check, region, resource, tags +): try: - for allowlisted_check in allowlist["Accounts"][audited_account][ - "Checks" - ].keys(): + allowlisted_checks = allowlist["Accounts"][account]["Checks"] + for allowlisted_check in allowlisted_checks.keys(): + # Check if there are exceptions + if is_excepted( + allowlisted_checks, + allowlisted_check, + audited_account, + region, + resource, + tags, + ): + return False # If there is a *, it affects to all checks if "*" == allowlisted_check: check = "*" if is_allowlisted_in_region( - allowlist, audited_account, check, region, resource, tags + allowlist, account, check, region, resource, tags ): return True # Check if there is the specific check elif check == allowlisted_check: if is_allowlisted_in_region( - allowlist, audited_account, check, region, resource, tags + allowlist, account, check, region, resource, tags ): return True # Check if check is a regex elif re.search(allowlisted_check, check): if is_allowlisted_in_region( allowlist, - audited_account, + account, allowlisted_check, region, resource, @@ -165,27 +184,23 @@ def is_allowlisted_in_check(allowlist, audited_account, check, region, resource, sys.exit(1) -def is_allowlisted_in_region(allowlist, audited_account, check, region, resource, tags): +def is_allowlisted_in_region(allowlist, account, check, region, resource, tags): try: # If there is a *, it affects to all regions - if "*" in allowlist["Accounts"][audited_account]["Checks"][check]["Regions"]: - for elem in allowlist["Accounts"][audited_account]["Checks"][check][ - "Resources" - ]: + if "*" in allowlist["Accounts"][account]["Checks"][check]["Regions"]: + for elem in allowlist["Accounts"][account]["Checks"][check]["Resources"]: if is_allowlisted_in_tags( - allowlist["Accounts"][audited_account]["Checks"][check], + allowlist["Accounts"][account]["Checks"][check], elem, resource, tags, ): return True # Check if there is the specific region - if region in allowlist["Accounts"][audited_account]["Checks"][check]["Regions"]: - for elem in allowlist["Accounts"][audited_account]["Checks"][check][ - "Resources" - ]: + if region in allowlist["Accounts"][account]["Checks"][check]["Regions"]: + for elem in allowlist["Accounts"][account]["Checks"][check]["Resources"]: if is_allowlisted_in_tags( - allowlist["Accounts"][audited_account]["Checks"][check], + allowlist["Accounts"][account]["Checks"][check], elem, resource, tags, @@ -228,3 +243,52 @@ def is_allowlisted_in_tags(check_allowlist, elem, resource, tags): f"{error.__class__.__name__} -- {error}[{error.__traceback__.tb_lineno}]" ) sys.exit(1) + + +def is_excepted( + allowlisted_checks, allowlisted_check, audited_account, region, resource, tags +): + try: + excepted = False + is_account_excepted = False + is_region_excepted = False + is_resource_excepted = False + is_tag_excepted = False + exceptions = allowlisted_checks[allowlisted_check].get("Exceptions") + if exceptions: + excepted_accounts = exceptions.get("Accounts", []) + excepted_regions = exceptions.get("Regions", []) + excepted_resources = exceptions.get("Resources", []) + excepted_tags = exceptions.get("Tags", []) + if exceptions: + if audited_account in excepted_accounts: + is_account_excepted = True + if region in excepted_regions: + is_region_excepted = True + for excepted_resource in excepted_resources: + if re.search(excepted_resource, resource): + is_resource_excepted = True + if tags in excepted_tags: + is_tag_excepted = True + if ( + ( + (excepted_accounts and is_account_excepted) + or not excepted_accounts + ) + and ( + (excepted_regions and is_region_excepted) + or not excepted_regions + ) + and ( + (excepted_resources and is_resource_excepted) + or not excepted_resources + ) + and ((excepted_tags and is_tag_excepted) or not excepted_tags) + ): + excepted = True + return excepted + except Exception as error: + logger.critical( + f"{error.__class__.__name__} -- {error}[{error.__traceback__.tb_lineno}]" + ) + sys.exit(1) diff --git a/tests/providers/aws/lib/allowlist/allowlist_test.py b/tests/providers/aws/lib/allowlist/allowlist_test.py index 8355d9b1..a7aa525f 100644 --- a/tests/providers/aws/lib/allowlist/allowlist_test.py +++ b/tests/providers/aws/lib/allowlist/allowlist_test.py @@ -7,6 +7,7 @@ from prowler.providers.aws.lib.allowlist.allowlist import ( is_allowlisted_in_check, is_allowlisted_in_region, is_allowlisted_in_tags, + is_excepted, parse_allowlist_file, ) from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info @@ -308,20 +309,44 @@ class Test_Allowlist: } assert is_allowlisted_in_check( - allowlist, AWS_ACCOUNT_NUMBER, "check_test", AWS_REGION, "prowler", "" + allowlist, + AWS_ACCOUNT_NUMBER, + AWS_ACCOUNT_NUMBER, + "check_test", + AWS_REGION, + "prowler", + "", ) assert is_allowlisted_in_check( - allowlist, AWS_ACCOUNT_NUMBER, "check_test", AWS_REGION, "prowler-test", "" + allowlist, + AWS_ACCOUNT_NUMBER, + AWS_ACCOUNT_NUMBER, + "check_test", + AWS_REGION, + "prowler-test", + "", ) assert is_allowlisted_in_check( - allowlist, AWS_ACCOUNT_NUMBER, "check_test", AWS_REGION, "test-prowler", "" + allowlist, + AWS_ACCOUNT_NUMBER, + AWS_ACCOUNT_NUMBER, + "check_test", + AWS_REGION, + "test-prowler", + "", ) assert not ( is_allowlisted_in_check( - allowlist, AWS_ACCOUNT_NUMBER, "check_test", "us-east-2", "test", "" + allowlist, + AWS_ACCOUNT_NUMBER, + AWS_ACCOUNT_NUMBER, + "check_test", + "us-east-2", + "test", + "", ) ) @@ -343,6 +368,7 @@ class Test_Allowlist: assert is_allowlisted_in_check( allowlist, AWS_ACCOUNT_NUMBER, + AWS_ACCOUNT_NUMBER, "s3_bucket_public_access", AWS_REGION, "prowler", @@ -352,6 +378,7 @@ class Test_Allowlist: assert is_allowlisted_in_check( allowlist, AWS_ACCOUNT_NUMBER, + AWS_ACCOUNT_NUMBER, "s3_bucket_public_access", AWS_REGION, "prowler-test", @@ -361,6 +388,7 @@ class Test_Allowlist: assert is_allowlisted_in_check( allowlist, AWS_ACCOUNT_NUMBER, + AWS_ACCOUNT_NUMBER, "s3_bucket_public_access", AWS_REGION, "test-prowler", @@ -371,6 +399,7 @@ class Test_Allowlist: is_allowlisted_in_check( allowlist, AWS_ACCOUNT_NUMBER, + AWS_ACCOUNT_NUMBER, "iam_user_hardware_mfa_enabled", AWS_REGION, "test", @@ -482,3 +511,73 @@ class Test_Allowlist: "prowler-test", "environment=prod | project=myproj", ) + + def test_is_excepted(self): + # Allowlist example + check_allowlist = { + "check_test": { + "Regions": ["us-east-1", "eu-west-1"], + "Resources": ["*"], + "Tags": ["environment=dev"], + "Exceptions": { + "Accounts": [AWS_ACCOUNT_NUMBER], + "Regions": ["eu-central-1", "eu-south-3"], + "Resources": ["test"], + "Tags": ["environment=test", "project=.*"], + }, + } + } + + assert is_excepted( + check_allowlist, + "check_test", + AWS_ACCOUNT_NUMBER, + "eu-central-1", + "test", + "environment=test", + ) + + assert is_excepted( + check_allowlist, + "check_test", + AWS_ACCOUNT_NUMBER, + "eu-south-3", + "test", + "environment=test", + ) + + assert is_excepted( + check_allowlist, + "check_test", + AWS_ACCOUNT_NUMBER, + "eu-south-3", + "test123", + "environment=test", + ) + + assert not is_excepted( + check_allowlist, + "check_test", + AWS_ACCOUNT_NUMBER, + "eu-south-2", + "test", + "environment=test", + ) + + assert not is_excepted( + check_allowlist, + "check_test", + AWS_ACCOUNT_NUMBER, + "eu-south-3", + "prowler", + "environment=test", + ) + + assert not is_excepted( + check_allowlist, + "check_test", + AWS_ACCOUNT_NUMBER, + "eu-south-3", + "test", + "environment=pro", + )