#!/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())