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:
thanhnv
2026-07-06 22:21:34 +09:00
co-authored by Claude Opus 4.8
parent 8c3c5e8bff
commit e70f0815ab
17 changed files with 611 additions and 62 deletions
@@ -12,6 +12,7 @@ writes so the governance logic exists exactly once — in the harness.
Store file: $CASAN_CP_STORE_FILE (default .specify/level5/control-plane-settings.json)
"""
import argparse
import contextlib
import hashlib
import json
import os
@@ -20,6 +21,12 @@ import subprocess
import sys
from datetime import datetime, timezone
try:
import fcntl
_HAVE_FCNTL = True
except ImportError: # non-POSIX (e.g. Windows): best-effort, no OS lock
_HAVE_FCNTL = False
SETTINGS_POLICY = {
"compression.enabled": {"securitySensitive": False, "description": "Toggle context/token compression"},
@@ -62,11 +69,37 @@ def load_store():
def save_store(store) -> None:
# SEC-19 (ARCH-05): write atomically (tmp + rename) so a crash or a concurrent
# reader never sees a half-written store / broken hash chain.
path = store_path()
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as fh:
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(store, fh, indent=2, ensure_ascii=False)
fh.write("\n")
fh.flush()
os.fsync(fh.fileno())
os.replace(tmp, path)
@contextlib.contextmanager
def store_lock():
"""SEC-19 (ARCH-05): serialize the load→modify→save critical section so two
concurrent `set`/`rollback` runs cannot lose a write or fork the audit chain.
POSIX flock; a best-effort no-op where fcntl is unavailable."""
lock_path = store_path() + ".lock"
os.makedirs(os.path.dirname(lock_path) or ".", exist_ok=True)
fh = open(lock_path, "w")
try:
if _HAVE_FCNTL:
fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
yield
finally:
try:
if _HAVE_FCNTL:
fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
finally:
fh.close()
def hash_entry(entry) -> str:
@@ -162,52 +195,54 @@ def do_set(key, value, actor, reason, approval):
if policy["securitySensitive"] and not (approval or "").strip():
print(f"APPROVAL_REQUIRED {key}", file=sys.stderr)
return 3
store = load_store()
prev = store["settings"].get(key)
if prev is not None:
store["history"].setdefault(key, []).append(prev)
nxt = {
"value": value,
"version": (prev["version"] if prev else 0) + 1,
"updatedAt": now_iso(),
"actor": actor,
"reason": reason,
}
store["settings"][key] = nxt
append_audit(store, {
"key": key, "action": "set", "value": value,
"prevValue": prev["value"] if prev else None,
"actor": actor, "reason": reason, "at": nxt["updatedAt"],
})
save_store(store)
sign_head(store)
with store_lock(): # SEC-19: atomic read-modify-write
store = load_store()
prev = store["settings"].get(key)
if prev is not None:
store["history"].setdefault(key, []).append(prev)
nxt = {
"value": value,
"version": (prev["version"] if prev else 0) + 1,
"updatedAt": now_iso(),
"actor": actor,
"reason": reason,
}
store["settings"][key] = nxt
append_audit(store, {
"key": key, "action": "set", "value": value,
"prevValue": prev["value"] if prev else None,
"actor": actor, "reason": reason, "at": nxt["updatedAt"],
})
save_store(store)
sign_head(store)
print(json.dumps(nxt, ensure_ascii=False))
return 0
def do_rollback(key, actor, reason):
store = load_store()
history = store["history"].get(key, [])
if not history:
print(f"NO_PRIOR_VERSION {key}", file=sys.stderr)
return 4
previous = history.pop()
current = store["settings"].get(key)
restored = {
"value": previous["value"],
"version": ((current["version"] if current else previous["version"]) + 1),
"updatedAt": now_iso(),
"actor": actor,
"reason": reason,
}
store["settings"][key] = restored
append_audit(store, {
"key": key, "action": "rollback", "value": previous["value"],
"prevValue": current["value"] if current else None,
"actor": actor, "reason": reason, "at": restored["updatedAt"],
})
save_store(store)
sign_head(store)
with store_lock(): # SEC-19: atomic read-modify-write
store = load_store()
history = store["history"].get(key, [])
if not history:
print(f"NO_PRIOR_VERSION {key}", file=sys.stderr)
return 4
previous = history.pop()
current = store["settings"].get(key)
restored = {
"value": previous["value"],
"version": ((current["version"] if current else previous["version"]) + 1),
"updatedAt": now_iso(),
"actor": actor,
"reason": reason,
}
store["settings"][key] = restored
append_audit(store, {
"key": key, "action": "rollback", "value": previous["value"],
"prevValue": current["value"] if current else None,
"actor": actor, "reason": reason, "at": restored["updatedAt"],
})
save_store(store)
sign_head(store)
print(json.dumps(restored, ensure_ascii=False))
return 0