diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/__init__.py b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/cloudstorage_bucket_lifecycle_management_enabled.metadata.json b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/cloudstorage_bucket_lifecycle_management_enabled.metadata.json new file mode 100644 index 0000000000..0180768494 --- /dev/null +++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/cloudstorage_bucket_lifecycle_management_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "gcp", + "CheckID": "cloudstorage_bucket_lifecycle_management_enabled", + "CheckTitle": "Ensure Cloud Storage lifecycle management is enabled on buckets", + "CheckType": [], + "ServiceName": "cloudstorage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Bucket", + "Description": "Ensure that Google Cloud Storage buckets have lifecycle management enabled with at least one valid rule (supported action and non-empty condition) to automatically transition or delete objects, optimizing storage costs and supporting retention policies.", + "Risk": "Buckets without lifecycle rules can accumulate stale data, increase storage costs, and fail to meet data retention and internal compliance requirements.", + "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/enable-lifecycle-management.html", + "Remediation": { + "Code": { + "CLI": "gcloud storage buckets update gs:// --lifecycle-file=", + "NativeIaC": "", + "Other": "", + "Terraform": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket#lifecycle_rule" + }, + "Recommendation": { + "Text": "Configure lifecycle rules to automatically delete stale objects or transition them to colder storage classes according to your organization's retention and cost-optimization policy.", + "Url": "https://cloud.google.com/storage/docs/lifecycle" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/cloudstorage_bucket_lifecycle_management_enabled.py b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/cloudstorage_bucket_lifecycle_management_enabled.py new file mode 100644 index 0000000000..3a936a4f29 --- /dev/null +++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/cloudstorage_bucket_lifecycle_management_enabled.py @@ -0,0 +1,51 @@ +from prowler.lib.check.models import Check, Check_Report_GCP +from prowler.providers.gcp.services.cloudstorage.cloudstorage_client import cloudstorage_client + +class cloudstorage_bucket_lifecycle_management_enabled(Check): + + """Ensure Cloud Storage buckets have lifecycle management enabled with at least one valid rule. + + Reports PASS if a bucket has at least one valid lifecycle rule + (with a supported action and condition), otherwise FAIL. + + """ + + def execute(self) -> list[Check_Report_GCP]: + + """Run the lifecycle management check for each Cloud Storage bucket. + + Returns: + list[Check_Report_GCP]: Results for all evaluated buckets. + """ + + findings = [] + for bucket in cloudstorage_client.buckets: + report = Check_Report_GCP(metadata=self.metadata(), resource=bucket) + report.status = "FAIL" + report.status_extended = ( + f"Bucket {bucket.name} does not have lifecycle management enabled." + ) + + rules = bucket.lifecycle_rules + + if rules: + valid_rules = [] + for rule in rules: + action_type = rule.get("action", {}).get("type") + condition = rule.get("condition") + if action_type in ("Delete", "SetStorageClass", "AbortIncompleteMultipartUpload") and condition: + valid_rules.append(rule) + + if valid_rules: + report.status = "PASS" + report.status_extended = ( + f"Bucket {bucket.name} has lifecycle management enabled with {len(valid_rules)} valid rule(s)." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Bucket {bucket.name} has lifecycle rules configured but none are valid." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_service.py b/prowler/providers/gcp/services/cloudstorage/cloudstorage_service.py index 294455077c..f7ec966ece 100644 --- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_service.py +++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_service.py @@ -31,6 +31,14 @@ class CloudStorage(GCPService): bucket_iam ) or "allUsers" in str(bucket_iam): public = True + + lifecycle_rules = None + lifecycle = bucket.get("lifecycle") + if isinstance(lifecycle, dict): + rules = lifecycle.get("rule") + if isinstance(rules, list): + lifecycle_rules = rules + self.buckets.append( Bucket( name=bucket["name"], @@ -42,6 +50,7 @@ class CloudStorage(GCPService): public=public, retention_policy=bucket.get("retentionPolicy"), project_id=project_id, + lifecycle_rules=lifecycle_rules, ) ) @@ -62,3 +71,4 @@ class Bucket(BaseModel): public: bool project_id: str retention_policy: Optional[dict] = None + lifecycle_rules: Optional[list[dict]] = None diff --git a/tests/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/cloudstorage_bucket_lifecycle_management_enabled_test.py b/tests/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/cloudstorage_bucket_lifecycle_management_enabled_test.py new file mode 100644 index 0000000000..096c2950b9 --- /dev/null +++ b/tests/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/cloudstorage_bucket_lifecycle_management_enabled_test.py @@ -0,0 +1,173 @@ +from unittest import mock + +from tests.providers.gcp.gcp_fixtures import ( + GCP_PROJECT_ID, + GCP_US_CENTER1_LOCATION, + set_mocked_gcp_provider, +) + + +class TestCloudStorageBucketLifecycleManagementEnabled: + def test_bucket_without_lifecycle_rules(self): + cloudstorage_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_lifecycle_management_enabled.cloudstorage_bucket_lifecycle_management_enabled.cloudstorage_client", + new=cloudstorage_client, + ), + ): + from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_lifecycle_management_enabled.cloudstorage_bucket_lifecycle_management_enabled import ( + cloudstorage_bucket_lifecycle_management_enabled, + ) + from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import ( + Bucket, + ) + + cloudstorage_client.project_ids = [GCP_PROJECT_ID] + cloudstorage_client.region = GCP_US_CENTER1_LOCATION + + # Bucket sin lifecycle rules + cloudstorage_client.buckets = [ + Bucket( + name="no-lifecycle", + id="no-lifecycle", + region=GCP_US_CENTER1_LOCATION, + uniform_bucket_level_access=True, + public=False, + retention_policy=None, + project_id=GCP_PROJECT_ID, + lifecycle_rules=[], # sin reglas + ) + ] + + check = cloudstorage_bucket_lifecycle_management_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Bucket {cloudstorage_client.buckets[0].name} does not have lifecycle management enabled." + ) + assert result[0].resource_id == "no-lifecycle" + assert result[0].resource_name == "no-lifecycle" + assert result[0].location == GCP_US_CENTER1_LOCATION + assert result[0].project_id == GCP_PROJECT_ID + + def test_bucket_with_minimal_delete_rule(self): + cloudstorage_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_lifecycle_management_enabled.cloudstorage_bucket_lifecycle_management_enabled.cloudstorage_client", + new=cloudstorage_client, + ), + ): + from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_lifecycle_management_enabled.cloudstorage_bucket_lifecycle_management_enabled import ( + cloudstorage_bucket_lifecycle_management_enabled, + ) + from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import ( + Bucket, + ) + + cloudstorage_client.project_ids = [GCP_PROJECT_ID] + cloudstorage_client.region = GCP_US_CENTER1_LOCATION + + # Regla mínima: Delete after 30 days + cloudstorage_client.buckets = [ + Bucket( + name="delete-rule", + id="delete-rule", + region=GCP_US_CENTER1_LOCATION, + uniform_bucket_level_access=True, + public=False, + retention_policy=None, + project_id=GCP_PROJECT_ID, + lifecycle_rules=[ + {"action": {"type": "Delete"}, "condition": {"age": 30}} + ], + ) + ] + + check = cloudstorage_bucket_lifecycle_management_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Bucket {cloudstorage_client.buckets[0].name} has lifecycle management enabled with 1 valid rule(s)." + ) + assert result[0].resource_id == "delete-rule" + assert result[0].resource_name == "delete-rule" + assert result[0].location == GCP_US_CENTER1_LOCATION + assert result[0].project_id == GCP_PROJECT_ID + + def test_bucket_with_transition_and_delete_rules(self): + cloudstorage_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_lifecycle_management_enabled.cloudstorage_bucket_lifecycle_management_enabled.cloudstorage_client", + new=cloudstorage_client, + ), + ): + from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_lifecycle_management_enabled.cloudstorage_bucket_lifecycle_management_enabled import ( + cloudstorage_bucket_lifecycle_management_enabled, + ) + from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import ( + Bucket, + ) + + cloudstorage_client.project_ids = [GCP_PROJECT_ID] + cloudstorage_client.region = GCP_US_CENTER1_LOCATION + + # Dos reglas: transición (SetStorageClass) + borrado (Delete) + cloudstorage_client.buckets = [ + Bucket( + name="transition-delete", + id="transition-delete", + region=GCP_US_CENTER1_LOCATION, + uniform_bucket_level_access=True, + public=False, + retention_policy=None, + project_id=GCP_PROJECT_ID, + lifecycle_rules=[ + { + "action": { + "type": "SetStorageClass", + "storageClass": "NEARLINE", + }, + "condition": {"matchesStorageClass": ["STANDARD"]}, + }, + {"action": {"type": "Delete"}, "condition": {"age": 365}}, + ], + ) + ] + + check = cloudstorage_bucket_lifecycle_management_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Bucket {cloudstorage_client.buckets[0].name} has lifecycle management enabled with 2 valid rule(s)." + ) + assert result[0].resource_id == "transition-delete" + assert result[0].resource_name == "transition-delete" + assert result[0].location == GCP_US_CENTER1_LOCATION + assert result[0].project_id == GCP_PROJECT_ID