harkaq: primer barrido en la granja — la deuda irreducible de la muestra es perl (§4.10)
harkaq-farm-setup.sh: provisiona un worker para barrer (idempotente; gatea por
ABI>=7 y aborta si no llega — con la golden vieja el barrido daría SinEvidencia
en TODO). Deriva el runtime base DEL ROOTFS DEL WORKER, no copia el del laptop:
es por-rootfs (§4.3) y los sonames dependen de la versión de Alpine.
Barrido: 24 recetas, 14 con veredicto. Primer número crudo: 4 irreducibles (29%).
ERA FALSO, y por dos motivos propios:
1. GAP DE POLÍTICA. Los `list` salían sólo de los ancestros de la clausura ⇒ una
receta SIN deps (bash) no generaba ni uno y todo escaneo del árbol caía como
denegación (/usr/lib). Listar NO es leer: Landlock separa READ_DIR de
READ_FILE ⇒ la estructura del árbol es CONTRATO. Se puede `ls /usr/lib` sin
leer un solo fichero no declarado; la deuda es leer lo ajeno, no saber que
existe. Idem /var/tmp: con --tmp-overlay / es scratch descartable, como /tmp.
2. La heurística del catálogo es por NOMBRE y un fichero no se llama como su
paquete: /usr/bin/ranlib lo trae `binutils`, /usr/bin/diff lo trae
`diffutils`. El único que sabe la verdad es el STORE (conoce la lista de
ficheros de cada artefacto) — y el worker tiene 103 artefactos contra los
cientos del hub.
⇒ EL WORKER MIDE, EL HUB CLASIFICA. Misma separación que lector/clasificador: el
que tiene el privilegio —o los datos— hace lo mínimo. Reclasificado en el hub:
bash ranlib,/usr/lib,/var/tmp → nada (gap de política)
doas /usr/bin/diff → nada (diffutils lo provee)
ca-certificates /usr/bin/perl → /usr/bin/perl
curl /usr/bin/perl → /usr/bin/perl
RESULTADO: la deuda irreducible de toda la muestra es UN path — /usr/bin/perl
(2 de 14 = 14%). No hay recipes/perl.toml ni artefacto: deuda genuina y nombrada.
Todo lo demás era declarable (make ×10, ranlib→binutils, diff→diffutils).
Sigue por encima del <5% del §7, pero el perfil es el que importa: NO hay cola
larga de sorpresas. La deuda de un catálogo entero se resume en "declarar make" y
"no tenemos perl".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/bin/sh
|
||||
# harkaq-farm-setup.sh <ip> — deja un worker de la granja listo para barrer con harkaq.
|
||||
#
|
||||
# La granja es el compilador continuo (SDD 16 §4.8), así que esto tiene que ser reproducible y
|
||||
# no un copy-paste. Idempotente: re-correrlo sobre un worker ya provisto no rompe nada.
|
||||
#
|
||||
# Precondición dura: la golden 408909310 (kernel 6.17 ⇒ Landlock ABI 7). Con la vieja (6.8, ABI 4)
|
||||
# el barrido correría y daría `SinEvidencia` en TODO — que es lo que D7 manda y lo que no sirve
|
||||
# para medir. Por eso lo primero que hace es verificar el ABI y abortar si no llega.
|
||||
set -eu
|
||||
|
||||
IP="${1:?uso: harkaq-farm-setup.sh <ip>}"
|
||||
KEY="${SSH_KEY:-$HOME/.ssh/github5}"
|
||||
REMOTE="${REMOTE:-/opt/hammer}"
|
||||
SSH="ssh -i $KEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes"
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
echo "== 1/4 verificando el ABI (gate: >=7 o no hay evidencia)"
|
||||
abi=$($SSH "root@$IP" 'python3 -c "
|
||||
import ctypes; libc=ctypes.CDLL(\"libc.so.6\",use_errno=True)
|
||||
print(libc.syscall(444,None,0,1))"' 2>/dev/null | tr -d '\r')
|
||||
echo " kernel: $($SSH "root@$IP" uname -r 2>/dev/null) → Landlock ABI = $abi"
|
||||
if [ "${abi:-0}" -lt 7 ]; then
|
||||
echo " ABORTO: ABI $abi < 7 ⇒ el kernel no audita denegaciones. Todos los veredictos"
|
||||
echo " saldrían SinEvidencia. ¿El worker salió de la golden 408909310?"
|
||||
exit 4
|
||||
fi
|
||||
|
||||
echo "== 2/4 copiando y compilando las piezas"
|
||||
$SSH "root@$IP" "mkdir -p /root/.cache/harkaq $REMOTE/scripts/harkaq"
|
||||
scp -q -i "$KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
"$HERE"/harkaq-uapi.h "$HERE"/harkaq-audit.c "$HERE"/harkaq-exec.c \
|
||||
"$HERE"/harkaq-policy.sh "$HERE"/harkaq-verdict.py "$HERE"/harkaq-suggest.py \
|
||||
"$HERE"/harkaq-base-closure.py "$HERE"/fase2-barrido.sh \
|
||||
"root@$IP:$REMOTE/scripts/harkaq/"
|
||||
# harkaq-uapi.h no es una comodidad: el worker tiene headers 6.8 sobre kernel 6.17 y NO define
|
||||
# ni AUDIT_LANDLOCK_ACCESS. Sin él el lector filtraría por un número que no conoce y vería cero
|
||||
# denegaciones — el falso `Hermetico` de D9, por headers viejos (§4.8).
|
||||
$SSH "root@$IP" "cd $REMOTE/scripts/harkaq && chmod +x *.sh *.py &&
|
||||
gcc -O1 -Wall -o /root/.cache/harkaq/harkaq-audit harkaq-audit.c &&
|
||||
gcc -O1 -Wall -static -o /root/.cache/harkaq/harkaq-exec harkaq-exec.c &&
|
||||
echo ' compiladas ✓'"
|
||||
|
||||
echo "== 3/4 derivando el runtime base DEL ROOTFS DEL WORKER"
|
||||
# El runtime base es POR ROOTFS (§4.3) — derivarlo del rootfs del worker, no copiar el del
|
||||
# laptop: los sonames de las libs (libz.so.1.3.2, …) dependen de la versión de Alpine que tenga.
|
||||
$SSH "root@$IP" "cd $REMOTE &&
|
||||
python3 scripts/harkaq/harkaq-base-closure.py .dev-fs/alpine \
|
||||
/bin/sh /bin/busybox /bin/bash /bin/coreutils /usr/bin/env 2>/dev/null \
|
||||
> /root/.cache/harkaq/base.policy
|
||||
printf 'ro /usr/lib/os-release\n' >> /root/.cache/harkaq/base.policy
|
||||
# Sondas de compilador de Alpine: se DENIEGAN y se clasifican (D3). Concederlas dejaría a
|
||||
# zig/configure usar el gcc de Alpine por detrás — justo lo que la campaña matar-gcc caza.
|
||||
for p in /usr/bin/gcc /usr/bin/ldd /usr/bin/c89 /usr/bin/c99; do
|
||||
printf '# expect %s\n' \"\$p\" >> /root/.cache/harkaq/base.policy
|
||||
done
|
||||
echo \" runtime base: \$(grep -c '^ro ' /root/.cache/harkaq/base.policy) reglas + \$(grep -c '^# expect' /root/.cache/harkaq/base.policy) esperadas\""
|
||||
|
||||
echo "== 4/4 comprobando la cadena de evidencia de punta a punta"
|
||||
$SSH "root@$IP" "cd $REMOTE && cargo build -q -p hammer-cli 2>/dev/null || true; ls target/debug/hammer >/dev/null 2>&1 && echo ' hammer-cli listo ✓' || echo ' OJO: falta target/debug/hammer (cargo build -p hammer-cli)'"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
listo. En el worker:
|
||||
cd $REMOTE
|
||||
export HARKAQ_BIN=/root/.cache/harkaq HARKAQ_BASE=/root/.cache/harkaq/base.policy
|
||||
scripts/harkaq/fase2-barrido.sh recipes/zlib.toml recipes/bzip2.toml ...
|
||||
EOF
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""harkaq-suggest — convierte la deuda en una acción concreta (SDD 16 §4.5).
|
||||
|
||||
harkaq-suggest.py <verdict-crudo.json> <politica> <store>
|
||||
harkaq-suggest.py <verdict-crudo.json> <politica> <store> [recipes-dir]
|
||||
|
||||
Un veredicto `Impuro` dice *qué* usó el build sin declarar. Esto dice *qué hacer*: por cada path
|
||||
de deuda busca en el store un artefacto sellado que provea ese mismo path relativo. Si lo hay, la
|
||||
@@ -50,12 +50,31 @@ def nombre_de(art):
|
||||
return art.split("-", 1)[1] if "-" in art else art
|
||||
|
||||
|
||||
def catalogo(recipes_dir):
|
||||
"""Nombres de receta que hammer SABE construir, tenga o no el artefacto a mano.
|
||||
|
||||
Sin esto, "declarable" sería relativo a la máquina: el store del worker de la granja trae 103
|
||||
artefactos y el del laptop cientos, así que el MISMO path (`/usr/bin/make`) salía declarable
|
||||
en uno e irreducible en el otro. Eso no es una propiedad de la receta: es del disco donde
|
||||
corrés el barrido, y habría inflado la métrica de la Fase 2 en la granja — que es justo donde
|
||||
se va a medir en serio.
|
||||
|
||||
El catálogo es la respuesta correcta a "¿esto lo sabemos construir?". El store sólo dice
|
||||
"¿lo tenemos construido AHORA?", que es otra pregunta.
|
||||
"""
|
||||
if not recipes_dir or not os.path.isdir(recipes_dir):
|
||||
return set()
|
||||
return {f[:-5] for f in os.listdir(recipes_dir) if f.endswith(".toml")}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 4:
|
||||
print(__doc__.split("\n")[2].strip(), file=sys.stderr)
|
||||
return 2
|
||||
crudo = json.load(open(sys.argv[1]))
|
||||
politica, store = sys.argv[2], sys.argv[3]
|
||||
recipes_dir = sys.argv[4] if len(sys.argv) > 4 else os.path.join(
|
||||
os.path.dirname(os.path.abspath(store)), "recipes")
|
||||
|
||||
esperadas = set()
|
||||
with open(politica) as f:
|
||||
@@ -70,11 +89,28 @@ def main():
|
||||
return 0
|
||||
|
||||
idx = indice_del_store(store)
|
||||
recetas = catalogo(recipes_dir)
|
||||
declarable, irreducible = {}, []
|
||||
for p in deuda:
|
||||
provs = idx.get(p)
|
||||
if provs:
|
||||
# El store es autoritativo: sabe qué ficheros aporta cada artefacto.
|
||||
declarable[p] = sorted({nombre_de(a) for a in provs})
|
||||
elif os.path.basename(p) in recetas:
|
||||
# Respaldo por catálogo: el artefacto no está construido AQUÍ, pero hammer sabe
|
||||
# construirlo. Es declarable igual — la deuda es de la receta, no del disco.
|
||||
#
|
||||
# OJO, heurística por NOMBRE y por eso sólo un respaldo: acierta con `make`, pero
|
||||
# falla con `/usr/bin/ranlib` (lo trae `binutils`) y `/usr/bin/diff` (lo trae
|
||||
# `diffutils`). Un fichero no se llama como su paquete. El único que sabe la verdad
|
||||
# es el store, porque conoce la lista de ficheros de cada artefacto.
|
||||
#
|
||||
# ⇒ CLASIFICAR EN EL HUB, no en el worker. El worker de la granja mide (produce los
|
||||
# veredictos crudos) y el hub clasifica, porque es quien tiene el store COMPLETO. Con
|
||||
# el store parcial del worker (103 artefactos) esto marcaba bash y doas como deuda
|
||||
# IRREDUCIBLE y habría dado una métrica de Fase 2 inflada. Misma separación que
|
||||
# lector/clasificador: el que tiene el privilegio (o los datos) hace lo mínimo.
|
||||
declarable[p] = [os.path.basename(p) + " (receta en el catálogo, sin construir aquí)"]
|
||||
else:
|
||||
irreducible.append(p)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user