mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 12:31:54 +00:00
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:
@@ -72,7 +72,7 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) |
|
||||
|---|---|---|---|---|
|
||||
| AWS | 564 | 82 | 33 | 10 |
|
||||
| GCP | 77 | 13 | 6 | 3 |
|
||||
| GCP | 78 | 13 | 6 | 3 |
|
||||
| Azure | 140 | 18 | 7 | 3 |
|
||||
| Kubernetes | 83 | 7 | 4 | 7 |
|
||||
| Microsoft365 | 5 | 2 | 1 | 0 |
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
"iam_organization_essential_contacts_configured",
|
||||
"iam_role_kms_enforce_separation_of_duties",
|
||||
"iam_role_sa_enforce_separation_of_duties",
|
||||
"iam_sa_user_managed_key_unused",
|
||||
"iam_sa_no_administrative_privileges",
|
||||
"iam_sa_no_user_managed_keys",
|
||||
"iam_sa_user_managed_key_rotate_90_days",
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Provider": "gcp",
|
||||
"CheckID": "iam_sa_user_managed_key_unused",
|
||||
"CheckTitle": "Ensure That There Are No Unused Service Account Keys for Each Service Account",
|
||||
"CheckType": [],
|
||||
"ServiceName": "iam",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "ServiceAccountKey",
|
||||
"Description": "Ensure That There Are No Dormant Service Account Keys for Each Service Account. A key is considered dormant if it has been inactive for more than 180 days.",
|
||||
"Risk": "Anyone who has access to the keys will be able to access resources through the service account. GCP-managed keys are used by Cloud Platform services such as App Engine and Compute Engine. These keys cannot be downloaded. Google will keep the keys and automatically rotate them on an approximately weekly basis. User-managed keys are created, downloadable, and managed by users.",
|
||||
"RelatedUrl": "https://cloud.google.com/iam/docs/service-account-overview#identify-unused",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-user-managed-service-account-keys.html",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "It is recommended to prevent user-managed service account keys.",
|
||||
"Url": "https://cloud.google.com/iam/docs/creating-managing-service-account-keys"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_GCP
|
||||
from prowler.providers.gcp.services.iam.iam_client import iam_client
|
||||
from prowler.providers.gcp.services.monitoring.monitoring_client import (
|
||||
monitoring_client,
|
||||
)
|
||||
|
||||
|
||||
class iam_sa_user_managed_key_unused(Check):
|
||||
def execute(self) -> Check_Report_GCP:
|
||||
findings = []
|
||||
keys_used = monitoring_client.sa_keys_metrics
|
||||
for account in iam_client.service_accounts:
|
||||
for key in account.keys:
|
||||
if key.type == "USER_MANAGED":
|
||||
report = Check_Report_GCP(
|
||||
metadata=self.metadata(),
|
||||
resource=account,
|
||||
resource_id=key.name,
|
||||
resource_name=account.email,
|
||||
location=iam_client.region,
|
||||
)
|
||||
if key.name in keys_used:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"User-managed key {key.name} for Service Account {account.email} was used over the last 180 days."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"User-managed key {key.name} for Service Account {account.email} was not used over the last 180 days."
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -1,3 +1,5 @@
|
||||
import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
@@ -9,7 +11,11 @@ class Monitoring(GCPService):
|
||||
def __init__(self, provider: GcpProvider):
|
||||
super().__init__(__class__.__name__, provider, api_version="v3")
|
||||
self.alert_policies = []
|
||||
self.sa_keys_metrics = set()
|
||||
self._get_alert_policies()
|
||||
self._get_sa_keys_metrics(
|
||||
"iam.googleapis.com/service_account/key/authn_events_count"
|
||||
)
|
||||
|
||||
def _get_alert_policies(self):
|
||||
for project_id in self.project_ids:
|
||||
@@ -46,6 +52,50 @@ class Monitoring(GCPService):
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_sa_keys_metrics(self, metric_type):
|
||||
try:
|
||||
end_time = (
|
||||
datetime.datetime.now(datetime.timezone.utc)
|
||||
.replace(microsecond=0)
|
||||
.isoformat()
|
||||
)
|
||||
start_time = (
|
||||
(
|
||||
datetime.datetime.now(datetime.timezone.utc)
|
||||
- datetime.timedelta(days=180)
|
||||
)
|
||||
.replace(microsecond=0)
|
||||
.isoformat()
|
||||
)
|
||||
for project_id in self.project_ids:
|
||||
try:
|
||||
request = (
|
||||
self.client.projects()
|
||||
.timeSeries()
|
||||
.list(
|
||||
name=f"projects/{project_id}",
|
||||
filter=f'metric.type = "{metric_type}"',
|
||||
interval_startTime=start_time,
|
||||
interval_endTime=end_time,
|
||||
view="HEADERS",
|
||||
)
|
||||
)
|
||||
response = request.execute()
|
||||
|
||||
for metric in response.get("timeSeries", []):
|
||||
key_id = metric["metric"]["labels"].get("key_id")
|
||||
if key_id:
|
||||
self.sa_keys_metrics.add(key_id)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class AlertPolicy(BaseModel):
|
||||
name: str
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
+286
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user