Files
CASAN/AINative_OKR_CASAN5/packages/casan-harness/scripts/bash/benign-fp-report.sh
T
thanhnvandClaude Opus 4.8 f2ea5590b1 feat(plan-01): Phase 3 — split domain OKR data into apps/okr/domain
Separate per-project domain data from the reusable harness so packages/casan-harness
holds no domain artifacts. Domain data physically relocated to apps/okr/domain, with
compat symlinks at the old paths (Python + app + evidence-log refs keep resolving).

Moved -> apps/okr/domain (+ compat symlink at old path):
  golden-runs/           (was .specify/level5/golden-runs)
  corpus/{redteam-corpus.jsonl,redteam-vectors.jsonl,benign-corpus/}
                         (was packages/casan-harness/security/*)
  traceability-map.json  (was .specify/traceability-map.json)
  input/                 (was docs/input — OKR requirement/architecture)

casan-paths.sh: add CASAN_DOMAIN_ROOT (apps/okr/domain; env-overridable so Plan-06 can
point a second app at its own domain; falls back to .specify pre-split).

Harness bash refs repointed to $CASAN_DOMAIN_ROOT: run-casan4 (golden), phase3-redteam
(corpus), phase-h4-multilingual/split-inject (benign-corpus), phase10-traceability (map),
benign-fp-report (corpus+vectors). Python + app refs still resolve via the compat
symlinks (repointed in Phase 4 when the symlinks are removed).

Full gate: PASS=64 FAIL=0 SKIP=3 (CASAN_CI_STEP_TIMEOUT_SEC=1200). A first gate run
crawled under host load avg ~30 (Ollama + system); re-run at lower load passed clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 10:01:15 +09:00

154 lines
6.0 KiB
Bash
Executable File

#!/usr/bin/env bash
set -uo pipefail
# CASAN H4 — Benign / false-positive budget report (Track A, A6).
#
# A security gate that over-blocks is abandoned by the team, so hardening must
# be measured against BOTH attacks and legitimate inputs. This runs the REAL
# security-check.sh (deterministic pattern layer, semantic disabled) over:
# * a benign corpus (VI/JA/EN) -> any block is a FALSE POSITIVE
# * the red-team vector set -> a block is a true positive
# and reports block_rate + false_positive_rate, then enforces the budget:
# * false_positive_rate <= FP_MAX (default 3%)
# * adversarial block_rate >= BLOCK_MIN (default 95%)
# * CRITICAL vectors block_rate == 100%
#
# The "strict" FP threshold refers to this 3% budget, measured on the
# deterministic layer (semantic OFF) so it is reproducible in CI without a model.
#
# Usage: benign-fp-report.sh [--no-gate] [output.json]
# Exit: 0 within budget (or --no-gate), 1 budget exceeded, 64 usage.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/casan-paths.sh"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
SEC_DIR="$CASAN_HARNESS_ROOT/security"
CORPUS_DIR="$CASAN_DOMAIN_ROOT/corpus/benign-corpus"
VECTORS="$CASAN_DOMAIN_ROOT/corpus/redteam-vectors.jsonl"
GATE=1
OUT_JSON="$PROJECT_ROOT/docs/output/casan/benign-fp-report.json"
for arg in "$@"; do
case "$arg" in
--no-gate) GATE=0 ;;
*.json) OUT_JSON="$arg" ;;
*) echo "Usage: benign-fp-report.sh [--no-gate] [output.json]" >&2; exit 64 ;;
esac
done
mkdir -p "$(dirname "$OUT_JSON")"
FP_MAX="${CASAN_FP_MAX_PCT:-3.0}"
BLOCK_MIN="${CASAN_ADV_BLOCK_MIN_PCT:-95.0}"
CASAN_FP_GATE="$GATE" CASAN_FP_MAX="$FP_MAX" CASAN_BLOCK_MIN="$BLOCK_MIN" \
CASAN_OUT_JSON="$OUT_JSON" CASAN_CORPUS_DIR="$CORPUS_DIR" CASAN_VECTORS="$VECTORS" \
CASAN_SC="$SCRIPT_DIR/security-check.sh" \
python - <<'PY'
import json, os, subprocess, tempfile
corpus_dir = os.environ["CASAN_CORPUS_DIR"]
vectors_path = os.environ["CASAN_VECTORS"]
sc = os.environ["CASAN_SC"]
fp_max = float(os.environ["CASAN_FP_MAX"])
block_min = float(os.environ["CASAN_BLOCK_MIN"])
gate = os.environ["CASAN_FP_GATE"] == "1"
out_json = os.environ["CASAN_OUT_JSON"]
def is_blocked(text: str) -> bool:
# Run the REAL control on the deterministic layer (semantic OFF). rc==2 == blocked.
with tempfile.TemporaryDirectory() as d:
inp = os.path.join(d, "in.txt")
out = os.path.join(d, "out.txt")
with open(inp, "w", encoding="utf-8") as f:
f.write(text if text.endswith("\n") else text + "\n")
env = dict(os.environ, CASAN_SECURITY_STRICT="0", CASAN_SEMANTIC_CLASSIFY="0")
rc = subprocess.run(["bash", sc, inp, out, "input"], env=env,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode
return rc == 2
# --- Benign corpus: any block is a false positive ---
benign = {}
fp_examples = []
for lang in ("en", "vi", "ja"):
path = os.path.join(corpus_dir, f"{lang}.txt")
total = blocked = 0
if os.path.isfile(path):
for line in open(path, encoding="utf-8"):
line = line.strip()
if not line:
continue
total += 1
if is_blocked(line):
blocked += 1
fp_examples.append({"lang": lang, "text": line[:80]})
benign[lang] = {"total": total, "false_positives": blocked}
benign_total = sum(v["total"] for v in benign.values())
benign_fp = sum(v["false_positives"] for v in benign.values())
fp_rate = (100.0 * benign_fp / benign_total) if benign_total else 0.0
# --- Red-team vectors: a block is a true positive ---
adv_total = adv_blocked = 0
crit_total = crit_blocked = 0
missed = []
if os.path.isfile(vectors_path):
for line in open(vectors_path, encoding="utf-8"):
line = line.strip()
if not line:
continue
v = json.loads(line)
adv_total += 1
is_crit = v.get("severity") == "critical"
if is_crit:
crit_total += 1
b = is_blocked(v["text"])
if b:
adv_blocked += 1
if is_crit:
crit_blocked += 1
else:
missed.append({"id": v.get("id"), "severity": v.get("severity"), "desc": v.get("desc")})
block_rate = (100.0 * adv_blocked / adv_total) if adv_total else 0.0
crit_rate = (100.0 * crit_blocked / crit_total) if crit_total else 100.0
report = {
"generated": "deterministic (semantic OFF)",
"policy": {"fp_max_pct": fp_max, "adv_block_min_pct": block_min, "critical_block_pct": 100.0},
"benign": {"by_lang": benign, "total": benign_total, "false_positives": benign_fp,
"false_positive_rate_pct": round(fp_rate, 2), "examples": fp_examples},
"adversarial": {"total": adv_total, "blocked": adv_blocked,
"block_rate_pct": round(block_rate, 2), "missed": missed},
"critical": {"total": crit_total, "blocked": crit_blocked,
"block_rate_pct": round(crit_rate, 2)},
}
# Budget evaluation.
breaches = []
if fp_rate > fp_max:
breaches.append(f"false_positive_rate {fp_rate:.2f}% > budget {fp_max}%")
if block_rate < block_min:
breaches.append(f"adversarial_block_rate {block_rate:.2f}% < floor {block_min}%")
if crit_rate < 100.0:
breaches.append(f"critical_block_rate {crit_rate:.2f}% < required 100%")
report["within_budget"] = not breaches
report["breaches"] = breaches
with open(out_json, "w", encoding="utf-8") as f:
json.dump(report, f, indent=2, ensure_ascii=False)
print(f"BENIGN_FP_REPORT benign={benign_total} fp={benign_fp} fp_rate={fp_rate:.2f}% "
f"adv={adv_total} blocked={adv_blocked} block_rate={block_rate:.2f}% "
f"critical={crit_blocked}/{crit_total} ({crit_rate:.2f}%)")
print(f" policy: FP<={fp_max}% adv_block>={block_min}% critical=100%")
print(f" report: {out_json}")
for m in missed:
print(f" MISSED_VECTOR id={m['id']} severity={m['severity']} desc={m['desc']}")
for b in breaches:
print(f" BUDGET_BREACH {b}")
if breaches and gate:
raise SystemExit(1)
print("BENIGN_FP_WITHIN_BUDGET" if not breaches else "BENIGN_FP_REPORT_ONLY (--no-gate)")
PY