feat: plan 16 P2 batch 2 (SEC-12 drift, SEC-29 audit fail-closed, SEC-30 replay, SEC-15 low)

- SEC-12: drift-detect adds semantic invariants — negation-flip detection (a dropped
  "not" now FAILS despite high char-similarity) + env must-keep patterns.
- SEC-29 (X-05): governance-check audit write fails CLOSED — an unwritable audit log
  denies the action and empties the output (no unaudited output).
- SEC-30 (X-06): approval-verify records a one-time-use nonce (sha of token/sig) and
  rejects replays (enforced mode / when a nonce ledger is set); dev unchanged.
- SEC-15 (low): typosquat distance<=2 with the levenshtein length-sentinel bug fixed
  (no false positives); tool-exec fails closed with no timeout backend in enforced
  mode; validate-tool-input now validates nested objects/arrays recursively.

Verify: new SEC suites all green via gate, run-casan4 0-FAIL, adversarial 44/44,
track-c 29/0, h5-approval 12/0, no regressions.

Plan-16 P2 remaining: infra-gated only (SEC-14/22/23/24/25/26).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
thanhnv
2026-07-06 22:55:24 +09:00
co-authored by Claude Opus 4.8
parent 8c06a55aed
commit d695a598ee
14 changed files with 365 additions and 30 deletions
@@ -155,7 +155,10 @@ TRACE_FILE="$TRACE_DIR/governance-$TRACE_ID.json"
# raw, so a value containing `"` + newline could inject a SECOND forged audit record
# (a fabricated "approved" decision). The record_hash is still computed from
# RECORD_CORE above, so verify-audit-chain.sh recomputes and matches unchanged.
CASAN_GC_REASONS="$REASONS_JSON" python - "$TRACE_FILE" "$AUDIT_LOG" \
# SEC-29 (X-05): the audit write must FAIL CLOSED. If the audit log cannot be
# written (disk full, read-only, quota), there must be NO governed action without
# its accountability record — deny and empty the output rather than proceed.
if ! CASAN_GC_REASONS="$REASONS_JSON" python - "$TRACE_FILE" "$AUDIT_LOG" \
"$TIMESTAMP" "$TRACE_ID" "$ACTION_NAME" "$ACTOR" "$RISK_LEVEL" "$DECISION" \
"$APPROVAL_STATUS" "$APPROVER" "$INPUT_HASH" "$OUTPUT_HASH" "$PREV_HASH" "$RECORD_HASH" <<'PY'
import json, os, sys
@@ -180,7 +183,14 @@ with open(audit_log, "a", encoding="utf-8") as f:
# Compact separators: the chain line is regex-parsed elsewhere and must match
# the original printf format (no space after ':' / ',').
f.write(json.dumps(rec, separators=(",", ":")) + "\n")
f.flush()
os.fsync(f.fileno())
PY
then
: > "$OUTPUT_FILE" 2>/dev/null || true
echo "GOVERNANCE_DENIED trace_id=$TRACE_ID reason=audit_unwritable (fail-closed: no governed action without an audit record)" >&2
exit 2
fi
# --- External anchor: cryptographically sign the new chain head ---
# A re-forged chain (recomputed hashes) changes the head; without the private