diff --git a/crates/hammer-cli/src/main.rs b/crates/hammer-cli/src/main.rs index 1f85091c..3a8c4977 100644 --- a/crates/hammer-cli/src/main.rs +++ b/crates/hammer-cli/src/main.rs @@ -37,6 +37,18 @@ enum Cmd { /// Ruta a la receta TOML. recipe: String, }, + /// [Fase 0] Calcula el `ArtifactHash` de una receta SIN construir — puro sobre las recetas + /// (source_id + compiler/target/link + patches + flags + fases + hashes de deps). No baja + /// fuentes ni compila. Responde "¿cuál es el hash VIGENTE de esta receta?", que el store no + /// puede dar sin sellar. Con `--check`: exit 0 si el artefacto vigente ya está en el store, + /// 1 si no — el dry-run que le faltaba a scripts/static-audit.sh para no auditar sellados rancios. + Hash { + /// Ruta a la receta TOML. + recipe: String, + /// No imprime el hash; verifica si el artefacto vigente está sellado en el store (exit 0/1). + #[arg(long)] + check: bool, + }, /// [Atestación] Verifica un árbol FHS (p.ej. un product-rootfs hidratado) contra su /// `/ente/attest.json`: recomputa el BLAKE3 de cada binario crítico y lo compara. Es el gate de /// integridad que arje aplicará al boot, hecho hoy por hammer ("reproducir, no confiar"). Exit≠0 @@ -755,6 +767,24 @@ fn main() -> anyhow::Result<()> { let hash = hammer_build::build(&recipe, &cfg, &store)?; println!("{hash}"); } + Cmd::Hash { recipe, check } => { + let store = hammer_core::Store::open(&cli.store)?; + let recipe = hammer_core::Recipe::load_from_path(&recipe)?; + let hash = hammer_build::artifact_hash(&recipe, &store)?; + if check { + // ¿el artefacto VIGENTE (el que corresponde a la receta de HOY) ya está sellado? + // Es lo que static-audit.sh no podía saber: `ls -dt store/*-` da el más RECIENTE, + // no el vigente. Exit 0 = sellado al día; 1 = haría falta construir. + if store.has(&hash, &recipe.name) { + println!("{hash}\tSELLADO"); + } else { + println!("{hash}\tNO-SELLADO"); + std::process::exit(1); + } + } else { + println!("{hash}"); + } + } Cmd::Attest { rootfs } => { let report = hammer_bootstrap::verify_attestation(std::path::Path::new(&rootfs))?; for p in &report.ok { diff --git a/scripts/static-audit.sh b/scripts/static-audit.sh index d420297b..b6d9db02 100755 --- a/scripts/static-audit.sh +++ b/scripts/static-audit.sh @@ -37,6 +37,7 @@ set -eu HUB="$(cd "$(dirname "$0")/.." && pwd)" cd "$HUB" STORE="${STORE:-$PWD/store}" +HAMMER="${HAMMER:-$HUB/target/release/hammer}" if [ $# -gt 0 ]; then recetas="" @@ -49,17 +50,27 @@ mienten=0; honestos=0; sin=0 for f in $recetas; do n=$(basename "$f" .toml) grep -qE '^[[:space:]]*link[[:space:]]*=[[:space:]]*"static"' "$f" || continue - # `ls -dt`: el artefacto MÁS RECIENTE, no el alfabéticamente primero. El store guarda TODOS los - # sellados de una receta (expat tenía 5, de junio a hoy) y el glob ordena por hash, que es - # ruido: auditaba un artefacto de hace tres semanas y reportaba dinámica una receta ya - # arreglada. Mismo bug de clase que comparar `libpng.a` con `libpng16.a` por `find | head -1`. + # EL ARTEFACTO VIGENTE, por hash — NO el más reciente por mtime. `hammer hash` calcula el + # ArtifactHash de la receta de HOY sin construir (~2ms, puro sobre las recetas). El store guarda + # TODOS los sellados históricos de una receta (expat tenía 5); auditar el más reciente por mtime + # sobre-reportaba (acusaba a dbus/libnl, ya sanas, por un sellado anterior a sus flags). Ahora + # auditamos EXACTAMENTE el sellado que corresponde a la receta actual, o ninguno. # - # HONESTIDAD: "más reciente" ≠ "vigente". Lo vigente sería el artefacto cuyo hash corresponde a - # la receta de HOY, y hammer no expone ese hash sin construir (no hay `hammer hash`/dry-run). - # Con el store al día ambos coinciden; si acabás de construir una receta vieja, no. Por eso el - # audit se corre DESPUÉS del rebuild, nunca antes. - art=$(ls -dt "$STORE"/*-"$n" 2>/dev/null | head -1) || true - [ -z "${art:-}" ] && { sin=$((sin + 1)); continue; } + # Fallback a `ls -dt` si `hammer` no está compilado: el audit sigue corriendo, sólo vuelve a + # poder sobre-reportar (como antes de que existiera `hammer hash`). + if [ -x "$HAMMER" ]; then + h=$("$HAMMER" --store "$STORE" hash "$f" 2>/dev/null) || h="" + if [ -n "$h" ] && [ -d "$STORE/${h#b3:}-$n" ]; then + art="$STORE/${h#b3:}-$n" + else + # No hay sellado para la receta VIGENTE ⇒ no hay nada honesto que auditar (rancio no + # cuenta). Es el "sin artefacto" legítimo: la receta cambió y falta rebuild. + sin=$((sin + 1)); continue + fi + else + art=$(ls -dt "$STORE"/*-"$n" 2>/dev/null | head -1) || true + [ -z "${art:-}" ] && { sin=$((sin + 1)); continue; } + fi # El primer ELF *ejecutable* del artefacto. Una `.so` tiene NEEDED por definición y no prueba # nada; sólo el ejecutable dice si el enlace final fue estático.