Skip to content

Commit aaa5abd

Browse files
feat(gcp): add cloudstorage_bucket_soft_delete_enabled check (#9028)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
1 parent 0a2749b commit aaa5abd

File tree

6 files changed

+265
-0
lines changed

6 files changed

+265
-0
lines changed

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
99
- Add OCI mapping to scan and check classes [(#8927)](https://github.com/prowler-cloud/prowler/pull/8927)
1010
- `codepipeline_project_repo_private` check for AWS provider [(#5915)](https://github.com/prowler-cloud/prowler/pull/5915)
1111
- `cloudstorage_bucket_versioning_enabled` check for GCP provider [(#9014)](https://github.com/prowler-cloud/prowler/pull/9014)
12+
- `cloudstorage_bucket_soft_delete_enabled` check for GCP provider [(#9028)](https://github.com/prowler-cloud/prowler/pull/9028)
1213

1314
### Changed
1415
- Update AWS Direct Connect service metadata to new format [(#8855)](https://github.com/prowler-cloud/prowler/pull/8855)

prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_soft_delete_enabled/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"Provider": "gcp",
3+
"CheckID": "cloudstorage_bucket_soft_delete_enabled",
4+
"CheckTitle": "Cloud Storage buckets have Soft Delete enabled",
5+
"CheckType": [],
6+
"ServiceName": "cloudstorage",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "medium",
10+
"ResourceType": "storage.googleapis.com/Bucket",
11+
"Description": "**Google Cloud Storage buckets** are evaluated to ensure that **Soft Delete** is enabled. Soft Delete helps protect data from accidental or malicious deletion by retaining deleted objects for a specified duration, allowing recovery within that retention window.",
12+
"Risk": "Buckets without Soft Delete enabled are at higher risk of irreversible data loss caused by accidental or unauthorized deletions, since deleted objects cannot be recovered once removed.",
13+
"RelatedUrl": "",
14+
"AdditionalURLs": [
15+
"https://cloud.google.com/storage/docs/soft-delete",
16+
"https://cloud.google.com/blog/products/storage-data-transfer/understanding-cloud-storages-new-soft-delete-feature"
17+
],
18+
"Remediation": {
19+
"Code": {
20+
"CLI": "gcloud storage buckets update gs://<BUCKET_NAME> --soft-delete-retention-duration=<SECONDS>",
21+
"NativeIaC": "",
22+
"Other": "1) Open Google Cloud Console → Storage → Buckets → <BUCKET_NAME>\n2) Tab 'Configuration'\n3) Under 'Soft Delete', click 'Enable Soft Delete'\n4) Set the desired retention duration and save changes",
23+
"Terraform": "```hcl\n# Example: enable Soft Delete on a Cloud Storage bucket\nresource \"google_storage_bucket\" \"example\" {\n name = var.bucket_name\n location = var.location\n\n soft_delete_policy {\n retention_duration_seconds = 604800 # 7 days\n }\n}\n```"
24+
},
25+
"Recommendation": {
26+
"Text": "Enable Soft Delete on Cloud Storage buckets to retain deleted objects for a defined period, improving data recoverability and resilience against accidental or malicious deletions.",
27+
"Url": "https://hub.prowler.com/check/cloudstorage_bucket_soft_delete_enabled"
28+
}
29+
},
30+
"Categories": [
31+
"resilience"
32+
],
33+
"DependsOn": [],
34+
"RelatedTo": [],
35+
"Notes": ""
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from prowler.lib.check.models import Check, Check_Report_GCP
2+
from prowler.providers.gcp.services.cloudstorage.cloudstorage_client import (
3+
cloudstorage_client,
4+
)
5+
6+
7+
class cloudstorage_bucket_soft_delete_enabled(Check):
8+
"""
9+
Ensure Cloud Storage buckets have Soft Delete enabled.
10+
11+
Reports PASS if a bucket has Soft Delete enabled (retentionDurationSeconds > 0),
12+
otherwise FAIL.
13+
"""
14+
15+
def execute(self) -> list[Check_Report_GCP]:
16+
findings = []
17+
for bucket in cloudstorage_client.buckets:
18+
report = Check_Report_GCP(metadata=self.metadata(), resource=bucket)
19+
report.status = "FAIL"
20+
report.status_extended = (
21+
f"Bucket {bucket.name} does not have Soft Delete enabled."
22+
)
23+
24+
if bucket.soft_delete_enabled:
25+
report.status = "PASS"
26+
report.status_extended = (
27+
f"Bucket {bucket.name} has Soft Delete enabled."
28+
)
29+
30+
findings.append(report)
31+
return findings

prowler/providers/gcp/services/cloudstorage/cloudstorage_service.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ def _get_buckets(self):
4343
"enabled", False
4444
)
4545

46+
soft_delete_enabled = False
47+
soft_delete_policy = bucket.get("softDeletePolicy")
48+
if isinstance(soft_delete_policy, dict):
49+
retention = soft_delete_policy.get(
50+
"retentionDurationSeconds"
51+
)
52+
if retention and int(retention) > 0:
53+
soft_delete_enabled = True
54+
4655
self.buckets.append(
4756
Bucket(
4857
name=bucket["name"],
@@ -56,6 +65,7 @@ def _get_buckets(self):
5665
project_id=project_id,
5766
lifecycle_rules=lifecycle_rules,
5867
versioning_enabled=versioning_enabled,
68+
soft_delete_enabled=soft_delete_enabled,
5969
)
6070
)
6171

@@ -78,3 +88,4 @@ class Bucket(BaseModel):
7888
retention_policy: Optional[dict] = None
7989
lifecycle_rules: Optional[list[dict]] = None
8090
versioning_enabled: Optional[bool] = False
91+
soft_delete_enabled: Optional[bool] = False
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
from unittest import mock
2+
3+
from tests.providers.gcp.gcp_fixtures import (
4+
GCP_PROJECT_ID,
5+
GCP_US_CENTER1_LOCATION,
6+
set_mocked_gcp_provider,
7+
)
8+
9+
10+
class TestCloudStorageBucketSoftDeleteEnabled:
11+
def test_no_buckets(self):
12+
cloudstorage_client = mock.MagicMock()
13+
cloudstorage_client.buckets = []
14+
15+
with (
16+
mock.patch(
17+
"prowler.providers.common.provider.Provider.get_global_provider",
18+
return_value=set_mocked_gcp_provider(),
19+
),
20+
mock.patch(
21+
"prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_soft_delete_enabled.cloudstorage_bucket_soft_delete_enabled.cloudstorage_client",
22+
new=cloudstorage_client,
23+
),
24+
):
25+
from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_soft_delete_enabled.cloudstorage_bucket_soft_delete_enabled import (
26+
cloudstorage_bucket_soft_delete_enabled,
27+
)
28+
29+
check = cloudstorage_bucket_soft_delete_enabled()
30+
result = check.execute()
31+
assert len(result) == 0
32+
33+
def test_bucket_with_soft_delete_disabled(self):
34+
cloudstorage_client = mock.MagicMock()
35+
36+
with (
37+
mock.patch(
38+
"prowler.providers.common.provider.Provider.get_global_provider",
39+
return_value=set_mocked_gcp_provider(),
40+
),
41+
mock.patch(
42+
"prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_soft_delete_enabled.cloudstorage_bucket_soft_delete_enabled.cloudstorage_client",
43+
new=cloudstorage_client,
44+
),
45+
):
46+
from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_soft_delete_enabled.cloudstorage_bucket_soft_delete_enabled import (
47+
cloudstorage_bucket_soft_delete_enabled,
48+
)
49+
from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import (
50+
Bucket,
51+
)
52+
53+
cloudstorage_client.project_ids = [GCP_PROJECT_ID]
54+
cloudstorage_client.region = GCP_US_CENTER1_LOCATION
55+
56+
cloudstorage_client.buckets = [
57+
Bucket(
58+
name="soft-delete-disabled",
59+
id="soft-delete-disabled",
60+
region=GCP_US_CENTER1_LOCATION,
61+
uniform_bucket_level_access=True,
62+
public=False,
63+
retention_policy=None,
64+
project_id=GCP_PROJECT_ID,
65+
lifecycle_rules=[],
66+
versioning_enabled=False,
67+
soft_delete_enabled=False,
68+
)
69+
]
70+
71+
check = cloudstorage_bucket_soft_delete_enabled()
72+
result = check.execute()
73+
74+
assert len(result) == 1
75+
assert result[0].status == "FAIL"
76+
assert (
77+
result[0].status_extended
78+
== f"Bucket {cloudstorage_client.buckets[0].name} does not have Soft Delete enabled."
79+
)
80+
assert result[0].resource_id == "soft-delete-disabled"
81+
assert result[0].resource_name == "soft-delete-disabled"
82+
assert result[0].location == GCP_US_CENTER1_LOCATION
83+
assert result[0].project_id == GCP_PROJECT_ID
84+
85+
def test_bucket_with_soft_delete_enabled(self):
86+
cloudstorage_client = mock.MagicMock()
87+
88+
with (
89+
mock.patch(
90+
"prowler.providers.common.provider.Provider.get_global_provider",
91+
return_value=set_mocked_gcp_provider(),
92+
),
93+
mock.patch(
94+
"prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_soft_delete_enabled.cloudstorage_bucket_soft_delete_enabled.cloudstorage_client",
95+
new=cloudstorage_client,
96+
),
97+
):
98+
from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_soft_delete_enabled.cloudstorage_bucket_soft_delete_enabled import (
99+
cloudstorage_bucket_soft_delete_enabled,
100+
)
101+
from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import (
102+
Bucket,
103+
)
104+
105+
cloudstorage_client.project_ids = [GCP_PROJECT_ID]
106+
cloudstorage_client.region = GCP_US_CENTER1_LOCATION
107+
108+
cloudstorage_client.buckets = [
109+
Bucket(
110+
name="with-soft-delete",
111+
id="with-soft-delete",
112+
region=GCP_US_CENTER1_LOCATION,
113+
uniform_bucket_level_access=True,
114+
public=False,
115+
retention_policy=None,
116+
project_id=GCP_PROJECT_ID,
117+
lifecycle_rules=[],
118+
versioning_enabled=True,
119+
soft_delete_enabled=True,
120+
)
121+
]
122+
123+
check = cloudstorage_bucket_soft_delete_enabled()
124+
result = check.execute()
125+
126+
assert len(result) == 1
127+
assert result[0].status == "PASS"
128+
assert (
129+
result[0].status_extended
130+
== f"Bucket {cloudstorage_client.buckets[0].name} has Soft Delete enabled."
131+
)
132+
assert result[0].resource_id == "with-soft-delete"
133+
assert result[0].resource_name == "with-soft-delete"
134+
assert result[0].location == GCP_US_CENTER1_LOCATION
135+
assert result[0].project_id == GCP_PROJECT_ID
136+
137+
def test_bucket_without_soft_delete_configured_treated_as_disabled(self):
138+
cloudstorage_client = mock.MagicMock()
139+
140+
with (
141+
mock.patch(
142+
"prowler.providers.common.provider.Provider.get_global_provider",
143+
return_value=set_mocked_gcp_provider(),
144+
),
145+
mock.patch(
146+
"prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_soft_delete_enabled.cloudstorage_bucket_soft_delete_enabled.cloudstorage_client",
147+
new=cloudstorage_client,
148+
),
149+
):
150+
from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_soft_delete_enabled.cloudstorage_bucket_soft_delete_enabled import (
151+
cloudstorage_bucket_soft_delete_enabled,
152+
)
153+
from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import (
154+
Bucket,
155+
)
156+
157+
cloudstorage_client.project_ids = [GCP_PROJECT_ID]
158+
cloudstorage_client.region = GCP_US_CENTER1_LOCATION
159+
160+
cloudstorage_client.buckets = [
161+
Bucket(
162+
name="no-soft-delete-policy",
163+
id="no-soft-delete-policy",
164+
region=GCP_US_CENTER1_LOCATION,
165+
uniform_bucket_level_access=True,
166+
public=False,
167+
retention_policy=None,
168+
project_id=GCP_PROJECT_ID,
169+
lifecycle_rules=[],
170+
versioning_enabled=False,
171+
)
172+
]
173+
174+
check = cloudstorage_bucket_soft_delete_enabled()
175+
result = check.execute()
176+
177+
assert len(result) == 1
178+
assert result[0].status == "FAIL"
179+
assert (
180+
result[0].status_extended
181+
== f"Bucket {cloudstorage_client.buckets[0].name} does not have Soft Delete enabled."
182+
)
183+
assert result[0].resource_id == "no-soft-delete-policy"
184+
assert result[0].resource_name == "no-soft-delete-policy"
185+
assert result[0].location == GCP_US_CENTER1_LOCATION
186+
assert result[0].project_id == GCP_PROJECT_ID

0 commit comments

Comments
 (0)