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
@@ -38,6 +38,25 @@ fi
deny() { echo "APPROVAL_DENIED reason=$1 approver=$APPROVER action=$ACTION" >&2; exit 3; }
# SEC-30 (X-06): one-time-use. A verified approval token (JWT or offline signature)
# is valid for its whole exp window, so it could be REPLAYED. Record a per-token
# nonce (sha256 of the token/signature) and reject any repeat. Active in enforced
# mode or when an explicit nonce ledger is configured (dev default: off).
sha_stdin() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum | awk '{print $1}'
else shasum -a 256 | awk '{print $1}'; fi
}
record_nonce_or_deny() {
local nonce="$1"
[[ "${CASAN_PROFILE:-}" == "prod" || -n "${CASAN_APPROVAL_NONCE_FILE:-}" ]] || return 0
local ledger="${CASAN_APPROVAL_NONCE_FILE:-$PROJECT_ROOT/.specify/logs/level5/approval-nonces.txt}"
mkdir -p "$(dirname "$ledger")" 2>/dev/null || true
if [[ -f "$ledger" ]] && grep -qxF "$nonce" "$ledger" 2>/dev/null; then
deny "approval_replayed(nonce=${nonce:0:12}…)"
fi
printf '%s\n' "$nonce" >> "$ledger" || deny "nonce_ledger_unwritable"
}
[[ -f "$INPUT_FILE" ]] || deny "input_file_missing"
[[ -f "$REVIEWERS_FILE" ]] || deny "reviewer_registry_missing"
command -v openssl >/dev/null 2>&1 || deny "openssl_unavailable"
@@ -164,6 +183,7 @@ print(f"OK role={role}")
PY
)" || deny "oidc_jwt_invalid(${JWT_OUT:-see_stderr})"
ROLE="${JWT_OUT#OK role=}"
record_nonce_or_deny "$(printf '%s' "$CASAN_APPROVAL_JWT" | sha_stdin)"
echo "APPROVAL_OK mechanism=oidc role=$ROLE approver=$APPROVER action=$ACTION"
exit 0
fi
@@ -192,5 +212,6 @@ printf '%s' "$MSG" > "$TMP"
openssl dgst -sha256 -verify "$PUB" -signature "$SIG_FILE" "$TMP" >/dev/null 2>&1 \
|| deny "approval_signature_invalid"
record_nonce_or_deny "$(sha_stdin < "$SIG_FILE")"
echo "APPROVAL_OK role=$ROLE approver=$APPROVER action=$ACTION"
exit 0