#!/usr/bin/env python3 """Render the sweep matrix from the per-app probe.json evidence. Run on DooPlex after pulling the evidence back from the scratch guest.""" import json, sys from pathlib import Path EV = Path(sys.argv[1] if len(sys.argv) > 1 else "audits/persistence-sweep-2026-08-02/evidence") MOUNT_SHORT = {"named-declared": "vol", "named-external": "vol-ext", "anonymous": "ANON", "bind": "bind", "tmpfs": "tmpfs"} def summarise(p): v = p["verdict"] probe = p.get("probe") or {} ctrs = probe.get("containers") or [] mounts, wrote, unwritable = [], [], [] for c in ctrs: for m in c.get("mounts") or []: if m["class"] == "tmpfs": continue mounts.append(f"{m['target']}[{MOUNT_SHORT.get(m['class'], m['class'])}" f"{'' if m['files'] > 0 else ',EMPTY'}]") if m["files"] > 0: wrote.append(f"{m['target']}({m['files']})") if m.get("writable_by_app") == "NO": unwritable.append(m["target"]) for d in c.get("diff_data_dirs") or []: wrote.append(f"**{d['dir']} (writable layer)**") return { "verdict": v, "mounted": ", ".join(mounts) or "—", "wrote": ", ".join(wrote) or "nothing", "unwritable": ", ".join(unwritable) or "—", "reason": "; ".join(p.get("reasons") or []), "containers": len(ctrs), } rows = {} for d in sorted(EV.iterdir()): f = d / "probe.json" if f.is_file(): rows[d.name] = summarise(json.loads(f.read_text())) order = {"BROKEN": 0, "UNDETERMINED": 1, "CLEAN": 2} print("| app | verdict | what the template mounts | where the app actually wrote | mount not writable by app uid |") print("|---|---|---|---|---|") for app in sorted(rows, key=lambda a: (order.get(rows[a]["verdict"], 3), a)): r = rows[app] print(f"| `{app}` | **{r['verdict']}** | {r['mounted']} | {r['wrote']} | {r['unwritable']} |") print() for v in ("BROKEN", "UNDETERMINED", "CLEAN"): apps = [a for a in sorted(rows) if rows[a]["verdict"] == v] print(f"{v}: {len(apps)} — {', '.join(apps) if apps else '(none)'}") print(f"TOTAL: {len(rows)}") print("\n--- UNDETERMINED reasons ---") for a in sorted(rows): if rows[a]["verdict"] == "UNDETERMINED": print(f"| `{a}` | {rows[a]['reason'][:400]} |") print("\n--- BROKEN reasons ---") for a in sorted(rows): if rows[a]["verdict"] == "BROKEN": print(f"| `{a}` | {rows[a]['reason'][:600]} |")