mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(gcp): add secretmanager_secret_rotation_enabled check (#11026)
Co-authored-by: Lydia Vilchez <lydiavilchezlopez@gmail.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
+40
@@ -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 <SECRET_NAME> --rotation-period=7776000s --next-rotation-time=<RFC3339_TIMESTAMP>",
|
||||
"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\" \"<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```"
|
||||
},
|
||||
"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."
|
||||
}
|
||||
+83
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
+385
@@ -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
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user