Files
keyhunter-parser/maven_eternal.py

223 lines
6.9 KiB
Python

"""ETERNAL Maven Central firehose: Central Portal browse (publishedDate desc)
+ repo1 POM/JAR scan.
Portal POST /api/internal/browse/components: 20 comps/page, 500 pages max
(10k newest). seen=g:a (one scan per artifact, version churn skipped).
POM text + JAR/WAR/AAR/ZIP (zipfile) scan via key_patterns.scan_text.
mark_seen ONLY on success (fail=-1 retried next cycle).
"""
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 threading
import time
import zipfile
import requests
DB = os.environ.get("KF_DB") or os.path.join(KF_BASE, "data/keys.db")
LOG = os.path.join(KF_BASE, "data/maven_eternal.log")
PORTAL_URL = "https://central.sonatype.com/api/internal/browse/components"
SEEN_FILE = os.path.join(KF_BASE, "data/maven_seen.txt")
MAX_PAGES = 10 # portal pages per cycle (20 comps each)
N_THREADS = 6
JAR_CAP = 8 * 1024 * 1024
FILE_CAP = 500_000
SLEEP = 60
from key_patterns import scan_text
GARBAGE = ("xxxx", "your", "YOUR", "EXAMPLE", "example", "n0tr3al",
"placeholder", "CHANGEME", "changeme", "dummy", "DUMMY",
"abcdefghijklmnopqrst", "1234567890abcdef")
SCAN_EXT = (".xml", ".properties", ".json", ".yaml", ".yml", ".toml", ".env",
".sh", ".py", ".js", ".ts", ".cfg", ".ini", ".gradle", ".MF", ".txt")
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", "maven", repo))
total += 1
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 scan_bytes(data, repo, n):
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, repo)
n += 1
return n
def work(g, a, v, packaging):
"""Fetch POM (+packaged zip if jar/war/aar) for g:a:v, scan. -1 on final fail."""
n = 0
path = f"{g.replace('.', '/')}/{a}/{v}/{a}-{v}"
base = f"https://repo1.maven.org/maven2/{path}"
targets = [(base + ".pom", False)]
if packaging in ("jar", "war", "aar", "zip"):
targets.append((base + "." + packaging, True))
ok_any = False
for url, is_zip in targets:
for attempt in (1, 2):
try:
r = requests.get(url, timeout=20)
if r.status_code == 404:
ok_any = True
break
if r.status_code != 200:
raise RuntimeError(f"http {r.status_code}")
if len(r.content) > JAR_CAP:
ok_any = True
break
if is_zip:
try:
z = zipfile.ZipFile(io.BytesIO(r.content))
except Exception:
ok_any = True
break
for fn in z.namelist():
if not fn.lower().endswith(SCAN_EXT):
continue
try:
data = z.read(fn)
except Exception:
continue
if len(data) > FILE_CAP:
continue
n = scan_bytes(data, f"maven:{g}:{a}", n)
else:
n = scan_bytes(r.content, f"maven:{g}:{a}", n)
ok_any = True
break
except Exception:
if attempt == 2:
log(f" mvn-err {path} {url.rsplit('.', 1)[-1]}")
return -1 if not ok_any else n
time.sleep(3)
if not ok_any:
return -1
return n
def cycle():
global total
total = 0
UA = {"User-Agent": "keyfarm-maven/1.0",
"Content-Type": "application/json"}
items = []
for page in range(MAX_PAGES):
payload = {"page": page, "sortField": "publishedDate",
"sortDirection": "desc"}
for attempt in (1, 2, 3):
try:
r = requests.post(PORTAL_URL, headers=UA, json=payload,
timeout=40)
if r.status_code != 200:
raise RuntimeError(f"http {r.status_code}")
d = r.json()
break
except Exception as e:
if attempt == 3:
log(f"portal-err page={page} {type(e).__name__}: "
f"{str(e)[:100]}")
return
time.sleep(8)
for comp in d.get("components", []):
g = comp.get("namespace")
a = comp.get("name")
lvi = comp.get("latestVersionInfo") or {}
v = lvi.get("version")
p = comp.get("packaging") or "jar"
if not (g and a and v):
continue
if p not in ("jar", "war", "aar", "zip", "pom"):
p = "jar"
sid = f"{g}:{a}"
if sid in seen:
continue
items.append((g, a, v, p))
ok = fail = 0
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=N_THREADS) as ex:
futs = {ex.submit(work, g, a, v, p): (g, a, v) for g, a, v, p in items}
for fut in as_completed(futs):
g, a, v = futs[fut]
try:
res = fut.result()
except Exception:
res = -1
if res == -1:
fail += 1 # NOT marked seen: retried next cycle
else:
ok += 1
mark_seen(f"{g}:{a}")
log(f"cycle: items={len(items)} ok={ok} fail={fail} +{total} keys")
def main():
log("maven_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()