mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-01 13:47:21 +00:00
Compare commits
2 Commits
PROWLER-12
...
feat/s3-bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6df8261a7e | ||
|
|
b447f64ab0 |
@@ -2,6 +2,14 @@
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [5.22.0] (Prowler UNRELEASED)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- `s3_bucket_website_hosting_disabled` check for AWS provider [(#10415)](https://github.com/prowler-cloud/prowler/pull/10415)
|
||||
|
||||
---
|
||||
|
||||
## [5.21.0] (Prowler v5.21.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "s3_bucket_website_hosting_disabled",
|
||||
"CheckTitle": "S3 bucket does not have static website hosting enabled",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "s3",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsS3Bucket",
|
||||
"ResourceGroup": "storage",
|
||||
"Description": "**Amazon S3 buckets** are evaluated to ensure **static website hosting** is not enabled. When website hosting is enabled, objects in the bucket may be publicly accessible via an HTTP endpoint, potentially exposing sensitive data.",
|
||||
"Risk": "Enabling static website hosting on an S3 bucket creates an HTTP endpoint that serves bucket content directly. If bucket permissions are misconfigured, sensitive data could be exposed to the internet. Attackers can discover these endpoints through DNS enumeration or brute-force and access unprotected objects, leading to data leakage.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html",
|
||||
"https://docs.aws.amazon.com/AmazonS3/latest/userguide/EnableWebsiteHosting.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws s3api delete-bucket-website --bucket <BUCKET_NAME>",
|
||||
"NativeIaC": "```yaml\nResources:\n S3Bucket:\n Type: AWS::S3::Bucket\n Properties:\n BucketName: <example_bucket_name>\n # CRITICAL: Remove WebsiteConfiguration to disable static website hosting\n```",
|
||||
"Other": "1. Open the AWS Management Console and go to S3\n2. Select the bucket with the finding\n3. Go to the Properties tab\n4. In Static website hosting, click Edit\n5. Select Disable and Save changes",
|
||||
"Terraform": "```hcl\n# CRITICAL: Remove the aws_s3_bucket_website_configuration resource to disable website hosting\n# resource \"aws_s3_bucket_website_configuration\" \"example\" {\n# bucket = aws_s3_bucket.example.id\n# index_document { suffix = \"index.html\" }\n# }\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Disable **static website hosting** on S3 buckets that do not require it. If website hosting is needed, ensure the bucket policy restricts access appropriately and consider using **Amazon CloudFront** with an Origin Access Identity (OAI) instead of direct S3 website endpoints for better security controls.",
|
||||
"Url": "https://hub.prowler.com/check/s3_bucket_website_hosting_disabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"internet-exposed"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.s3.s3_client import s3_client
|
||||
|
||||
|
||||
class s3_bucket_website_hosting_disabled(Check):
|
||||
"""Ensure that S3 buckets do not have static website hosting enabled."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Execute the check logic.
|
||||
|
||||
Returns:
|
||||
A list of reports containing the result of the check.
|
||||
"""
|
||||
findings = []
|
||||
for bucket in s3_client.buckets.values():
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=bucket)
|
||||
if bucket.website_hosting_enabled:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"S3 Bucket {bucket.name} has static website hosting enabled."
|
||||
)
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"S3 Bucket {bucket.name} does not have static website hosting enabled."
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -36,6 +36,7 @@ class S3(AWSService):
|
||||
self.__threading_call__(
|
||||
self._get_bucket_notification_configuration, self.buckets.values()
|
||||
)
|
||||
self.__threading_call__(self._get_bucket_website, self.buckets.values())
|
||||
|
||||
def _list_buckets(self, provider):
|
||||
logger.info("S3 - Listing buckets...")
|
||||
@@ -487,6 +488,28 @@ class S3(AWSService):
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_bucket_website(self, bucket):
|
||||
logger.info("S3 - Get bucket's website configuration...")
|
||||
try:
|
||||
regional_client = self.regional_clients[bucket.region]
|
||||
regional_client.get_bucket_website(Bucket=bucket.name)
|
||||
bucket.website_hosting_enabled = True
|
||||
except ClientError as error:
|
||||
if error.response["Error"]["Code"] == "NoSuchWebsiteConfiguration":
|
||||
bucket.website_hosting_enabled = False
|
||||
elif error.response["Error"]["Code"] == "NoSuchBucket":
|
||||
logger.warning(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _head_bucket(self, bucket_name):
|
||||
logger.info("S3 - Checking if bucket exists...")
|
||||
try:
|
||||
@@ -703,3 +726,4 @@ class Bucket(BaseModel):
|
||||
lifecycle: List[LifeCycleRule] = Field(default_factory=list)
|
||||
replication_rules: List[ReplicationRule] = Field(default_factory=list)
|
||||
notification_config: Dict = Field(default_factory=dict)
|
||||
website_hosting_enabled: bool = False
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
from unittest import mock
|
||||
|
||||
from boto3 import client
|
||||
from moto import mock_aws
|
||||
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
|
||||
|
||||
|
||||
class Test_s3_bucket_website_hosting_disabled:
|
||||
@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_website_hosting_disabled.s3_bucket_website_hosting_disabled.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
from prowler.providers.aws.services.s3.s3_bucket_website_hosting_disabled.s3_bucket_website_hosting_disabled import (
|
||||
s3_bucket_website_hosting_disabled,
|
||||
)
|
||||
|
||||
check = s3_bucket_website_hosting_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
@mock_aws
|
||||
def test_bucket_no_website_hosting(self):
|
||||
s3_client_us_east_1 = client("s3", region_name=AWS_REGION_US_EAST_1)
|
||||
bucket_name_us = "bucket_test_us"
|
||||
s3_client_us_east_1.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_website_hosting_disabled.s3_bucket_website_hosting_disabled.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
from prowler.providers.aws.services.s3.s3_bucket_website_hosting_disabled.s3_bucket_website_hosting_disabled import (
|
||||
s3_bucket_website_hosting_disabled,
|
||||
)
|
||||
|
||||
check = s3_bucket_website_hosting_disabled()
|
||||
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 static website hosting enabled."
|
||||
)
|
||||
assert result[0].resource_id == bucket_name_us
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== f"arn:{aws_provider.identity.partition}:s3:::{bucket_name_us}"
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_bucket_with_website_hosting(self):
|
||||
s3_client_us_east_1 = client("s3", region_name=AWS_REGION_US_EAST_1)
|
||||
bucket_name_us = "bucket_test_us"
|
||||
s3_client_us_east_1.create_bucket(Bucket=bucket_name_us)
|
||||
|
||||
s3_client_us_east_1.put_bucket_website(
|
||||
Bucket=bucket_name_us,
|
||||
WebsiteConfiguration={
|
||||
"IndexDocument": {"Suffix": "index.html"},
|
||||
"ErrorDocument": {"Key": "error.html"},
|
||||
},
|
||||
)
|
||||
|
||||
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_website_hosting_disabled.s3_bucket_website_hosting_disabled.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
from prowler.providers.aws.services.s3.s3_bucket_website_hosting_disabled.s3_bucket_website_hosting_disabled import (
|
||||
s3_bucket_website_hosting_disabled,
|
||||
)
|
||||
|
||||
check = s3_bucket_website_hosting_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"S3 Bucket {bucket_name_us} has static website hosting enabled."
|
||||
)
|
||||
assert result[0].resource_id == bucket_name_us
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== f"arn:{aws_provider.identity.partition}:s3:::{bucket_name_us}"
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_multiple_buckets_mixed_website_hosting(self):
|
||||
s3_client_us_east_1 = client("s3", region_name=AWS_REGION_US_EAST_1)
|
||||
|
||||
bucket_fail = "bucket_with_website"
|
||||
bucket_pass = "bucket_without_website"
|
||||
|
||||
s3_client_us_east_1.create_bucket(Bucket=bucket_fail)
|
||||
s3_client_us_east_1.create_bucket(Bucket=bucket_pass)
|
||||
|
||||
s3_client_us_east_1.put_bucket_website(
|
||||
Bucket=bucket_fail,
|
||||
WebsiteConfiguration={
|
||||
"IndexDocument": {"Suffix": "index.html"},
|
||||
},
|
||||
)
|
||||
|
||||
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_website_hosting_disabled.s3_bucket_website_hosting_disabled.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
from prowler.providers.aws.services.s3.s3_bucket_website_hosting_disabled.s3_bucket_website_hosting_disabled import (
|
||||
s3_bucket_website_hosting_disabled,
|
||||
)
|
||||
|
||||
check = s3_bucket_website_hosting_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 2
|
||||
|
||||
by_id = {finding.resource_id: finding for finding in result}
|
||||
|
||||
assert by_id[bucket_fail].status == "FAIL"
|
||||
assert (
|
||||
by_id[bucket_fail].status_extended
|
||||
== f"S3 Bucket {bucket_fail} has static website hosting enabled."
|
||||
)
|
||||
|
||||
assert by_id[bucket_pass].status == "PASS"
|
||||
assert (
|
||||
by_id[bucket_pass].status_extended
|
||||
== f"S3 Bucket {bucket_pass} does not have static website hosting enabled."
|
||||
)
|
||||
Reference in New Issue
Block a user