Skip to content

Commit 48acb3b

Browse files
feat(gcp): add secretmanager_secret_rotation_enabled check (#11026)
Co-authored-by: Lydia Vilchez <lydiavilchezlopez@gmail.com>
1 parent c6c0795 commit 48acb3b

10 files changed

Lines changed: 589 additions & 0 deletions

File tree

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
1414
- `cloudfunction_function_inside_vpc` check for GCP provider, verifying Cloud Functions have a Serverless VPC Access connector for private egress [(#11021)](https://github.com/prowler-cloud/prowler/pull/11021)
1515
- `cloudfunction_function_not_publicly_accessible` check for GCP provider, detecting Cloud Functions with `allUsers` or `allAuthenticatedUsers` IAM invocation bindings [(#11022)](https://github.com/prowler-cloud/prowler/pull/11022)
1616
- `secretmanager_secret_not_publicly_accessible` check for GCP provider, detecting Secret Manager secrets with public IAM bindings [(#11025)](https://github.com/prowler-cloud/prowler/pull/11025)
17+
- `secretmanager_secret_rotation_enabled` check for GCP provider, verifying Secret Manager secrets have automatic rotation configured within 90 days [(#11026)](https://github.com/prowler-cloud/prowler/pull/11026)
1718
- `identity_storage_service_level_admins_scoped` check for OCI provider CIS 3.1 control 1.15, ensuring storage service-level administrators exclude delete permissions [(#11523)](https://github.com/prowler-cloud/prowler/pull/11523)
1819
- `cosmosdb_account_automatic_failover_enabled` check for Azure provider [(#11031)](https://github.com/prowler-cloud/prowler/pull/11031)
1920
- `cosmosdb_account_backup_policy_continuous` check for Azure provider [(#11032)](https://github.com/prowler-cloud/prowler/pull/11032)

prowler/config/config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,9 @@ gcp:
582582
# GCP Storage Sufficient Retention Period
583583
# gcp.cloudstorage_bucket_sufficient_retention_period
584584
storage_min_retention_days: 90
585+
# GCP Secret Manager Rotation Period
586+
# gcp.secretmanager_secret_rotation_enabled
587+
secretmanager_max_rotation_days: 90
585588

586589
# Kubernetes Configuration
587590
kubernetes:

prowler/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"Provider": "gcp",
3+
"CheckID": "secretmanager_secret_rotation_enabled",
4+
"CheckTitle": "Secret Manager secret is rotated every 90 days or less",
5+
"CheckType": [],
6+
"ServiceName": "secretmanager",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "medium",
10+
"ResourceType": "secretmanager.googleapis.com/Secret",
11+
"Description": "Secret Manager secrets have **automatic rotation** configured with a rotation period of `90` days or less and the next scheduled rotation has not been missed.\n\nThe evaluation reviews each secret's `rotation` settings to confirm both the period and the upcoming rotation time are within bounds.",
12+
"Risk": "Without timely rotation, a leaked or compromised secret remains valid indefinitely, eroding **confidentiality** and widening the **blast radius** of any credential exposure. Stale secrets also bypass periodic re-authorization controls expected by frameworks such as PCI-DSS and ISO 27001.",
13+
"RelatedUrl": "",
14+
"AdditionalURLs": [
15+
"https://cloud.google.com/secret-manager/docs/rotation-recommendations",
16+
"https://cloud.google.com/secret-manager/docs/reference/rest/v1/projects.secrets"
17+
],
18+
"Remediation": {
19+
"Code": {
20+
"CLI": "gcloud secrets update <SECRET_NAME> --rotation-period=7776000s --next-rotation-time=<RFC3339_TIMESTAMP>",
21+
"NativeIaC": "",
22+
"Other": "1. In Google Cloud Console, go to Security > Secret Manager\n2. Select the secret and click Edit secret\n3. Under Rotation, enable Automatic rotation with a period of 90 days or less\n4. Configure a Pub/Sub topic to receive rotation notifications\n5. Click Save",
23+
"Terraform": "```hcl\nresource \"google_secret_manager_secret\" \"<example_resource_name>\" {\n secret_id = \"<example_resource_id>\"\n\n replication {\n auto {}\n }\n\n rotation {\n rotation_period = \"7776000s\" # Critical: enables rotation within 90 days\n next_rotation_time = \"<RFC3339_TIMESTAMP>\"\n }\n}\n```"
24+
},
25+
"Recommendation": {
26+
"Text": "Enable **automatic rotation** for every Secret Manager secret with a period of `90` days or less.\n\nWire **Pub/Sub notifications** to trigger rotation logic and apply **defense in depth** by versioning each secret update so consumers can roll back without losing availability.",
27+
"Url": "https://hub.prowler.com/check/secretmanager_secret_rotation_enabled"
28+
}
29+
},
30+
"Categories": [
31+
"secrets"
32+
],
33+
"DependsOn": [],
34+
"RelatedTo": [
35+
"secretmanager_secret_not_publicly_accessible",
36+
"kms_key_rotation_enabled",
37+
"iam_sa_user_managed_key_rotate_90_days"
38+
],
39+
"Notes": "A secret without a rotationPeriod field has no automatic rotation configured and will be marked as FAIL. Rotation periods exceeding 90 days are also marked as FAIL."
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import datetime
2+
3+
from prowler.lib.check.models import Check, Check_Report_GCP
4+
from prowler.providers.gcp.services.secretmanager.secretmanager_client import (
5+
secretmanager_client,
6+
)
7+
8+
9+
class secretmanager_secret_rotation_enabled(Check):
10+
"""
11+
Ensure Secret Manager secrets have automatic rotation configured within the max rotation period.
12+
13+
- PASS: Secret has a rotation period within the maximum (default 90 days) and the next rotation is not overdue.
14+
- FAIL: Secret has no rotation, the period exceeds the maximum, or the next rotation has been missed.
15+
"""
16+
17+
def execute(self) -> list[Check_Report_GCP]:
18+
"""Evaluate every Secret Manager secret's rotation configuration against the maximum rotation period."""
19+
findings = []
20+
21+
max_rotation_days = int(
22+
getattr(secretmanager_client, "audit_config", {}).get(
23+
"secretmanager_max_rotation_days", 90
24+
)
25+
)
26+
27+
for secret in secretmanager_client.secrets:
28+
report = Check_Report_GCP(
29+
metadata=self.metadata(),
30+
resource=secret,
31+
resource_id=secret.name,
32+
)
33+
34+
rotation_seconds = None
35+
if secret.rotation_period:
36+
try:
37+
rotation_seconds = float(secret.rotation_period[:-1])
38+
except (ValueError, IndexError):
39+
rotation_seconds = None
40+
41+
rotation_overdue = False
42+
if rotation_seconds is not None and secret.next_rotation_time:
43+
try:
44+
parsed = secret.next_rotation_time.replace("Z", "+00:00")
45+
next_rotation_time = datetime.datetime.fromisoformat(parsed)
46+
rotation_overdue = next_rotation_time < datetime.datetime.now(
47+
datetime.timezone.utc
48+
)
49+
except (ValueError, AttributeError):
50+
rotation_overdue = True
51+
52+
max_rotation_seconds = max_rotation_days * 86400
53+
rotation_days = (
54+
int(rotation_seconds // 86400) if rotation_seconds is not None else None
55+
)
56+
57+
if rotation_seconds is None:
58+
report.status = "FAIL"
59+
report.status_extended = (
60+
f"Secret {secret.name} does not have automatic rotation enabled."
61+
)
62+
elif rotation_seconds > max_rotation_seconds:
63+
report.status = "FAIL"
64+
report.status_extended = (
65+
f"Secret {secret.name} has rotation enabled but the period "
66+
f"({rotation_days} days) exceeds the {max_rotation_days}-day maximum."
67+
)
68+
elif rotation_overdue:
69+
report.status = "FAIL"
70+
report.status_extended = (
71+
f"Secret {secret.name} has rotation configured "
72+
f"({rotation_days} days) but the scheduled rotation is overdue."
73+
)
74+
else:
75+
report.status = "PASS"
76+
report.status_extended = (
77+
f"Secret {secret.name} has automatic rotation enabled "
78+
f"with a period of {rotation_days} days."
79+
)
80+
81+
findings.append(report)
82+
83+
return findings

prowler/providers/gcp/services/secretmanager/secretmanager_service.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Optional
2+
13
from pydantic.v1 import BaseModel
24

35
from prowler.lib.logger import logger
@@ -33,11 +35,14 @@ def _get_secrets(self) -> None:
3335
while request is not None:
3436
response = request.execute(num_retries=DEFAULT_RETRY_ATTEMPTS)
3537
for secret in response.get("secrets", []):
38+
rotation = secret.get("rotation") or {}
3639
self.secrets.append(
3740
Secret(
3841
id=secret["name"],
3942
name=secret["name"].split("/")[-1],
4043
project_id=project_id,
44+
rotation_period=rotation.get("rotationPeriod"),
45+
next_rotation_time=rotation.get("nextRotationTime"),
4146
)
4247
)
4348
request = (
@@ -84,4 +89,6 @@ class Secret(BaseModel):
8489
name: str
8590
project_id: str
8691
location: str = "global"
92+
rotation_period: Optional[str] = None
93+
next_rotation_time: Optional[str] = None
8794
publicly_accessible: bool = False

tests/providers/gcp/gcp_provider_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def test_gcp_provider(self):
9292
"shodan_api_key": None,
9393
"max_unused_account_days": 180,
9494
"storage_min_retention_days": 90,
95+
"secretmanager_max_rotation_days": 90,
9596
"mig_min_zones": 2,
9697
"max_snapshot_age_days": 90,
9798
}

tests/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)