feat(s3): Add new check s3_bucket_cross_account_access (#5082)

Co-authored-by: Sergio <sergio@prowler.com>
This commit is contained in:
Hugo Pereira Brito
2024-09-25 16:01:52 +02:00
committed by GitHub
parent 744e7ff5ac
commit 8e087196c9
4 changed files with 308 additions and 0 deletions
@@ -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": ""
}
@@ -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
@@ -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