feat(allowlist): add exceptions to allowlist (#2527)

This commit is contained in:
Sergio Garcia
2023-06-27 12:57:18 +02:00
committed by GitHub
parent 6efe634850
commit fa99ee9d5b
4 changed files with 264 additions and 44 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

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