mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-22 03:08:23 +00:00
chore(aws): add support for trusted aws accounts in cross account checks for s3, eventbridge bus, eventbridge schema and dynamodb (#9692)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
@@ -66,6 +66,11 @@ The following list includes all the AWS checks with configurable variables that
|
||||
| `secretsmanager_secret_rotated_periodically` | `max_days_secret_unrotated` | Integer |
|
||||
| `ssm_document_secrets` | `secrets_ignore_patterns` | List of Strings |
|
||||
| `trustedadvisor_premium_support_plan_subscribed` | `verify_premium_support_plans` | Boolean |
|
||||
| `dynamodb_table_cross_account_access` | `trusted_account_ids` | List of Strings |
|
||||
| `eventbridge_bus_cross_account_access` | `trusted_account_ids` | List of Strings |
|
||||
| `eventbridge_schema_registry_cross_account_access` | `trusted_account_ids` | List of Strings |
|
||||
| `s3_bucket_cross_account_access` | `trusted_account_ids` | List of Strings |
|
||||
| `ssm_documents_set_as_public` | `trusted_account_ids` | List of Strings |
|
||||
| `vpc_endpoint_connections_trust_boundaries` | `trusted_account_ids` | List of Strings |
|
||||
| `vpc_endpoint_services_allowed_principals_trust_boundaries` | `trusted_account_ids` | List of Strings |
|
||||
|
||||
@@ -202,7 +207,10 @@ aws:
|
||||
]
|
||||
|
||||
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
|
||||
# AWS SSM Configuration (aws.ssm_documents_set_as_public)
|
||||
# AWS SSM Configuration (ssm_documents_set_as_public)
|
||||
# AWS S3 Configuration (s3_bucket_cross_account_access)
|
||||
# AWS EventBridge Configuration (eventbridge_schema_registry_cross_account_access, eventbridge_bus_cross_account_access)
|
||||
# AWS DynamoDB Configuration (dynamodb_table_cross_account_access)
|
||||
# Single account environment: No action required. The AWS account number will be automatically added by the checks.
|
||||
# Multi account environment: Any additional trusted account number should be added as a space separated list, e.g.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
|
||||
@@ -27,6 +27,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Update Azure Policy service metadata to new format [(#9625)](https://github.com/prowler-cloud/prowler/pull/9625)
|
||||
- Update Azure MySQL service metadata to new format [(#9623)](https://github.com/prowler-cloud/prowler/pull/9623)
|
||||
- Update Azure Defender service metadata to new format [(#9618)](https://github.com/prowler-cloud/prowler/pull/9618)
|
||||
- Make AWS cross-account checks configurable through `trusted_account_ids` config parameter [(#9692)](https://github.com/prowler-cloud/prowler/pull/9692)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,10 @@ aws:
|
||||
fargate_windows_latest_version: "1.0.0"
|
||||
|
||||
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
|
||||
# AWS SSM Configuration (aws.ssm_documents_set_as_public)
|
||||
# AWS SSM Configuration (ssm_documents_set_as_public)
|
||||
# AWS S3 Configuration (s3_bucket_cross_account_access)
|
||||
# AWS EventBridge Configuration (eventbridge_schema_registry_cross_account_access, eventbridge_bus_cross_account_access)
|
||||
# AWS DynamoDB Configuration (dynamodb_table_cross_account_access)
|
||||
# Single account environment: No action required. The AWS account number will be automatically added by the checks.
|
||||
# Multi account environment: Any additional trusted account number should be added as a space separated list, e.g.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
|
||||
@@ -38,5 +38,5 @@
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
"Notes": "This check supports the `trusted_account_ids` configuration in config.yaml to allow specific cross-account access without triggering a finding."
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ from prowler.providers.aws.services.iam.lib.policy import is_policy_public
|
||||
class dynamodb_table_cross_account_access(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
trusted_account_ids = dynamodb_client.audit_config.get(
|
||||
"trusted_account_ids", []
|
||||
)
|
||||
for table in dynamodb_client.tables.values():
|
||||
if table.policy is None:
|
||||
continue
|
||||
@@ -20,6 +23,7 @@ class dynamodb_table_cross_account_access(Check):
|
||||
table.policy,
|
||||
dynamodb_client.audited_account,
|
||||
is_cross_account_allowed=False,
|
||||
trusted_account_ids=trusted_account_ids,
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"DynamoDB table {table.name} has a resource-based policy allowing cross account access."
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
"Notes": "This check supports the `trusted_account_ids` configuration in config.yaml to allow specific cross-account access without triggering a finding."
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ from prowler.providers.aws.services.iam.lib.policy import is_policy_public
|
||||
class eventbridge_bus_cross_account_access(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
trusted_account_ids = eventbridge_client.audit_config.get(
|
||||
"trusted_account_ids", []
|
||||
)
|
||||
for bus in eventbridge_client.buses.values():
|
||||
if bus.policy is None:
|
||||
continue
|
||||
@@ -20,6 +23,7 @@ class eventbridge_bus_cross_account_access(Check):
|
||||
bus.policy,
|
||||
eventbridge_client.audited_account,
|
||||
is_cross_account_allowed=False,
|
||||
trusted_account_ids=trusted_account_ids,
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
|
||||
@@ -39,5 +39,5 @@
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
"Notes": "This check supports the `trusted_account_ids` configuration in config.yaml to allow specific cross-account access without triggering a finding."
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ from prowler.providers.aws.services.iam.lib.policy import is_policy_public
|
||||
class eventbridge_schema_registry_cross_account_access(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
trusted_account_ids = schema_client.audit_config.get("trusted_account_ids", [])
|
||||
for registry in schema_client.registries.values():
|
||||
if registry.policy is None:
|
||||
continue
|
||||
@@ -16,6 +17,7 @@ class eventbridge_schema_registry_cross_account_access(Check):
|
||||
registry.policy,
|
||||
schema_client.audited_account,
|
||||
is_cross_account_allowed=False,
|
||||
trusted_account_ids=trusted_account_ids,
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"EventBridge schema registry {registry.name} allows cross-account access."
|
||||
|
||||
@@ -387,6 +387,7 @@ def is_policy_public(
|
||||
is_cross_account_allowed=True,
|
||||
not_allowed_actions: list = [],
|
||||
check_cross_service_confused_deputy=False,
|
||||
trusted_account_ids: list = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if the policy allows public access to the resource.
|
||||
@@ -397,10 +398,19 @@ def is_policy_public(
|
||||
is_cross_account_allowed (bool): If the policy can allow cross-account access, default: True (https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html#cross-service-confused-deputy-prevention)
|
||||
not_allowed_actions (list): List of actions that are not allowed, default: []. If not_allowed_actions is empty, the function will not consider the actions in the policy.
|
||||
check_cross_service_confused_deputy (bool): If the policy is checked for cross-service confused deputy, default: False
|
||||
trusted_account_ids (list): A list of trusted accound ids to reduce false positives on cross-account checks
|
||||
Returns:
|
||||
bool: True if the policy allows public access, False otherwise
|
||||
"""
|
||||
is_public = False
|
||||
|
||||
if trusted_account_ids is None:
|
||||
trusted_account_ids = []
|
||||
|
||||
trusted_accounts = set(trusted_account_ids)
|
||||
if source_account:
|
||||
trusted_accounts.add(source_account)
|
||||
|
||||
if policy:
|
||||
for statement in policy.get("Statement", []):
|
||||
# Only check allow statements
|
||||
@@ -414,13 +424,19 @@ def is_policy_public(
|
||||
isinstance(principal.get("AWS"), str)
|
||||
and source_account
|
||||
and not is_cross_account_allowed
|
||||
and source_account not in principal.get("AWS", "")
|
||||
and not any(
|
||||
trusted_account in principal.get("AWS", "")
|
||||
for trusted_account in trusted_accounts
|
||||
)
|
||||
) or (
|
||||
isinstance(principal.get("AWS"), list)
|
||||
and source_account
|
||||
and not is_cross_account_allowed
|
||||
and not any(
|
||||
source_account in principal_aws
|
||||
and not all(
|
||||
any(
|
||||
trusted_account in principal_aws
|
||||
for trusted_account in trusted_accounts
|
||||
)
|
||||
for principal_aws in principal["AWS"]
|
||||
)
|
||||
):
|
||||
|
||||
@@ -39,5 +39,5 @@
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
"Notes": "This check supports the `trusted_account_ids` configuration in config.yaml to allow specific cross-account access without triggering a finding."
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ from prowler.providers.aws.services.s3.s3_client import s3_client
|
||||
class s3_bucket_cross_account_access(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
trusted_account_ids = s3_client.audit_config.get("trusted_account_ids", [])
|
||||
for bucket in s3_client.buckets.values():
|
||||
if bucket.policy is None:
|
||||
continue
|
||||
@@ -19,7 +20,10 @@ class s3_bucket_cross_account_access(Check):
|
||||
f"S3 Bucket {bucket.name} does not have a bucket policy."
|
||||
)
|
||||
elif is_policy_public(
|
||||
bucket.policy, s3_client.audited_account, is_cross_account_allowed=False
|
||||
bucket.policy,
|
||||
s3_client.audited_account,
|
||||
is_cross_account_allowed=False,
|
||||
trusted_account_ids=trusted_account_ids,
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"S3 Bucket {bucket.name} has a bucket policy allowing cross account access."
|
||||
|
||||
@@ -63,7 +63,10 @@ aws:
|
||||
fargate_windows_latest_version: "1.0.0"
|
||||
|
||||
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
|
||||
# AWS SSM Configuration (aws.ssm_documents_set_as_public)
|
||||
# AWS SSM Configuration (ssm_documents_set_as_public)
|
||||
# AWS S3 Configuration (s3_bucket_cross_account_access)
|
||||
# AWS EventBridge Configuration (eventbridge_schema_registry_cross_account_access, eventbridge_bus_cross_account_access)
|
||||
# AWS DynamoDB Configuration (dynamodb_table_cross_account_access)
|
||||
# Single account environment: No action required. The AWS account number will be automatically added by the checks.
|
||||
# Multi account environment: Any additional trusted account number should be added as a space separated list, e.g.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
|
||||
@@ -104,6 +104,7 @@ class Test_dynamodb_table_cross_account_access:
|
||||
def test_no_tables(self):
|
||||
dynamodb_client = mock.MagicMock
|
||||
dynamodb_client.tables = {}
|
||||
dynamodb_client.audit_config = {}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB",
|
||||
|
||||
@@ -46,10 +46,10 @@ self_asterisk_policy = {
|
||||
|
||||
|
||||
class Test_eventbridge_schema_registry_cross_account_access:
|
||||
|
||||
def test_no_schemas(self):
|
||||
schema_client = mock.MagicMock
|
||||
schema_client.registries = {}
|
||||
schema_client.audit_config = {}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
@@ -76,6 +76,7 @@ class Test_eventbridge_schema_registry_cross_account_access:
|
||||
|
||||
schema_client = mock.MagicMock
|
||||
schema_client.audited_account = AWS_ACCOUNT_NUMBER
|
||||
schema_client.audit_config = {}
|
||||
schema_client.registries = {
|
||||
test_schema_arn: Registry(
|
||||
name=test_schema_name,
|
||||
@@ -119,6 +120,7 @@ class Test_eventbridge_schema_registry_cross_account_access:
|
||||
|
||||
schema_client = mock.MagicMock
|
||||
schema_client.audited_account = AWS_ACCOUNT_NUMBER
|
||||
schema_client.audit_config = {}
|
||||
schema_client.registries = {
|
||||
test_schema_arn: Registry(
|
||||
name=test_schema_name,
|
||||
@@ -162,6 +164,7 @@ class Test_eventbridge_schema_registry_cross_account_access:
|
||||
|
||||
schema_client = mock.MagicMock
|
||||
schema_client.audited_account = AWS_ACCOUNT_NUMBER
|
||||
schema_client.audit_config = {}
|
||||
schema_client.registries = {
|
||||
test_schema_arn: Registry(
|
||||
name=test_schema_name,
|
||||
|
||||
@@ -18,6 +18,7 @@ from prowler.providers.aws.services.iam.lib.policy import (
|
||||
|
||||
TRUSTED_AWS_ACCOUNT_NUMBER = "123456789012"
|
||||
NON_TRUSTED_AWS_ACCOUNT_NUMBER = "111222333444"
|
||||
TRUSTED_AWS_ACCOUNT_NUMBER_LIST = ["123456789012", "123456789013", "123456789014"]
|
||||
|
||||
TRUSTED_ORGANIZATION_ID = "o-123456789012"
|
||||
NON_TRUSTED_ORGANIZATION_ID = "o-111222333444"
|
||||
@@ -1652,6 +1653,49 @@ class Test_Policy:
|
||||
is_cross_account_allowed=False,
|
||||
)
|
||||
|
||||
def test_cross_account_access_trusted_account_list(self):
|
||||
policy = {
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": f"arn:aws:iam::{TRUSTED_AWS_ACCOUNT_NUMBER_LIST[0]}:root"
|
||||
},
|
||||
"Action": "*",
|
||||
"Resource": "*",
|
||||
}
|
||||
]
|
||||
}
|
||||
assert not is_policy_public(
|
||||
policy,
|
||||
TRUSTED_AWS_ACCOUNT_NUMBER,
|
||||
is_cross_account_allowed=False,
|
||||
trusted_account_ids=TRUSTED_AWS_ACCOUNT_NUMBER_LIST,
|
||||
)
|
||||
|
||||
def test_cross_account_access_with_principal_list_trusted_account_list(self):
|
||||
policy = {
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": [
|
||||
f"arn:aws:iam::{TRUSTED_AWS_ACCOUNT_NUMBER_LIST[0]}:root",
|
||||
f"arn:aws:iam::{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:root",
|
||||
]
|
||||
},
|
||||
"Action": "*",
|
||||
"Resource": "*",
|
||||
}
|
||||
]
|
||||
}
|
||||
assert is_policy_public(
|
||||
policy,
|
||||
TRUSTED_AWS_ACCOUNT_NUMBER,
|
||||
is_cross_account_allowed=False,
|
||||
trusted_account_ids=TRUSTED_AWS_ACCOUNT_NUMBER_LIST,
|
||||
)
|
||||
|
||||
def test_policy_allows_public_access_with_wildcard_principal(self):
|
||||
policy_allow_wildcard_principal = {
|
||||
"Statement": [
|
||||
|
||||
Reference in New Issue
Block a user