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:
co-authored by
Claude Opus 4.8
parent
8c06a55aed
commit
d695a598ee
@@ -72,18 +72,52 @@ length_delta = abs(len(candidate) - len(golden)) / max(len(golden), 1)
|
||||
|
||||
status = "pass"
|
||||
action = "allow"
|
||||
reasons = []
|
||||
if similarity < 0.70 or length_delta > 0.50:
|
||||
status = "fail"
|
||||
action = "block_or_fallback"
|
||||
reasons.append("low_similarity_or_length_delta")
|
||||
elif similarity < 0.85 or length_delta > 0.30:
|
||||
status = "warn"
|
||||
action = "require_review"
|
||||
|
||||
# SEC-12: char-similarity alone misses SEMANTIC inversion — dropping a negation
|
||||
# ("must NOT deploy" -> "must deploy") keeps similarity high but flips meaning. A
|
||||
# candidate that removes negation tokens present in the golden is treated as drift.
|
||||
import re as _re
|
||||
NEG = _re.compile(
|
||||
r"\b(not|no|never|cannot|can't|don't|must not|mustn't|deny|denied|reject|disable|"
|
||||
r"disabled|forbid|prohibit|block|blocked|không|đừng|cấm|từ chối)\b",
|
||||
_re.IGNORECASE,
|
||||
)
|
||||
golden_neg = len(NEG.findall(golden))
|
||||
cand_neg = len(NEG.findall(candidate))
|
||||
if golden_neg > cand_neg:
|
||||
# The dangerous case: looks nearly identical but a negation vanished.
|
||||
status, action = "fail", "block_or_fallback"
|
||||
reasons.append(f"negation_dropped(golden={golden_neg},candidate={cand_neg})")
|
||||
|
||||
# must-keep invariants: regex patterns that MUST still appear in the candidate.
|
||||
_mk = os.environ.get("CASAN_DRIFT_MUSTKEEP_FILE", "")
|
||||
missing = []
|
||||
if _mk and os.path.isfile(_mk):
|
||||
for _line in open(_mk, encoding="utf-8", errors="replace"):
|
||||
_pat = _line.strip()
|
||||
if _pat and not _re.search(_pat, candidate):
|
||||
missing.append(_pat)
|
||||
if missing:
|
||||
status, action = "fail", "block_or_fallback"
|
||||
reasons.append(f"must_keep_missing={len(missing)}")
|
||||
|
||||
report = {
|
||||
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"harness": "L5-drift-detection",
|
||||
"status": status,
|
||||
"action": action,
|
||||
"reasons": reasons,
|
||||
"golden_negations": golden_neg,
|
||||
"candidate_negations": cand_neg,
|
||||
"must_keep_missing": missing,
|
||||
"similarity_ratio": round(similarity, 4),
|
||||
"length_delta_ratio": round(length_delta, 4),
|
||||
"golden_hash": hashlib.sha256(golden.encode()).hexdigest(),
|
||||
|
||||
Reference in New Issue
Block a user