feat(gcp): add check for dormant (unused) SA keys (#7348)

Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
Co-authored-by: Sergio Garcia <sergargar1@gmail.com>
This commit is contained in:
Bogdan A
2025-04-09 16:10:47 +02:00
committed by Pepe Fagoaga
co-authored by MrCloudSec Sergio Garcia
parent b3d7bb4e8d
commit 0b2dac83bd
9 changed files with 459 additions and 1 deletions
+39
View File
@@ -351,6 +351,45 @@ def mock_api_projects_calls(client: MagicMock):
},
]
}
client.projects().timeSeries().list().execute.return_value = {
"timeSeries": [
{
"metric": {
"labels": {
"key_id": "key1",
"type": "iam.googleapis.com/service_account/key/authn_events_count",
},
"resource": {
"type": "iam_service_account",
"labels": {
"project_id": "{GCP_PROJECT_ID}",
"unique_id": "111222233334444",
},
},
"metricKind": "DELTA",
"valueType": "INT64",
}
},
{
"metric": {
"labels": {
"key_id": "key2",
"type": "iam.googleapis.com/service_account/key/authn_events_count",
},
"resource": {
"type": "iam_service_account",
"labels": {
"project_id": "{GCP_PROJECT_ID}",
"unique_id": "111222233334444",
},
},
"metricKind": "DELTA",
"valueType": "INT64",
}
},
]
}
client.projects().alertPolicies().list_next.return_value = None
# Used by IAM
client.projects().serviceAccounts().list().execute.return_value = {
@@ -0,0 +1,286 @@
from datetime import datetime
from unittest import mock
from tests.providers.gcp.gcp_fixtures import (
GCP_PROJECT_ID,
GCP_US_CENTER1_LOCATION,
set_mocked_gcp_provider,
)
class Test_iam_sa_user_managed_key_unused:
def test_iam_no_sa(self):
iam_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.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused.iam_client",
new=iam_client,
),
):
from prowler.providers.gcp.services.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused import (
iam_sa_user_managed_key_unused,
)
iam_client.project_ids = [GCP_PROJECT_ID]
iam_client.region = GCP_US_CENTER1_LOCATION
iam_client.service_accounts = []
check = iam_sa_user_managed_key_unused()
result = check.execute()
assert len(result) == 0
def test_iam_sa_dormant_no_keys(self):
iam_client = mock.MagicMock()
monitoring_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.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused.iam_client",
new=iam_client,
),
mock.patch(
"prowler.providers.gcp.services.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused.monitoring_client",
new=monitoring_client,
),
):
from prowler.providers.gcp.services.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused import (
iam_sa_user_managed_key_unused,
)
from prowler.providers.gcp.services.iam.iam_service import ServiceAccount
iam_client.project_ids = [GCP_PROJECT_ID]
iam_client.region = GCP_US_CENTER1_LOCATION
iam_client.service_accounts = [
ServiceAccount(
name="projects/my-project/serviceAccounts/my-service-account@my-project.iam.gserviceaccount.com",
email="my-service-account@my-project.iam.gserviceaccount.com",
display_name="My service account",
keys=[],
project_id=GCP_PROJECT_ID,
)
]
monitoring_client.sa_keys_metrics = set(
["90c48f61c65cd56224a12ab18e6ee9ca9c3aee7c"]
)
check = iam_sa_user_managed_key_unused()
result = check.execute()
assert len(result) == 0
def test_iam_sa_dormant_system_managed_keys(self):
iam_client = mock.MagicMock()
monitoring_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.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused.iam_client",
new=iam_client,
),
mock.patch(
"prowler.providers.gcp.services.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused.monitoring_client",
new=monitoring_client,
),
):
from prowler.providers.gcp.services.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused import (
iam_sa_user_managed_key_unused,
)
from prowler.providers.gcp.services.iam.iam_service import (
Key,
ServiceAccount,
)
iam_client.project_ids = [GCP_PROJECT_ID]
iam_client.region = GCP_US_CENTER1_LOCATION
iam_client.service_accounts = [
ServiceAccount(
name="projects/my-project/serviceAccounts/my-service-account@my-project.iam.gserviceaccount.com",
email="my-service-account@my-project.iam.gserviceaccount.com",
display_name="My service account",
keys=[
Key(
name="90c48f61c65cd56224a12ab18e6ee9ca9c3aee7c",
origin="GOOGLE_PROVIDED",
type="SYSTEM_MANAGED",
valid_after=datetime.strptime("2024-07-10", "%Y-%m-%d"),
valid_before=datetime.strptime("9999-12-31", "%Y-%m-%d"),
)
],
project_id=GCP_PROJECT_ID,
)
]
monitoring_client.sa_keys_metrics = set(
["90c48f61c65cd56224a12ab18e6ee9ca9c3aee7c"]
)
check = iam_sa_user_managed_key_unused()
result = check.execute()
assert len(result) == 0
def test_iam_sa_user_managed_key_unused(self):
iam_client = mock.MagicMock()
monitoring_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.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused.iam_client",
new=iam_client,
),
mock.patch(
"prowler.providers.gcp.services.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused.monitoring_client",
new=monitoring_client,
),
):
from prowler.providers.gcp.services.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused import (
iam_sa_user_managed_key_unused,
)
from prowler.providers.gcp.services.iam.iam_service import (
Key,
ServiceAccount,
)
iam_client.project_ids = [GCP_PROJECT_ID]
iam_client.region = GCP_US_CENTER1_LOCATION
iam_client.service_accounts = [
ServiceAccount(
name="projects/my-project/serviceAccounts/my-service-account@my-project.iam.gserviceaccount.com",
email="my-service-account@my-project.iam.gserviceaccount.com",
display_name="My service account",
keys=[
Key(
name="90c48f61c65cd56224a12ab18e6ee9ca9c3aee7c",
origin="GOOGLE_PROVIDED",
type="USER_MANAGED",
valid_after=datetime.strptime("2024-07-10", "%Y-%m-%d"),
valid_before=datetime.strptime("9999-12-31", "%Y-%m-%d"),
)
],
project_id=GCP_PROJECT_ID,
)
]
monitoring_client.sa_keys_metrics = set(
["90c48f61c65cd56224a12ab18e6ee9ca9c3aee7c"]
)
check = iam_sa_user_managed_key_unused()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"User-managed key {iam_client.service_accounts[0].keys[0].name} for Service Account {iam_client.service_accounts[0].email} was used over the last 180 days."
)
assert result[0].resource_id == iam_client.service_accounts[0].keys[0].name
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_US_CENTER1_LOCATION
assert result[0].resource_name == iam_client.service_accounts[0].email
def test_iam_sa_dormant_mixed_keys(self):
iam_client = mock.MagicMock()
monitoring_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.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused.iam_client",
new=iam_client,
),
mock.patch(
"prowler.providers.gcp.services.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused.monitoring_client",
new=monitoring_client,
),
):
from prowler.providers.gcp.services.iam.iam_sa_user_managed_key_unused.iam_sa_user_managed_key_unused import (
iam_sa_user_managed_key_unused,
)
from prowler.providers.gcp.services.iam.iam_service import (
Key,
ServiceAccount,
)
iam_client.project_ids = [GCP_PROJECT_ID]
iam_client.region = GCP_US_CENTER1_LOCATION
iam_client.service_accounts = [
ServiceAccount(
name="projects/my-project/serviceAccounts/my-service-account@my-project.iam.gserviceaccount.com",
email="my-service-account@my-project.iam.gserviceaccount.com",
display_name="My service account",
keys=[
Key(
name="90c48f61c65cd56224a12ab18e6ee9ca9c3aee7c",
origin="GOOGLE_PROVIDED",
type="SYSTEM_MANAGED",
valid_after=datetime.strptime("2024-07-10", "%Y-%m-%d"),
valid_before=datetime.strptime("9999-12-31", "%Y-%m-%d"),
),
Key(
name="e5e3800831ac1adc8a5849da7d827b4724b1fce8",
origin="GOOGLE_PROVIDED",
type="USER_MANAGED",
valid_after=datetime.strptime("2024-07-10", "%Y-%m-%d"),
valid_before=datetime.strptime("9999-12-31", "%Y-%m-%d"),
),
Key(
name="f8e4771561be5cda9b1267add7006c5143e3a220",
origin="GOOGLE_PROVIDED",
type="USER_MANAGED",
valid_after=datetime.strptime("2024-07-10", "%Y-%m-%d"),
valid_before=datetime.strptime("9999-12-31", "%Y-%m-%d"),
),
],
project_id=GCP_PROJECT_ID,
)
]
monitoring_client.sa_keys_metrics = set(
["f8e4771561be5cda9b1267add7006c5143e3a220"]
)
check = iam_sa_user_managed_key_unused()
result = check.execute()
assert len(result) == 2
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"User-managed key {iam_client.service_accounts[0].keys[1].name} for Service Account {iam_client.service_accounts[0].email} was not used over the last 180 days."
)
assert result[0].resource_id == iam_client.service_accounts[0].keys[1].name
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_US_CENTER1_LOCATION
assert result[0].resource_name == iam_client.service_accounts[0].email
assert result[1].status == "PASS"
assert (
result[1].status_extended
== f"User-managed key {iam_client.service_accounts[0].keys[2].name} for Service Account {iam_client.service_accounts[0].email} was used over the last 180 days."
)
assert result[1].resource_id == iam_client.service_accounts[0].keys[2].name
assert result[1].project_id == GCP_PROJECT_ID
assert result[1].location == GCP_US_CENTER1_LOCATION
assert result[1].resource_name == iam_client.service_accounts[0].email
@@ -41,3 +41,25 @@ class TestMonitoringService:
assert monitoring_client.alert_policies[1].filters == [
'metric.type="compute.googleapis.com/instance/disk/write_bytes_count"'
]
def test_sa_keys_metrics(self):
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,
),
):
monitoring_client = Monitoring(
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
)
assert monitoring_client.service == "monitoring"
assert monitoring_client.project_ids == [GCP_PROJECT_ID]
assert len(monitoring_client.sa_keys_metrics) == 2
assert "key1" in monitoring_client.sa_keys_metrics
assert "key2" in monitoring_client.sa_keys_metrics
assert "key3" not in monitoring_client.sa_keys_metrics