diff --git a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical index 2ae85c6..e695156 100755 --- a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical +++ b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical @@ -276,7 +276,9 @@ declare -A ARCHIVE_SUB=( [recovery]="recovery" [remediation]="candidates" ) # aus Build-Session (41e357a1..., aktuell im Store fehlend). RECOVERY_ARCHIVE_ID="" # zur Laufzeit aus State gesetzt (load_recovery_state) # Remediation-Archiv-Session-ID: kurze ID der Build-Session-Remendation (41e357a1...). -REMEDIATION_ARCHIVE_ID="41e357a12b90" +# (RAIN Hotfix) Statische REMEDIATION_ARCHIVE_ID ENTFERNT — historische 41e357a12b90 +# darf keine aktive Archive-Identity sein. Aktive ID ausschliesslich aus der aktuellen, +# inspectable Build-Session ableiten (remediation_session / remediation_full_image_id). # Remediation-Archiv-Referenz (aus Build-Session falls vorhanden, sonst leer): # wird zur Laufzeit per remediation_full_image_id() aufgeloest. # Pin-Referenz-Ziel (Schicht A): exakt diese Image-ID muss der Pin-Container referenzieren. @@ -1453,11 +1455,18 @@ rollback_runner_patch() { # KEIN echter Build/Recreate durch Rain. Red Queen fuehrt nach Freigabe aus. # Helfer: Read-only-Session-Bestand ("Build-Session vorhanden?") — muss zusaetzlich # FULL_IMAGE_ID tragen, da Verify/Recreate nur der gespeicherten Image-ID vertrauen. -remediation_session() { # 0 wenn giltige Build-Session mit FULL_IMAGE_ID vorhanden +remediation_session() { # 0 NUR wenn AKTUELL erfolgreicher Build-State mit inspectable Image existiert + # (RAIN Hotfix §9/§10) Eine historische/stale FULL_IMAGE_ID darf KEINE aktive Identity + # sein. Nur wenn das referenzierte Image im Docker-Store tatsaechlich inspectable ist, + # ist die Session "aktiv und aktuell". Sonst -> NO ACTIVE REMEDIATION IMAGE STATE. [ -f "$REMEDIATION_SESSION_META" ] || return 1 grep -q '^FULL_IMAGE_ID=sha256:[a-f0-9]\{64\}$' "$REMEDIATION_SESSION_META" || return 1 [ "$(stat -c '%F' "$REMEDIATION_SESSION_META")" = "regular file" ] || return 1 [ -L "$REMEDIATION_SESSION_META" ] && return 1 + local rid + rid="$(grep '^FULL_IMAGE_ID=' "$REMEDIATION_SESSION_META" | cut -d= -f2)" + # NO ACTIVE fallback: historische ID ist nur Evidence. Image muss inspectable sein. + docker image inspect "$rid" >/dev/null 2>&1 || return 1 return 0 } remediation_full_image_id() { # FULL_IMAGE_ID aus Session (echo) — nur wenn rem_remediation_session OK @@ -2390,12 +2399,25 @@ cleanup_partial_remediation_staging() { die "cleanup: Remediation Image existiert (FAIL CLOSED)" fi echo " B) Kein Remediation Image" - # C) Kein Remediation Archive - local arch_dir="$ARCHIVE_ROOT/candidates" - if [ -d "$arch_dir" ] && [ -n "$(ls -A "$arch_dir" 2>/dev/null)" ]; then - die "cleanup: Remediation Archive existiert (FAIL CLOSED)" - fi - echo " C) Kein Remediation Archive" + # C) VALID-Archive-Semantik (RAIN Hotfix §4): NICHT nackte Directory-Existence. + # VALID -> strikt DENY (gueltiges Archive blockiert Staging-Cleanup). + # PARTIAL -> erst sanktionierten cleanup-partial-remediation-archive ausfuehren, + # dann Retry (Lifecycle-getrennt, bevorzugte Variante). + # NO -> kein Blocker, Staging-Cleanup faehrt fort. + # INVALID/UNKNOWN -> FAIL CLOSED. + local cls + cls="$(remediation_archive_classification)" + case "$cls" in + VALID) die "cleanup: VALID Remediation Archive existiert — Cleanup strikt DENY (FAIL CLOSED)" ;; + PARTIAL) + echo " C) PARTIAL_ARCHIVE_PRESENT=YES" + echo " PARTIAL_ARCHIVE_CLASSIFICATION=SAFE_ORPHAN" + die "cleanup: PARTIAL Remediation Archive vorhanden — zuerst cleanup-partial-remediation-archive, dann Retry (Lifecycle-getrennt)" ;; + INVALID) die "cleanup: Archive-Zustand INVALID — FAIL CLOSED, kein automatischer Umgang" ;; + UNKNOWN) die "cleanup: Archive-Zustand UNKNOWN — FAIL CLOSED, kein automatischer Umgang" ;; + NO) echo " C) Remediation Archive = NO (kein VALID-Archive-Blocker)" ;; + *) die "cleanup: ungueltige Archive-Klassifikation (FAIL CLOSED)" ;; + esac # D) Kein Remediation Pin if docker ps -a --format '{{.Names}}' | grep -q "rq-historical-pin-phase13-5-runner-remediation"; then die "cleanup: Remediation Pin existiert (FAIL CLOSED)" @@ -2457,6 +2479,147 @@ cleanup_partial_remediation_staging() { echo " Retry bereit: build-runner-remediation wird neues Staging materialisieren." } + +# ============================================================================= +# cleanup-partial-remediation-archive (Phase 13.5 · RAIN Hotfix) +# Kontrollierter Cleanup eines NACHWEISLICH unvollstaendigen / verwaisten +# Remediation-Archive-Ordners. KEINE freie Pfadangabe, KEINE freie Session. +# Zielidentitaet vollstaendig aus kontrolliertem State / Build-Session abgeleitet. +# Sicher entfernbar NUR klar klassifizierbare PARTIAL-Orphans. Valid/Invalid/ +# Unknown/aktive Zustände -> FAIL CLOSED. +# ============================================================================= +# --- Archive-Zustands-Klassifikation (Mission §2) --- +# A) NO_ARCHIVE : kein Archive-Ziel vorhanden. +# B) PARTIAL_ARCHIVE : Verzeichnis vorhanden, aber mind. ein Pflichtartefakt fehlt +# oder ist unvollstaendig (z.B. image.tar fehlt/leer). +# C) VALID_ARCHIVE : alle Pflichtartefakte da UND konsistent gebunden UND verify PASS. +# D) INVALID_ARCHIVE : vollstaendig erscheinend, aber SHA/Metadata/Binding falsch. +# E) UNKNOWN_ARCHIVE_STATE : nicht sicher klassifizierbar. +# Fail-closed: VALID blockiert Cleanup; PARTIAL separat bereinigbar; INVALID/UNKNOWN FAIL. +remediation_archive_classification() { # echo: NO|PARTIAL|VALID|INVALID|UNKNOWN + local sdir base img meta manifest runtime + sdir="$(role_arch_dir remediation)" # = $ARCHIVE_ROOT/candidates + local sid + if remediation_session; then + local ridsess; ridsess="$(remediation_full_image_id)" + sid="${ridsess#sha256:}"; sid="${sid:0:12}" + else + sid="" + fi + echo " REMEDIATION_ARCHIVE_BASE=$sdir" >&2 + [ -d "$sdir" ] || { echo "NO"; return; } + if [ -n "$sid" ]; then + base="$sdir/$sid" + else + local found="" cnt=0 + for d in "$sdir"/*/; do + [ -d "$d" ] || continue + cnt=$((cnt+1)); found="${d%/}"; found="${found##*/}" + done + if [ "$cnt" = "0" ]; then echo "NO"; return; fi + base="$sdir/$found" + fi + [ -d "$base" ] || { echo "NO"; return; } + img="$base/image.tar"; meta="$base/session.meta"; manifest="$base/app.manifest"; runtime="$base/runtime-config.txt" + # --- VALID-Pruefung (Mission §3) --- + local ok=1 + [ -f "$img" ] && [ ! -L "$img" ] && [ -s "$img" ] || ok=0 + [ -f "$meta" ] && [ ! -L "$meta" ] || ok=0 + [ -f "$manifest" ] && [ ! -L "$manifest" ] || ok=0 + [ -f "$runtime" ] && [ ! -L "$runtime" ] || ok=0 + if [ "$ok" = "1" ]; then + local want got + want="$(awk -F'=' '$1=="ARCHIVE_SHA256"{print $2}' "$meta" 2>/dev/null)" + got="$(sha256sum "$img" 2>/dev/null | awk '{print $1}')" + if [ -n "$want" ] && [[ "$want" =~ ^[a-f0-9]{64}$ ]] && [ "$got" = "$want" ]; then + local role + role=remediation + if ( verify_archive remediation ) >/dev/null 2>&1; then echo "VALID"; return; fi + echo "INVALID"; return + fi + echo "INVALID"; return + fi + # --- PARTIAL vs UNKNOWN (RAIN Hotfix: fail-closed-Semantik) --- + # PARTIAL nur wenn klar unvollstaendig/orphan: image.tar fehlt oder ist leer. + # (NICHT bei blosser Anomalie — Symlink/widerspruechliche Metadata ist UNKNOWN.) + if [ ! -f "$img" ] || [ ! -s "$img" ]; then + echo "PARTIAL"; return + fi + # Sonst: image.tar vorhanden+valide, aber Metadata/Manifest/Runtime unvollstaendig + # oder widerspruechlich -> nicht sicher klassifizierbar -> UNKNOWN (fail closed). + echo "UNKNOWN"; return +} + +# cleanup-partial-remediation-archive — dry-run + real (Mission §5/§6/§7) +cleanup_partial_remediation_archive() { + expect_zero "$@" + echo "=== CLEANUP-PARTIAL-REMEDIATION-ARCHIVE ($RUN_MODE) ===" + local sdir sid base + sdir="$(role_arch_dir remediation)" # = $ARCHIVE_ROOT/candidates + if remediation_session; then + local ridsess; ridsess="$(remediation_full_image_id)" + sid="${ridsess#sha256:}"; sid="${sid:0:12}" + else + sid="" + fi + if [ -n "$sid" ]; then + base="$sdir/$sid" + else + local found="" cnt=0 + for d in "$sdir"/*/; do + [ -d "$d" ] || continue + cnt=$((cnt+1)); found="${d%/}"; found="${found##*/}" + done + if [ "$cnt" = "0" ]; then die "cleanup-partial-remediation-archive: kein Archive-Ordner (FAIL CLOSED)"; fi + if [ "$cnt" != "1" ]; then die "cleanup-partial-remediation-archive: mehrere Candidate-Ordner — Ziel nicht eindeutig (FAIL CLOSED)"; fi + sid="$found"; base="$sdir/$sid" + fi + echo " TARGET=$base" + # --- SAFETY GATES (Mission §6) --- + [ "$sdir" = "$ARCHIVE_ROOT/candidates" ] || die "cleanup-partial: Root != $ARCHIVE_ROOT/candidates (FAIL CLOSED)" + [[ "$sid" =~ ^[a-f0-9]{12}$ ]] || die "cleanup-partial: Ordnername ungueltig '$sid' (FAIL CLOSED)" + [ -e "$base" ] || { echo " PARTIAL_ARCHIVE=NO (kein Ziel)"; echo "DRY_RUN_PASS: nichts zu entfernen."; exit 0; } + [ ! -L "$base" ] || die "cleanup-partial: Ziel ist Symlink (FAIL CLOSED)" + if mountpoint -q "$base" 2>/dev/null; then die "cleanup-partial: Ziel ist Mountpoint (FAIL CLOSED)"; fi + if mountpoint -q "$sdir" 2>/dev/null; then die "cleanup-partial: Parent ist Mountpoint (FAIL CLOSED)"; fi + if grep -qE " $base( |$)" /proc/mounts 2>/dev/null; then die "cleanup-partial: Ziel ist Bind/Volume Mount (FAIL CLOSED)"; fi + if ps aux 2>/dev/null | grep -E "docker save|docker build|docker tag" | grep -v grep | grep -q "$sdir"; then + die "cleanup-partial: Build-/Save-Prozess aktiv (FAIL CLOSED)" + fi + if docker ps --format '{{.Names}}' 2>/dev/null | grep -qE "build|remediation"; then + die "cleanup-partial: Build-Container aktiv (FAIL CLOSED)" + fi + # --- KLASSIFIKATION (Mission §2/§7) --- + local cls + cls="$(remediation_archive_classification)" + echo " CLASSIFICATION=$cls" + case "$cls" in + VALID) die "cleanup-partial: Archive ist VALID/verify PASS — Cleanup strikt DENY (FAIL CLOSED)" ;; + INVALID) die "cleanup-partial: Archive INVALID (voll, aber SHA/Metadata/Binding falsch) — FAIL CLOSED, kein Cleanup" ;; + UNKNOWN) die "cleanup-partial: Archive-Zustand UNKNOWN — FAIL CLOSED, kein Cleanup" ;; + PARTIAL) : ;; + NO) echo " PARTIAL_ARCHIVE=NO"; echo "DRY_RUN_PASS: kein partielles Archive."; exit 0 ;; + *) die "cleanup-partial: ungueltige Klassifikation (FAIL CLOSED)" ;; + esac + echo " PARTIAL_ARCHIVE=CONFIRMED" + echo " IMAGE_TAR=$( [ -f "$base/image.tar" ] && [ -s "$base/image.tar" ] && echo PRESENT || echo ABSENT )" + echo " ACTIVE_REMEDIATION_IMAGE=$( if remediation_session && docker image inspect "$(remediation_full_image_id)" >/dev/null 2>&1; then echo PRESENT; else echo NONE; fi )" + echo " ACTIVE_PIN=$( docker ps -a --format '{{.Names}}' 2>/dev/null | grep -Eq 'rq-historical-pin-phase13-5-runner-remediation' && echo PRESENT || echo NONE )" + echo " ACTIVE_CONTAINER=$( docker ps --format '{{.Names}}' 2>/dev/null | grep -q 'remediation' && echo PRESENT || echo NONE )" + echo " VALID_ARCHIVE=NO" + if [ "$RUN_MODE" = "DRY_RUN" ]; then + echo " ACTION=WOULD_REMOVE_PARTIAL_REMEDIATION_ARCHIVE" + echo "DRY_RUN_PASS: cleanup-partial-remediation-archive wuerde $base entfernen (keine Aktion)." + echo " ZERO MUTATION." + exit 0 + fi + mode_or_die + rm -rf -- "$base" || die "cleanup-partial: Entfernen fehlgeschlagen (FAIL CLOSED)" + [ ! -e "$base" ] || die "cleanup-partial: Ziel nach Entfernen noch vorhanden (FAIL CLOSED)" + echo "CLEANUP-PARTIAL-REMEDIATION-ARCHIVE OK: $base entfernt" + echo " Retry bereit: cleanup-partial-remediation-staging ist nun logisch freigegeben." +} + # restore-recovery-image-archive (DESIGN; realer Restore NICHT in dieser Mission ausfuehren) restore_recovery_dry() { # dry-run expect_zero "$@" @@ -2806,7 +2969,7 @@ if [ "$SUBCMD" = "dry-run" ]; then SUBCMD="${1:-}" shift || true case "$SUBCMD" in - build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|pin-current-candidate|recreate-candidate-safe|rollback-to-recovery-gold|promote-runner-patch|rollback-runner-patch|build-runner-remediation|recreate-runner-remediation-safe|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image|cleanup-partial-recovery-archive|cleanup-partial-remediation-staging|recover-gold-build-source) ;; + build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|pin-current-candidate|recreate-candidate-safe|rollback-to-recovery-gold|promote-runner-patch|rollback-runner-patch|build-runner-remediation|recreate-runner-remediation-safe|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image|cleanup-partial-recovery-archive|cleanup-partial-remediation-staging|cleanup-partial-remediation-archive|recover-gold-build-source) ;; *) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|build-runner-remediation|recreate-runner-remediation-safe|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image|cleanup-partial-recovery-archive zulaessig" ;; esac fi @@ -3219,6 +3382,7 @@ case "$SUBCMD" in verify-remediation-archive) verify_archive_cmd remediation "$@" ;; cleanup-partial-recovery-archive) cleanup_partial_recovery_archive "$@" ;; cleanup-partial-remediation-staging) cleanup_partial_remediation_staging "$@" ;; + cleanup-partial-remediation-archive) cleanup_partial_remediation_archive "$@" ;; verify-recovery-pin) verify_pin_cmd recovery "$@" ;; verify-remediation-pin) verify_pin_cmd remediation "$@" ;; verify-recovery-ready) verify_ready_cmd recovery "$@" ;;