import sys sys.dont_write_bytecode = True import argparse import base64 import json import os import re import time import requests sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from keycheck_common import ( append_jsonl, commit_status_transaction, default_input_file, default_proxy_file, ensure_output_files, iter_findings, keycheck_input_mode, load_checked_statuses, load_known_keys, load_proxies, mask_secret, recover_status_transaction, request_error_message, 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 = "dockerhub" 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, "dockerhub.txt") CHECKED_FILE = os.path.join(OUTPUT_DIR, "dockerhubChecked.txt") RESULTS_FILE = os.path.join(OUTPUT_DIR, "dockerhubResults.jsonl") STATUS_FILES = { "VALID": os.path.join(OUTPUT_DIR, "dockerhubAlive.txt"), "VALID_2FA": os.path.join(OUTPUT_DIR, "dockerhubAlive.txt"), "DEAD": os.path.join(OUTPUT_DIR, "dockerhubDead.txt"), "NO_USERNAME": os.path.join(OUTPUT_DIR, "dockerhubNoUsername.txt"), "RATE_LIMITED": os.path.join(OUTPUT_DIR, "dockerhubRateLimited.txt"), "NETWORK": os.path.join(OUTPUT_DIR, "dockerhubNetwork.txt"), "UNKNOWN": os.path.join(OUTPUT_DIR, "dockerhubUnknown.txt"), } DOCKER_PAT_RE = re.compile(r"\bdckr_pat_[A-Za-z0-9_-]{27}\b") def ensure_files(): ensure_output_files([CHECKED_FILE, RESULTS_FILE, *set(STATUS_FILES.values()), PLAIN_FILE]) recover_status_transaction(CHECKED_FILE, STATUS_FILES) def candidate_key(username, token): return f"{username}:{token}" if username else token def parse_username_token(raw, raw_v2, finding): raw = raw or "" raw_v2 = raw_v2 or "" token = "" username = "" if ":" in raw_v2: maybe_user, maybe_token = raw_v2.split(":", 1) if DOCKER_PAT_RE.fullmatch(maybe_token): username, token = maybe_user.strip(), maybe_token.strip() if not token: match = DOCKER_PAT_RE.search(raw_v2) or DOCKER_PAT_RE.search(raw) if match: token = match.group(0) extra = finding.get("ExtraData") if isinstance(finding, dict) else {} analysis = finding.get("AnalysisInfo") if isinstance(finding, dict) else {} if isinstance(extra, dict): username = username or extra.get("hub_username") or "" if isinstance(analysis, dict): username = username or analysis.get("username") or "" return username, token def extract_candidates(input_file, plain_file): seen_plain = set() for item in iter_findings(input_file, ["Dockerhub"]): username, token = parse_username_token(item.get("raw"), item.get("raw_v2"), item.get("finding") or {}) if not token: continue key = candidate_key(username, token) yield key, username, token, item["source"], item["finding"] if keycheck_input_mode() == 'jsonl' and os.path.exists(plain_file): with open(plain_file, "r", encoding="utf-8") as f: for line_num, line in enumerate(f, 1): line = line.strip() if not line: continue username = "" token = "" if ":" in line: maybe_user, rest = line.split(":", 1) match = DOCKER_PAT_RE.search(rest) if match: username, token = maybe_user.strip(), match.group(0) else: match = DOCKER_PAT_RE.search(line) if match: token = match.group(0) if not token: continue key = candidate_key(username, token) if key not in seen_plain: seen_plain.add(key) yield key, username, token, f"{plain_file}:{line_num}", {} def decode_jwt_payload(jwt_token): try: payload = jwt_token.split(".")[1] payload += "=" * (-len(payload) % 4) return json.loads(base64.urlsafe_b64decode(payload.encode()).decode()) except Exception: return {} def check_token(username, token, proxy, timeout): if not username: return {"status": "NO_USERNAME", "message": "DockerHub PAT requires username/email for login check"} url = "https://hub.docker.com/v2/users/login" payload = {"username": username, "password": token} try: response = requests.post(url, json=payload, proxies=proxy, timeout=timeout) except requests.RequestException as exc: return {"status": "NETWORK", "message": str(exc), "username": username} message = request_error_message(response) if response.status_code == 200: data = response.json() hub_token = data.get("token", "") claims = decode_jwt_payload(hub_token) if hub_token else {} hub_claims = claims.get("https://hub.docker.com", {}) if isinstance(claims, dict) else {} return { "status": "VALID", "message": "login accepted", "username": username, "hub_username": hub_claims.get("username", username), "hub_email": hub_claims.get("email", ""), "scope": claims.get("scope", "") if isinstance(claims, dict) else "", } if response.status_code == 401: try: data = response.json() except ValueError: data = {} if data.get("login_2fa_token"): return {"status": "VALID_2FA", "message": "credentials accepted; 2FA required", "username": username} return {"status": "DEAD", "http_status": 401, "message": message, "username": username} if response.status_code == 429: return {"status": "RATE_LIMITED", "http_status": 429, "message": message, "username": username} if response.status_code >= 500: return {"status": "NETWORK", "http_status": response.status_code, "message": message, "username": username} return {"status": "UNKNOWN", "http_status": response.status_code, "message": message, "username": username} def write_result(key, result, source, finding): write_keycheck_event(SERVICE, RESULTS_FILE, key, result, source, finding, "Dockerhub") extra = result.get("hub_username") or result.get("username") or source commit_status_transaction( CHECKED_FILE, STATUS_FILES, key, result["status"], result.get("message", ""), extra, ) record_validation_result(SERVICE, key, result, source, finding, "Dockerhub") def parse_args(): parser = argparse.ArgumentParser(description="DockerHub PAT 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=20) 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-no-username", action="store_true") parser.add_argument("--recheck-all", action="store_true") 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 = load_known_keys(CHECKED_FILE, STATUS_FILES) 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_no_username: retry_statuses.add("NO_USERNAME") processed = 0 skipped = 0 for key, username, token, source, finding in extract_candidates(args.input, args.plain): if should_skip_key(key, checked, known, args, retry_statuses, service=SERVICE, source=source, finding=finding, detector="Dockerhub"): skipped += 1 continue if args.max_keys and processed >= args.max_keys: break processed += 1 print(f"\n[{processed}] DockerHub candidate {mask_secret(key)} from {source}") proxy = next(proxy_cycler) if proxy_cycler else None result = check_token(username, token, proxy, args.timeout) print(f" STATUS: {result['status']} | {str(result.get('message', ''))[:200]}") write_result(key, 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()