From 8187a4bc1d664f45f83fdc6838e03e0b8a2489e2 Mon Sep 17 00:00:00 2001 From: Rain Ocampo Date: Sat, 29 Aug 2026 23:56:14 +0000 Subject: [PATCH] Phase 13.5: Authorized Remediation Divergence Verify Fix - verify-runner-remediation-image nutzt gemeinsame authorized_gold_delta-Policy (exakt 1 autorisierte Runner-Divergenz, alles andere FAIL CLOSED); Candidate-vs-Host-SoT direkter Vergleich; verify_critical_files runner-Flag --- .../control-plane/cp2a2_2b/rq-historical | 95 ++++++++++++++----- 1 file changed, 69 insertions(+), 26 deletions(-) diff --git a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical index ed06c0a..5b7389b 100644 --- a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical +++ b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical @@ -526,10 +526,14 @@ gen_host_manifest() { } # read-only: Critical-Files-SHAs Recovery-Image + laufender Gold-Container + Gold-Baseline (Punkt 11) +# $2 (optional) = "runner" -> erlaubt die EINE autorisierte Runner-Divergenz im IMAGE: +# run_backfill_year.py image=RUNNER_REMEDIATION_SHA (Container bleibt Gold). +# Nur fuer verify-runner-remediation-image; alle anderen Aufrufer bleiben strikt 0-Divergenz. verify_critical_files() { # $1 = Basisverzeichnis mit app/ (kopierter Recovery-Tree) ODER leer -> nur Container+Baseline local tmpc tmpdir k path imgsha csha local base_dir="${1:-}" + local allow_runner="${2:-}" local img_shas="" if [ -n "$base_dir" ]; then # Recovery-Image-SHAs aus kopiertem Tree. Der Tree ist prefixlos (docker cp @@ -549,6 +553,12 @@ verify_critical_files() { is=$(echo "$img_shas" | grep -oE "${k}:[a-f0-9]+|${k}:MISSING" | cut -d: -f2) img="$is" fi + # Autorisierte Runner-Divergenz: NUR run_backfill_year.py, NUR im Image, NUR auf RUNNER_REMEDIATION_SHA. + if [ "$allow_runner" = "runner" ] && [ "$k" = "run_backfill_year.py" ] \ + && [ "$img" = "$RUNNER_REMEDIATION_SHA" ] && [ "$csha" = "$gold" ]; then + printf ' AUTH %-22s image=%s container=%s gold=%s (autorisierte Runner-Divergenz)\n' "$k" "${img:0:12}" "${csha:0:12}" "${gold:0:12}" + continue + fi if [ "$csha" = "$gold" ] && { [ -z "$base_dir" ] || [ "$img" = "$gold" ]; }; then printf ' MATCH %-22s container=%s gold=%s\n' "$k" "${csha:0:12}" "${gold:0:12}" else @@ -1106,41 +1116,55 @@ remediation_guards() { echo " [guard O] kein freier Parameter/User-Input:" echo " OK (keine Argumente akzeptiert)" } -# Read-only Build-Input-Check: nur app/run_backfill_year.py darf vom Gold-Manifest -# divergieren (und zwar exakt auf RUNNER_REMEDIATION_SHA). alles andere -> FAIL. -verify_remediation_build_input() { - local hm gold d=0 m=0 x=0 mat=0 extra_ok=0 runner_seen=0 - hm="$(mktemp)"; gen_host_manifest > "$hm" +# ---- GEMEINSAME AUTHORIZED-GOLD-DELTA-POLICY (Phase 13.5) ---- +# Einzige autorisierte Divergenz gegen GOLD_MANIFEST: +# app/run_backfill_year.py ALT=RUNNER_GOLD_SHA -> NEU=RUNNER_REMEDIATION_SHA +# (exakte Pfad- UND SHA-Bindung; KEINE generische "1 Divergenz ok"-Lockerung). +# Setzt globale Zaehler AD_MATCH/AD_AUTH/AD_UNAUTH/AD_MISSING/AD_EXTRA und +# AD_RET (ok|fail). fail wenn UNAUTH>0 || MISSING>0 || EXTRA>0. +# Wird von verify_remediation_build_input UND verify_runner_remediation_image +# verwendet -> EINE Policy, keine unterschiedlichen Policies. +authorized_gold_delta() { + local new="$1" gold goldrel goldsha nsha nrel found + AD_MATCH=0; AD_AUTH=0; AD_UNAUTH=0; AD_MISSING=0; AD_EXTRA=0 while read -r goldsha goldrel; do goldrel="${goldrel#./}"; [ -n "$goldrel" ] || continue - local nsha - nsha="$(awk -v r="$goldrel" '$2==r{print $1}' "$hm")" - if [ -z "$nsha" ]; then m=$((m+1)) - elif [ "$nsha" = "$goldsha" ]; then mat=$((mat+1)) + nsha="$(awk -v r="$goldrel" '$2==r{print $1}' "$new")" + if [ -z "$nsha" ]; then AD_MISSING=$((AD_MISSING+1)) + elif [ "$nsha" = "$goldsha" ]; then AD_MATCH=$((AD_MATCH+1)) else - if [ "$goldrel" = "app/run_backfill_year.py" ]; then - runner_seen=1 - if [ "$nsha" = "$RUNNER_REMEDIATION_SHA" ]; then extra_ok=1; else die "BUILD-INPUT FAIL: Runner divergiert auf unerwartete SHA $nsha"; fi + if [ "$goldrel" = "app/run_backfill_year.py" ] \ + && [ "$goldsha" = "$RUNNER_GOLD_SHA" ] \ + && [ "$nsha" = "$RUNNER_REMEDIATION_SHA" ]; then + AD_AUTH=$((AD_AUTH+1)) else - echo " DIVERGED(UNERWARTET) $goldrel"; d=$((d+1)) + AD_UNAUTH=$((AD_UNAUTH+1)) fi fi done < "$GOLD_MANIFEST" while read -r nsha nrel; do nrel="${nrel#./}" - local found found="$(awk -v r="$nrel" '$2==r{print $1}' "$GOLD_MANIFEST")" if [ -z "$found" ]; then case "$nrel" in app/app/backfill.py|app/app/test_idempotency.py|app/eligibility.py) ;; - *) echo " EXTRA(UNERLAUBT) $nrel"; x=$((x+1)) ;; + *) AD_EXTRA=$((AD_EXTRA+1)) ;; esac fi - done < "$hm" + done < "$new" + AD_RET="ok" + [ $AD_UNAUTH -eq 0 ] && [ $AD_MISSING -eq 0 ] && [ $AD_EXTRA -eq 0 ] || AD_RET="fail" +} +# Read-only Build-Input-Check: nur app/run_backfill_year.py darf vom Gold-Manifest +# divergieren (und zwar exakt auf RUNNER_REMEDIATION_SHA). alles andere -> FAIL. +verify_remediation_build_input() { + local hm + hm="$(mktemp)"; gen_host_manifest > "$hm" + authorized_gold_delta "$hm" rm -f "$hm" - echo " Gold $((mat+d+m)): MATCH=$mat DIVERGED=$d MISSING=$m ; unerlaubte EXTRA=$x ; autorisierte Runner-Divergenz=$extra_ok" - [ "$runner_seen" = "1" ] && [ "$extra_ok" = "1" ] || die "BUILD-INPUT FAIL: autorisierte Runner-Divergenz fehlt (FAIL CLOSED)" - [ $d -eq 0 ] && [ $m -eq 0 ] && [ $x -eq 0 ] || die "BUILD-INPUT FAIL: unerwartete Divergenz (FAIL CLOSED)" + echo " Gold $((AD_MATCH+AD_AUTH+AD_UNAUTH+AD_MISSING)): MATCH=$AD_MATCH AUTHORIZED_DIVERGED=$AD_AUTH UNAUTHORIZED_DIVERGED=$AD_UNAUTH MISSING=$AD_MISSING ; unerlaubte EXTRA=$AD_EXTRA" + [ "$AD_AUTH" = "1" ] || die "BUILD-INPUT FAIL: autorisierte Runner-Divergenz fehlt (FAIL CLOSED)" + [ "$AD_RET" = "ok" ] || die "BUILD-INPUT FAIL: unerwartete Divergenz (FAIL CLOSED)" return 0 } # ---- build-runner-remediation (DRY_RUN = kein Build; KEIN Overwrite) ---- @@ -1202,13 +1226,32 @@ verify_runner_remediation_image() { tmpf="$(mktemp)" ( cd "$tmpdir/appapp" && find . -type f -name '*.py' ! -path '*/__pycache__/*' -printf '%P\n' 2>/dev/null | sort \ | while IFS= read -r rel; do [ -f "$rel" ] && sha256sum "$rel"; done | awk '{print $1" app/"$2}' ) > "$tmpf" - classify_and_report "$tmpf"; local gret="$G_RET"; rm -f "$tmpf" - if [ "$gret" != "ok" ]; then - rm -rf "$tmpdir"; die "verify: Image != Gold-Manifest (unzulaessige Divergenz) (FAIL CLOSED)" - fi - echo " Image vs GOLD (63 MATCH / 0 DIVERGED / 0 MISSING / 0 unerlaubte EXTRA)" + # [Gold-Delta] Image vs GOLD_MANIFEST: exakt 1 autorisierte Runner-Divergenz erlaubt. + authorized_gold_delta "$tmpf" + echo " Image vs GOLD: MATCH=$AD_MATCH AUTHORIZED_DIVERGED=$AD_AUTH UNAUTHORIZED_DIVERGED=$AD_UNAUTH MISSING=$AD_MISSING unerlaubte EXTRA=$AD_EXTRA" + [ "$AD_AUTH" = "1" ] || { rm -rf "$tmpdir"; die "verify: Gold-Delta != exakt 1 autorisierte Runner-Divergenz (FAIL CLOSED)"; } + [ "$AD_RET" = "ok" ] || { rm -rf "$tmpdir"; die "verify: Image != Gold-Manifest (unzulaessige Divergenz) (FAIL CLOSED)"; } + # [Candidate vs Host-SoT] Image-App-Manifest == Host-SoT-App-Manifest (direkter + # Manifest-zu-Manifest-Vergleich, OHNE Gold-Bezug; vollstaendig MATCH). + # Host-only-Legacy-Dateien (im Image via .dockerignore ausgeschlossen) sind erlaubt. + local hmf cmiss cextra + hmf="$(mktemp)"; gen_host_manifest > "$hmf" + cmiss=0; cextra=0 + while read -r hsha hrel; do + hrel="${hrel#./}"; [ -n "$hrel" ] || continue + case "$hrel" in + app/app/backfill.py|app/app/test_idempotency.py|app/eligibility.py) continue ;; + esac + isha="$(awk -v r="$hrel" '$2==r{print $1}' "$tmpf")" + if [ -z "$isha" ]; then cmiss=$((cmiss+1)) + elif [ "$isha" != "$hsha" ]; then cextra=$((cextra+1)); fi + done < "$hmf" + rm -f "$hmf" + echo " Candidate vs Host-SoT: MATCH (direkt) MISSING=$cmiss DIVERGED=$cextra" + [ "$cmiss" = "0" ] && [ "$cextra" = "0" ] || { rm -rf "$tmpdir"; die "verify: Candidate != Host-SoT (FAIL CLOSED)"; } + rm -f "$tmpf" echo " [Critical-Files] $REMEDIATION_TAG vs gold:" - verify_critical_files "$tmpdir/appapp" + verify_critical_files "$tmpdir/appapp" runner rm -rf "$tmpdir" echo " [Runner] Image vs Host-SoT persistent:" local ir; ir="$(docker run --rm --entrypoint /bin/sh "$imgid" -c "sha256sum /app/app/run_backfill_year.py 2>/dev/null" | awk '{print $1}')" || ir="" @@ -1230,7 +1273,7 @@ verify_runner_remediation_image() { local srunner; srunner="$(grep '^HOST_SOT_RUNNER_SHA=' "$REMEDIATION_SESSION_META" | cut -d= -f2)" [ "$srunner" = "$RUNNER_REMEDIATION_SHA" ] || die "verify: Session-Runner != erwartet (FAIL CLOSED)" echo " OK ($srunner)" - echo "VERDICT: REMEDIATION IMAGE OK (63 MATCH / 0 DIVERGED / 0 MISSING / 0 unerlaubte EXTRA; Runner=Host-SoT; Critical-SHAs + Config PASS)" + echo "VERDICT: REMEDIATION IMAGE OK (Candidate==Host-SoT; Gold-Delta exakt 1 autorisierte Runner-Divergenz; Critical-SHAs + Config PASS)" exit 0 } # ---- recreate-runner-remediation-safe (NUR historical-service; nur Red Queen) ----