From 48acb3bd2eb84efc5bf625475ca7db8691cd3f19 Mon Sep 17 00:00:00 2001 From: s1ns3nz0 Date: Tue, 23 Jun 2026 18:30:15 +0900 Subject: [PATCH] feat(gcp): add secretmanager_secret_rotation_enabled check (#11026) Co-authored-by: Lydia Vilchez --- prowler/CHANGELOG.md | 1 + prowler/config/config.yaml | 3 + .../__init__.py | 0 ...ager_secret_rotation_enabled.metadata.json | 40 ++ .../secretmanager_secret_rotation_enabled.py | 83 ++++ .../secretmanager/secretmanager_service.py | 7 + tests/providers/gcp/gcp_provider_test.py | 1 + .../__init__.py | 0 ...retmanager_secret_rotation_enabled_test.py | 385 ++++++++++++++++++ .../secretmanager_service_test.py | 69 ++++ 10 files changed, 589 insertions(+) create mode 100644 prowler/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/__init__.py create mode 100644 prowler/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/secretmanager_secret_rotation_enabled.metadata.json create mode 100644 prowler/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/secretmanager_secret_rotation_enabled.py create mode 100644 tests/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/__init__.py create mode 100644 tests/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/secretmanager_secret_rotation_enabled_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 3cab5e423c..baac7b8625 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `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) - `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) - `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) +- `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) - `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) - `cosmosdb_account_automatic_failover_enabled` check for Azure provider [(#11031)](https://github.com/prowler-cloud/prowler/pull/11031) - `cosmosdb_account_backup_policy_continuous` check for Azure provider [(#11032)](https://github.com/prowler-cloud/prowler/pull/11032) diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 59f69ec821..9d41a0e592 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -582,6 +582,9 @@ gcp: # GCP Storage Sufficient Retention Period # gcp.cloudstorage_bucket_sufficient_retention_period storage_min_retention_days: 90 + # GCP Secret Manager Rotation Period + # gcp.secretmanager_secret_rotation_enabled + secretmanager_max_rotation_days: 90 # Kubernetes Configuration kubernetes: diff --git a/prowler/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/__init__.py b/prowler/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/secretmanager_secret_rotation_enabled.metadata.json b/prowler/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/secretmanager_secret_rotation_enabled.metadata.json new file mode 100644 index 0000000000..09dd158b49 --- /dev/null +++ b/prowler/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/secretmanager_secret_rotation_enabled.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "gcp", + "CheckID": "secretmanager_secret_rotation_enabled", + "CheckTitle": "Secret Manager secret is rotated every 90 days or less", + "CheckType": [], + "ServiceName": "secretmanager", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "secretmanager.googleapis.com/Secret", + "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.", + "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.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/secret-manager/docs/rotation-recommendations", + "https://cloud.google.com/secret-manager/docs/reference/rest/v1/projects.secrets" + ], + "Remediation": { + "Code": { + "CLI": "gcloud secrets update --rotation-period=7776000s --next-rotation-time=", + "NativeIaC": "", + "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", + "Terraform": "```hcl\nresource \"google_secret_manager_secret\" \"\" {\n secret_id = \"\"\n\n replication {\n auto {}\n }\n\n rotation {\n rotation_period = \"7776000s\" # Critical: enables rotation within 90 days\n next_rotation_time = \"\"\n }\n}\n```" + }, + "Recommendation": { + "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.", + "Url": "https://hub.prowler.com/check/secretmanager_secret_rotation_enabled" + } + }, + "Categories": [ + "secrets" + ], + "DependsOn": [], + "RelatedTo": [ + "secretmanager_secret_not_publicly_accessible", + "kms_key_rotation_enabled", + "iam_sa_user_managed_key_rotate_90_days" + ], + "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." +} diff --git a/prowler/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/secretmanager_secret_rotation_enabled.py b/prowler/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/secretmanager_secret_rotation_enabled.py new file mode 100644 index 0000000000..84d53e7f3a --- /dev/null +++ b/prowler/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/secretmanager_secret_rotation_enabled.py @@ -0,0 +1,83 @@ +import datetime + +from prowler.lib.check.models import Check, Check_Report_GCP +from prowler.providers.gcp.services.secretmanager.secretmanager_client import ( + secretmanager_client, +) + + +class secretmanager_secret_rotation_enabled(Check): + """ + Ensure Secret Manager secrets have automatic rotation configured within the max rotation period. + + - PASS: Secret has a rotation period within the maximum (default 90 days) and the next rotation is not overdue. + - FAIL: Secret has no rotation, the period exceeds the maximum, or the next rotation has been missed. + """ + + def execute(self) -> list[Check_Report_GCP]: + """Evaluate every Secret Manager secret's rotation configuration against the maximum rotation period.""" + findings = [] + + max_rotation_days = int( + getattr(secretmanager_client, "audit_config", {}).get( + "secretmanager_max_rotation_days", 90 + ) + ) + + for secret in secretmanager_client.secrets: + report = Check_Report_GCP( + metadata=self.metadata(), + resource=secret, + resource_id=secret.name, + ) + + rotation_seconds = None + if secret.rotation_period: + try: + rotation_seconds = float(secret.rotation_period[:-1]) + except (ValueError, IndexError): + rotation_seconds = None + + rotation_overdue = False + if rotation_seconds is not None and secret.next_rotation_time: + try: + parsed = secret.next_rotation_time.replace("Z", "+00:00") + next_rotation_time = datetime.datetime.fromisoformat(parsed) + rotation_overdue = next_rotation_time < datetime.datetime.now( + datetime.timezone.utc + ) + except (ValueError, AttributeError): + rotation_overdue = True + + max_rotation_seconds = max_rotation_days * 86400 + rotation_days = ( + int(rotation_seconds // 86400) if rotation_seconds is not None else None + ) + + if rotation_seconds is None: + report.status = "FAIL" + report.status_extended = ( + f"Secret {secret.name} does not have automatic rotation enabled." + ) + elif rotation_seconds > max_rotation_seconds: + report.status = "FAIL" + report.status_extended = ( + f"Secret {secret.name} has rotation enabled but the period " + f"({rotation_days} days) exceeds the {max_rotation_days}-day maximum." + ) + elif rotation_overdue: + report.status = "FAIL" + report.status_extended = ( + f"Secret {secret.name} has rotation configured " + f"({rotation_days} days) but the scheduled rotation is overdue." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"Secret {secret.name} has automatic rotation enabled " + f"with a period of {rotation_days} days." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/gcp/services/secretmanager/secretmanager_service.py b/prowler/providers/gcp/services/secretmanager/secretmanager_service.py index 6a193d8ece..ca01663083 100644 --- a/prowler/providers/gcp/services/secretmanager/secretmanager_service.py +++ b/prowler/providers/gcp/services/secretmanager/secretmanager_service.py @@ -1,3 +1,5 @@ +from typing import Optional + from pydantic.v1 import BaseModel from prowler.lib.logger import logger @@ -33,11 +35,14 @@ class SecretManager(GCPService): while request is not None: response = request.execute(num_retries=DEFAULT_RETRY_ATTEMPTS) for secret in response.get("secrets", []): + rotation = secret.get("rotation") or {} self.secrets.append( Secret( id=secret["name"], name=secret["name"].split("/")[-1], project_id=project_id, + rotation_period=rotation.get("rotationPeriod"), + next_rotation_time=rotation.get("nextRotationTime"), ) ) request = ( @@ -84,4 +89,6 @@ class Secret(BaseModel): name: str project_id: str location: str = "global" + rotation_period: Optional[str] = None + next_rotation_time: Optional[str] = None publicly_accessible: bool = False diff --git a/tests/providers/gcp/gcp_provider_test.py b/tests/providers/gcp/gcp_provider_test.py index 7b30b828da..2e80ac7899 100644 --- a/tests/providers/gcp/gcp_provider_test.py +++ b/tests/providers/gcp/gcp_provider_test.py @@ -92,6 +92,7 @@ class TestGCPProvider: "shodan_api_key": None, "max_unused_account_days": 180, "storage_min_retention_days": 90, + "secretmanager_max_rotation_days": 90, "mig_min_zones": 2, "max_snapshot_age_days": 90, } diff --git a/tests/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/__init__.py b/tests/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/secretmanager_secret_rotation_enabled_test.py b/tests/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/secretmanager_secret_rotation_enabled_test.py new file mode 100644 index 0000000000..497dd7428b --- /dev/null +++ b/tests/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/secretmanager_secret_rotation_enabled_test.py @@ -0,0 +1,385 @@ +from unittest import mock + +from tests.providers.gcp.gcp_fixtures import ( + GCP_PROJECT_ID, + set_mocked_gcp_provider, +) + +_CHECK_PATH = ( + "prowler.providers.gcp.services.secretmanager." + "secretmanager_secret_rotation_enabled." + "secretmanager_secret_rotation_enabled" +) +_CLIENT_PATH = f"{_CHECK_PATH}.secretmanager_client" + + +def _secret_id(name: str) -> str: + return f"projects/{GCP_PROJECT_ID}/secrets/{name}" + + +class Test_secretmanager_secret_rotation_enabled: + def test_no_secrets(self): + secretmanager_client = mock.MagicMock() + secretmanager_client.audit_config = {"secretmanager_max_rotation_days": 90} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + _CLIENT_PATH, + new=secretmanager_client, + ), + ): + from prowler.providers.gcp.services.secretmanager.secretmanager_secret_rotation_enabled.secretmanager_secret_rotation_enabled import ( + secretmanager_secret_rotation_enabled, + ) + + secretmanager_client.secrets = [] + + check = secretmanager_secret_rotation_enabled() + result = check.execute() + assert len(result) == 0 + + def test_rotation_within_90_days_pass(self): + secretmanager_client = mock.MagicMock() + secretmanager_client.audit_config = {"secretmanager_max_rotation_days": 90} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + _CLIENT_PATH, + new=secretmanager_client, + ), + ): + from prowler.providers.gcp.services.secretmanager.secretmanager_secret_rotation_enabled.secretmanager_secret_rotation_enabled import ( + secretmanager_secret_rotation_enabled, + ) + from prowler.providers.gcp.services.secretmanager.secretmanager_service import ( + Secret, + ) + + secretmanager_client.secrets = [ + Secret( + id=_secret_id("secret-rotated"), + name="secret-rotated", + project_id=GCP_PROJECT_ID, + rotation_period="7776000s", + ) + ] + + check = secretmanager_secret_rotation_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Secret secret-rotated has automatic rotation enabled with a period of 90 days." + ) + assert result[0].resource_id == "secret-rotated" + assert result[0].resource_name == "secret-rotated" + assert result[0].location == "global" + assert result[0].project_id == GCP_PROJECT_ID + + def test_rotation_period_exceeds_max_fail(self): + secretmanager_client = mock.MagicMock() + secretmanager_client.audit_config = {"secretmanager_max_rotation_days": 90} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + _CLIENT_PATH, + new=secretmanager_client, + ), + ): + from prowler.providers.gcp.services.secretmanager.secretmanager_secret_rotation_enabled.secretmanager_secret_rotation_enabled import ( + secretmanager_secret_rotation_enabled, + ) + from prowler.providers.gcp.services.secretmanager.secretmanager_service import ( + Secret, + ) + + secretmanager_client.secrets = [ + Secret( + id=_secret_id("secret-stale"), + name="secret-stale", + project_id=GCP_PROJECT_ID, + rotation_period="9504000s", + ) + ] + + check = secretmanager_secret_rotation_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "exceeds the 90-day maximum" in result[0].status_extended + assert result[0].resource_id == "secret-stale" + + def test_rotation_period_one_second_over_max_fail(self): + """90 days + 1 second must fail — comparison is on seconds, not floored days.""" + secretmanager_client = mock.MagicMock() + secretmanager_client.audit_config = {"secretmanager_max_rotation_days": 90} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + _CLIENT_PATH, + new=secretmanager_client, + ), + ): + from prowler.providers.gcp.services.secretmanager.secretmanager_secret_rotation_enabled.secretmanager_secret_rotation_enabled import ( + secretmanager_secret_rotation_enabled, + ) + from prowler.providers.gcp.services.secretmanager.secretmanager_service import ( + Secret, + ) + + # 90 days = 7_776_000 seconds. Add 1 second. + secretmanager_client.secrets = [ + Secret( + id=_secret_id("secret-90d-plus-1s"), + name="secret-90d-plus-1s", + project_id=GCP_PROJECT_ID, + rotation_period="7776001s", + ) + ] + + check = secretmanager_secret_rotation_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "exceeds the 90-day maximum" in result[0].status_extended + + def test_no_rotation_fail(self): + secretmanager_client = mock.MagicMock() + secretmanager_client.audit_config = {"secretmanager_max_rotation_days": 90} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + _CLIENT_PATH, + new=secretmanager_client, + ), + ): + from prowler.providers.gcp.services.secretmanager.secretmanager_secret_rotation_enabled.secretmanager_secret_rotation_enabled import ( + secretmanager_secret_rotation_enabled, + ) + from prowler.providers.gcp.services.secretmanager.secretmanager_service import ( + Secret, + ) + + secretmanager_client.secrets = [ + Secret( + id=_secret_id("secret-no-rotation"), + name="secret-no-rotation", + project_id=GCP_PROJECT_ID, + rotation_period=None, + ) + ] + + check = secretmanager_secret_rotation_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Secret secret-no-rotation does not have automatic rotation enabled." + ) + + def test_fractional_seconds_period_pass(self): + secretmanager_client = mock.MagicMock() + secretmanager_client.audit_config = {"secretmanager_max_rotation_days": 90} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + _CLIENT_PATH, + new=secretmanager_client, + ), + ): + from prowler.providers.gcp.services.secretmanager.secretmanager_secret_rotation_enabled.secretmanager_secret_rotation_enabled import ( + secretmanager_secret_rotation_enabled, + ) + from prowler.providers.gcp.services.secretmanager.secretmanager_service import ( + Secret, + ) + + secretmanager_client.secrets = [ + Secret( + id=_secret_id("secret-fractional"), + name="secret-fractional", + project_id=GCP_PROJECT_ID, + rotation_period="2592000.500000000s", + ) + ] + + check = secretmanager_secret_rotation_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + + def test_sub_day_rotation_period_pass(self): + secretmanager_client = mock.MagicMock() + secretmanager_client.audit_config = {"secretmanager_max_rotation_days": 90} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + _CLIENT_PATH, + new=secretmanager_client, + ), + ): + from prowler.providers.gcp.services.secretmanager.secretmanager_secret_rotation_enabled.secretmanager_secret_rotation_enabled import ( + secretmanager_secret_rotation_enabled, + ) + from prowler.providers.gcp.services.secretmanager.secretmanager_service import ( + Secret, + ) + + secretmanager_client.secrets = [ + Secret( + id=_secret_id("secret-sub-day"), + name="secret-sub-day", + project_id=GCP_PROJECT_ID, + rotation_period="3600s", + ) + ] + + check = secretmanager_secret_rotation_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + + def test_overdue_next_rotation_fail(self): + secretmanager_client = mock.MagicMock() + secretmanager_client.audit_config = {"secretmanager_max_rotation_days": 90} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + _CLIENT_PATH, + new=secretmanager_client, + ), + ): + from prowler.providers.gcp.services.secretmanager.secretmanager_secret_rotation_enabled.secretmanager_secret_rotation_enabled import ( + secretmanager_secret_rotation_enabled, + ) + from prowler.providers.gcp.services.secretmanager.secretmanager_service import ( + Secret, + ) + + secretmanager_client.secrets = [ + Secret( + id=_secret_id("secret-overdue"), + name="secret-overdue", + project_id=GCP_PROJECT_ID, + rotation_period="7776000s", + next_rotation_time="2020-01-01T00:00:00Z", + ) + ] + + check = secretmanager_secret_rotation_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "overdue" in result[0].status_extended + + def test_invalid_rotation_period_format_fail(self): + """Unparseable rotation_period falls back to None → FAIL with no-rotation message.""" + secretmanager_client = mock.MagicMock() + secretmanager_client.audit_config = {"secretmanager_max_rotation_days": 90} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + _CLIENT_PATH, + new=secretmanager_client, + ), + ): + from prowler.providers.gcp.services.secretmanager.secretmanager_secret_rotation_enabled.secretmanager_secret_rotation_enabled import ( + secretmanager_secret_rotation_enabled, + ) + from prowler.providers.gcp.services.secretmanager.secretmanager_service import ( + Secret, + ) + + secretmanager_client.secrets = [ + Secret( + id=_secret_id("secret-bad-period"), + name="secret-bad-period", + project_id=GCP_PROJECT_ID, + rotation_period="not-a-duration", + ) + ] + + check = secretmanager_secret_rotation_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Secret secret-bad-period does not have automatic rotation enabled." + ) + + def test_invalid_next_rotation_time_fail_closed(self): + """Unparseable next_rotation_time fails closed → FAIL as overdue.""" + secretmanager_client = mock.MagicMock() + secretmanager_client.audit_config = {"secretmanager_max_rotation_days": 90} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + _CLIENT_PATH, + new=secretmanager_client, + ), + ): + from prowler.providers.gcp.services.secretmanager.secretmanager_secret_rotation_enabled.secretmanager_secret_rotation_enabled import ( + secretmanager_secret_rotation_enabled, + ) + from prowler.providers.gcp.services.secretmanager.secretmanager_service import ( + Secret, + ) + + secretmanager_client.secrets = [ + Secret( + id=_secret_id("secret-bad-timestamp"), + name="secret-bad-timestamp", + project_id=GCP_PROJECT_ID, + rotation_period="7776000s", + next_rotation_time="not-a-timestamp", + ) + ] + + check = secretmanager_secret_rotation_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "overdue" in result[0].status_extended diff --git a/tests/providers/gcp/services/secretmanager/secretmanager_service_test.py b/tests/providers/gcp/services/secretmanager/secretmanager_service_test.py index 213134c245..f393b10a59 100644 --- a/tests/providers/gcp/services/secretmanager/secretmanager_service_test.py +++ b/tests/providers/gcp/services/secretmanager/secretmanager_service_test.py @@ -60,8 +60,77 @@ class TestSecretManagerService: assert secret.id == f"projects/{GCP_PROJECT_ID}/secrets/my-secret" assert secret.project_id == GCP_PROJECT_ID assert secret.location == "global" + assert secret.rotation_period is None + assert secret.next_rotation_time is None assert secret.publicly_accessible is False + def test_get_secrets_with_rotation(self): + def mock_api_client(*args, **kwargs): + return _make_secretmanager_client( + secrets_list=[ + { + "name": f"projects/{GCP_PROJECT_ID}/secrets/secret-with-rotation", + "rotation": { + "rotationPeriod": "7776000s", + "nextRotationTime": "2026-09-01T00:00:00Z", + }, + } + ] + ) + + with ( + patch( + "prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__", + new=mock_is_api_active, + ), + patch( + "prowler.providers.gcp.lib.service.service.GCPService.__generate_client__", + new=mock_api_client, + ), + ): + sm_client = SecretManager( + set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID]) + ) + + assert len(sm_client.secrets) == 1 + secret = sm_client.secrets[0] + assert secret.name == "secret-with-rotation" + assert secret.rotation_period == "7776000s" + assert secret.next_rotation_time == "2026-09-01T00:00:00Z" + + def test_get_secrets_with_null_rotation(self): + """API returning explicit `rotation: null` must not break enumeration.""" + + def mock_api_client(*args, **kwargs): + return _make_secretmanager_client( + secrets_list=[ + { + "name": f"projects/{GCP_PROJECT_ID}/secrets/null-rotation", + "rotation": None, + } + ] + ) + + with ( + patch( + "prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__", + new=mock_is_api_active, + ), + patch( + "prowler.providers.gcp.lib.service.service.GCPService.__generate_client__", + new=mock_api_client, + ), + ): + sm_client = SecretManager( + set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID]) + ) + + assert len(sm_client.secrets) == 1 + secret = sm_client.secrets[0] + assert secret.name == "null-rotation" + assert secret.rotation_period is None + assert secret.next_rotation_time is None + def test_get_secrets_iam_policy_all_users(self): def mock_api_client(*args, **kwargs): return _make_secretmanager_client(