Initial server source import

This commit is contained in:
sashatrask
2026-09-30 20:30:56 +03:00
commit 170dd941b9
498 changed files with 261563 additions and 0 deletions
+789
View File
@@ -0,0 +1,789 @@
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()