Harden secmonlib release evidence privacy

ober

f630151ff12d612ebbbb4f734a46692bf31e9243

diff --git a/.jerboa/security.json b/.jerboa/security.json
index bb6df06..4c4c5ce 100644
--- a/.jerboa/security.json
+++ b/.jerboa/security.json
@@ -74,7 +74,17 @@
           "agent_no_secret_startup_status=pass",
           "target_platform_status=linux-or-freebsd-recorded"
         ]
-      }
+      },
+      "proofMaterialPolicy": "reject-overlarge-private-sensitive",
+      "forbiddenEvidenceMaterial": [
+        "host-private-paths",
+        "raw-ssh-remotes",
+        "private-keys",
+        "authorization-headers",
+        "credential-shaped-material",
+        "raw-production-agent-output"
+      ],
+      "sanitizeEvidence": "scripts/sanitize-evidence.sh"
     },
     "ffi": {
       "requireCollectSafeBlockingCalls": true,
diff --git a/Makefile b/Makefile
index 9017b4d..d735a0d 100644
--- a/Makefile
+++ b/Makefile
@@ -23,7 +23,7 @@ REPRO_DIR ?= dist/reproducibility
 SOAK_DIR ?= dist/soak
 BINARY_SMOKE_DIR ?= dist/binary-smoke
 
-.PHONY: all help build binary keygen agent collector analyze test security audit verify sbom reproducibility-report soak-evidence binary-smoke release-evidence install clean ensure-jsqlite
+.PHONY: all help build binary keygen agent collector analyze test security audit verify sbom reproducibility-report soak-evidence binary-smoke sanitize-evidence release-evidence install clean ensure-jsqlite
 .DEFAULT_GOAL := help
 
 all: binary
@@ -46,6 +46,7 @@ help:
 	@echo "  make reproducibility-report"
 	@echo "                         Compare two clean local binary builds"
 	@echo "  make soak-evidence     Record load/soak evidence status"
+	@echo "  make sanitize-evidence Redact/check release evidence"
 	@echo "  make release-evidence  Write release evidence under dist/"
 
 # Standalone native binary via .jerbuild (entry bin/agent.ss -> secmon-agent).
@@ -115,14 +116,18 @@ soak-evidence:
 binary-smoke:
 	JSECMONLIB_BINARY_SMOKE_DIR="$(BINARY_SMOKE_DIR)" scripts/binary-smoke.sh
 
+sanitize-evidence:
+	scripts/sanitize-evidence.sh "$(DIST_DIR)"
+
 release-evidence: verify sbom reproducibility-report soak-evidence binary-smoke
 	rm -rf "$(DIST_DIR)"
 	mkdir -p "$(DIST_DIR)"
 	git rev-parse HEAD > "$(DIST_DIR)/git-commit.txt"
 	git status --short > "$(DIST_DIR)/git-status.txt"
-	uname -a > "$(DIST_DIR)/build-environment.txt"
+	uname -srm > "$(DIST_DIR)/build-environment.txt"
+	printf 'jerbuild_version=' >> "$(DIST_DIR)/build-environment.txt"
 	$(JERBUILD) --version >> "$(DIST_DIR)/build-environment.txt"
-	$(JERBUILD) --jerboa-home >> "$(DIST_DIR)/build-environment.txt"
+	printf 'jerboa_home_status=present\n' >> "$(DIST_DIR)/build-environment.txt"
 	cargo metadata --manifest-path patches/jerboa-native-rs/Cargo.toml --locked --format-version 1 > "$(DIST_DIR)/cargo-metadata-native.json"
 	cd patches/jerboa-native-rs && "$(CARGO_AUDIT)" audit > "$(CURDIR)/$(DIST_DIR)/rustsec-native.txt"
 	shasum -a 256 secmon-agent > "$(DIST_DIR)/secmon-agent-sha256.txt"
@@ -135,6 +140,7 @@ release-evidence: verify sbom reproducibility-report soak-evidence binary-smoke
 		docs/release-evidence.md \
 		docs/threat-model.md \
 		scripts/binary-smoke.sh \
+		scripts/sanitize-evidence.sh \
 		scripts/reproducibility-report.sh \
 		scripts/sbom.sh \
 		scripts/security-check.sh \
@@ -149,6 +155,13 @@ release-evidence: verify sbom reproducibility-report soak-evidence binary-smoke
 	cp -R "$(REPRO_DIR)" "$(DIST_DIR)/reproducibility"
 	cp -R "$(SOAK_DIR)" "$(DIST_DIR)/soak"
 	cp -R "$(BINARY_SMOKE_DIR)" "$(DIST_DIR)/binary-smoke"
+	scripts/sanitize-evidence.sh "$(DIST_DIR)"
+	{ \
+	  echo "repo=jerboa-secmonlib"; \
+	  echo "release_evidence_status=pass"; \
+	  echo "evidence_privacy_status=sanitized"; \
+	  echo "private_path_scan_status=pass"; \
+	} > "$(DIST_DIR)/result.txt"
 
 install: binary
 	mkdir -p $(BIN_DIR)
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
index e7cbed5..1730929 100644
--- a/docs/release-evidence.md
+++ b/docs/release-evidence.md
@@ -17,14 +17,15 @@ The `release-evidence` target runs `make verify` first, then writes untracked
 artifacts under `dist/release-evidence/`:
 
 - `git-commit.txt` and `git-status.txt`.
-- `build-environment.txt` with OS and Jerboa toolchain identity.
+- `build-environment.txt` with host-neutral OS and Jerboa toolchain identity.
 - Cargo dependency metadata and RustSec audit output for the patched native
   Rust crate.
 - SHA-256 hash for `secmon-agent`.
 - Static-binary source input hashes, generated FFI symbol list, and dynamic
   linkage output from `otool` or `ldd`.
 - `sbom/` with a source manifest, source hashes, native Cargo metadata, and
-  release input hashes.
+  release input hashes. Host-private paths are not preserved in copied release
+  evidence.
 - `reproducibility/` with a recorded WPO prewarm build, two measured local
   binary builds, release-input hashes for policy/evidence scripts, and
   `result.txt`.
@@ -32,6 +33,13 @@ artifacts under `dist/release-evidence/`:
   this records an explicit platform block instead of implying runtime success.
 - `soak/` with agent/collector load and sandbox status. Local default evidence
   records blocked statuses until a Linux or FreeBSD release-host soak is run.
+- `result.txt` with the top-level `release_evidence_status=pass` and evidence
+  privacy scan status.
+
+Before finalizing the release evidence directory, `scripts/sanitize-evidence.sh`
+redacts local paths, SSH clone references, authorization headers, and
+credential-shaped fields from copied evidence and fails if private or sensitive
+material remains.
 
 Reproducibility is claimed only when `reproducibility/result.txt` records
 `prewarm_status=present`, matching `binary_status`, `ffi_symbols_status`,
@@ -70,6 +78,8 @@ proof is absent or incomplete.
   `target_platform_status=linux-or-freebsd-recorded`.
 
 When proof is valid, the scripts copy it into `dist/release-evidence/soak/` or
-`dist/release-evidence/binary-smoke/` and write a `.sha256` sidecar. When proof
-is required but missing or incomplete, the status is `blocked-target-proof` and
-the evidence target fails.
+`dist/release-evidence/binary-smoke/` and write a `.sha256` sidecar. Proof files
+are rejected before copy if they are oversized or contain host-private paths,
+SSH remotes, private keys, authorization headers, or credential-shaped fields.
+When proof is required but missing, incomplete, private, or sensitive, the
+status is `blocked-target-proof` and the evidence target fails.
diff --git a/scripts/binary-smoke.sh b/scripts/binary-smoke.sh
index 3ce0748..888eec2 100755
--- a/scripts/binary-smoke.sh
+++ b/scripts/binary-smoke.sh
@@ -8,12 +8,14 @@ OUT=${JSECMONLIB_BINARY_SMOKE_DIR:-dist/binary-smoke}
 REQUIRE=${JSECMONLIB_REQUIRE_BINARY_SMOKE:-0}
 TARGET_BINARY_SMOKE_PROOF=${JSECMONLIB_TARGET_BINARY_SMOKE_PROOF_FILE:-}
 REQUIRE_TARGET_BINARY_SMOKE_PROOF=${JSECMONLIB_REQUIRE_TARGET_BINARY_SMOKE_PROOF:-0}
+MAX_PROOF_BYTES=${JSECMONLIB_MAX_PROOF_BYTES:-65536}
 PLATFORM=$(uname -s)
 
 rm -rf "$OUT"
 mkdir -p "$OUT"
 
 target_binary_smoke_proof_status=not-run
+proof_rejection_reason=
 
 write_status() {
   local status=$1
@@ -58,6 +60,26 @@ record_proof() {
   shasum -a 256 "$destination" > "$destination.sha256"
 }
 
+reject_proof_material() {
+  local proof=$1
+  local bytes
+
+  proof_rejection_reason=
+  bytes=$(wc -c < "$proof" | tr -d ' ')
+  if [ "$bytes" -gt "$MAX_PROOF_BYTES" ]; then
+    proof_rejection_reason="target binary smoke proof is larger than $MAX_PROOF_BYTES bytes"
+    return 1
+  fi
+  if [ -n "${HOME:-}" ] && grep -F -q "$HOME" "$proof"; then
+    proof_rejection_reason="target binary smoke proof contains a host-private home path"
+    return 1
+  fi
+  if LC_ALL=C grep -E -q '/Users/|/private/var/folders/|/var/folders/|git@|ssh://|BEGIN (RSA |DSA |EC |OPENSSH )?PRIVATE KEY|Authorization:|AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|GITHUB_TOKEN|(api[_-]?key|token|password|secret)[[:space:]]*[:=]' "$proof"; then
+    proof_rejection_reason="target binary smoke proof contains private paths, remotes, or credential-shaped material"
+    return 1
+  fi
+}
+
 proof_requested=0
 if [ -n "$TARGET_BINARY_SMOKE_PROOF" ] || [ "$REQUIRE_TARGET_BINARY_SMOKE_PROOF" = "1" ]; then
   proof_requested=1
@@ -73,6 +95,15 @@ if [ "$proof_requested" = "1" ]; then
       "set JSECMONLIB_TARGET_BINARY_SMOKE_PROOF_FILE to a non-empty target-host binary smoke proof"
     exit 1
   fi
+  if ! reject_proof_material "$TARGET_BINARY_SMOKE_PROOF"; then
+    write_status \
+      "blocked-target-proof" \
+      "blocked-target-proof" \
+      "not-run" \
+      "blocked-private-or-sensitive" \
+      "$proof_rejection_reason"
+    exit 1
+  fi
   if ! require_marker "$TARGET_BINARY_SMOKE_PROOF" "target_binary_smoke_status=target-evidence-recorded" ||
      ! require_marker "$TARGET_BINARY_SMOKE_PROOF" "agent_no_secret_startup_status=pass" ||
      ! require_marker "$TARGET_BINARY_SMOKE_PROOF" "target_platform_status=linux-or-freebsd-recorded"; then
diff --git a/scripts/reproducibility-report.sh b/scripts/reproducibility-report.sh
index 7483448..f4b15ae 100755
--- a/scripts/reproducibility-report.sh
+++ b/scripts/reproducibility-report.sh
@@ -25,6 +25,7 @@ hash_release_inputs() {
     docs/release-evidence.md \
     docs/threat-model.md \
     scripts/binary-smoke.sh \
+    scripts/sanitize-evidence.sh \
     scripts/reproducibility-report.sh \
     scripts/sbom.sh \
     scripts/security-check.sh \
diff --git a/scripts/sanitize-evidence.sh b/scripts/sanitize-evidence.sh
new file mode 100755
index 0000000..5f6df7e
--- /dev/null
+++ b/scripts/sanitize-evidence.sh
@@ -0,0 +1,61 @@
+#!/bin/sh
+set -eu
+
+if [ "$#" -eq 0 ]; then
+    set -- dist/release-evidence
+fi
+
+sanitize_file() {
+    file=$1
+
+    if ! LC_ALL=C grep -Iq . "$file" 2>/dev/null; then
+        return 0
+    fi
+
+    perl -0pi -e '
+      s#/Users/[^[:space:]\x22\x27()]+#<local-path>#g;
+      s#/private/var/folders/[^[:space:]\x22\x27()]+#<local-temp-path>#g;
+      s#/var/folders/[^[:space:]\x22\x27()]+#<local-temp-path>#g;
+      if (defined $ENV{HOME} && length $ENV{HOME}) {
+        my $home = quotemeta($ENV{HOME});
+        s#$home#<home>#g;
+      }
+      s#~/mine#<local-path>#g;
+      s#\$\((HOME)\)/mine#<local-path>#g;
+      s#git\@#git-at-#g;
+      s#ssh://[^[:space:]\x22\x27()]+#ssh://<redacted>#g;
+      s#(Authorization:[[:space:]]*)[^[:space:]\x0a]+#${1}<redacted>#gi;
+      s#((api[_-]?key|token|password|secret)[[:space:]]*[:=][[:space:]]*)[^[:space:]\x0a]+#${1}<redacted>#gi;
+    ' "$file"
+}
+
+scan_for_private_material() {
+    evidence_dir=$1
+
+    if command -v rg >/dev/null 2>&1; then
+        if rg -n -S --hidden \
+            -e '/Users/' \
+            -e '/private/var/folders/' \
+            -e '/var/folders/' \
+            -e 'git@' \
+            -e 'ssh://[^[:space:]]+' \
+            -e 'BEGIN (RSA |DSA |EC |OPENSSH )?PRIVATE KEY' \
+            -e 'Authorization:[[:space:]]*[^<[:space:]]' \
+            -e '(api[_-]?key|token|password|secret)[[:space:]]*[:=][[:space:]]*[^<[:space:]]' \
+            "$evidence_dir"; then
+            printf '[secmonlib-sanitize] private or sensitive evidence material remains in %s\n' "$evidence_dir" >&2
+            return 1
+        fi
+    fi
+}
+
+for evidence_dir in "$@"; do
+    if [ ! -d "$evidence_dir" ]; then
+        continue
+    fi
+
+    find "$evidence_dir" -type f -print | while IFS= read -r file; do
+        sanitize_file "$file"
+    done
+    scan_for_private_material "$evidence_dir"
+done
diff --git a/scripts/sbom.sh b/scripts/sbom.sh
index 366c126..70c20fa 100755
--- a/scripts/sbom.sh
+++ b/scripts/sbom.sh
@@ -17,8 +17,11 @@ mkdir -p "$OUT"
   date -u '+%Y-%m-%dT%H:%M:%SZ'
   printf 'jerbuild_version='
   "$JERBUILD" --version
-  printf 'jerboa_home='
-  "$JERBUILD" --jerboa-home
+  if "$JERBUILD" --jerboa-home >/dev/null 2>&1; then
+    echo "jerboa_home_status=present"
+  else
+    echo "jerboa_home_status=unavailable"
+  fi
   echo "static_binary=secmon-agent"
   echo "native_crate=patches/jerboa-native-rs"
   echo "vendored_jsqlite=vendor/jsqlite"
@@ -52,5 +55,6 @@ shasum -a 256 \
   patches/jerboa-native-rs/Cargo.lock \
   support/static-main.c \
   support/gen-ffi-symbols.sh \
+  scripts/sanitize-evidence.sh \
   Makefile \
   .jerbuild > "$OUT/release-inputs-sha256.txt"
diff --git a/scripts/security-check.sh b/scripts/security-check.sh
index 4a04745..fa13682 100755
--- a/scripts/security-check.sh
+++ b/scripts/security-check.sh
@@ -53,6 +53,7 @@ require_file "LICENSE"
 require_file "SECURITY.md"
 require_file ".gitignore"
 require_file ".jerboa/security.json"
+require_file "scripts/sanitize-evidence.sh"
 find . -maxdepth 1 -iname "README*" -type f | grep -q . || {
   printf '[secmonlib-security] missing README\n' >&2
   fail=1
@@ -67,12 +68,17 @@ require_text "scripts/soak-evidence.sh" "blocked-target-proof"
 require_text "scripts/binary-smoke.sh" "JSECMONLIB_TARGET_BINARY_SMOKE_PROOF_FILE"
 require_text "scripts/binary-smoke.sh" "JSECMONLIB_REQUIRE_TARGET_BINARY_SMOKE_PROOF"
 require_text "scripts/binary-smoke.sh" "blocked-target-proof"
+require_text "scripts/soak-evidence.sh" "reject_proof_material"
+require_text "scripts/binary-smoke.sh" "reject_proof_material"
+require_text "scripts/sanitize-evidence.sh" "private or sensitive evidence material remains"
+require_text "Makefile" "scripts/sanitize-evidence.sh"
 require_text "docs/threat-model.md" "target_load_proof_status: documented"
 require_text "docs/threat-model.md" "target_sandbox_proof_status: documented"
 require_text "docs/threat-model.md" "target_binary_smoke_proof_status: documented"
 require_text ".jerboa/security.json" "\"targetLoadProof\""
 require_text ".jerboa/security.json" "\"targetSandboxProof\""
 require_text ".jerboa/security.json" "\"targetBinarySmokeProof\""
+require_text ".jerboa/security.json" "\"proofMaterialPolicy\""
 
 if command -v rg >/dev/null 2>&1; then
   say "running high-confidence secret scan"
diff --git a/scripts/soak-evidence.sh b/scripts/soak-evidence.sh
index 6818611..5f832e6 100755
--- a/scripts/soak-evidence.sh
+++ b/scripts/soak-evidence.sh
@@ -11,6 +11,7 @@ TARGET_LOAD_PROOF=${JSECMONLIB_TARGET_LOAD_PROOF_FILE:-}
 REQUIRE_TARGET_LOAD_PROOF=${JSECMONLIB_REQUIRE_TARGET_LOAD_PROOF:-0}
 TARGET_SANDBOX_PROOF=${JSECMONLIB_TARGET_SANDBOX_PROOF_FILE:-}
 REQUIRE_TARGET_SANDBOX_PROOF=${JSECMONLIB_REQUIRE_TARGET_SANDBOX_PROOF:-0}
+MAX_PROOF_BYTES=${JSECMONLIB_MAX_PROOF_BYTES:-65536}
 PLATFORM=$(uname -s)
 
 rm -rf "$OUT"
@@ -18,6 +19,7 @@ mkdir -p "$OUT"
 
 target_load_proof_status=not-run
 target_sandbox_proof_status=not-run
+proof_rejection_reason=
 
 write_status() {
   local status=$1
@@ -71,6 +73,27 @@ record_proof() {
   shasum -a 256 "$destination" > "$destination.sha256"
 }
 
+reject_proof_material() {
+  local proof=$1
+  local label=$2
+  local bytes
+
+  proof_rejection_reason=
+  bytes=$(wc -c < "$proof" | tr -d ' ')
+  if [ "$bytes" -gt "$MAX_PROOF_BYTES" ]; then
+    proof_rejection_reason="$label proof is larger than $MAX_PROOF_BYTES bytes"
+    return 1
+  fi
+  if [ -n "${HOME:-}" ] && grep -F -q "$HOME" "$proof"; then
+    proof_rejection_reason="$label proof contains a host-private home path"
+    return 1
+  fi
+  if LC_ALL=C grep -E -q '/Users/|/private/var/folders/|/var/folders/|git@|ssh://|BEGIN (RSA |DSA |EC |OPENSSH )?PRIVATE KEY|Authorization:|AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|GITHUB_TOKEN|(api[_-]?key|token|password|secret)[[:space:]]*[:=]' "$proof"; then
+    proof_rejection_reason="$label proof contains private paths, remotes, or credential-shaped material"
+    return 1
+  fi
+}
+
 load_proof_requested=0
 if [ -n "$TARGET_LOAD_PROOF" ] || [ "$REQUIRE_TARGET_LOAD_PROOF" = "1" ]; then
   load_proof_requested=1
@@ -89,6 +112,18 @@ if [ "$load_proof_requested" = "1" ]; then
       "set JSECMONLIB_TARGET_LOAD_PROOF_FILE to a non-empty target-host load proof"
     exit 1
   fi
+  if ! reject_proof_material "$TARGET_LOAD_PROOF" "target load"; then
+    write_status \
+      "blocked-target-proof" \
+      "blocked-target-proof" \
+      "blocked-target-proof" \
+      "blocked-target-proof" \
+      "blocked-target-proof" \
+      "blocked-private-or-sensitive" \
+      "$target_sandbox_proof_status" \
+      "$proof_rejection_reason"
+    exit 1
+  fi
   if ! require_marker "$TARGET_LOAD_PROOF" "production_load_status=current-run-recorded" "target load" ||
      ! require_marker "$TARGET_LOAD_PROOF" "agent_runtime_status=release-host-runtime-recorded" "target load" ||
      ! require_marker "$TARGET_LOAD_PROOF" "collector_load_status=release-host-sustained-recorded" "target load" ||
@@ -126,6 +161,18 @@ if [ "$sandbox_proof_requested" = "1" ]; then
       "set JSECMONLIB_TARGET_SANDBOX_PROOF_FILE to a non-empty target-host sandbox proof"
     exit 1
   fi
+  if ! reject_proof_material "$TARGET_SANDBOX_PROOF" "target sandbox"; then
+    write_status \
+      "blocked-target-proof" \
+      "blocked-target-proof" \
+      "blocked-target-proof" \
+      "blocked-target-proof" \
+      "blocked-target-proof" \
+      "$target_load_proof_status" \
+      "blocked-private-or-sensitive" \
+      "$proof_rejection_reason"
+    exit 1
+  fi
   if ! require_marker "$TARGET_SANDBOX_PROOF" "sandbox_status=target-evidence-recorded" "target sandbox" ||
      ! require_marker "$TARGET_SANDBOX_PROOF" "privilege_drop_status=target-evidence-recorded" "target sandbox" ||
      ! require_marker "$TARGET_SANDBOX_PROOF" "negative_access_test_status=target-denied-recorded" "target sandbox"; then