From a5bc226f1141436e71a5c7e150df448fe772969d Mon Sep 17 00:00:00 2001 From: Aline Almeida Date: Fri, 5 Jun 2026 12:07:30 +0200 Subject: [PATCH] fix(gcp): pass iam_service_account_unused for disabled service accounts (#11467) --- prowler/CHANGELOG.md | 1 + .../providers/gcp/services/iam/iam_service.py | 2 + .../iam_service_account_unused.py | 7 ++- .../iam_service_account_unused_test.py | 57 +++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index db7c87f4c7..ec2e68c30e 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🐞 Fixed - GCP `logging_sink_created` now recognizes organization-level aggregated sinks with `includeChildren=True`, avoiding false failures for covered projects [(#11355)](https://github.com/prowler-cloud/prowler/pull/11355) +- GCP `iam_service_account_unused` now passes disabled service accounts instead of failing them, since a disabled account cannot authenticate or be used [(#11467)](https://github.com/prowler-cloud/prowler/pull/11467) --- diff --git a/prowler/providers/gcp/services/iam/iam_service.py b/prowler/providers/gcp/services/iam/iam_service.py index 13e96276e7..987a86068c 100644 --- a/prowler/providers/gcp/services/iam/iam_service.py +++ b/prowler/providers/gcp/services/iam/iam_service.py @@ -37,6 +37,7 @@ class IAM(GCPService): display_name=account.get("displayName", ""), project_id=project_id, uniqueId=account.get("uniqueId", ""), + disabled=account.get("disabled", False), ) ) @@ -102,6 +103,7 @@ class ServiceAccount(BaseModel): keys: list[Key] = [] project_id: str uniqueId: str + disabled: bool = False class AccessApproval(GCPService): diff --git a/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.py b/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.py index 12440aff25..912237b9e5 100644 --- a/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.py +++ b/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.py @@ -19,7 +19,12 @@ class iam_service_account_unused(Check): resource_id=account.email, location=iam_client.region, ) - if account.uniqueId in sa_ids_used: + if account.disabled: + report.status = "PASS" + report.status_extended = ( + f"Service Account {account.email} is disabled and cannot be used." + ) + elif account.uniqueId in sa_ids_used: report.status = "PASS" report.status_extended = f"Service Account {account.email} was used over the last {max_unused_days} days." else: diff --git a/tests/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused_test.py b/tests/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused_test.py index d76200734d..79c0eb23c3 100644 --- a/tests/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused_test.py +++ b/tests/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused_test.py @@ -179,3 +179,60 @@ class Test_iam_service_account_unused: assert result[1].project_id == GCP_PROJECT_ID assert result[1].location == GCP_US_CENTER1_LOCATION assert result[1].resource == iam_client.service_accounts[1] + + def test_iam_service_account_disabled(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_service_account_unused.iam_service_account_unused.iam_client", + new=iam_client, + ), + mock.patch( + "prowler.providers.gcp.services.iam.iam_service_account_unused.iam_service_account_unused.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.iam.iam_service import ServiceAccount + from prowler.providers.gcp.services.iam.iam_service_account_unused.iam_service_account_unused import ( + iam_service_account_unused, + ) + + iam_client.project_ids = [GCP_PROJECT_ID] + iam_client.region = GCP_US_CENTER1_LOCATION + + iam_client.service_accounts = [ + ServiceAccount( + name="projects/my-project/serviceAccounts/disabled-sa@my-project.iam.gserviceaccount.com", + email="disabled-sa@my-project.iam.gserviceaccount.com", + display_name="Disabled service account", + keys=[], + project_id=GCP_PROJECT_ID, + uniqueId="999888877776666", + disabled=True, + ) + ] + + # The account is absent from the usage metrics, so a non-disabled + # account here would FAIL. Being disabled must take precedence and + # PASS, since a disabled account cannot authenticate or be used. + monitoring_client.sa_api_metrics = set() + monitoring_client.audit_config = {"max_unused_account_days": 30} + + check = iam_service_account_unused() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Service Account {iam_client.service_accounts[0].email} is disabled and cannot be used." + ) + assert result[0].resource_id == iam_client.service_accounts[0].email + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_US_CENTER1_LOCATION + assert result[0].resource == iam_client.service_accounts[0]