From 8e087196c9cab5f7cdbcf2d46ce777261076a484 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:01:52 +0200 Subject: [PATCH] feat(s3): Add new check `s3_bucket_cross_account_access` (#5082) Co-authored-by: Sergio --- .../__init__.py | 0 ..._bucket_cross_account_access.metadata.json | 32 +++ .../s3_bucket_cross_account_access.py | 31 +++ .../s3_bucket_cross_account_access_test.py | 245 ++++++++++++++++++ 4 files changed, 308 insertions(+) create mode 100644 prowler/providers/aws/services/s3/s3_bucket_cross_account_access/__init__.py create mode 100644 prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.metadata.json create mode 100644 prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.py create mode 100644 tests/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access_test.py diff --git a/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/__init__.py b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.metadata.json new file mode 100644 index 0000000000..5881dcf67f --- /dev/null +++ b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "s3_bucket_cross_account_access", + "CheckTitle": "Ensure that general-purpose bucket policies restrict access to other AWS accounts.", + "CheckType": [ + "Effects/Data Exposure" + ], + "ServiceName": "s3", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "Severity": "high", + "ResourceType": "AwsS3Bucket", + "Description": "This check verifies that S3 bucket policies are configured in a way that limits access to the intended AWS accounts only, preventing unauthorized access by external or unintended accounts.", + "Risk": "Allowing other AWS accounts to perform sensitive actions (e.g., modifying bucket policies, ACLs, or encryption settings) on your S3 buckets can lead to data exposure, unauthorized access, or misconfigurations, increasing the risk of insider threats or attacks.", + "RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-6", + "Terraform": "" + }, + "Recommendation": { + "Text": "Review and update your S3 bucket policies to remove permissions that grant external AWS accounts access to critical actions and implement least privilege principles to ensure sensitive operations are restricted to trusted accounts only", + "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.py b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.py new file mode 100644 index 0000000000..ea1c812436 --- /dev/null +++ b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.py @@ -0,0 +1,31 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.lib.policy import is_policy_public +from prowler.providers.aws.services.s3.s3_client import s3_client + + +class s3_bucket_cross_account_access(Check): + def execute(self): + findings = [] + for arn, bucket in s3_client.buckets.items(): + report = Check_Report_AWS(self.metadata()) + report.region = bucket.region + report.resource_id = bucket.name + report.resource_arn = arn + report.resource_tags = bucket.tags + report.status = "PASS" + report.status_extended = f"S3 Bucket {bucket.name} has a bucket policy but it does not allow cross account access." + + if not bucket.policy: + report.status = "PASS" + report.status_extended = ( + f"S3 Bucket {bucket.name} does not have a bucket policy." + ) + elif is_policy_public( + bucket.policy, source_account=s3_client.audited_account + ): + report.status = "FAIL" + report.status_extended = f"S3 Bucket {bucket.name} has a bucket policy allowing cross account access." + + findings.append(report) + + return findings diff --git a/tests/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access_test.py b/tests/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access_test.py new file mode 100644 index 0000000000..176e013561 --- /dev/null +++ b/tests/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access_test.py @@ -0,0 +1,245 @@ +import json +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_s3_bucket_cross_account_access: + @mock_aws + def test_no_buckets(self): + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access.s3_client", + new=S3(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access import ( + s3_bucket_cross_account_access, + ) + + check = s3_bucket_cross_account_access() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_bucket_no_policy(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access.s3_client", + new=S3(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access import ( + s3_bucket_cross_account_access, + ) + + check = s3_bucket_cross_account_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} does not have a bucket policy." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.identity.partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bucket_policy_allow_delete(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + delete_bucket_policy = "s3:DeleteBucketPolicy" + bucket_name_us = "bucket_test_us" + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": delete_bucket_policy, + "Resource": "arn:aws:s3:::*", + } + ], + } + s3_client.create_bucket(Bucket=bucket_name_us) + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=json.dumps(policy), + ) + + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access.s3_client", + new=S3(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access import ( + s3_bucket_cross_account_access, + ) + + check = s3_bucket_cross_account_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} has a bucket policy allowing cross account access." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.identity.partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bucket_policy_allow_multiple_other_accounts(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + put_encryption_configuration = "s3:PutEncryptionConfiguration" + put_bucket_policy = "s3:PutBucketPolicy" + bucket_name_us = "bucket_test_us" + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "arn:aws:iam::*:root"}, + "Action": [ + put_encryption_configuration, + put_bucket_policy, + ], + "Resource": "arn:aws:s3:::*", + } + ], + } + s3_client.create_bucket(Bucket=bucket_name_us) + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=json.dumps(policy), + ) + + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access.s3_client", + new=S3(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access import ( + s3_bucket_cross_account_access, + ) + + check = s3_bucket_cross_account_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} has a bucket policy allowing cross account access." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.identity.partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bucket_policy_allow_same_account(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + delete_bucket_policy = "s3:DeleteBucketPolicy" + bucket_name_us = "bucket_test_us" + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"}, + "Action": delete_bucket_policy, + "Resource": "arn:aws:s3:::*", + } + ], + } + s3_client.create_bucket(Bucket=bucket_name_us) + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=json.dumps(policy), + ) + + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access.s3_client", + new=S3(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access import ( + s3_bucket_cross_account_access, + ) + + check = s3_bucket_cross_account_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} has a bucket policy but it does not allow cross account access." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.identity.partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1