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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user