import sys sys.dont_write_bytecode = True import argparse import base64 import binascii import hashlib import json import os import re import time from urllib.parse import urlsplit import requests sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from keycheck_common import ( append_jsonl, append_status, commit_status_transaction, default_input_file, default_proxy_file, ensure_output_files, iter_findings, keycheck_input_mode, load_checked_statuses, load_known_keys, load_known_statuses, load_proxies, mask_secret, recover_status_transaction, request_error_message, record_cached_keycheck_occurrence, record_validation_result, require_provider_authority, service_output_dir, should_skip_key, write_keycheck_event, ) SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PARENT_DIR = os.path.dirname(SCRIPT_DIR) SERVICE = "gcp" OUTPUT_DIR = os.getenv("KEYCHECK_OUTPUT_DIR") or service_output_dir(SERVICE) INPUT_FILE = os.getenv("KEYCHECK_INPUT_FILE") or default_input_file() PROXY_FILE = os.getenv("KEYCHECK_PROXY_FILE") or default_proxy_file() PLAIN_FILE = os.path.join(OUTPUT_DIR, "gcp.txt") CHECKED_FILE = os.path.join(OUTPUT_DIR, "gcpChecked.txt") RESULTS_FILE = os.path.join(OUTPUT_DIR, "gcpResults.jsonl") STATUS_FILES = { "VALID": os.path.join(OUTPUT_DIR, "gcpAlive.txt"), "VERTEX": os.path.join(OUTPUT_DIR, "gcpVertex.txt"), "DEAD": os.path.join(OUTPUT_DIR, "gcpDead.txt"), "RESTRICTED": os.path.join(OUTPUT_DIR, "gcpRestricted.txt"), "NETWORK": os.path.join(OUTPUT_DIR, "gcpNetwork.txt"), "UNKNOWN": os.path.join(OUTPUT_DIR, "gcpUnknown.txt"), "NO_CONTEXT": os.path.join(OUTPUT_DIR, "gcpNoContext.txt"), } VERTEX_GEMINI_FILE = os.path.join(OUTPUT_DIR, "gcpVertexGemini.txt") VERTEX_ANTHROPIC_FILE = os.path.join(OUTPUT_DIR, "gcpVertexAnthropic.txt") GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token" TRUSTED_GOOGLE_TOKEN_ENDPOINTS = frozenset({ GOOGLE_TOKEN_URL, "https://accounts.google.com/o/oauth2/token", }) TOKEN_REDIRECT_STATUSES = {301, 302, 303, 307, 308} SA_SCOPE = "https://www.googleapis.com/auth/cloud-platform" MAX_PEM_BYTES = 24 * 1024 MAX_DER_BYTES = 16 * 1024 MAX_DER_LENGTH_BYTES = 2 MIN_RSA_BITS = 2048 MAX_RSA_BITS = 8192 MAX_RSA_INTEGER_BYTES = MAX_RSA_BITS // 8 RSA_ENCRYPTION_OID = bytes.fromhex("2a864886f70d010101") VERTEX_LOCATIONS = ["global", "us", "eu"] VERTEX_MODELS = ["gemini-3.6-flash", "gemini-3.1-pro-preview"] VERTEX_ANTHROPIC_LOCATIONS = ["global", "us", "eu", "us-east5", "europe-west1"] VERTEX_ANTHROPIC_MODELS = ["claude-opus-5", "claude-opus-4-7", "claude-opus-4-6", "claude-fable-5"] def transaction_status_files(): return { **STATUS_FILES, "RATE_LIMITED": STATUS_FILES["UNKNOWN"], "AUX_VERTEX_GEMINI": VERTEX_GEMINI_FILE, "AUX_VERTEX_ANTHROPIC": VERTEX_ANTHROPIC_FILE, } def ensure_files(): ensure_output_files([CHECKED_FILE, RESULTS_FILE, *STATUS_FILES.values(), VERTEX_GEMINI_FILE, VERTEX_ANTHROPIC_FILE, PLAIN_FILE]) recover_status_transaction(CHECKED_FILE, transaction_status_files()) def b64url(data): return base64.urlsafe_b64encode(data).rstrip(b"=").decode() def validate_google_token_uri(value): token_uri = str(value or GOOGLE_TOKEN_URL).strip() try: parsed = urlsplit(token_uri) port = parsed.port except (TypeError, ValueError) as exc: raise ValueError("invalid Google OAuth token_uri") from exc if ( parsed.scheme != "https" or parsed.username is not None or parsed.password is not None or port not in (None, 443) or parsed.query or parsed.fragment or token_uri not in TRUSTED_GOOGLE_TOKEN_ENDPOINTS ): raise ValueError("untrusted Google OAuth token_uri") return token_uri class InvalidRSAPrivateKey(ValueError): pass class DERReader: def __init__(self, data): if not isinstance(data, (bytes, bytearray, memoryview)): raise InvalidRSAPrivateKey("DER value is not binary") if len(data) > MAX_DER_BYTES: raise InvalidRSAPrivateKey("DER value exceeds size limit") self.data = data self.pos = 0 def read_tlv(self): if len(self.data) - self.pos < 2: raise InvalidRSAPrivateKey("truncated DER tag or length") tag = self.data[self.pos] self.pos += 1 first_len = self.data[self.pos] self.pos += 1 if first_len & 0x80: length_len = first_len & 0x7F if length_len == 0: raise InvalidRSAPrivateKey("indefinite DER length is not allowed") if length_len > MAX_DER_LENGTH_BYTES: raise InvalidRSAPrivateKey("DER length-of-length exceeds limit") if len(self.data) - self.pos < length_len: raise InvalidRSAPrivateKey("truncated DER length") length_bytes = self.data[self.pos:self.pos + length_len] if length_bytes[0] == 0: raise InvalidRSAPrivateKey("non-minimal DER length") length = int.from_bytes(length_bytes, "big") self.pos += length_len if length < 0x80: raise InvalidRSAPrivateKey("non-minimal DER length") else: length = first_len if length > MAX_DER_BYTES: raise InvalidRSAPrivateKey("DER value length exceeds limit") if length > len(self.data) - self.pos: raise InvalidRSAPrivateKey("truncated DER value") value = self.data[self.pos:self.pos + length] self.pos += length return tag, value def expect(self, tag): actual, value = self.read_tlv() if actual != tag: raise InvalidRSAPrivateKey(f"expected DER tag {tag:#x}, got {actual:#x}") return value def at_end(self): return self.pos == len(self.data) def require_eof(self, context="DER structure"): if not self.at_end(): raise InvalidRSAPrivateKey(f"trailing data in {context}") def der_int(value, name="integer", max_bytes=MAX_RSA_INTEGER_BYTES): if not value: raise InvalidRSAPrivateKey(f"empty RSA {name}") if len(value) > max_bytes + 1: raise InvalidRSAPrivateKey(f"RSA {name} exceeds size limit") if value[0] & 0x80: raise InvalidRSAPrivateKey(f"negative RSA {name}") if value[0] == 0: if len(value) > 1 and not value[1] & 0x80: raise InvalidRSAPrivateKey(f"non-minimal RSA {name}") unsigned = value[1:] else: unsigned = value if len(unsigned) > max_bytes: raise InvalidRSAPrivateKey(f"RSA {name} exceeds size limit") return int.from_bytes(unsigned, "big") if unsigned else 0 def parse_pkcs1_rsa_private_key(data): rsa = DERReader(data) version = der_int(rsa.expect(0x02), "version", 1) if version != 0: raise InvalidRSAPrivateKey("unsupported RSA private key version") n = der_int(rsa.expect(0x02), "modulus") public_exponent = der_int(rsa.expect(0x02), "public exponent") d = der_int(rsa.expect(0x02), "private exponent") for name in ("prime1", "prime2", "exponent1", "exponent2", "coefficient"): der_int(rsa.expect(0x02), name) rsa.require_eof("RSA private key") modulus_bits = n.bit_length() if not MIN_RSA_BITS <= modulus_bits <= MAX_RSA_BITS: raise InvalidRSAPrivateKey( f"RSA modulus must be between {MIN_RSA_BITS} and {MAX_RSA_BITS} bits" ) if public_exponent == 0: raise InvalidRSAPrivateKey("RSA public exponent is zero") if d == 0 or d >= n: raise InvalidRSAPrivateKey("RSA private exponent is out of range") return n, d def validate_rsa_algorithm_identifier(data): algorithm = DERReader(data) if algorithm.expect(0x06) != RSA_ENCRYPTION_OID: raise InvalidRSAPrivateKey("PKCS#8 key does not use rsaEncryption") if not algorithm.at_end() and algorithm.expect(0x05): raise InvalidRSAPrivateKey("invalid rsaEncryption parameters") algorithm.require_eof("PKCS#8 algorithm identifier") def parse_rsa_private_key_from_pem(pem): pem = str(pem or "") if len(pem) > MAX_PEM_BYTES: raise InvalidRSAPrivateKey("PEM private key exceeds size limit") try: pem_bytes = pem.encode("utf-8") except UnicodeEncodeError as exc: raise InvalidRSAPrivateKey("PEM private key is not valid UTF-8") from exc if len(pem_bytes) > MAX_PEM_BYTES: raise InvalidRSAPrivateKey("PEM private key exceeds size limit") pem = pem.replace("\\n", "\n") match = re.fullmatch( r"\s*-----BEGIN (RSA PRIVATE KEY|PRIVATE KEY)-----\s*(.*?)\s*-----END \1-----\s*", pem, re.DOTALL, ) if not match: raise InvalidRSAPrivateKey("missing complete PEM private key block") body = re.sub(r"\s+", "", match.group(2)) if len(body) < 256: raise InvalidRSAPrivateKey("PEM private key body is too short") try: der = base64.b64decode(body + ("=" * (-len(body) % 4)), validate=True) except (binascii.Error, ValueError) as exc: raise InvalidRSAPrivateKey("invalid PEM base64") from exc if len(der) > MAX_DER_BYTES: raise InvalidRSAPrivateKey("DER private key exceeds size limit") reader = DERReader(der) top_bytes = reader.expect(0x30) reader.require_eof("DER private key") top = DERReader(top_bytes) # PKCS#8 PrivateKeyInfo: SEQUENCE(version, alg, OCTET STRING(RSAPrivateKey)) first_tag, first_val = top.read_tlv() if first_tag != 0x02: raise InvalidRSAPrivateKey("unexpected private key structure") second_tag, second_val = top.read_tlv() if second_tag == 0x30: if der_int(first_val, "PKCS#8 version", 1) != 0: raise InvalidRSAPrivateKey("unsupported PKCS#8 version") validate_rsa_algorithm_identifier(second_val) private_octet = top.expect(0x04) top.require_eof("PKCS#8 private key") wrapped = DERReader(private_octet) rsa_bytes = wrapped.expect(0x30) wrapped.require_eof("PKCS#8 private key octets") return parse_pkcs1_rsa_private_key(rsa_bytes) if second_tag != 0x02: raise InvalidRSAPrivateKey("unexpected private key structure") return parse_pkcs1_rsa_private_key(top_bytes) def rsa_pkcs1v15_sha256_sign(message, pem): n, d = parse_rsa_private_key_from_pem(pem) digest = hashlib.sha256(message).digest() digest_info = bytes.fromhex("3031300d060960864801650304020105000420") + digest key_len = (n.bit_length() + 7) // 8 if key_len < len(digest_info) + 11: raise InvalidRSAPrivateKey("RSA key too small") encoded = b"\x00\x01" + b"\xff" * (key_len - len(digest_info) - 3) + b"\x00" + digest_info sig = pow(int.from_bytes(encoded, "big"), d, n).to_bytes(key_len, "big") return sig def make_service_account_assertion(creds): now = int(time.time()) token_uri = validate_google_token_uri(creds.get("token_uri")) header = {"alg": "RS256", "typ": "JWT", "kid": creds.get("private_key_id")} payload = { "iss": creds["client_email"], "scope": SA_SCOPE, "aud": token_uri, "iat": now, "exp": now + 3600, } signing_input = (b64url(json.dumps(header, separators=(",", ":")).encode()) + "." + b64url(json.dumps(payload, separators=(",", ":")).encode())).encode() signature = rsa_pkcs1v15_sha256_sign(signing_input, creds["private_key"]) return signing_input.decode() + "." + b64url(signature), token_uri def compact_json(data): return json.dumps(data, ensure_ascii=False, separators=(",", ":"), sort_keys=True) def scanner_context_text(finding): context = finding.get("ScannerContext") if isinstance(finding, dict) else None if isinstance(context, dict): return str(context.get("nearby") or "") return "" def parse_json_object(text): try: return json.loads(text) except (TypeError, ValueError): pass match = re.search(r"\{.*\}", str(text or ""), re.DOTALL) if not match: return None try: return json.loads(match.group(0)) except ValueError: return None def parse_service_account(raw_v2): data = parse_json_object(raw_v2) if not isinstance(data, dict): return None required = ["client_email", "private_key", "private_key_id"] if not all(data.get(item) for item in required): return None private_key = str(data.get("private_key") or "") if "-----BEGIN" not in private_key or "-----END" not in private_key or len(private_key) < 800: return None return data def parse_adc(raw_v2, finding): data = parse_json_object(scanner_context_text(finding)) or parse_json_object(raw_v2) if not isinstance(data, dict): return None required = ["client_id", "client_secret", "refresh_token"] if not all(data.get(item) for item in required): return None return data def extract_candidates(input_file, plain_file): seen_plain = set() for item in iter_findings(input_file, ["GCP", "GCPApplicationDefaultCredentials"]): if item["detector"] == "GCP": parsed = parse_service_account(item["raw_v2"]) detector = "GCP" else: parsed = parse_adc(item["raw_v2"], item["finding"]) detector = "GCPApplicationDefaultCredentials" if not parsed: key = f"{detector}:no_context:{item['source']}" yield key, detector, item["source"], item["finding"], None continue key = compact_json(parsed) yield key, detector, item["source"], item["finding"], parsed if keycheck_input_mode() == 'jsonl' and os.path.exists(plain_file): with open(plain_file, "r", encoding="utf-8", errors="replace") as f: content = f.read() candidates = [] whole_file = parse_json_object(content) if whole_file: candidates.append((plain_file, whole_file)) for line_num, line in enumerate(content.splitlines(), 1): key_text = line.split("\t", 1)[0].strip() parsed = parse_json_object(key_text) or parse_json_object(line) if parsed: candidates.append((f"{plain_file}:{line_num}", parsed)) for source, parsed in candidates: detector = "GCP" if parsed.get("private_key") else "GCPApplicationDefaultCredentials" key = compact_json(parsed) if key not in seen_plain: seen_plain.add(key) yield key, detector, source, {}, parsed def vertex_api_host(location): location = str(location or "").strip().lower() if location == "global": return "aiplatform.googleapis.com" if location in ("us", "eu"): return f"aiplatform.{location}.rep.googleapis.com" return f"{location}-aiplatform.googleapis.com" def probe_vertex_llm(access_token, project_id, proxy, timeout, locations=None, models=None, max_attempts=2): if not project_id: return {"enabled": False, "message": "project_id unavailable"} headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"} payload = {"contents": [{"role": "user", "parts": [{"text": "ping"}]}]} attempts = [] accepted = [] tried = 0 for location in (locations or VERTEX_LOCATIONS): for model in (models or VERTEX_MODELS): if max_attempts and tried >= max_attempts: if accepted: first = accepted[0] return { "enabled": True, "location": first.get("location", ""), "model": first.get("model", ""), "total_tokens": first.get("total_tokens"), "available_models": [f"google/{item['location']}/{item['model']}" for item in accepted], "message": "Vertex countTokens accepted", } return {"enabled": False, "message": "; ".join(attempts[:8]) or "Vertex probe attempt limit reached"} tried += 1 url = f"https://{vertex_api_host(location)}/v1/projects/{project_id}/locations/{location}/publishers/google/models/{model}:countTokens" try: response = requests.post(url, headers=headers, json=payload, proxies=proxy, timeout=timeout) except requests.RequestException as exc: attempts.append(f"{location}:{model}:network:{str(exc)[:120]}") continue if response.status_code == 200: data = response.json() accepted.append({ "location": location, "model": model, "total_tokens": data.get("totalTokens") or data.get("total_tokens"), }) continue message = request_error_message(response) if response.status_code in (400, 401, 403, 404, 429): attempts.append(f"{location}:{model}:http_{response.status_code}:{message[:160]}") continue if response.status_code >= 500: attempts.append(f"{location}:{model}:server_{response.status_code}:{message[:160]}") continue attempts.append(f"{location}:{model}:http_{response.status_code}:{message[:160]}") if accepted: first = accepted[0] return { "enabled": True, "location": first.get("location", ""), "model": first.get("model", ""), "total_tokens": first.get("total_tokens"), "available_models": [f"google/{item['location']}/{item['model']}" for item in accepted], "message": "Vertex countTokens accepted", } return {"enabled": False, "message": "; ".join(attempts[:8])} def probe_vertex_anthropic(access_token, project_id, proxy, timeout, locations=None, models=None, max_attempts=2): if not project_id: return {"enabled": False, "message": "project_id unavailable"} models = models or [] if not models: return {"enabled": False, "message": "no Anthropic models configured"} headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"} payload = { "anthropic_version": "vertex-2023-10-16", "messages": [{"role": "user", "content": "ping"}], "max_tokens": 1, } attempts = [] accepted = [] tried = 0 for location in (locations or VERTEX_ANTHROPIC_LOCATIONS): for model in models: if max_attempts and tried >= max_attempts: if accepted: first = accepted[0] return { "enabled": True, "location": first.get("location", ""), "model": first.get("model", ""), "available_models": [f"anthropic/{item['location']}/{item['model']}" for item in accepted], "message": "Vertex Anthropic rawPredict accepted", } return {"enabled": False, "message": "; ".join(attempts[:8]) or "Vertex Anthropic probe attempt limit reached"} tried += 1 url = f"https://{vertex_api_host(location)}/v1/projects/{project_id}/locations/{location}/publishers/anthropic/models/{model}:rawPredict" try: response = requests.post(url, headers=headers, json=payload, proxies=proxy, timeout=timeout) except requests.RequestException as exc: attempts.append(f"{location}:{model}:network:{str(exc)[:120]}") continue if response.status_code == 200: accepted.append({"location": location, "model": model}) continue message = request_error_message(response) if response.status_code in (400, 401, 403, 404, 429): attempts.append(f"{location}:{model}:http_{response.status_code}:{message[:160]}") continue if response.status_code >= 500: attempts.append(f"{location}:{model}:server_{response.status_code}:{message[:160]}") continue attempts.append(f"{location}:{model}:http_{response.status_code}:{message[:160]}") if accepted: first = accepted[0] return { "enabled": True, "location": first.get("location", ""), "model": first.get("model", ""), "available_models": [f"anthropic/{item['location']}/{item['model']}" for item in accepted], "message": "Vertex Anthropic rawPredict accepted", } return {"enabled": False, "message": "; ".join(attempts[:8])} def merge_vertex_results(google_vertex, anthropic_vertex): google_vertex = google_vertex or {"enabled": False, "message": ""} anthropic_vertex = anthropic_vertex or {"enabled": False, "message": ""} available = [] available.extend(google_vertex.get("available_models") or []) available.extend(anthropic_vertex.get("available_models") or []) first = google_vertex if google_vertex.get("enabled") else anthropic_vertex if anthropic_vertex.get("enabled") else {} messages = [] if google_vertex.get("message"): messages.append(f"google: {google_vertex.get('message')}") if anthropic_vertex.get("message"): messages.append(f"anthropic: {anthropic_vertex.get('message')}") return { "enabled": bool(available), "location": first.get("location", ""), "model": first.get("model", ""), "total_tokens": first.get("total_tokens"), "available_models": available, "google_enabled": bool(google_vertex.get("enabled")), "anthropic_enabled": bool(anthropic_vertex.get("enabled")), "message": "; ".join(messages), } def check_service_account(creds, proxy, timeout, probe_vertex=False, vertex_timeout=6, vertex_locations=None, vertex_models=None, vertex_max_attempts=2, vertex_anthropic_locations=None, vertex_anthropic_models=None, vertex_anthropic_max_attempts=2): try: assertion, token_uri = make_service_account_assertion(creds) except InvalidRSAPrivateKey as exc: return { "status": "DEAD", "classification": "invalid_private_key", "message": f"invalid RSA private key: {exc}", "project_id": creds.get("project_id"), "client_email": creds.get("client_email"), } except Exception as exc: return {"status": "UNKNOWN", "message": f"failed to build JWT assertion: {exc}"} data = {"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": assertion} try: response = requests.post( token_uri, data=data, proxies=proxy, timeout=timeout, allow_redirects=False, ) except requests.RequestException as exc: return {"status": "NETWORK", "message": str(exc), "project_id": creds.get("project_id"), "client_email": creds.get("client_email")} if response.status_code in TOKEN_REDIRECT_STATUSES: return { "status": "UNKNOWN", "http_status": response.status_code, "message": "Google OAuth token endpoint redirect refused", "project_id": creds.get("project_id"), "client_email": creds.get("client_email"), } if response.status_code == 200: payload = response.json() result = { "status": "VALID", "message": "OAuth token issued", "project_id": creds.get("project_id"), "client_email": creds.get("client_email"), "private_key_id": creds.get("private_key_id"), "expires_in": payload.get("expires_in"), } if probe_vertex: google_vertex = probe_vertex_llm( payload.get("access_token"), creds.get("project_id"), proxy, vertex_timeout, vertex_locations, vertex_models, vertex_max_attempts, ) anthropic_vertex = probe_vertex_anthropic( payload.get("access_token"), creds.get("project_id"), proxy, vertex_timeout, vertex_anthropic_locations, vertex_anthropic_models, vertex_anthropic_max_attempts, ) if vertex_anthropic_models else {"enabled": False, "message": ""} vertex = merge_vertex_results(google_vertex, anthropic_vertex) result.update({ "vertex_enabled": vertex.get("enabled"), "vertex_location": vertex.get("location", ""), "vertex_model": vertex.get("model", ""), "vertex_available_models": vertex.get("available_models") or [], "vertex_google_enabled": vertex.get("google_enabled"), "vertex_anthropic_enabled": vertex.get("anthropic_enabled"), "vertex_total_tokens": vertex.get("total_tokens"), "vertex_message": vertex.get("message", ""), }) if vertex.get("enabled"): result["status"] = "VERTEX" result["message"] = "OAuth token issued; Vertex countTokens accepted" return result message = request_error_message(response) lower = message.lower() if response.status_code in (400, 401) and any(item in lower for item in ("invalid_grant", "invalid_client", "invalid jwt", "invalid signature")): return {"status": "DEAD", "http_status": response.status_code, "message": message, "project_id": creds.get("project_id"), "client_email": creds.get("client_email")} if response.status_code in (401, 403): return {"status": "RESTRICTED", "http_status": response.status_code, "message": message, "project_id": creds.get("project_id"), "client_email": creds.get("client_email")} if response.status_code == 429: return {"status": "RATE_LIMITED", "http_status": 429, "message": message, "project_id": creds.get("project_id"), "client_email": creds.get("client_email")} if response.status_code >= 500: return {"status": "NETWORK", "http_status": response.status_code, "message": message, "project_id": creds.get("project_id"), "client_email": creds.get("client_email")} return {"status": "UNKNOWN", "http_status": response.status_code, "message": message, "project_id": creds.get("project_id"), "client_email": creds.get("client_email")} def check_adc(creds, proxy, timeout, probe_vertex=False, vertex_timeout=6, vertex_locations=None, vertex_models=None, vertex_max_attempts=2, vertex_anthropic_locations=None, vertex_anthropic_models=None, vertex_anthropic_max_attempts=2): try: token_uri = validate_google_token_uri(creds.get("token_uri")) except ValueError as exc: return {"status": "UNKNOWN", "message": str(exc), "client_id": creds.get("client_id")} data = { "client_id": creds["client_id"], "client_secret": creds["client_secret"], "refresh_token": creds["refresh_token"], "grant_type": "refresh_token", } try: response = requests.post( token_uri, data=data, proxies=proxy, timeout=timeout, allow_redirects=False, ) except requests.RequestException as exc: return {"status": "NETWORK", "message": str(exc), "client_id": creds.get("client_id")} if response.status_code in TOKEN_REDIRECT_STATUSES: return { "status": "UNKNOWN", "http_status": response.status_code, "message": "Google OAuth token endpoint redirect refused", "client_id": creds.get("client_id"), } if response.status_code == 200: payload = response.json() project_id = creds.get("quota_project_id") or creds.get("project_id") result = {"status": "VALID", "message": "refresh token accepted", "client_id": creds.get("client_id"), "project_id": project_id, "expires_in": payload.get("expires_in")} if probe_vertex: google_vertex = probe_vertex_llm( payload.get("access_token"), project_id, proxy, vertex_timeout, vertex_locations, vertex_models, vertex_max_attempts, ) anthropic_vertex = probe_vertex_anthropic( payload.get("access_token"), project_id, proxy, vertex_timeout, vertex_anthropic_locations, vertex_anthropic_models, vertex_anthropic_max_attempts, ) if vertex_anthropic_models else {"enabled": False, "message": ""} vertex = merge_vertex_results(google_vertex, anthropic_vertex) result.update({ "vertex_enabled": vertex.get("enabled"), "vertex_location": vertex.get("location", ""), "vertex_model": vertex.get("model", ""), "vertex_available_models": vertex.get("available_models") or [], "vertex_google_enabled": vertex.get("google_enabled"), "vertex_anthropic_enabled": vertex.get("anthropic_enabled"), "vertex_total_tokens": vertex.get("total_tokens"), "vertex_message": vertex.get("message", ""), }) if vertex.get("enabled"): result["status"] = "VERTEX" result["message"] = "refresh token accepted; Vertex countTokens accepted" return result message = request_error_message(response) lower = message.lower() if response.status_code in (400, 401) and any(item in lower for item in ("invalid_grant", "invalid_client", "unauthorized_client")): return {"status": "DEAD", "http_status": response.status_code, "message": message, "client_id": creds.get("client_id")} if response.status_code in (401, 403): return {"status": "RESTRICTED", "http_status": response.status_code, "message": message, "client_id": creds.get("client_id")} if response.status_code == 429: return {"status": "RATE_LIMITED", "http_status": 429, "message": message, "client_id": creds.get("client_id")} if response.status_code >= 500: return {"status": "NETWORK", "http_status": response.status_code, "message": message, "client_id": creds.get("client_id")} return {"status": "UNKNOWN", "http_status": response.status_code, "message": message, "client_id": creds.get("client_id")} def write_result(key, detector, result, source, finding): write_keycheck_event(SERVICE, RESULTS_FILE, key, {"detector": detector, **result}, source, finding, detector) extra = result.get("client_email") or result.get("client_id") or result.get("project_id") or source message = result.get("message", "") if result.get("status") == "VERTEX": models = result.get("vertex_available_models") or [] model_text = ",".join(str(item) for item in models) or f"{result.get('vertex_location', '')}/{result.get('vertex_model', '')}".strip("/") message = f"{message}; models={model_text}" commit_status_transaction( CHECKED_FILE, transaction_status_files(), key, result["status"], message, extra, ) if result.get("status") == "VERTEX" and result.get("vertex_google_enabled"): append_status(VERTEX_GEMINI_FILE, key, result["status"], message, extra) if result.get("status") == "VERTEX" and result.get("vertex_anthropic_enabled"): append_status(VERTEX_ANTHROPIC_FILE, key, result["status"], message, extra) record_validation_result(SERVICE, key, {"detector": detector, **result}, source, finding, detector) def parse_args(): parser = argparse.ArgumentParser(description="GCP credential checker") parser.add_argument("--input", default=INPUT_FILE) parser.add_argument("--plain", default=PLAIN_FILE) parser.add_argument("--proxy-file", default=PROXY_FILE) parser.add_argument("--timeout", type=int, default=25) parser.add_argument("--max-keys", type=int, default=0) parser.add_argument("--retry-network", action="store_true") parser.add_argument("--retry-limited", action="store_true") parser.add_argument("--retry-unknown", action="store_true") parser.add_argument("--retry-valid", action="store_true") parser.add_argument("--recheck-all", action="store_true") parser.add_argument("--probe-vertex", action="store_true", help="After OAuth succeeds, probe Vertex AI Gemini with countTokens through the configured proxy") parser.add_argument("--vertex-timeout", type=int, default=6, help="Seconds per Vertex countTokens request") parser.add_argument("--vertex-max-attempts", type=int, default=6, help="Maximum location/model countTokens attempts per credential") parser.add_argument("--vertex-locations", default=",".join(VERTEX_LOCATIONS), help="Comma-separated Vertex locations to probe") parser.add_argument("--vertex-models", default=",".join(VERTEX_MODELS), help="Comma-separated Vertex models to probe") parser.add_argument("--vertex-anthropic-locations", default=",".join(VERTEX_ANTHROPIC_LOCATIONS), help="Comma-separated Vertex Anthropic locations to probe") parser.add_argument("--vertex-anthropic-models", default=",".join(VERTEX_ANTHROPIC_MODELS), help="Comma-separated Vertex Anthropic model IDs to probe with rawPredict") parser.add_argument("--vertex-anthropic-max-attempts", type=int, default=20, help="Maximum Anthropic location/model attempts per credential") return parser.parse_args() def main(): require_provider_authority(SERVICE) args = parse_args() ensure_files() proxy_cycler = load_proxies(args.proxy_file) checked = load_checked_statuses(CHECKED_FILE) known_statuses = load_known_statuses(CHECKED_FILE, STATUS_FILES) known = set(known_statuses) retry_statuses = set() if args.retry_network: retry_statuses.add("NETWORK") if args.retry_limited: retry_statuses.add("RATE_LIMITED") if args.retry_unknown: retry_statuses.add("UNKNOWN") if args.retry_valid: retry_statuses.update({"VALID", "VERTEX"}) vertex_locations = [item.strip() for item in str(args.vertex_locations or "").split(",") if item.strip()] vertex_models = [item.strip() for item in str(args.vertex_models or "").split(",") if item.strip()] vertex_anthropic_locations = [item.strip() for item in str(args.vertex_anthropic_locations or "").split(",") if item.strip()] vertex_anthropic_models = [item.strip() for item in str(args.vertex_anthropic_models or "").split(",") if item.strip()] processed = 0 skipped = 0 for key, detector, source, finding, parsed in extract_candidates(args.input, args.plain): if should_skip_key( key, checked, known, args, retry_statuses, service=SERVICE, source=source, finding=finding, detector=detector, known_statuses=known_statuses, ): skipped += 1 continue if args.max_keys and processed >= args.max_keys: break processed += 1 print(f"\n[{processed}] {detector} candidate {mask_secret(key)} from {source}", flush=True) proxy = next(proxy_cycler) if proxy_cycler else None if not parsed: result = {"status": "NO_CONTEXT", "message": "credential JSON is incomplete or unavailable"} elif detector == "GCP": result = check_service_account( parsed, proxy, args.timeout, args.probe_vertex, args.vertex_timeout, vertex_locations, vertex_models, args.vertex_max_attempts, vertex_anthropic_locations, vertex_anthropic_models, args.vertex_anthropic_max_attempts, ) else: result = check_adc( parsed, proxy, args.timeout, args.probe_vertex, args.vertex_timeout, vertex_locations, vertex_models, args.vertex_max_attempts, vertex_anthropic_locations, vertex_anthropic_models, args.vertex_anthropic_max_attempts, ) print(f" STATUS: {result['status']} | {str(result.get('message', ''))[:200]}", flush=True) write_result(key, detector, result, source, finding) known.add(key) checked[key] = result["status"] time.sleep(0.1) print(f"\nDone. Processed={processed}, skipped={skipped}, results={RESULTS_FILE}") if __name__ == "__main__": main()