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:
mohd4adil
2026-01-29 13:43:34 +05:30
committed by GitHub
parent ad7be95dc3
commit e97e31c7ca
16 changed files with 105 additions and 12 deletions

View File

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

View File

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

View File

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

View File

@@ -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."
}

View File

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

View File

@@ -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."
}

View File

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

View File

@@ -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."
}

View File

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

View File

@@ -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"]
)
):

View File

@@ -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."
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": [