From 9e67f31913063b3bd7aa3b70c309eee97397206a Mon Sep 17 00:00:00 2001 From: Felix Dreissig Date: Tue, 24 Mar 2026 14:11:27 +0100 Subject: [PATCH] feat(gcp): Add checks for GCP Gemini (Generative Language) API (#10280) Co-authored-by: Daniel Barranquero --- prowler/CHANGELOG.md | 9 + prowler/compliance/gcp/c5_gcp.json | 12 +- .../gcp/fedramp_20x_ksi_low_gcp.json | 3 +- prowler/compliance/gcp/hipaa_gcp.json | 33 +- prowler/compliance/gcp/iso27001_2022_gcp.json | 14 +- prowler/compliance/gcp/mitre_attack_gcp.json | 10 +- prowler/compliance/gcp/pci_4.0_gcp.json | 42 ++ prowler/compliance/gcp/soc2_gcp.json | 28 +- prowler/providers/gcp/lib/constants.py | 1 + .../__init__.py | 0 ...i_restricted_with_gemini_api.metadata.json | 41 ++ .../apikeys_api_restricted_with_gemini_api.py | 53 +++ .../providers/gcp/services/gemini/__init__.py | 0 .../gemini/gemini_api_disabled/__init__.py | 0 .../gemini_api_disabled.metadata.json | 41 ++ .../gemini_api_disabled.py | 39 ++ ...eys_api_restricted_with_gemini_api_test.py | 407 ++++++++++++++++++ .../gemini_api_disabled_test.py | 110 +++++ 18 files changed, 826 insertions(+), 17 deletions(-) create mode 100644 prowler/providers/gcp/lib/constants.py create mode 100644 prowler/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/__init__.py create mode 100644 prowler/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/apikeys_api_restricted_with_gemini_api.metadata.json create mode 100644 prowler/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/apikeys_api_restricted_with_gemini_api.py create mode 100644 prowler/providers/gcp/services/gemini/__init__.py create mode 100644 prowler/providers/gcp/services/gemini/gemini_api_disabled/__init__.py create mode 100644 prowler/providers/gcp/services/gemini/gemini_api_disabled/gemini_api_disabled.metadata.json create mode 100644 prowler/providers/gcp/services/gemini/gemini_api_disabled/gemini_api_disabled.py create mode 100644 tests/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/apikeys_api_restricted_with_gemini_api_test.py create mode 100644 tests/providers/gcp/services/gemini/gemini_api_disabled/gemini_api_disabled_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 883b49eb76..3747889629 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to the **Prowler SDK** are documented in this file. +## [5.23.0] (Prowler UNRELEASED) + +### šŸš€ Added + +- `apikeys_api_restricted_with_gemini_api` check for GCP provider [(#10280)](https://github.com/prowler-cloud/prowler/pull/10280) +- `gemini_api_disabled` check for GCP provider [(#10280)](https://github.com/prowler-cloud/prowler/pull/10280) + +--- + ## [5.22.0] (Prowler v5.22.0) ### šŸž Fixed diff --git a/prowler/compliance/gcp/c5_gcp.json b/prowler/compliance/gcp/c5_gcp.json index b78afab354..e9d8b15b17 100644 --- a/prowler/compliance/gcp/c5_gcp.json +++ b/prowler/compliance/gcp/c5_gcp.json @@ -136,7 +136,9 @@ "ComplementaryCriteria": "Cloud service customers ensure through suitable controls that the guidelines and requirements for compliance with the contractual agreements with the cloud service provider (i.e., responsibilities, cooperation obligations and interfaces for reporting security incidents) are adequately defined, documented and set up." } ], - "Checks": [] + "Checks": [ + "gemini_api_disabled" + ] }, { "Id": "OIS-03.02B", @@ -7230,7 +7232,9 @@ "ComplementaryCriteria": "" } ], - "Checks": [] + "Checks": [ + "gemini_api_disabled" + ] }, { "Id": "SSO-01.02AC", @@ -7258,7 +7262,9 @@ "ComplementaryCriteria": "" } ], - "Checks": [] + "Checks": [ + "gemini_api_disabled" + ] }, { "Id": "SSO-02.02B", diff --git a/prowler/compliance/gcp/fedramp_20x_ksi_low_gcp.json b/prowler/compliance/gcp/fedramp_20x_ksi_low_gcp.json index 420601d810..5638b0c51e 100644 --- a/prowler/compliance/gcp/fedramp_20x_ksi_low_gcp.json +++ b/prowler/compliance/gcp/fedramp_20x_ksi_low_gcp.json @@ -248,7 +248,8 @@ "compute_public_address_shodan", "cloudsql_instance_automated_backups", "iam_sa_user_managed_key_rotate_90_days", - "iam_service_account_unused" + "iam_service_account_unused", + "gemini_api_disabled" ] }, { diff --git a/prowler/compliance/gcp/hipaa_gcp.json b/prowler/compliance/gcp/hipaa_gcp.json index 6bacc72e9e..37d9838e59 100644 --- a/prowler/compliance/gcp/hipaa_gcp.json +++ b/prowler/compliance/gcp/hipaa_gcp.json @@ -51,7 +51,8 @@ "iam_no_service_roles_at_project_level", "bigquery_dataset_public_access", "bigquery_dataset_cmek_encryption", - "kms_key_rotation_enabled" + "kms_key_rotation_enabled", + "gemini_api_disabled" ] }, { @@ -203,6 +204,36 @@ "cloudstorage_bucket_object_versioning" ] }, + { + "Id": "164_308_b_1", + "Name": "164.308(b)(1) Business associate contracts and other arrangements", + "Description": "A covered entity may permit a business associate to create, receive, maintain, or transmit electronic protected health information on the covered entity's behalf only if the covered entity obtains satisfactory assurances, in accordance with § 164.314(a), that the business associate will appropriately safeguard the information. A covered entity is not required to obtain such satisfactory assurances from a business associate that is a subcontractor.", + "Attributes": [ + { + "ItemId": "164_308_b_1", + "Section": "164.308 Administrative Safeguards", + "Service": "gcp" + } + ], + "Checks": [ + "gemini_api_disabled" + ] + }, + { + "Id": "164_308_b_2", + "Name": "164.308(b)(2)", + "Description": "A business associate may permit a business associate that is a subcontractor to create, receive, maintain, or transmit electronic protected health information on its behalf only if the business associate obtains satisfactory assurances, in accordance with § 164.314(a), that the subcontractor will appropriately safeguard the information.", + "Attributes": [ + { + "ItemId": "164_308_b_2", + "Section": "164.308 Administrative Safeguards", + "Service": "gcp" + } + ], + "Checks": [ + "gemini_api_disabled" + ] + }, { "Id": "164_310_a_1", "Name": "164.310(a)(1) Facility access controls", diff --git a/prowler/compliance/gcp/iso27001_2022_gcp.json b/prowler/compliance/gcp/iso27001_2022_gcp.json index d44d710442..9b814e7769 100644 --- a/prowler/compliance/gcp/iso27001_2022_gcp.json +++ b/prowler/compliance/gcp/iso27001_2022_gcp.json @@ -295,7 +295,8 @@ } ], "Checks": [ - "iam_cloud_asset_inventory_enabled" + "iam_cloud_asset_inventory_enabled", + "gemini_api_disabled" ] }, { @@ -325,7 +326,8 @@ } ], "Checks": [ - "iam_cloud_asset_inventory_enabled" + "iam_cloud_asset_inventory_enabled", + "gemini_api_disabled" ] }, { @@ -340,7 +342,9 @@ "Check_Summary": "The organisation should regularly monitor, review, evaluate and manage change in supplier information security practices and service delivery." } ], - "Checks": [] + "Checks": [ + "gemini_api_disabled" + ] }, { "Id": "A.5.23", @@ -354,7 +358,9 @@ "Check_Summary": "Processes for acquisition, use, management and exit from cloud services should be established in accordance with the organisation’s information security requirements." } ], - "Checks": [] + "Checks": [ + "gemini_api_disabled" + ] }, { "Id": "A.5.24", diff --git a/prowler/compliance/gcp/mitre_attack_gcp.json b/prowler/compliance/gcp/mitre_attack_gcp.json index adf93a32ca..22133caca8 100644 --- a/prowler/compliance/gcp/mitre_attack_gcp.json +++ b/prowler/compliance/gcp/mitre_attack_gcp.json @@ -148,7 +148,8 @@ "iam_sa_user_managed_key_rotate_90_days", "iam_service_account_unused", "apikeys_key_rotated_in_90_days", - "apikeys_api_restrictions_configured" + "apikeys_api_restrictions_configured", + "apikeys_api_restricted_with_gemini_api" ], "Attributes": [ { @@ -1127,7 +1128,9 @@ "iam_role_kms_enforce_separation_of_duties", "iam_role_sa_enforce_separation_of_duties", "iam_sa_no_administrative_privileges", - "iam_sa_no_user_managed_keys" + "iam_sa_no_user_managed_keys", + "apikeys_api_restricted_with_gemini_api", + "gemini_api_disabled" ], "Attributes": [ { @@ -1703,7 +1706,8 @@ "cloudsql_instance_ssl_connections", "dataproc_encrypted_with_cmks_disabled", "dns_rsasha1_in_use_to_key_sign_in_dnssec", - "dns_rsasha1_in_use_to_zone_sign_in_dnssec" + "dns_rsasha1_in_use_to_zone_sign_in_dnssec", + "apikeys_api_restricted_with_gemini_api" ], "Attributes": [ { diff --git a/prowler/compliance/gcp/pci_4.0_gcp.json b/prowler/compliance/gcp/pci_4.0_gcp.json index 4a5c4bbb38..e8e30a97e1 100644 --- a/prowler/compliance/gcp/pci_4.0_gcp.json +++ b/prowler/compliance/gcp/pci_4.0_gcp.json @@ -20225,6 +20225,48 @@ } ] }, + { + "Id": "12.8.3.1", + "Description": "Checks that the Gemini API (a.k.a. Generative Language API) is disabled since it is out of scope of GCP's PCI DSS conformance.", + "Name": "Gemini API", + "Checks": [ + "gemini_api_disabled" + ], + "Attributes": [ + { + "Section": "12.8.3: Risk to information assets associated with third-party service provider (TPSP) relationships is managed.", + "Service": "Gemini API" + } + ] + }, + { + "Id": "12.8.4.1", + "Description": "Checks that the Gemini API (a.k.a. Generative Language API) is disabled since it is out of scope of GCP's PCI DSS conformance.", + "Name": "Gemini API", + "Checks": [ + "gemini_api_disabled" + ], + "Attributes": [ + { + "Section": "12.8.4: Risk to information assets associated with third-party service provider (TPSP) relationships is managed.", + "Service": "Gemini API" + } + ] + }, + { + "Id": "12.9.1.1", + "Description": "Checks that the Gemini API (a.k.a. Generative Language API) is disabled since it is out of scope of GCP's PCI DSS conformance.", + "Name": "Gemini API", + "Checks": [ + "gemini_api_disabled" + ], + "Attributes": [ + { + "Section": "12.9.1: Third-party service providers (TPSPs) support their customers’ PCI DSS compliance.", + "Service": "Gemini API" + } + ] + }, { "Id": "A1.1.2.1", "Description": "Checks if a backup vault has an attached IAM policy that prevents deletion of backup recovery points. The rule is NON_COMPLIANT if the Backup Vault does not have IAM policies or has policies lacking a suitable 'Deny' statement for deleting backups.", diff --git a/prowler/compliance/gcp/soc2_gcp.json b/prowler/compliance/gcp/soc2_gcp.json index 03437ed347..8450967e29 100644 --- a/prowler/compliance/gcp/soc2_gcp.json +++ b/prowler/compliance/gcp/soc2_gcp.json @@ -78,12 +78,13 @@ { "ItemId": "cc_3_2", "Section": "CC3.0 - Common Criteria Related to Risk Assessment", - "Service": "gcr", + "Service": "gcp", "Type": "automated" } ], "Checks": [ - "gcr_container_scanning_enabled" + "gcr_container_scanning_enabled", + "gemini_api_disabled" ] }, { @@ -282,12 +283,13 @@ { "ItemId": "cc_7_1", "Section": "CC7.0 - System Operations", - "Service": "iam", + "Service": "gcp", "Type": "automated" } ], "Checks": [ - "iam_cloud_asset_inventory_enabled" + "iam_cloud_asset_inventory_enabled", + "gemini_api_disabled" ] }, { @@ -420,6 +422,22 @@ "iam_cloud_asset_inventory_enabled" ] }, + { + "Id": "cc_9_2", + "Name": "CC9.2 The entity assesses and manages risks associated with vendors and business partners", + "Description": "Establishes Requirements for Vendor and Business Partner Engagements—The entity establishes specific requirements for a vendor and business partner engagement that includes (1) scope of services and product specifications, (2) roles and responsibilities, (3) compliance requirements, and (4) service levels. Assesses Vendor and Business Partner Risks—The entity assesses, on a periodic basis, the risks that vendors and business partners (and those entities' vendors and business partners) represent to the achievement of the entity's objectives. Assigns Responsibility and Accountability for Managing Vendors and Business Partners—The entity assigns responsibility and accountability for the management of risks associated with vendors and business partners. Establishes Communication Protocols for Vendors and Business Partners—The entity establishes communication and resolution protocols for service or product issues related to vendors and business partners. Establishes Exception Handling Procedures From Vendors and Business Partners —The entity establishes exception handling procedures for service or product issues related to vendors and business partners. Assesses Vendor and Business Partner Performance—The entity periodically assesses the performance of vendors and business partners. Implements Procedures for Addressing Issues Identified During Vendor and Business Partner Assessments—The entity implements procedures for addressing issues identified with vendor and business partner relationships. Implements Procedures for Terminating Vendor and Business Partner Relationships — The entity implements procedures for terminating vendor and business partner relationships. Obtains Confidentiality Commitments from Vendors and Business Partners—The entity obtains confidentiality commitments that are consistent with the entity's confidentiality commitments and requirements from vendors and business partners who have access to confidential information. Assesses Compliance With Confidentiality Commitments of Vendors and Business Partners — On a periodic and as-needed basis, the entity assesses compliance by vendors and business partners with the entity's confidentiality commitments and requirements. Obtains Privacy Commitments from Vendors and Business Partners—The entity obtains privacy commitments, consistent with the entity's privacy commitments and requirements, from vendors and business partners who have access to personal information. Assesses Compliance with Privacy Commitments of Vendors and Business Partners— On a periodic and as-needed basis, the entity assesses compliance by vendors and business partners with the entity's privacy commitments and requirements and takes corrective action as necessary.", + "Attributes": [ + { + "ItemId": "cc_9_2", + "Section": "CC9.0 - Risk Mitigation", + "Service": "gcp", + "Type": "automated" + } + ], + "Checks": [ + "gemini_api_disabled" + ] + }, { "Id": "cc_a_1_1", "Name": "A1.2 The entity authorizes, designs, develops or acquires, implements, operates, approves, maintains, and monitors environmental protections, software, data back-up processes, and recovery infrastructure to meet its objectives", @@ -575,4 +593,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/prowler/providers/gcp/lib/constants.py b/prowler/providers/gcp/lib/constants.py new file mode 100644 index 0000000000..35756bd386 --- /dev/null +++ b/prowler/providers/gcp/lib/constants.py @@ -0,0 +1 @@ +GEMINI_SERVICE_NAME = "generativelanguage.googleapis.com" diff --git a/prowler/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/__init__.py b/prowler/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/apikeys_api_restricted_with_gemini_api.metadata.json b/prowler/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/apikeys_api_restricted_with_gemini_api.metadata.json new file mode 100644 index 0000000000..b1a80ad12d --- /dev/null +++ b/prowler/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/apikeys_api_restricted_with_gemini_api.metadata.json @@ -0,0 +1,41 @@ +{ + "Provider": "gcp", + "CheckID": "apikeys_api_restricted_with_gemini_api", + "CheckTitle": "API key is restricted to specific Google APIs when the Gemini (Generative Language) API is enabled", + "CheckType": [], + "ServiceName": "apikeys", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "apikeys.googleapis.com/Key", + "ResourceGroup": "IAM", + "Description": "The Gemini API (a.k.a. Generative Language API, `generativelanguage.googleapis.com`) uses API keys to authenticate requests for uploaded files and cached content. For projects with the Gemini API enabled, each API key should have API restrictions configured to limit access to specific services.", + "Risk": "Leaked, unrestricted API keys allow access to sensitive uploaded files and cached content, impacting their **confidentiality**. The risk of leaks is particularly high because traditionally, Google API keys were *not* considered sensitive and might be embedded in website source code, mobile apps, etc.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://trufflesecurity.com/blog/google-api-keys-werent-secrets-but-then-gemini-changed-the-rules", + "https://cloud.google.com/docs/authentication/api-keys" + ], + "Remediation": { + "Code": { + "CLI": "gcloud services api-keys update --api-target=service=", + "NativeIaC": "", + "Other": "1. In Google Cloud Console, go to APIs & Services > Credentials\n2. Click the API key name to edit it\n3. In API restrictions, select \"Restrict key\"\n4. Choose only the specific API(s) needed (do not select \"All Google APIs\" and select \"Generative Language API\" only if specifically required)\n5. Click Save", + "Terraform": "```hcl\nresource \"google_apikeys_key\" \"key\" {\n display_name = \"\"\n\n restrictions {\n api_targets {\n service = \"\" # Critical: restricts the key to a specific API, removing any \"All Google APIs\" (cloudapis.googleapis.com)\n }\n }\n}\n```" + }, + "Recommendation": { + "Text": "Apply **API restrictions** so that each key only has access to specific APIs. Do *not* allow access to the \"Generative Language API\" in particular.\nIf access to the \"Generative Language API\" is required, be aware of the consequences. Only use the key for this specific purpose and ensure adequate **secrets storage**.\n\nAlternatively, completely disable the Gemini API (a.k.a. Generative Language API, `generativelanguage.googleapis.com`) in the project's enabled services.", + "Url": "https://hub.prowler.com/check/apikeys_api_restricted_with_gemini_api" + } + }, + "Categories": [ + "secrets", + "gen-ai", + "identity-access" + ], + "DependsOn": [ + "apikeys_api_restrictions_configured" + ], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/apikeys_api_restricted_with_gemini_api.py b/prowler/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/apikeys_api_restricted_with_gemini_api.py new file mode 100644 index 0000000000..80002fb727 --- /dev/null +++ b/prowler/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/apikeys_api_restricted_with_gemini_api.py @@ -0,0 +1,53 @@ +from prowler.lib.check.models import Check, Check_Report_GCP +from prowler.providers.gcp.lib.constants import GEMINI_SERVICE_NAME +from prowler.providers.gcp.services.apikeys.apikeys_client import apikeys_client +from prowler.providers.gcp.services.serviceusage.serviceusage_client import ( + serviceusage_client, +) + + +class apikeys_api_restricted_with_gemini_api(Check): + def execute(self) -> Check_Report_GCP: + findings = [] + + for key in apikeys_client.keys: + report = Check_Report_GCP( + metadata=self.metadata(), + resource=key, + location=apikeys_client.region, + ) + report.status = "PASS" + report.status_extended = f"API key {key.name} has restrictions configured." + + genlang_enabled = any( + active_service.name == GEMINI_SERVICE_NAME + for active_service in serviceusage_client.active_services.get( + key.project_id, [] + ) + ) + + if not genlang_enabled: + report.status = "PASS" + report.status_extended = f"Gemini (Generative Language) API is not enabled for project {key.project_id} of key {key.name}" + findings.append(report) + continue + + key_restrictions = key.restrictions.get("apiTargets", []) + + if len(key_restrictions) > 1 and any( + target.get("service") == GEMINI_SERVICE_NAME + for target in key_restrictions + ): + report.status = "FAIL" + report.status_extended = f"API key {key.name} has access to Gemini (Generative Language) API as well as other APIs." + + elif not key_restrictions or any( + target.get("service") == "cloudapis.googleapis.com" + for target in key_restrictions + ): + report.status = "FAIL" + report.status_extended = f"API key {key.name} does not have restrictions configured and Gemini (Generative Language) API is enabled for project {key.project_id}." + + findings.append(report) + + return findings diff --git a/prowler/providers/gcp/services/gemini/__init__.py b/prowler/providers/gcp/services/gemini/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/gcp/services/gemini/gemini_api_disabled/__init__.py b/prowler/providers/gcp/services/gemini/gemini_api_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/gcp/services/gemini/gemini_api_disabled/gemini_api_disabled.metadata.json b/prowler/providers/gcp/services/gemini/gemini_api_disabled/gemini_api_disabled.metadata.json new file mode 100644 index 0000000000..727c6b666d --- /dev/null +++ b/prowler/providers/gcp/services/gemini/gemini_api_disabled/gemini_api_disabled.metadata.json @@ -0,0 +1,41 @@ +{ + "Provider": "gcp", + "CheckID": "gemini_api_disabled", + "CheckTitle": "Gemini (Generative Language) API is disabled", + "CheckType": [], + "ServiceName": "gemini", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "serviceusage.googleapis.com/Service", + "ResourceGroup": "ai_ml", + "Description": "The Gemini API (a.k.a. Generative Language API, `generativelanguage.googleapis.com`) should not be used. It does not adhere to GCP's general security & compliance standards.", + "Risk": "The API does not support GCP IAM for authentication, impacting confidentiality of uploaded files and cached content. It provides no SLA, reducing availability guarantees. It is not covered by compliance certifications such as ISO 27001, SOC 2, C5, and HIPAA, which can diminish your compliance posture.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/docs/authentication/api-keys", + "https://cloud.google.com/security/compliance/iso-27001", + "https://cloud.google.com/security/compliance/soc-2", + "https://cloud.google.com/security/compliance/bsi-c5", + "https://cloud.google.com/security/compliance/hipaa-compliance" + ], + "Remediation": { + "Code": { + "CLI": "gcloud services disable generativelanguage.googleapis.com", + "NativeIaC": "", + "Other": "1. In Google Cloud Console, go to \"APIs & Services\" > \"Enabled APIs & services\"\n2. Click \"Generative Language API\" to view service details.\n3. Click \"Disable API\" in the top bar and confirm the action.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Do not use the Gemini API. Instead, consider using Gemini through the Vertex AI API, which does not have the described issues.", + "Url": "https://hub.prowler.com/check/gemini_api_disabled" + } + }, + "Categories": [ + "gen-ai", + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/gcp/services/gemini/gemini_api_disabled/gemini_api_disabled.py b/prowler/providers/gcp/services/gemini/gemini_api_disabled/gemini_api_disabled.py new file mode 100644 index 0000000000..0b979df5e0 --- /dev/null +++ b/prowler/providers/gcp/services/gemini/gemini_api_disabled/gemini_api_disabled.py @@ -0,0 +1,39 @@ +from prowler.lib.check.models import Check, Check_Report_GCP +from prowler.providers.gcp.lib.constants import GEMINI_SERVICE_NAME +from prowler.providers.gcp.services.serviceusage.serviceusage_client import ( + serviceusage_client, +) + + +class gemini_api_disabled(Check): + def execute(self) -> Check_Report_GCP: + findings = [] + + for project_id in serviceusage_client.project_ids: + report = Check_Report_GCP( + metadata=self.metadata(), + resource=serviceusage_client.projects[project_id], + resource_id=GEMINI_SERVICE_NAME, + resource_name="Gemini (Generative Language) API", + project_id=project_id, + location=serviceusage_client.region, + ) + report.status = "FAIL" + report.status_extended = ( + f"Gemini (Generative Language) API is enabled for project {project_id}" + ) + + genlang_enabled = any( + active_service.name == GEMINI_SERVICE_NAME + for active_service in serviceusage_client.active_services.get( + project_id, [] + ) + ) + + if not genlang_enabled: + report.status = "PASS" + report.status_extended = f"Gemini (Generative Language) API is disabled for project {project_id}" + + findings.append(report) + + return findings diff --git a/tests/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/apikeys_api_restricted_with_gemini_api_test.py b/tests/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/apikeys_api_restricted_with_gemini_api_test.py new file mode 100644 index 0000000000..46b4b746e9 --- /dev/null +++ b/tests/providers/gcp/services/apikeys/apikeys_api_restricted_with_gemini_api/apikeys_api_restricted_with_gemini_api_test.py @@ -0,0 +1,407 @@ +from re import search +from unittest import mock + +from prowler.providers.gcp.services.apikeys.apikeys_service import Key +from prowler.providers.gcp.services.serviceusage.serviceusage_service import Service +from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_provider + + +class Test_apikeys_api_restricted_with_gemini_api: + def test_unrestricted_key_gemini_disabled(self): + key = Key( + name="test", + id="123", + creation_time="2026-02-01T11:21:41.627509Z", + restrictions={}, + project_id=GCP_PROJECT_ID, + ) + + apikeys_client = mock.MagicMock() + apikeys_client.project_ids = [GCP_PROJECT_ID] + apikeys_client.keys = [key] + apikeys_client.region = "global" + + serviceusage_client = mock.MagicMock() + serviceusage_client.active_services = {GCP_PROJECT_ID: []} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.apikeys_client", + new=apikeys_client, + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.serviceusage_client", + new=serviceusage_client, + ), + ): + from prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api import ( + apikeys_api_restricted_with_gemini_api, + ) + + check = apikeys_api_restricted_with_gemini_api() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert search( + "Gemini .* API is not enabled", + result[0].status_extended, + ) + assert result[0].resource_id == key.id + + def test_no_keys(self): + apikeys_client = mock.MagicMock() + apikeys_client.project_ids = [GCP_PROJECT_ID] + apikeys_client.keys = [] + apikeys_client.region = "global" + + serviceusage_client = mock.MagicMock() + serviceusage_client.active_services = { + GCP_PROJECT_ID: [ + Service( + name="generativelanguage.googleapis.com", + title="Gemini API", + project_id=GCP_PROJECT_ID, + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.apikeys_client", + new=apikeys_client, + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.serviceusage_client", + new=serviceusage_client, + ), + ): + from prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api import ( + apikeys_api_restricted_with_gemini_api, + ) + + check = apikeys_api_restricted_with_gemini_api() + result = check.execute() + + assert len(result) == 0 + + def test_key_restricted_to_gemini_only(self): + key = Key( + name="test", + id="123", + creation_time="2026-02-01T11:21:41.627509Z", + restrictions={ + "apiTargets": [ + {"service": "generativelanguage.googleapis.com"}, + ] + }, + project_id=GCP_PROJECT_ID, + ) + + apikeys_client = mock.MagicMock() + apikeys_client.project_ids = [GCP_PROJECT_ID] + apikeys_client.keys = [key] + apikeys_client.region = "global" + + serviceusage_client = mock.MagicMock() + serviceusage_client.active_services = { + GCP_PROJECT_ID: [ + Service( + name="generativelanguage.googleapis.com", + title="Gemini API", + project_id=GCP_PROJECT_ID, + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.apikeys_client", + new=apikeys_client, + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.serviceusage_client", + new=serviceusage_client, + ), + ): + from prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api import ( + apikeys_api_restricted_with_gemini_api, + ) + + check = apikeys_api_restricted_with_gemini_api() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert search( + f"API key {key.name} has restrictions configured", + result[0].status_extended, + ) + assert result[0].resource_id == key.id + + def test_key_restricted_to_bigquery_and_gemini(self): + key = Key( + name="test", + id="123", + creation_time="2026-02-01T11:21:41.627509Z", + restrictions={ + "apiTargets": [ + {"service": "bigquery.googleapis.com"}, + {"service": "generativelanguage.googleapis.com"}, + ] + }, + project_id=GCP_PROJECT_ID, + ) + + apikeys_client = mock.MagicMock() + apikeys_client.project_ids = [GCP_PROJECT_ID] + apikeys_client.keys = [key] + apikeys_client.region = "global" + + serviceusage_client = mock.MagicMock() + serviceusage_client.active_services = { + GCP_PROJECT_ID: [ + Service( + name="bigquery.googleapis.com", + title="BigQuery API", + project_id=GCP_PROJECT_ID, + ), + Service( + name="generativelanguage.googleapis.com", + title="Gemini API", + project_id=GCP_PROJECT_ID, + ), + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.apikeys_client", + new=apikeys_client, + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.serviceusage_client", + new=serviceusage_client, + ), + ): + from prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api import ( + apikeys_api_restricted_with_gemini_api, + ) + + check = apikeys_api_restricted_with_gemini_api() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert search( + f"API key {key.name} has access to Gemini", + result[0].status_extended, + ) + assert result[0].resource_id == key.id + + def test_key_restricted_to_bigquery_only(self): + key = Key( + name="test", + id="123", + creation_time="2026-02-01T11:21:41.627509Z", + restrictions={ + "apiTargets": [ + {"service": "bigquery.googleapis.com"}, + ] + }, + project_id=GCP_PROJECT_ID, + ) + + apikeys_client = mock.MagicMock() + apikeys_client.project_ids = [GCP_PROJECT_ID] + apikeys_client.keys = [key] + apikeys_client.region = "global" + + serviceusage_client = mock.MagicMock() + serviceusage_client.active_services = { + GCP_PROJECT_ID: [ + Service( + name="bigquery.googleapis.com", + title="BigQuery API", + project_id=GCP_PROJECT_ID, + ), + Service( + name="generativelanguage.googleapis.com", + title="Gemini API", + project_id=GCP_PROJECT_ID, + ), + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.apikeys_client", + new=apikeys_client, + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.serviceusage_client", + new=serviceusage_client, + ), + ): + from prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api import ( + apikeys_api_restricted_with_gemini_api, + ) + + check = apikeys_api_restricted_with_gemini_api() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert search( + f"API key {key.name} has restrictions configured", + result[0].status_extended, + ) + assert result[0].resource_id == key.id + + def test_key_restricted_to_cloudapis(self): + key = Key( + name="test", + id="123", + creation_time="2026-02-01T11:21:41.627509Z", + restrictions={ + "apiTargets": [ + {"service": "cloudapis.googleapis.com"}, + ] + }, + project_id=GCP_PROJECT_ID, + ) + + apikeys_client = mock.MagicMock() + apikeys_client.project_ids = [GCP_PROJECT_ID] + apikeys_client.keys = [key] + apikeys_client.region = "global" + + serviceusage_client = mock.MagicMock() + serviceusage_client.active_services = { + GCP_PROJECT_ID: [ + Service( + name="generativelanguage.googleapis.com", + title="Gemini API", + project_id=GCP_PROJECT_ID, + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.apikeys_client", + new=apikeys_client, + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.serviceusage_client", + new=serviceusage_client, + ), + ): + from prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api import ( + apikeys_api_restricted_with_gemini_api, + ) + + check = apikeys_api_restricted_with_gemini_api() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert search( + f"API key {key.name} does not have restrictions configured", + result[0].status_extended, + ) + assert result[0].resource_id == key.id + + def test_two_keys_one_restricted_one_unrestricted(self): + key_restricted = Key( + name="restricted-key", + id="123", + creation_time="2026-02-01T11:21:41.627509Z", + restrictions={ + "apiTargets": [ + {"service": "bigquery.googleapis.com"}, + ] + }, + project_id=GCP_PROJECT_ID, + ) + + key_unrestricted = Key( + name="unrestricted-key", + id="456", + creation_time="2026-02-01T11:21:41.627509Z", + restrictions={}, + project_id=GCP_PROJECT_ID, + ) + + apikeys_client = mock.MagicMock() + apikeys_client.project_ids = [GCP_PROJECT_ID] + apikeys_client.keys = [key_restricted, key_unrestricted] + apikeys_client.region = "global" + + serviceusage_client = mock.MagicMock() + serviceusage_client.active_services = { + GCP_PROJECT_ID: [ + Service( + name="generativelanguage.googleapis.com", + title="Gemini API", + project_id=GCP_PROJECT_ID, + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.apikeys_client", + new=apikeys_client, + ), + mock.patch( + "prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api.serviceusage_client", + new=serviceusage_client, + ), + ): + from prowler.providers.gcp.services.apikeys.apikeys_api_restricted_with_gemini_api.apikeys_api_restricted_with_gemini_api import ( + apikeys_api_restricted_with_gemini_api, + ) + + check = apikeys_api_restricted_with_gemini_api() + result = check.execute() + + assert len(result) == 2 + + assert result[0].status == "PASS" + assert result[0].resource_id == key_restricted.id + + assert result[1].status == "FAIL" + assert search( + f"API key {key_unrestricted.name} does not have restrictions configured", + result[1].status_extended, + ) + assert result[1].resource_id == key_unrestricted.id diff --git a/tests/providers/gcp/services/gemini/gemini_api_disabled/gemini_api_disabled_test.py b/tests/providers/gcp/services/gemini/gemini_api_disabled/gemini_api_disabled_test.py new file mode 100644 index 0000000000..4797c392f1 --- /dev/null +++ b/tests/providers/gcp/services/gemini/gemini_api_disabled/gemini_api_disabled_test.py @@ -0,0 +1,110 @@ +from unittest import mock + +from prowler.providers.gcp.models import GCPProject +from prowler.providers.gcp.services.serviceusage.serviceusage_service import Service +from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_provider + + +class Test_gemini_api_disabled: + def test_gemini_api_disabled(self): + serviceusage_client = mock.MagicMock() + serviceusage_client.active_services = { + GCP_PROJECT_ID: [ + Service( + name="storage.googleapis.com", + title="Cloud Storage API", + project_id=GCP_PROJECT_ID, + ) + ] + } + serviceusage_client.project_ids = [GCP_PROJECT_ID] + serviceusage_client.region = "global" + serviceusage_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="test", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.gemini.gemini_api_disabled.gemini_api_disabled.serviceusage_client", + new=serviceusage_client, + ), + ): + from prowler.providers.gcp.services.gemini.gemini_api_disabled.gemini_api_disabled import ( + gemini_api_disabled, + ) + + check = gemini_api_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Gemini (Generative Language) API is disabled for project {GCP_PROJECT_ID}" + ) + assert result[0].resource_id == "generativelanguage.googleapis.com" + assert result[0].project_id == GCP_PROJECT_ID + + def test_gemini_api_enabled(self): + serviceusage_client = mock.MagicMock() + serviceusage_client.active_services = { + GCP_PROJECT_ID: [ + Service( + name="storage.googleapis.com", + title="Cloud Storage API", + project_id=GCP_PROJECT_ID, + ), + Service( + name="generativelanguage.googleapis.com", + title="Gemini (Generative Language) API", + project_id=GCP_PROJECT_ID, + ), + ] + } + serviceusage_client.project_ids = [GCP_PROJECT_ID] + serviceusage_client.region = "global" + serviceusage_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="test", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.gemini.gemini_api_disabled.gemini_api_disabled.serviceusage_client", + new=serviceusage_client, + ), + ): + from prowler.providers.gcp.services.gemini.gemini_api_disabled.gemini_api_disabled import ( + gemini_api_disabled, + ) + + check = gemini_api_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Gemini (Generative Language) API is enabled for project {GCP_PROJECT_ID}" + ) + assert result[0].resource_id == "generativelanguage.googleapis.com" + assert result[0].project_id == GCP_PROJECT_ID