diff --git a/scripts/affected.py b/scripts/affected.py new file mode 100755 index 00000000..04603d81 --- /dev/null +++ b/scripts/affected.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""affected — frontera MÍNIMA de rebuild ante un CVE (SDD 17 §4). + + affected.py zlib # ¿qué hay que reconstruir si zlib tiene un CVE? + affected.py zlib --agenda # + el orden topológico (qué primero) + affected.py zlib --sellados # sólo lo que YA está en el store (el trabajo real) + +Un CVE en `zlib` no obliga a reconstruir el catálogo: obliga a reconstruir **el cono de lo que +depende de zlib**, y nada más. El grafo de dependencias declarado ya lo sabe — es el `dirty_cone` +de `dataflow` (crate no_std de tawasuyu), y el `topo_order` es la agenda. + +Que esto sea exacto (y no "reconstruí todo por las dudas") **cae solo del invariante**: las deps +están declaradas por hash, así que el cono es computable. Un distro sin clausura declarada tiene +que adivinar. + +Nota honesta: la frontera es exacta respecto de **lo declarado**. Si una receta usa algo sin +declararlo, queda fuera del cono — y ese es justo el agujero que harkaq cierra (SDD 16): la +campaña de esta semana declaró ~84 deps que el kernel midió y nadie había escrito. **El cono es +tan bueno como la clausura**, y por eso los dos frentes se necesitan. +""" +import collections +import glob +import os +import re +import sys + + +def grafo(): + """(deps, rev): receta → sus deps declaradas, y su inverso.""" + deps = collections.defaultdict(set) + for f in glob.glob("recipes/*.toml"): + n = os.path.basename(f)[:-5] + s = open(f, errors="ignore").read() + m = re.search(r"\[deps\](.*?)(\n\[|\Z)", s, re.S) + if m: + for d in re.findall(r'"([^"]+)"', m.group(1)): + deps[n].add(d) + rev = collections.defaultdict(set) + for n, ds in deps.items(): + for d in ds: + rev[d].add(n) + return deps, rev + + +def cono(rev, semilla): + """dirty_cone: todo lo que depende TRANSITIVAMENTE de `semilla`. La frontera de rebuild.""" + vistos, frontera = set(), [semilla] + while frontera: + x = frontera.pop() + for c in rev.get(x, ()): + if c not in vistos: + vistos.add(c) + frontera.append(c) + return vistos + + +def agenda(deps, afectadas): + """topo_order dentro del cono: qué reconstruir primero (una dep antes que su consumidor).""" + pend = {n: {d for d in deps.get(n, ()) if d in afectadas} for n in afectadas} + orden, listos = [], [n for n, p in pend.items() if not p] + while listos: + n = listos.pop() + orden.append(n) + for m in afectadas: + if n in pend[m]: + pend[m].discard(n) + if not pend[m]: + listos.append(m) + ciclo = [n for n in afectadas if n not in orden] + return orden, ciclo + + +def main(): + args = [a for a in sys.argv[1:] if not a.startswith("--")] + if not args: + print(__doc__.split("\n")[2].strip(), file=sys.stderr) + return 2 + pkg = args[0] + if not os.path.exists(f"recipes/{pkg}.toml"): + print(f"no existe recipes/{pkg}.toml", file=sys.stderr) + return 2 + + deps, rev = grafo() + afectadas = cono(rev, pkg) + total = len(glob.glob("recipes/*.toml")) + + print(f"══ CVE en «{pkg}» — frontera mínima de rebuild") + print(f" {len(afectadas)} de {total} recetas afectadas ({len(afectadas)*100//max(total,1)}%)" + f" — el resto NO se toca") + + if "--sellados" in sys.argv or "--agenda" in sys.argv: + sell = {n for n in afectadas if glob.glob(f"store/*-{n}")} + print(f" de ésas, {len(sell)} ya están selladas en el store ⇒ ése es el trabajo real") + + if "--agenda" in sys.argv: + orden, ciclo = agenda(deps, afectadas) + print("\n ── agenda (topo_order: una dep antes que su consumidor):") + for i, n in enumerate(orden, 1): + marca = "●" if glob.glob(f"store/*-{n}") else "○" + print(f" {i:3}. {marca} {n}") + if ciclo: + print(f" ⚠ {len(ciclo)} en ciclo (no ordenables): {', '.join(sorted(ciclo))}") + print(" ● = sellado (hay que reconstruir) ○ = sin artefacto") + else: + for n in sorted(afectadas): + print(f" {n}") + + print("\n La frontera es exacta respecto de lo DECLARADO. Lo no declarado queda fuera del" + "\n cono — es el agujero que harkaq cierra (SDD 16).") + return 0 + + +if __name__ == "__main__": + sys.exit(main())