feat: plan 16 P1 (SEC-08/09/19/20/21) — fail-open/DoS/authz hardening
- SEC-08 pii-mask: fail-closed on missing rules / broken regex (no unmasked leak) - SEC-09: input-size cap + fail-closed reads (security-check/drift-detect/context-compress); non-UTF8 no longer crashes - SEC-19: control-plane store POSIX flock + atomic tmp+rename write - SEC-20: new toolchain-verify.sh (missing/PATH-shadowed/in-workspace binary -> refuse); wired into harness-preflight - SEC-21: model-call timeout 180->60s configurable + per-run call budget - SEC-11 realized by SEC-17 prod profile (no code) - 5 fail-able test suites wired into ci-harness-gate.sh; test-integrity manifest regenerated Verify: SEC+integrity gate 16/0, run-casan4 0-FAIL, adversarial 44/44, no regressions. 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
8c3c5e8bff
commit
e70f0815ab
@@ -22,6 +22,7 @@ python - "$GOLDEN" "$CANDIDATE" "$REPORT" <<'PY'
|
||||
import difflib
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
@@ -30,8 +31,41 @@ golden_path = pathlib.Path(sys.argv[1])
|
||||
candidate_path = pathlib.Path(sys.argv[2])
|
||||
report_path = pathlib.Path(sys.argv[3])
|
||||
|
||||
golden = golden_path.read_text(encoding="utf-8")
|
||||
candidate = candidate_path.read_text(encoding="utf-8")
|
||||
# SEC-09 (M-10): cap input size (SequenceMatcher is O(n^2) → DoS) and read
|
||||
# fail-closed. A missing/oversize/undecodable input yields a BLOCK verdict, never
|
||||
# a crash/traceback (which under set -e would abort ambiguously) and never a silent
|
||||
# pass.
|
||||
MAX_BYTES = int(os.environ.get("CASAN_MAX_INPUT_BYTES", str(2 * 1024 * 1024)))
|
||||
|
||||
|
||||
def block(reason):
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report = {
|
||||
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"harness": "L5-drift-detection", "status": "fail", "action": "block",
|
||||
"reason": reason, "golden_file": str(golden_path), "candidate_file": str(candidate_path),
|
||||
}
|
||||
report_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"DRIFT_FAIL reason={reason} report={report_path}")
|
||||
raise SystemExit(2)
|
||||
|
||||
|
||||
def read_capped(path):
|
||||
try:
|
||||
size = path.stat().st_size
|
||||
except OSError:
|
||||
block(f"unreadable:{path.name}")
|
||||
if size > MAX_BYTES:
|
||||
block(f"oversize:{path.name}({size}>{MAX_BYTES})")
|
||||
try:
|
||||
# errors="replace" so non-UTF8 bytes degrade to a marker instead of crashing.
|
||||
return path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
block(f"unreadable:{path.name}")
|
||||
|
||||
|
||||
golden = read_capped(golden_path)
|
||||
candidate = read_capped(candidate_path)
|
||||
|
||||
similarity = difflib.SequenceMatcher(None, golden, candidate).ratio()
|
||||
length_delta = abs(len(candidate) - len(golden)) / max(len(golden), 1)
|
||||
|
||||
Reference in New Issue
Block a user