diff --git a/README.md b/README.md index 9922038378..2df9b00fb6 100644 --- a/README.md +++ b/README.md @@ -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 | 78 | 13 | 7 | 3 | +| GCP | 79 | 13 | 7 | 3 | | Azure | 140 | 18 | 8 | 3 | | Kubernetes | 83 | 7 | 4 | 7 | | Microsoft365 | 5 | 2 | 1 | 0 | diff --git a/prowler/compliance/gcp/mitre_attack_gcp.json b/prowler/compliance/gcp/mitre_attack_gcp.json index 6d57c7506b..af1a68f851 100644 --- a/prowler/compliance/gcp/mitre_attack_gcp.json +++ b/prowler/compliance/gcp/mitre_attack_gcp.json @@ -145,6 +145,7 @@ "iam_sa_no_administrative_privileges", "iam_sa_no_user_managed_keys", "iam_sa_user_managed_key_rotate_90_days", + "iam_service_account_unused", "apikeys_key_rotated_in_90_days", "apikeys_api_restrictions_configured" ], diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 2462578daa..c9b1e773bc 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -447,6 +447,10 @@ gcp: # GCP Compute Configuration # gcp.compute_public_address_shodan shodan_api_key: null + # GCP Service Account and user-managed keys unused configuration + # gcp.iam_service_account_unused + # gcp.iam_sa_user_managed_key_unused + max_unused_account_days: 180 # Kubernetes Configuration kubernetes: diff --git a/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.metadata.json b/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.metadata.json index bc56488f9e..bdc2b7684a 100644 --- a/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.metadata.json @@ -8,14 +8,14 @@ "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.", + "Description": "Ensure That There Are No Unused Service Account Keys for Each Service Account.", "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", + "Other": "", "Terraform": "" }, "Recommendation": { diff --git a/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.py b/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.py index 73e45df6bf..3c1f88fd1b 100644 --- a/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.py +++ b/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.py @@ -8,6 +8,9 @@ from prowler.providers.gcp.services.monitoring.monitoring_client import ( class iam_sa_user_managed_key_unused(Check): def execute(self) -> Check_Report_GCP: findings = [] + max_unused_days = monitoring_client.audit_config.get( + "max_unused_account_days", 180 + ) keys_used = monitoring_client.sa_keys_metrics for account in iam_client.service_accounts: for key in account.keys: @@ -21,10 +24,10 @@ class iam_sa_user_managed_key_unused(Check): ) 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." + report.status_extended = f"User-managed key {key.name} for Service Account {account.email} was used over the last {max_unused_days} 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." + report.status_extended = f"User-managed key {key.name} for Service Account {account.email} was not used over the last {max_unused_days} days." findings.append(report) return findings diff --git a/prowler/providers/gcp/services/iam/iam_service.py b/prowler/providers/gcp/services/iam/iam_service.py index d6f696d6aa..08e1ee4c3a 100644 --- a/prowler/providers/gcp/services/iam/iam_service.py +++ b/prowler/providers/gcp/services/iam/iam_service.py @@ -35,6 +35,7 @@ class IAM(GCPService): email=account["email"], display_name=account.get("displayName", ""), project_id=project_id, + uniqueId=account.get("uniqueId", ""), ) ) @@ -99,6 +100,7 @@ class ServiceAccount(BaseModel): display_name: str keys: list[Key] = [] project_id: str + uniqueId: str class AccessApproval(GCPService): diff --git a/prowler/providers/gcp/services/iam/iam_service_account_unused/__init__.py b/prowler/providers/gcp/services/iam/iam_service_account_unused/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.metadata.json b/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.metadata.json new file mode 100644 index 0000000000..6d4d889f78 --- /dev/null +++ b/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "gcp", + "CheckID": "iam_service_account_unused", + "CheckTitle": "Ensure That There Are No Unused Service Accounts", + "CheckType": [], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "ServiceAccount", + "Description": "Ensure That There Are No Unused Service Accounts.", + "Risk": "A malicious actor could make use of privilege escalation or impersonation to access an unused Service Account that is over-privileged.", + "RelatedUrl": "https://cloud.google.com/iam/docs/service-account-overview#identify-unused", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "It is recommended to disable or remove unused Service Accounts.", + "Url": "https://cloud.google.com/iam/docs/service-account-overview#identify-unused" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} 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 new file mode 100644 index 0000000000..12440aff25 --- /dev/null +++ b/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.py @@ -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_service_account_unused(Check): + def execute(self) -> Check_Report_GCP: + findings = [] + max_unused_days = monitoring_client.audit_config.get( + "max_unused_account_days", 180 + ) + sa_ids_used = monitoring_client.sa_api_metrics + for account in iam_client.service_accounts: + report = Check_Report_GCP( + metadata=self.metadata(), + resource=account, + resource_id=account.email, + location=iam_client.region, + ) + if 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: + report.status = "FAIL" + report.status_extended = f"Service Account {account.email} was not used over the last {max_unused_days} days." + findings.append(report) + + return findings diff --git a/prowler/providers/gcp/services/monitoring/monitoring_service.py b/prowler/providers/gcp/services/monitoring/monitoring_service.py index 426485c88f..575434a441 100644 --- a/prowler/providers/gcp/services/monitoring/monitoring_service.py +++ b/prowler/providers/gcp/services/monitoring/monitoring_service.py @@ -12,10 +12,12 @@ class Monitoring(GCPService): super().__init__(__class__.__name__, provider, api_version="v3") self.alert_policies = [] self.sa_keys_metrics = set() + self.sa_api_metrics = set() self._get_alert_policies() self._get_sa_keys_metrics( "iam.googleapis.com/service_account/key/authn_events_count" ) + self._get_sa_api_metrics("serviceruntime.googleapis.com/api/request_count") def _get_alert_policies(self): for project_id in self.project_ids: @@ -54,6 +56,7 @@ class Monitoring(GCPService): def _get_sa_keys_metrics(self, metric_type): try: + max_unused_days = int(self.audit_config.get("max_unused_account_days", 180)) end_time = ( datetime.datetime.now(datetime.timezone.utc) .replace(microsecond=0) @@ -62,7 +65,7 @@ class Monitoring(GCPService): start_time = ( ( datetime.datetime.now(datetime.timezone.utc) - - datetime.timedelta(days=180) + - datetime.timedelta(days=max_unused_days) ) .replace(microsecond=0) .isoformat() @@ -96,6 +99,53 @@ class Monitoring(GCPService): f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _get_sa_api_metrics(self, metric_type): + try: + max_unused_days = int(self.audit_config.get("max_unused_account_days", 180)) + end_time = ( + datetime.datetime.now(datetime.timezone.utc) + .replace(microsecond=0) + .isoformat() + ) + start_time = ( + ( + datetime.datetime.now(datetime.timezone.utc) + - datetime.timedelta(days=max_unused_days) + ) + .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", []): + sa_id = metric["resource"]["labels"].get("credential_id") + if sa_id and "serviceaccount:" in sa_id: + self.sa_api_metrics.add( + sa_id.replace("serviceaccount:", "") + ) + + 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 diff --git a/tests/config/config_test.py b/tests/config/config_test.py index 0573e0c19a..d796bfde1a 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -324,7 +324,7 @@ config_azure = { "recommended_minimal_tls_versions": ["1.2", "1.3"], } -config_gcp = {"shodan_api_key": None} +config_gcp = {"shodan_api_key": None, "max_unused_account_days": 30} config_kubernetes = { "audit_log_maxbackup": 10, diff --git a/tests/config/fixtures/config.yaml b/tests/config/fixtures/config.yaml index 95e8ff3332..2dc9bef561 100644 --- a/tests/config/fixtures/config.yaml +++ b/tests/config/fixtures/config.yaml @@ -399,6 +399,7 @@ gcp: # GCP Compute Configuration # gcp.compute_public_address_shodan shodan_api_key: null + max_unused_account_days: 30 # Kubernetes Configuration kubernetes: diff --git a/tests/providers/gcp/gcp_fixtures.py b/tests/providers/gcp/gcp_fixtures.py index 154b82f373..e41884d0c1 100644 --- a/tests/providers/gcp/gcp_fixtures.py +++ b/tests/providers/gcp/gcp_fixtures.py @@ -358,35 +358,73 @@ def mock_api_projects_calls(client: MagicMock): "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", - }, + "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", - } + }, + "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", - }, + "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", - } + }, + "metricKind": "DELTA", + "valueType": "INT64", + }, + { + "metric": { + "labels": { + "response_code": "200", + "protocol": "https", + }, + "type": "serviceruntime.googleapis.com/api/request_count", + }, + "resource": { + "type": "consumed_api", + "labels": { + "service": "securitycenter.googleapis.com", + "project_id": "{GCP_PROJECT_ID}", + "credential_id": "serviceaccount:111222233334444", + }, + }, + "metricKind": "DELTA", + "valueType": "INT64", + }, + { + "metric": { + "labels": { + "response_code": "200", + "protocol": "https", + }, + "type": "serviceruntime.googleapis.com/api/request_count", + }, + "resource": { + "type": "consumed_api", + "labels": { + "service": "securitycenter.googleapis.com", + "project_id": "{GCP_PROJECT_ID}", + "credential_id": "serviceaccount:55566666777888999", + }, + }, + "metricKind": "DELTA", + "valueType": "INT64", }, ] } @@ -398,11 +436,13 @@ def mock_api_projects_calls(client: MagicMock): "name": f"projects/{GCP_PROJECT_ID}/serviceAccounts/service-account1@{GCP_PROJECT_ID}.iam.gserviceaccount.com", "email": "service-account1@gmail.com", "displayName": "Service Account 1", + "uniqueId": "111222233334444", }, { "name": f"projects/{GCP_PROJECT_ID}/serviceAccounts/service-account2@{GCP_PROJECT_ID}.iam.gserviceaccount.com", "email": "service-account2@gmail.com", "displayName": "Service Account 2", + "uniqueId": "55566666777888999", }, ] } diff --git a/tests/providers/gcp/gcp_provider_test.py b/tests/providers/gcp/gcp_provider_test.py index a0096cc7fb..e3c7644f17 100644 --- a/tests/providers/gcp/gcp_provider_test.py +++ b/tests/providers/gcp/gcp_provider_test.py @@ -86,7 +86,10 @@ class TestGCPProvider: assert gcp_provider.projects == projects assert gcp_provider.default_project_id == "test-project" assert gcp_provider.identity == GCPIdentityInfo(profile="default") - assert gcp_provider.audit_config == {"shodan_api_key": None} + assert gcp_provider.audit_config == { + "shodan_api_key": None, + "max_unused_account_days": 180, + } @freeze_time(datetime.today()) def test_is_project_matching(self): diff --git a/tests/providers/gcp/services/iam/iam_sa_dormant_user_managed_keys/iam_sa_dormant_user_managed_keys_test.py b/tests/providers/gcp/services/iam/iam_sa_dormant_user_managed_keys/iam_sa_dormant_user_managed_keys_test.py index 8cb9c7c832..50a3932d6e 100644 --- a/tests/providers/gcp/services/iam/iam_sa_dormant_user_managed_keys/iam_sa_dormant_user_managed_keys_test.py +++ b/tests/providers/gcp/services/iam/iam_sa_dormant_user_managed_keys/iam_sa_dormant_user_managed_keys_test.py @@ -34,7 +34,7 @@ class Test_iam_sa_user_managed_key_unused: result = check.execute() assert len(result) == 0 - def test_iam_sa_dormant_no_keys(self): + def test_iam_sa_unused_no_keys(self): iam_client = mock.MagicMock() monitoring_client = mock.MagicMock() @@ -67,6 +67,7 @@ class Test_iam_sa_user_managed_key_unused: display_name="My service account", keys=[], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -78,7 +79,7 @@ class Test_iam_sa_user_managed_key_unused: result = check.execute() assert len(result) == 0 - def test_iam_sa_dormant_system_managed_keys(self): + def test_iam_sa_unused_system_managed_keys(self): iam_client = mock.MagicMock() monitoring_client = mock.MagicMock() @@ -122,6 +123,7 @@ class Test_iam_sa_user_managed_key_unused: ) ], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -177,12 +179,14 @@ class Test_iam_sa_user_managed_key_unused: ) ], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] monitoring_client.sa_keys_metrics = set( ["90c48f61c65cd56224a12ab18e6ee9ca9c3aee7c"] ) + monitoring_client.audit_config = {"max_unused_account_days": 30} check = iam_sa_user_managed_key_unused() result = check.execute() @@ -190,14 +194,14 @@ class Test_iam_sa_user_managed_key_unused: 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." + == 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 30 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): + def test_iam_sa_unused_mixed_keys(self): iam_client = mock.MagicMock() monitoring_client = mock.MagicMock() @@ -255,12 +259,14 @@ class Test_iam_sa_user_managed_key_unused: ), ], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] monitoring_client.sa_keys_metrics = set( ["f8e4771561be5cda9b1267add7006c5143e3a220"] ) + monitoring_client.audit_config = {"max_unused_account_days": 30} check = iam_sa_user_managed_key_unused() result = check.execute() @@ -268,7 +274,7 @@ class Test_iam_sa_user_managed_key_unused: 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." + == 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 30 days." ) assert result[0].resource_id == iam_client.service_accounts[0].keys[1].name assert result[0].project_id == GCP_PROJECT_ID @@ -278,7 +284,7 @@ class Test_iam_sa_user_managed_key_unused: 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." + == 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 30 days." ) assert result[1].resource_id == iam_client.service_accounts[0].keys[2].name assert result[1].project_id == GCP_PROJECT_ID diff --git a/tests/providers/gcp/services/iam/iam_sa_no_administrative_privileges/iam_sa_no_administrative_privileges_test.py b/tests/providers/gcp/services/iam/iam_sa_no_administrative_privileges/iam_sa_no_administrative_privileges_test.py index f55f8f3f10..f37d312f8a 100644 --- a/tests/providers/gcp/services/iam/iam_sa_no_administrative_privileges/iam_sa_no_administrative_privileges_test.py +++ b/tests/providers/gcp/services/iam/iam_sa_no_administrative_privileges/iam_sa_no_administrative_privileges_test.py @@ -66,6 +66,7 @@ class Test_iam_sa_no_administrative_privileges: display_name="My service account", keys=[], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -122,6 +123,7 @@ class Test_iam_sa_no_administrative_privileges: display_name="My service account", keys=[], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -187,6 +189,7 @@ class Test_iam_sa_no_administrative_privileges: display_name="My service account", keys=[], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -252,6 +255,7 @@ class Test_iam_sa_no_administrative_privileges: display_name="My service account", keys=[], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -317,6 +321,7 @@ class Test_iam_sa_no_administrative_privileges: display_name="My service account", keys=[], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -382,6 +387,7 @@ class Test_iam_sa_no_administrative_privileges: display_name="My service account", keys=[], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -447,6 +453,7 @@ class Test_iam_sa_no_administrative_privileges: display_name="My service account", keys=[], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -512,6 +519,7 @@ class Test_iam_sa_no_administrative_privileges: display_name="My service account", keys=[], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] diff --git a/tests/providers/gcp/services/iam/iam_sa_no_user_managed_keys/iam_sa_no_user_managed_keys_test.py b/tests/providers/gcp/services/iam/iam_sa_no_user_managed_keys/iam_sa_no_user_managed_keys_test.py index 9b49a21271..2fbd41df9f 100644 --- a/tests/providers/gcp/services/iam/iam_sa_no_user_managed_keys/iam_sa_no_user_managed_keys_test.py +++ b/tests/providers/gcp/services/iam/iam_sa_no_user_managed_keys/iam_sa_no_user_managed_keys_test.py @@ -62,6 +62,7 @@ class Test_iam_sa_no_user_managed_keys: display_name="My service account", keys=[], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -117,6 +118,7 @@ class Test_iam_sa_no_user_managed_keys: ) ], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -172,6 +174,7 @@ class Test_iam_sa_no_user_managed_keys: ) ], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -234,6 +237,7 @@ class Test_iam_sa_no_user_managed_keys: ), ], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] diff --git a/tests/providers/gcp/services/iam/iam_sa_user_managed_key_rotate_90_days/iam_sa_user_managed_key_rotate_90_days_test.py b/tests/providers/gcp/services/iam/iam_sa_user_managed_key_rotate_90_days/iam_sa_user_managed_key_rotate_90_days_test.py index 934f4f3222..8862136463 100644 --- a/tests/providers/gcp/services/iam/iam_sa_user_managed_key_rotate_90_days/iam_sa_user_managed_key_rotate_90_days_test.py +++ b/tests/providers/gcp/services/iam/iam_sa_user_managed_key_rotate_90_days/iam_sa_user_managed_key_rotate_90_days_test.py @@ -62,6 +62,7 @@ class Test_iam_sa_user_managed_key_rotate_90_days: display_name="My service account", keys=[], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -108,6 +109,7 @@ class Test_iam_sa_user_managed_key_rotate_90_days: ) ], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] @@ -166,6 +168,7 @@ class Test_iam_sa_user_managed_key_rotate_90_days: ) ], project_id=GCP_PROJECT_ID, + uniqueId="111222233334444", ) ] 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 new file mode 100644 index 0000000000..d76200734d --- /dev/null +++ b/tests/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused_test.py @@ -0,0 +1,181 @@ +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_service_account_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_service_account_unused.iam_service_account_unused.iam_client", + new=iam_client, + ), + ): + 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 = [] + + check = iam_service_account_unused() + result = check.execute() + assert len(result) == 0 + + def test_iam_service_account_unused_single(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 ( + Key, + 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/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, + uniqueId="111222233334444", + ) + ] + + monitoring_client.sa_api_metrics = set(["111222233334444"]) + 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} was used over the last 30 days." + ) + 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] + + def test_iam_service_account_unused_mix(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 ( + Key, + 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/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, + uniqueId="111222233334444", + ), + ServiceAccount( + name="projects/my-project/serviceAccounts/my-service-account2@my-project.iam.gserviceaccount.com", + email="my-service-account2@my-project.iam.gserviceaccount.com", + display_name="My service account 2", + keys=[], + project_id=GCP_PROJECT_ID, + uniqueId="55566666777888999", + ), + ] + + monitoring_client.sa_api_metrics = set(["111222233334444"]) + monitoring_client.audit_config = {"max_unused_account_days": 30} + + check = iam_service_account_unused() + result = check.execute() + assert len(result) == 2 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Service Account {iam_client.service_accounts[0].email} was used over the last 30 days." + ) + 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] + + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == f"Service Account {iam_client.service_accounts[1].email} was not used over the last 30 days." + ) + assert result[1].resource_id == iam_client.service_accounts[1].email + 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] diff --git a/tests/providers/gcp/services/iam/iamgcp_service_test.py b/tests/providers/gcp/services/iam/iamgcp_service_test.py index 3b40b89858..c2b5e655f2 100644 --- a/tests/providers/gcp/services/iam/iamgcp_service_test.py +++ b/tests/providers/gcp/services/iam/iamgcp_service_test.py @@ -41,6 +41,7 @@ class TestIAMService: ) assert iam_client.service_accounts[0].email == "service-account1@gmail.com" assert iam_client.service_accounts[0].display_name == "Service Account 1" + assert iam_client.service_accounts[0].uniqueId == "111222233334444" assert len(iam_client.service_accounts[0].keys) == 2 assert iam_client.service_accounts[0].keys[0].name == "key1" assert iam_client.service_accounts[0].keys[0].valid_after == datetime( @@ -66,6 +67,7 @@ class TestIAMService: ) assert iam_client.service_accounts[1].email == "service-account2@gmail.com" assert iam_client.service_accounts[1].display_name == "Service Account 2" + assert iam_client.service_accounts[1].uniqueId == "55566666777888999" assert len(iam_client.service_accounts[1].keys) == 1 assert iam_client.service_accounts[1].keys[0].name == "key3" assert iam_client.service_accounts[1].keys[0].valid_after == datetime( diff --git a/tests/providers/gcp/services/monitoring/monitoring_service_test.py b/tests/providers/gcp/services/monitoring/monitoring_service_test.py index 2e3ec30637..992589649e 100644 --- a/tests/providers/gcp/services/monitoring/monitoring_service_test.py +++ b/tests/providers/gcp/services/monitoring/monitoring_service_test.py @@ -56,6 +56,7 @@ class TestMonitoringService: monitoring_client = Monitoring( set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID]) ) + monitoring_client.audit_config = {"max_unused_account_days": 30} assert monitoring_client.service == "monitoring" assert monitoring_client.project_ids == [GCP_PROJECT_ID] @@ -63,3 +64,25 @@ class TestMonitoringService: 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 + + def test_sa_api_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_api_metrics) == 2 + assert "111222233334444" in monitoring_client.sa_api_metrics + assert "55566666777888999" in monitoring_client.sa_api_metrics + assert "0000000000000" not in monitoring_client.sa_api_metrics