Last active
February 27, 2026 18:25
-
-
Save F30/9fd4d4cbcfe11c6aabe44e5cc9d8358d to your computer and use it in GitHub Desktop.
Check for GCP API keys affected by the retroactive enablement of the Generative Language (Gemini) API. See https://trufflesecurity.com/blog/google-api-keys-werent-secrets-but-then-gemini-changed-the-rules for details. Use at your own discretion, provided 'as is' without any warranties or liability for potential issues.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Find GCP projects with the Generative Language (Gemini) API enabled and API keys that could access it | |
| (unrestricted or explicitly allowed). | |
| Checks for API keys affected by the issue described at: | |
| https://trufflesecurity.com/blog/google-api-keys-werent-secrets-but-then-gemini-changed-the-rules | |
| Requirements: | |
| pip install google-cloud-resource-manager google-cloud-service-usage google-cloud-api-keys | |
| Authentication: | |
| Uses Application Default Credentials (ADC). For example: | |
| gcloud auth application-default login | |
| You will have to set a project to use for API quotas: | |
| gcloud auth application-default set-quota-project <project-name> | |
| """ | |
| import argparse | |
| import re | |
| import sys | |
| from google.api_core.exceptions import GoogleAPICallError, PermissionDenied | |
| from google.cloud import api_keys_v2 | |
| from google.cloud import resourcemanager_v3 | |
| from google.cloud import service_usage_v1 | |
| TARGET_SERVICE = 'generativelanguage.googleapis.com' | |
| SERVICE_AGENT_PROJECT_ID_PATTERN = re.compile(r'^sys-[a-z0-9-]+$') | |
| def get_clients(): | |
| projects_client = resourcemanager_v3.ProjectsClient() | |
| service_usage_client = service_usage_v1.ServiceUsageClient() | |
| api_keys_client = api_keys_v2.ApiKeysClient() | |
| return projects_client, service_usage_client, api_keys_client | |
| def is_service_agent_project(project_id): | |
| return bool(SERVICE_AGENT_PROJECT_ID_PATTERN.fullmatch(project_id)) | |
| def iter_accessible_projects(projects_client, include_service_agent_projects=False): | |
| pager = projects_client.search_projects( | |
| request=resourcemanager_v3.SearchProjectsRequest(query='state:ACTIVE') | |
| ) | |
| for project in pager: | |
| project_name = project.display_name or '' | |
| project_id = project.project_id or '' | |
| if not project_id or not project.name: | |
| continue | |
| if not include_service_agent_projects and is_service_agent_project(project_id): | |
| continue | |
| # project.name is in the form 'projects/{project_number}' | |
| parts = project.name.split('/', maxsplit=1) | |
| if len(parts) != 2: | |
| continue | |
| project_number = parts[1] | |
| if not project_number: | |
| continue | |
| yield { | |
| 'project_id': project_id, | |
| 'project_name': project_name, | |
| 'project_number': project_number, | |
| } | |
| def collect_projects_matching_check(project_iterator, check_project, check_label='Checking'): | |
| matching_projects = [] | |
| checked_count = 0 | |
| for project in project_iterator: | |
| checked_count += 1 | |
| project_id = project['project_id'] | |
| status_line = f'[{checked_count}] {check_label} {project_id}... ' | |
| print(f'\r\033[2K{status_line}', end='', flush=True) | |
| try: | |
| if check_project(project): | |
| matching_projects.append(project) | |
| except (PermissionDenied, GoogleAPICallError) as error: | |
| print(f'\r\033[2K{status_line}ERROR') | |
| print( | |
| f' API call failed for project {project_id}: {error}', | |
| file=sys.stderr, | |
| ) | |
| if checked_count: | |
| print('\n') | |
| return matching_projects | |
| def find_projects_with_generative_language_enabled( | |
| projects_client, service_usage_client, include_service_agent_projects=False | |
| ): | |
| def has_generative_language_enabled(project): | |
| project_number = project['project_number'] | |
| resource_name = f'projects/{project_number}/services/{TARGET_SERVICE}' | |
| service = service_usage_client.get_service( | |
| request=service_usage_v1.GetServiceRequest(name=resource_name) | |
| ) | |
| return service.state == service_usage_v1.types.State.ENABLED | |
| return collect_projects_matching_check( | |
| iter_accessible_projects(projects_client, include_service_agent_projects), | |
| has_generative_language_enabled, | |
| check_label='Checking Generative Language API in', | |
| ) | |
| def is_unrestricted_key(restrictions, api_targets): | |
| return restrictions is None or not api_targets | |
| def is_explicit_key(_restrictions, api_targets): | |
| return bool(api_targets) and any( | |
| getattr(target, 'service', None) == TARGET_SERVICE for target in api_targets | |
| ) | |
| def find_projects_with_matching_api_keys(projects, api_keys_client, key_matches): | |
| def has_matching_api_keys(project): | |
| parent = f"projects/{project['project_number']}/locations/global" | |
| matching_keys = [] | |
| pager = api_keys_client.list_keys(request=api_keys_v2.ListKeysRequest(parent=parent)) | |
| for key in pager: | |
| restrictions = getattr(key, 'restrictions', None) | |
| api_targets = list(restrictions.api_targets) if restrictions else [] | |
| if key_matches(restrictions, api_targets): | |
| matching_keys.append( | |
| { | |
| 'name': key.name, | |
| 'display_name': key.display_name or '', | |
| } | |
| ) | |
| if matching_keys: | |
| project['matching_api_keys'] = matching_keys | |
| return True | |
| return False | |
| return collect_projects_matching_check( | |
| projects, | |
| has_matching_api_keys, | |
| check_label='Checking API keys in', | |
| ) | |
| def print_projects_with_generative_language(projects): | |
| if not projects: | |
| print('No accessible projects found with Generative Language API enabled.') | |
| return | |
| print('Projects with Generative Language API enabled:') | |
| for project in projects: | |
| print(f"- {project['project_id']}\t{project['project_name']}") | |
| def print_projects_with_matching_keys(projects, description): | |
| if not projects: | |
| print(f'No accessible projects found with Generative Language API enabled and {description}.') | |
| return | |
| print(f'Projects with Generative Language API enabled and {description}:') | |
| for project in projects: | |
| print(f"- {project['project_id']}\t{project['project_name']}") | |
| for key in project['matching_api_keys']: | |
| key_label = key['display_name'] or '(no display name)' | |
| print(f" - {key_label}\t{key['name']}") | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='Find projects with Generative Language API enabled and matching API keys.' | |
| ) | |
| parser.add_argument( | |
| '--include-service-agent-projects', | |
| action='store_true', | |
| help='Include Service Agent (Shadow/P4SA) projects in the initial project scan.', | |
| ) | |
| args = parser.parse_args() | |
| try: | |
| projects_client, service_usage_client, api_keys_client = get_clients() | |
| except (PermissionDenied, GoogleAPICallError) as error: | |
| print(f'Failed to initialize GCP clients: {error}', file=sys.stderr) | |
| sys.exit(1) | |
| projects_with_generative_language = find_projects_with_generative_language_enabled( | |
| projects_client, | |
| service_usage_client, | |
| include_service_agent_projects=args.include_service_agent_projects, | |
| ) | |
| print_projects_with_generative_language(projects_with_generative_language) | |
| print() | |
| projects_with_unrestricted_keys = find_projects_with_matching_api_keys( | |
| projects_with_generative_language, | |
| api_keys_client, | |
| key_matches=is_unrestricted_key, | |
| ) | |
| print_projects_with_matching_keys(projects_with_unrestricted_keys, 'unrestricted API keys') | |
| print() | |
| projects_with_explicit_keys = find_projects_with_matching_api_keys( | |
| projects_with_generative_language, | |
| api_keys_client, | |
| key_matches=is_explicit_key, | |
| ) | |
| print_projects_with_matching_keys(projects_with_explicit_keys, | |
| 'API keys explicitly allowing the Generative Language API') | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment