#!/usr/bin/env python3 """harkaq-verdict — clasifica las denegaciones crudas y emite el Verdict final (SDD 16 §4.2). harkaq-verdict.py [--human] Por qué es un proceso aparte y NO parte de harkaq-audit: el lector corre con CAP_AUDIT_READ, y clasificar es POLÍTICA, no privilegio. El componente privilegiado se mantiene lo más tonto posible — mismo argumento que Q1c (el privilegio en un helper chico, no en hammerd). Efecto lateral útil: se itera la clasificación sin recompilar el binario capabilitado (y sin perder el setcap en cada cambio). Las tres categorías salen de la medición de §4.2: base concedida por la política ⇒ nunca aparece como denegación. No se clasifica acá. esperada denegada A PROPÓSITO y previsible. `zig cc` sondea /usr/bin/gcc y el build sale rc=0 igual: concederlo lo dejaría usar el gcc de Alpine por detrás. Se declara en la política con `# expect ` — la MISMA fuente de verdad que las reglas, y harkaq-exec la ignora como comentario. D3: no se calla, se clasifica. deuda todo lo demás. ESTO es la de-Alpinización: lo que el build usó sin declarar. Y la consecuencia que obliga: `Hermetico` = **cero deuda**, no cero denegaciones. Con la definición estricta ningún build real lo alcanzaría jamás y el estado más importante del sistema quedaría vacío de uso. """ import json import sys def cargar_esperadas(politica): """Lee los `# expect ` de la política. Un fichero, una fuente de verdad.""" esperadas = set() with open(politica) as f: for linea in f: linea = linea.strip() if linea.startswith("# expect "): esperadas.add(linea[len("# expect "):].strip()) return esperadas def main(): if len(sys.argv) < 3: print(__doc__.split("\n")[2].strip(), file=sys.stderr) return 2 crudo = json.load(open(sys.argv[1])) esperadas = cargar_esperadas(sys.argv[2]) humano = "--human" in sys.argv # SinEvidencia manda sobre todo: si el lector no es confiable, no clasificamos nada. Un # veredicto sin canario no es "limpio", es "no sabemos" (D9), y reinterpretarlo acá sería # justo el falso Hermetico que el canario existe para impedir. if crudo["estado"] == "SinEvidencia": salida = dict(crudo, esperadas=[], deuda=[]) print(json.dumps(salida) if not humano else f"SinEvidencia — {crudo.get('motivo', 'sin motivo')}") return 3 vistas_esperadas, deuda = [], [] for d in crudo["denials"]: (vistas_esperadas if d["path"] in esperadas else deuda).append(d) estado = "Hermetico" if not deuda else "Impuro" salida = { "estado": estado, "canario_visto": crudo["canario_visto"], "domain": crudo["domain"], "contador_kernel": crudo["contador_kernel"], "esperadas": vistas_esperadas, "deuda": deuda, } if not humano: print(json.dumps(salida)) else: print(f" estado: {estado} (canario visto, contador kernel={crudo['contador_kernel']})") if vistas_esperadas: uniq = sorted({d["path"] for d in vistas_esperadas}) print(f" esperadas ({len(vistas_esperadas)}): " + ", ".join(uniq)) if deuda: print(f" DEUDA ({len(deuda)}) — usado sin declarar:") for p in sorted({d["path"] for d in deuda}): print(f" {p}") else: print(" deuda: ninguna ✓") return 0 if estado == "Hermetico" else 1 if __name__ == "__main__": sys.exit(main())