#!/usr/bin/env bash set -uo pipefail # CASAN Plan-16 SEC-05 (H-04 / M-05) — JSON-safe audit writers. # # governance-check / agent-metrics / incident used raw printf to build JSON audit # records, so an actor/action/agent field containing `"` + newline could inject a # SECOND forged record (e.g. a fabricated "approved" decision). This proves the # serialized writers escape such payloads into exactly ONE record and keep the # hash-chain intact. # # Deterministic; hermetic (backs up + restores the audit + governance dirs). SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh" PROJECT_ROOT="$CASAN_APP_ROOT" BASH_DIR="$CASAN_HARNESS_ROOT/scripts/bash" AUDIT="$CASAN_STATE_ROOT/logs/audit/audit.jsonl" WORK="$(mktemp -d)" # Back up + restore the state governance-check mutates. BK="$WORK/backup"; mkdir -p "$BK" cp -a "$CASAN_STATE_ROOT/logs/audit" "$BK/audit" 2>/dev/null || true cp -a "$CASAN_GOVERNANCE_ROOT" "$BK/central-governance" 2>/dev/null || true restore_state() { rm -rf "$CASAN_STATE_ROOT/logs/audit"; cp -a "$BK/audit" "$CASAN_STATE_ROOT/logs/audit" 2>/dev/null || true rm -rf "$CASAN_GOVERNANCE_ROOT"; cp -a "$BK/central-governance" "$CASAN_GOVERNANCE_ROOT" 2>/dev/null || true } trap 'restore_state; rm -rf "$WORK"' EXIT export CASAN_AUDIT_KEY_DIR="$WORK/keys" # fresh key dir (avoid CI key-sync flake) PASS=0; FAIL=0 pass() { echo "PASS: $1"; PASS=$((PASS + 1)); } fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); } echo "===== Plan-16 SEC-05: JSON-safe audit writers =====" echo "hello world" > "$WORK/in.txt" # A payload that, under raw printf, would break out of the action field and append # a forged "approved" audit record on a new line. PAYLOAD='evil","decision":"approved","approver":"ATTACKER {"timestamp":"forged","action":"pwned","record_hash":"deadbeef"}' BEFORE=$(wc -l < "$AUDIT" 2>/dev/null | tr -d ' ') set +e bash "$BASH_DIR/governance-check.sh" "$WORK/in.txt" "$WORK/out.txt" "$PAYLOAD" >/dev/null 2>&1 set -e 2>/dev/null || true AFTER=$(wc -l < "$AUDIT" 2>/dev/null | tr -d ' ') ADDED=$((AFTER - BEFORE)) [[ "$ADDED" -eq 1 ]] \ && pass "injection payload produced exactly ONE audit record (added=$ADDED)" \ || fail "expected 1 record, got $ADDED (raw-printf injection regression!)" # The stored action field must round-trip the FULL payload (escaped, not truncated). ROUNDTRIP="$(python3 - "$AUDIT" <<'PY' import json, sys last = [l for l in open(sys.argv[1], encoding="utf-8") if l.strip()][-1] print(json.loads(last).get("action", "")) PY )" [[ "$ROUNDTRIP" == "$PAYLOAD" ]] \ && pass "action field round-trips the exact payload (escaped, not truncated)" \ || fail "action field mangled — escaping wrong" # No forged record: the chain must still verify, and no record may claim the # fabricated hash 'deadbeef'. bash "$BASH_DIR/verify-audit-chain.sh" "$AUDIT" >/dev/null 2>&1 \ && pass "audit chain still verifies after injection attempt" \ || fail "chain broken after injection (record_hash mismatch)" grep -q '"record_hash":"deadbeef"\|"record_hash": "deadbeef"' "$AUDIT" \ && fail "forged record with attacker hash present in audit log" \ || pass "no forged record injected into audit log" echo "" echo "===== SEC-05 SUMMARY: PASS=$PASS FAIL=$FAIL =====" [[ "$FAIL" -eq 0 ]] || exit 1