Files
keyhunter-parser/pubdev_eternal.py

224 lines
6.4 KiB
Python

"""ETERNAL pub.dev firehose: /api/packages page-walk + latest archive scan.
Anonymous. Cursor = page number (next_url), walks a slice of pages per cycle,
resets to 1 when list exhausted (new packages appear at head). tar.gz scan
via key_patterns.scan_text. mark_seen ONLY on success.
"""
import os
KF_BASE = os.path.dirname(os.path.abspath(__file__))
os.makedirs(os.path.join(KF_BASE, "data"), exist_ok=True)
import hashlib
import io
import sqlite3
import sys
import tarfile
import threading
import time
import requests
DB = os.environ.get("KF_DB") or os.path.join(KF_BASE, "data/keys.db")
LOG = os.path.join(KF_BASE, "data/pubdev_eternal.log")
CURSOR_FILE = os.path.join(KF_BASE, "data/pubdev_cursor.txt")
SEEN_FILE = os.path.join(KF_BASE, "data/pubdev_seen.txt")
N_THREADS = 4
PAGES_PER_CYCLE = 5
TAR_CAP = 8 * 1024 * 1024
FILE_CAP = 500_000
SLEEP = 90
from key_patterns import scan_text
GARBAGE = ("xxxx", "your", "YOUR", "EXAMPLE", "example", "n0tr3al",
"placeholder", "CHANGEME", "changeme", "dummy", "DUMMY",
"abcdefghijklmnopqrst", "1234567890abcdef")
SCAN_EXT = (".dart", ".json", ".yaml", ".yml", ".env", ".sh", ".py", ".js",
".ts", ".cfg", ".ini", ".md", ".txt", ".gradle", ".lock")
UA = {"User-Agent": "keyfarm-research/1.0 (package security scan)"}
def log(msg):
line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}\n"
with open(LOG, "a", encoding="utf-8") as f:
f.write(line)
conn = sqlite3.connect(DB, timeout=120, check_same_thread=False,
isolation_level=None)
lock = threading.Lock()
total = 0
def db_add(key, prov, repo):
global total
with lock:
conn.execute(
"INSERT INTO keys (hash, val, prov, status, found, repo) "
"VALUES (?,?,?,?,?,?) "
"ON CONFLICT(hash) DO UPDATE SET status='NEW', "
"found=excluded.found, repo=excluded.repo "
"WHERE keys.status IN ('DEAD','ERR')",
(hashlib.sha1(key.encode()).hexdigest()[:16],
key, prov, "NEW", "pubdev", repo))
total += 1
def load_cursor():
try:
with open(CURSOR_FILE, encoding="utf-8") as f:
return int(f.read().strip() or 1)
except Exception:
return 1
def save_cursor(c):
with open(CURSOR_FILE, "w", encoding="utf-8") as f:
f.write(str(c))
def load_seen():
s = set()
try:
with open(SEEN_FILE, encoding="utf-8") as f:
for line in f:
s.add(line.strip())
except Exception:
pass
return s
seen = load_seen()
def mark_seen(p):
seen.add(p)
with open(SEEN_FILE, "a", encoding="utf-8") as f:
f.write(p + "\n")
def work(name):
"""Fetch package meta -> latest tar.gz, scan. -1 on final fail."""
n = 0
for attempt in (1, 2):
try:
r = requests.get(f"https://pub.dev/api/packages/{name}",
headers=UA, timeout=20)
if r.status_code == 404:
return 0
if r.status_code != 200:
raise RuntimeError(f"http {r.status_code}")
au = r.json().get("latest", {}).get("archive_url")
if not au:
return 0
if not au.startswith("https"):
au = "https://pub.dev" + au
r2 = requests.get(au, headers=UA, timeout=30)
if r2.status_code != 200:
raise RuntimeError(f"arch http {r2.status_code}")
if len(r2.content) > TAR_CAP:
return 0
tf = tarfile.open(fileobj=io.BytesIO(r2.content), mode="r:gz")
for m in tf.getmembers():
if not m.isfile() or m.size > FILE_CAP:
continue
fn = m.name.lower()
if not any(fn.endswith(e) for e in SCAN_EXT):
continue
ef = tf.extractfile(m)
if ef is None:
continue
data = ef.read()
text = data.decode("utf-8", errors="ignore")
for prov, key in scan_text(text):
if isinstance(key, tuple):
key = key[0]
if any(g in key for g in GARBAGE) or len(key) < 30:
continue
db_add(key, prov, f"pub:{name}")
n += 1
return n
except Exception:
if attempt == 2:
log(f" pkg-err {name}")
return -1
time.sleep(3)
return -1
def cycle():
global total
total = 0
page = load_cursor()
names = []
exhausted = False
for _ in range(PAGES_PER_CYCLE):
url = f"https://pub.dev/api/packages?page={page}"
for attempt in (1, 2):
try:
r = requests.get(url, headers=UA, timeout=25)
if r.status_code != 200:
raise RuntimeError(f"http {r.status_code}")
d = r.json()
pkgs = d.get("packages") or []
for pk in pkgs:
nm = pk.get("name")
if nm and nm not in seen:
names.append(nm)
nxt = d.get("next_url")
if not nxt:
exhausted = True
break
except Exception as e:
if attempt == 2:
log(f"list-err page={page} {type(e).__name__}")
return
time.sleep(5)
if exhausted:
break
page += 1
if exhausted:
save_cursor(1)
else:
save_cursor(page)
ok = fail = 0
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=N_THREADS) as ex:
futs = {ex.submit(work, nm): nm for nm in names}
for fut in as_completed(futs):
nm = futs[fut]
try:
res = fut.result()
except Exception:
res = -1
if res == -1:
fail += 1
else:
ok += 1
mark_seen(nm)
log(f"cycle: page_next={load_cursor()} pkgs={len(names)} ok={ok} "
f"fail={fail} +{total} keys")
def main():
log("pubdev_eternal started")
if "--once" in sys.argv:
cycle()
return
while True:
try:
cycle()
except Exception as e:
log(f"cycle-err {type(e).__name__}: {e}")
time.sleep(SLEEP)
if __name__ == "__main__":
main()