sergio/takana
> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
harkaq: Verdict clasificado (base|esperada|deuda) + el runtime base es por forma de build
1#!/usr/bin/env python3
2"""harkaq-verdict — clasifica las denegaciones crudas y emite el Verdict final (SDD 16 §4.2).
3
4 harkaq-verdict.py <verdict-crudo.json> <politica> [--human]
5
6Por qué es un proceso aparte y NO parte de harkaq-audit: el lector corre con CAP_AUDIT_READ, y
7clasificar es POLÍTICA, no privilegio. El componente privilegiado se mantiene lo más tonto
8posible — mismo argumento que Q1c (el privilegio en un helper chico, no en hammerd). Efecto
9lateral útil: se itera la clasificación sin recompilar el binario capabilitado (y sin perder el
10setcap en cada cambio).
11
12Las tres categorías salen de la medición de §4.2:
13
14 base concedida por la política ⇒ nunca aparece como denegación. No se clasifica acá.
15 esperada denegada A PROPÓSITO y previsible. `zig cc` sondea /usr/bin/gcc y el build sale rc=0
16 igual: concederlo lo dejaría usar el gcc de Alpine por detrás. Se declara en la
17 política con `# expect <path>` — la MISMA fuente de verdad que las reglas, y
18 harkaq-exec la ignora como comentario. D3: no se calla, se clasifica.
19 deuda todo lo demás. ESTO es la de-Alpinización: lo que el build usó sin declarar.
20
21Y la consecuencia que obliga: `Hermetico` = **cero deuda**, no cero denegaciones. Con la
22definición estricta ningún build real lo alcanzaría jamás y el estado más importante del sistema
23quedaría vacío de uso.
24"""
25import json
26import sys
27
28
29def cargar_esperadas(politica):
30 """Lee los `# expect <path>` de la política. Un fichero, una fuente de verdad."""
31 esperadas = set()
32 with open(politica) as f:
33 for linea in f:
34 linea = linea.strip()
35 if linea.startswith("# expect "):
36 esperadas.add(linea[len("# expect "):].strip())
37 return esperadas
38
39
40def main():
41 if len(sys.argv) < 3:
42 print(__doc__.split("\n")[2].strip(), file=sys.stderr)
43 return 2
44 crudo = json.load(open(sys.argv[1]))
45 esperadas = cargar_esperadas(sys.argv[2])
46 humano = "--human" in sys.argv
47
48 # SinEvidencia manda sobre todo: si el lector no es confiable, no clasificamos nada. Un
49 # veredicto sin canario no es "limpio", es "no sabemos" (D9), y reinterpretarlo acá sería
50 # justo el falso Hermetico que el canario existe para impedir.
51 if crudo["estado"] == "SinEvidencia":
52 salida = dict(crudo, esperadas=[], deuda=[])
53 print(json.dumps(salida) if not humano else
54 f"SinEvidencia — {crudo.get('motivo', 'sin motivo')}")
55 return 3
56
57 vistas_esperadas, deuda = [], []
58 for d in crudo["denials"]:
59 (vistas_esperadas if d["path"] in esperadas else deuda).append(d)
60
61 estado = "Hermetico" if not deuda else "Impuro"
62 salida = {
63 "estado": estado,
64 "canario_visto": crudo["canario_visto"],
65 "domain": crudo["domain"],
66 "contador_kernel": crudo["contador_kernel"],
67 "esperadas": vistas_esperadas,
68 "deuda": deuda,
69 }
70
71 if not humano:
72 print(json.dumps(salida))
73 else:
74 print(f" estado: {estado} (canario visto, contador kernel={crudo['contador_kernel']})")
75 if vistas_esperadas:
76 uniq = sorted({d["path"] for d in vistas_esperadas})
77 print(f" esperadas ({len(vistas_esperadas)}): " + ", ".join(uniq))
78 if deuda:
79 print(f" DEUDA ({len(deuda)}) — usado sin declarar:")
80 for p in sorted({d["path"] for d in deuda}):
81 print(f" {p}")
82 else:
83 print(" deuda: ninguna ✓")
84 return 0 if estado == "Hermetico" else 1
85
86
87if __name__ == "__main__":
88 sys.exit(main())