From edbe463d7396f1d8ab191d789af771cb11c08308 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Thu, 12 Sep 2024 16:58:24 +0200 Subject: [PATCH] feat(cloudfront): Add new check `cloudfront_distributions_default_root_object` (#4938) Co-authored-by: Sergio --- .../__init__.py | 0 ...ibutions_default_root_object.metadata.json | 30 +++++ ...front_distributions_default_root_object.py | 26 ++++ .../services/cloudfront/cloudfront_service.py | 4 + ..._distributions_default_root_object_test.py | 113 ++++++++++++++++++ 5 files changed, 173 insertions(+) create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/__init__.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.metadata.json create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.py create mode 100644 tests/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object_test.py diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/__init__.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.metadata.json b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.metadata.json new file mode 100644 index 0000000000..e3cabb888e --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "cloudfront_distributions_default_root_object", + "CheckTitle": "Check if CloudFront distributions have a default root object.", + "CheckType": [], + "ServiceName": "cloudfront", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:cloudfront:region:account-id:distribution/resource-id", + "Severity": "high", + "ResourceType": "AWSCloudFrontDistribution", + "Description": "Check if CloudFront distributions have a default root object.", + "Risk": "Without a default root object, requests to the root URL may result in an error or expose unintended content, leading to potential security risks and a poor user experience.", + "RelatedUrl": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DefaultRootObject.html#DefaultRootObjectHow", + "Remediation": { + "Code": { + "CLI": "aws cloudfront update-distribution --id --default-root-object ", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/cloudfront-controls.html#cloudfront-1", + "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/CloudFront/cloudfront-default-object.html" + }, + "Recommendation": { + "Text": "Configure a default root object for your CloudFront distribution to ensure that a specific file (such as index.html) is returned when users access the root URL. This improves user experience and ensures that sensitive content isn't accidentally exposed.", + "Url": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DefaultRootObject.html#DefaultRootObjectHowToDefine" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.py new file mode 100644 index 0000000000..1931625808 --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.py @@ -0,0 +1,26 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.cloudfront.cloudfront_client import ( + cloudfront_client, +) + + +class cloudfront_distributions_default_root_object(Check): + def execute(self): + findings = [] + for distribution in cloudfront_client.distributions.values(): + report = Check_Report_AWS(self.metadata()) + report.region = distribution.region + report.resource_arn = distribution.arn + report.resource_id = distribution.id + report.resource_tags = distribution.tags + + if distribution.default_root_object: + report.status = "PASS" + report.status_extended = f"CloudFront Distribution {distribution.id} does have a default root object ({distribution.default_root_object}) configured." + else: + report.status = "FAIL" + report.status_extended = f"CloudFront Distribution {distribution.id} does not have a default root object configured." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_service.py b/prowler/providers/aws/services/cloudfront/cloudfront_service.py index dab583176c..b35308134f 100644 --- a/prowler/providers/aws/services/cloudfront/cloudfront_service.py +++ b/prowler/providers/aws/services/cloudfront/cloudfront_service.py @@ -88,6 +88,9 @@ class CloudFront(AWSService): distributions[distribution_id].web_acl_id = distribution_config[ "DistributionConfig" ]["WebACLId"] + distributions[distribution_id].default_root_object = ( + distribution_config["DistributionConfig"].get("DefaultRootObject") + ) # Default Cache Config default_cache_config = DefaultCacheConfigBehaviour( @@ -181,6 +184,7 @@ class Distribution(BaseModel): geo_restriction_type: Optional[GeoRestrictionType] origins: list[Origin] web_acl_id: str = "" + default_root_object: Optional[str] tags: Optional[list] = [] ssl_support_method: Optional[SSLSupportMethod] certificate: Optional[str] diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object_test.py new file mode 100644 index 0000000000..dca7a2e3fd --- /dev/null +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object_test.py @@ -0,0 +1,113 @@ +from unittest import mock + +from prowler.providers.aws.services.cloudfront.cloudfront_service import ( + DefaultCacheConfigBehaviour, + Distribution, + ViewerProtocolPolicy, +) +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER + +DISTRIBUTION_ID = "E27LVI50CSW06W" +DISTRIBUTION_ARN = ( + f"arn:aws:cloudfront::{AWS_ACCOUNT_NUMBER}:distribution/{DISTRIBUTION_ID}" +) +REGION = "eu-west-1" + + +class Test_cloudfront_distributions_default_root_object: + def test_no_distributions(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = {} + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_default_root_object.cloudfront_distributions_default_root_object import ( + cloudfront_distributions_default_root_object, + ) + + check = cloudfront_distributions_default_root_object() + result = check.execute() + + assert len(result) == 0 + + def test_distribution_no_root_object(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + "DISTRIBUTION_ID": Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[], + default_cache_config=DefaultCacheConfigBehaviour( + realtime_log_config_arn="", + viewer_protocol_policy=ViewerProtocolPolicy.allow_all, + field_level_encryption_id="", + ), + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_default_root_object.cloudfront_distributions_default_root_object import ( + cloudfront_distributions_default_root_object, + ) + + check = cloudfront_distributions_default_root_object() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} does not have a default root object configured." + ) + assert result[0].resource_tags == [] + + def test_distribution_root_object(self): + cloudfront_client = mock.MagicMock + dro = "index.html" + cloudfront_client.distributions = { + "DISTRIBUTION_ID": Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[], + default_cache_config=DefaultCacheConfigBehaviour( + realtime_log_config_arn="", + viewer_protocol_policy=ViewerProtocolPolicy.allow_all, + field_level_encryption_id="", + ), + default_root_object=dro, + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_default_root_object.cloudfront_distributions_default_root_object import ( + cloudfront_distributions_default_root_object, + ) + + check = cloudfront_distributions_default_root_object() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} does have a default root object ({dro}) configured." + ) + assert result[0].resource_tags == []