Files
hack/tools/scripts/llm-key-hunter/verify_cache.py
T
chaos 5d215e1649 Add LLM key-hunter toolkit, vault, and skill
- tools/scripts/llm-key-hunter: GitHub leak hunting pipeline (hunt_*,
  pivot miner, two-layer verify/content caches, per-provider verification)
- usable_keys: verified key vault across 12 providers (deepseek, minimax,
  volcanoark, longcat, codingplan, zhipu free-tier, mimo, siliconflow, etc.)
- .grok/skills/llm-key-hunter: operator skill for the hunt/verify/vault flow
- NewAPI channel import scripts and CDP capture helpers
- Result verdict buckets (excluding multi-GB blob caches and dedup dumps)
2026-08-02 06:02:58 +08:00

114 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Persistent verification cache for key hunters.
Avoids re-querying keys whose verdict is already known. TTL by verdict:
DEAD -> never re-check (401 does not come back)
NO_BALANCE / NO_ACCESS -> 6h (account may be recharged / plan opened)
USABLE -> 30m (balance drains; refresh periodically)
UNKNOWN/network error -> never cached (retry next run)
Cache is one JSON file per provider under results/_cache/<provider>.json,
shape: { key: {"v": verdict, "d": detail, "t": epoch_seconds} }
Usage in a hunter:
from verify_cache import CachedVerifier
ver = CachedVerifier("deepseek", verify_key) # verify_key(key)->(verdict,detail)
verdict, detail = ver(key) # cached or live
ver.save()
Or wrap the whole batch loop:
with CachedVerifier("deepseek", verify_key) as ver:
results = list(pool.map(ver, keys))
"""
import json
import os
import threading
import time
from pathlib import Path
HERE = Path(__file__).parent
CACHE_DIR = HERE / "results" / "_cache"
# TTL in seconds per verdict. None = never expire.
TTL = {
"DEAD": None,
"NO_BALANCE": 6 * 3600,
"NO_ACCESS": 6 * 3600,
"USABLE": 30 * 60,
}
# verdicts that are never written to cache (re-run every time)
NEVER_CACHE = {"UNKNOWN"}
class CachedVerifier:
def __init__(self, provider, verify_fn, cache_dir=None, force=False, ttl=None):
self.provider = provider
self.verify_fn = verify_fn
self.force = force or os.environ.get("NO_CACHE") == "1"
self.ttl = {**TTL, **(ttl or {})}
self.path = Path(cache_dir) if cache_dir else (CACHE_DIR / f"{provider}.json")
self.path.parent.mkdir(parents=True, exist_ok=True)
self._lock = threading.Lock()
self._cache = {}
self.hits = 0
self.live = 0
self._load()
def _load(self):
if self.path.exists():
try:
self._cache = json.loads(self.path.read_text())
except Exception:
self._cache = {}
def save(self):
tmp = self.path.with_suffix(".tmp")
with self._lock:
tmp.write_text(json.dumps(self._cache, ensure_ascii=False))
os.replace(tmp, self.path)
def __enter__(self):
return self
def __exit__(self, *exc):
self.save()
def _fresh(self, entry):
ttl = self.ttl.get(entry.get("v"))
if ttl is None:
return True # never expires (DEAD)
return (time.time() - entry.get("t", 0)) < ttl
def __call__(self, key):
if not self.force:
with self._lock:
ent = self._cache.get(key)
if ent and self._fresh(ent):
self.hits += 1
return ent["v"], ent["d"]
verdict, detail = self.verify_fn(key)
self.live += 1
if verdict not in NEVER_CACHE:
with self._lock:
self._cache[key] = {"v": verdict, "d": detail, "t": time.time()}
return verdict, detail
def stats(self):
return {"hits": self.hits, "live": self.live, "cached": len(self._cache)}
def prune_dead(provider=None):
"""Utility: print cache size / drop nothing."""
for p in sorted(CACHE_DIR.glob("*.json")):
if provider and p.stem != provider:
continue
try:
d = json.loads(p.read_text())
except Exception:
continue
counts = {}
for v in d.values():
counts[v.get("v", "?")] = counts.get(v.get("v", "?"), 0) + 1
print(f"{p.stem:16s} {len(d):6d} entries {counts}")