From 6fdccecccd350df866a5365c7109591db5fc46e7 Mon Sep 17 00:00:00 2001 From: subh Date: Wed, 11 Feb 2026 00:55:02 +0530 Subject: Initial Commit --- README.md | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++++ gcpsecretfinder.py | 165 +++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 3 files changed, 358 insertions(+) create mode 100644 README.md create mode 100644 gcpsecretfinder.py create mode 100644 requirements.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..484c732 --- /dev/null +++ b/README.md @@ -0,0 +1,191 @@ +# GCP Secret Manager Scanner + +A Python tool for scanning Google Cloud Platform Secret Manager across all regions to discover and retrieve secrets. Useful for security audits, secret inventory, and cloud resource discovery. + +## Features + +- Scan all GCP regions for secrets in a single command +- Multiple authentication methods (access token, token file, or service account key) +- Optional secret value retrieval +- Export results to JSON +- Beautiful terminal UI with progress tracking +- Quiet mode for script integration + +## Installation +```bash +git clone https://github.com/5epi0l/GCPSecretFinder.git +cd GCPSecretFinder +pip install -r requirements.txt +``` + +## Authentication + +The tool supports three authentication methods: + +### 1. Access Token (Direct) +```bash +python3 gcpsecretfinder.py --project my-project --token "ya29.c.b0Aaek..." +``` + +### 2. Access Token (From File) +```bash +python3 gcpsecretfinder.py --project my-project --token-file ~/token.txt +``` + +### 3. Service Account Key +```bash +python3 gcpsecretfinder.py --project my-project --service-account-key ~/key.json +``` + +## Usage Examples + +### Basic Scanning + +List all secrets across all regions: +```bash +python3 gcpsecretfinder.py --project gr-proj-8 --token-file ~/token.txt +``` + +### Retrieve Secret Values + +Scan and retrieve the actual secret values: +```bash +python3 gcpsecretfinder.py --project my-project --token-file ~/token.txt --retrieve +``` + +### Retrieve Specific Version + +Get a specific version of secrets instead of latest: +```bash +python3 gcpsecretfinder.py --project my-project \ + --token-file ~/token.txt \ + --retrieve \ + --version 2 +``` + +## Command-Line Options +``` +required arguments: + -p, --project PROJECT GCP project ID + +authentication (one required): + -t, --token TOKEN Access token (String) + -f, --token-file FILE Path to file containing access token + -s, --service-account-key Path to service account key file + +optional arguments: + --retrieve Retrieve secret values (not just list them) + --version VERSION Secret version to retrieve (default: latest) + -h, --help Show help message +``` + +## Supported Regions + +The tool scans the following regions by default: + +**Asia Pacific:** +- asia-east1, asia-east2 +- asia-northeast1, asia-northeast2, asia-northeast3 +- asia-south1, asia-south2 +- asia-southeast1, asia-southeast2 +- australia-southeast1, australia-southeast2 + +**Europe:** +- europe-central2 +- europe-north1 +- europe-west1, europe-west2, europe-west3, europe-west4, europe-west6 + +**Middle East:** +- me-central1, me-west1 + +**North America:** +- northamerica-northeast1, northamerica-northeast2 +- us-central1 +- us-east1, us-east4 +- us-west1, us-west2, us-west3, us-west4 + +**South America:** +- southamerica-east1, southamerica-west1 + +## Output Format + + +### JSON Output + +With `--retrieve`, secrets are retrieved in JSON format: +```json +[ + { + "name": "projects/123456/secrets/api-key", + "region": "us-east1", + "value": "sk-abc123...", + "version": "latest", + "full_data": { + "name": "projects/123456/secrets/api-key", + "createTime": "2024-01-15T10:30:00Z", + "labels": {}, + "replication": { + "automatic": {} + } + } + } +] +``` + +## Common Use Cases + +### Security Audit +```bash +# Find all secrets and retrieve them +python3 gcpsecretfinder.py \ + --project prod-project \ + --service-account-key ~/audit-sa.json \ + --retrieve +``` + + +### Secret Inventory +```bash +# List all secrets without retrieving values +python3 gcpsecretfinder.py \ + --project my-project \ + --token-file ~/token.txt \ +``` + +### Integration with Other Tools +```bash +# Pipe to jq for filtering +python3 gcpsecretfinder.py \ + --project my-project \ + --token-file ~/token.txt \ + --retrieve \ + | jq '.[] | select(.name | contains("production"))' +``` + +## Permissions Required + +The service account or user must have the following IAM permissions: + +- `secretmanager.secrets.list` - To list secrets +- `secretmanager.versions.access` - To retrieve secret values (when using `--retrieve`) + + +## Error Handling + +The tool handles common errors gracefully: +- Network timeouts (10 second timeout per request) +- Invalid credentials +- Missing permissions +- Non-existent regions +- Failed secret retrieval + +Errors are logged to stderr while scanning continues for remaining regions. + +## Performance Considerations + +- Each region is scanned sequentially to avoid rate limiting +- Default timeout is 10 seconds per region +- Scanning all 31 regions typically takes 30-60 seconds +- Retrieval adds additional time proportional to number of secrets found + + diff --git a/gcpsecretfinder.py b/gcpsecretfinder.py new file mode 100644 index 0000000..b6c2cf7 --- /dev/null +++ b/gcpsecretfinder.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 + + +import argparse +import requests +from google.oauth2 import service_account +from typing import List, Optional, Tuple +import json +from google.auth.transport.requests import Request +import sys + +REGIONS = [ + "us-west1","us-west2", "us-west3", "us-west4", "us-central1", "us-east1", "us-east4", "us-east5", "us-south1", "northamerica-northeast1", "northamerica-northeast2", "southamerica-west1", "southamerica-east1", "northamerica-south1", "europe-west2", "europe-west1", "europe-west4", "europe-west6", "europe-west3", "europe-central2", "europe-west8", "europe-southwest1", "europe-west9", "europe-west12", "europe-west10", "europe-north2", "asia-south1", "asia-south2", "asia-southeast1", "asia-southeast2", "asia-east2", "asia-east1", "asia-northeast1", "asia-northeast2", "australia-southeast1", "australia-southeast2", "asia-northeast3", "asia-southeast3", "me-west1", "me-central1", "me-central2", "africa-south1" + ] + + +def getAccessTokenForServiceAccount(key_file: str) -> str: + credentials = service_account.Credentials.from_service_account_file( + key_file, + scopes=['https://www.googleapis.com/auth/cloud-platform'] + ) + credentials.refresh(Request()) + return credentials.token() + + +def scanRegionsForSecrets(project: str, region: str, access_token: str) -> Optional[List[str]]: + + url = f"https://secretmanager.{region}.rep.googleapis.com/v1/projects/{project}/locations/{region}/secrets" + headers = { + "Authorization": f"Bearer {access_token}" + } + + try: + response = requests.get(url, headers=headers, timeout=10) + response.raise_for_status() + + data = response.json() + secrets = data.get('secrets', []) + + if secrets: + return [secret.get('name') for secret in secrets if secret.get('name')] + return [] + + + except requests.exceptions.RequestException as e: + print(f"[!] An error has occured: {e}", file=sys.stderr) + +def retrieveFoundSecrets(secret_name: str, access_token: str, version: str = "latest") -> Optional[Tuple[str, bool]]: + """ + Args: + secret_name: Full secret name + access_token: GCP access Token + Version: Version to retrieve (default: "latest") + """ + + region = secret_name.split('/')[3] + project_num = secret_name.split('/')[1] + secret = secret_name.split('/')[5] + url = f"https://secretmanager.{region}.rep.googleapis.com/v1/projects/{project_num}/locations/{region}/secrets/{secret}/versions/latest:access" + headers = { + "Authorization": f"Bearer {access_token}" + } + + try: + response = requests.get(url, headers=headers, timeout=10) + response.raise_for_status() + + data = response.json() + payload = data.get('payload', {}) + + + if 'data' in payload: + import base64 + decoded = base64.b64decode(payload['data']).decode('utf-8') + return decoded, False + + return None, False + + + except requests.exceptions.RequestException as e: + print(f"[!] Error retrieving secret: {secret_name}: {e}", file=sys.stderr) + return None, False + +def main(): + + parser = argparse.ArgumentParser( + description='Enumerating GCP Secrets Across All Locations', + formatter_class = argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + '-p','--project', required=True, help='GCP Project ID' + ) + parser.add_argument( + '-t','--token', help='Access Token (String)' + ) + parser.add_argument( + '-f','--token-file', help='Access Token File' + ) + parser.add_argument( + '-s', '--service-account-key',help='Service Account Key JSON File' + ) + + parser.add_argument( + '-r', '--retrieve', action='store_true', help='Retrieve Secret Values' + ) + + parser.add_argument( + '-v', '--version', default='latest', help='Secret Version to retrieve. (default: Latest)' + ) + + args = parser.parse_args() + + if args.token: + access_token = args.token + elif args.token_file: + try: + with open(args.token_file, 'r') as f: + access_token = f.read().strip() + except IOError as e: + print(f"[!] Error occured while reading token from file: {e}", file=sys.stderr) + sys.exit(1) + else: + try: + access_token = get_access_token_from_service_account(args.service_account_key) + except Exception as e: + print(f"[!] Error occured while getting token from service Account: {e}", file=sys.stderr) + sys.exit(1) + + + regions_to_scan = REGIONS + + + all_secrets = [] + total_secrets = 0 + secretName = None + + print("[*] Hunting Secrets...") + + for region in regions_to_scan: + secrets = scanRegionsForSecrets(args.project, region, access_token) + + if secrets: + for secret in secrets: + if args.retrieve: + value, is_binary = retrieveFoundSecrets(secret, access_token, args.version) + if value is not None: + secretName = value + + + all_secrets.append(secretName) + + print(f"[*] secret found: {secret}") + total_secrets += len(secrets) + + if args.retrieve: + print("[*] Retrieving Value") + if all_secrets: + for s in all_secrets: + print(f"\n{s}") + + if total_secrets == 0: + print("[!] No Secrets Found") + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b5d334d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +google-auth +requests -- cgit v1.2.3