feat: casan chat optz

This commit is contained in:
thanhnv
2026-07-19 12:14:12 +07:00
parent 709b6cccd6
commit f462079435
16 changed files with 486 additions and 90 deletions
@@ -199,7 +199,10 @@ ALERT_PATTERNS=(
)
EMAIL_REGEX='[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'
PHONE_REGEX='(\+?[0-9][0-9 .-]{8,}[0-9])'
# Keep the fallback aligned with pii-rules.yaml. The previous separator-heavy
# expression treated ISO dates such as 2026-07-19 as phone numbers and corrupted
# timestamps in certified evidence.
PHONE_REGEX='([+]?[0-9]{9,15})'
PERSONAL_ID_REGEX='\b[0-9]{9,12}\b'
CREDIT_CARD_REGEX='\b([0-9]{4}[- ]?){3}[0-9]{4}\b'
SECRET_REGEX='(API[_-]?KEY|ACCESS[_-]?TOKEN|REFRESH[_-]?TOKEN|PASSWORD|JWT[_-]?SECRET|SECRET)[[:space:]]*[:=][[:space:]]*[^[:space:]]+'
@@ -313,15 +316,20 @@ if [[ "$MODE" == "input" ]]; then
# genuinely novel paraphrase slips through as low-risk, so a still-allowed
# input is routed to the model classifier. Semantic can only ADD a block,
# never remove one. Two modes:
# * CASAN_SECURITY_STRICT=1 — semantic is REQUIRED (V1). If the model is
# unreachable or returns no usable verdict we FAIL CLOSED (block); never a
# silent skip. Off by default so CI without a model stays non-strict.
# * CASAN_SECURITY_STRICT=1 — semantic is REQUIRED (V1). An explicitly
# requested strict scan still fails closed when the classifier is down.
# Production's implicit strict mode uses a risk-based degraded verdict so
# a classifier outage cannot take down every read-only Ask CASAN request.
# * CASAN_SEMANTIC_CLASSIFY=1 (non-strict) — best-effort. On model outage we
# keep the regex verdict but log SEMANTIC_SKIPPED loudly (no silent pass).
# SEC-17 (ARCH-03): strict is ON when explicitly set, OR unset under prod profile
# (secure-by-default). An explicit CASAN_SECURITY_STRICT=0 (internal scans) wins.
STRICT_ON=0
if [[ "${CASAN_SECURITY_STRICT:-0}" == "1" || ( -z "${CASAN_SECURITY_STRICT+x}" && "${CASAN_PROFILE:-}" == "prod" ) ]]; then
STRICT_EXPLICIT=0
if [[ "${CASAN_SECURITY_STRICT:-0}" == "1" ]]; then
STRICT_ON=1
STRICT_EXPLICIT=1
elif [[ -z "${CASAN_SECURITY_STRICT+x}" && "${CASAN_PROFILE:-}" == "prod" ]]; then
STRICT_ON=1
fi
SEMANTIC_REQUIRED=0
@@ -342,13 +350,19 @@ if [[ "$MODE" == "input" ]]; then
MATCHED_RULES+=("semantic-injection")
elif [[ -z "$SEM_VERDICT" ]]; then
# Model unreachable / no usable verdict.
if [[ "$STRICT_ON" == "1" ]]; then
if [[ "$STRICT_EXPLICIT" == "1" || "${CASAN_SECURITY_UNAVAILABLE_POLICY:-risk_based}" == "block" ]]; then
STATUS="blocked"; ACTION="block"; RISK_LEVEL="high"
MATCHED_RULES+=("semantic-strict-unavailable")
casan_log error security "SEMANTIC_STRICT_FAIL_CLOSED trace_id=$TRACE_ID reason=model_unavailable action=block"
else
[[ "$RISK_LEVEL" == "low" ]] && RISK_LEVEL="medium"
ACTION="alert"
MATCHED_RULES+=("semantic-unavailable")
casan_log warn security "SEMANTIC_SKIPPED trace_id=$TRACE_ID reason=model_unavailable action=keep_regex_verdict hint=set_CASAN_SECURITY_STRICT=1_to_fail_closed"
if [[ "$STRICT_ON" == "1" ]]; then
casan_log warn security "SEMANTIC_DEGRADED trace_id=$TRACE_ID reason=model_unavailable action=keep_deterministic_verdict hint=set_CASAN_SECURITY_UNAVAILABLE_POLICY=block_to_fail_closed"
else
casan_log warn security "SEMANTIC_SKIPPED trace_id=$TRACE_ID reason=model_unavailable action=keep_regex_verdict hint=set_CASAN_SECURITY_STRICT=1_to_fail_closed"
fi
fi
fi
fi
@@ -389,6 +403,12 @@ fi
INPUT_HASH="$(printf '%s' "$CONTENT" | hash_text)"
OUTPUT_HASH="$(printf '%s' "$SAFE_CONTENT" | hash_text)"
RULES_JSON="$(printf '%s\n' "${MATCHED_RULES[@]:-}" | "$PYTHON_BIN" -c 'import json,sys; print(json.dumps([x for x in sys.stdin.read().splitlines() if x]))')"
CATEGORIES_CSV="$(printf '%s\n' "${MATCHED_RULES[@]:-}" | "$PYTHON_BIN" -c 'import sys; rules=sys.stdin.read().splitlines(); cats=[]
for rule in rules:
cat = (rule.split(":", 1)[0] if ":" in rule else rule).replace("semantic-strict-unavailable", "semantic-availability").replace("semantic-unavailable", "semantic-availability")
if cat and cat not in cats: cats.append(cat)
print(",".join(cats))')"
CATEGORIES_JSON="$(printf '%s' "$CATEGORIES_CSV" | "$PYTHON_BIN" -c 'import json,sys; print(json.dumps([x for x in sys.stdin.read().split(",") if x]))')"
TRACE_FILE="$TRACE_DIR/security-$TRACE_ID.json"
cat > "$TRACE_FILE" <<EOF
@@ -401,13 +421,14 @@ cat > "$TRACE_FILE" <<EOF
"action": "$ACTION",
"risk_level": "$RISK_LEVEL",
"matched_rules": $RULES_JSON,
"matched_categories": $CATEGORIES_JSON,
"input_hash": "$INPUT_HASH",
"output_hash": "$OUTPUT_HASH"
}
EOF
printf '{"timestamp":"%s","trace_id":"%s","harness":"H4-security","mode":"%s","status":"%s","action":"%s","risk_level":"%s","input_hash":"%s","output_hash":"%s"}\n' \
"$TIMESTAMP" "$TRACE_ID" "$MODE" "$STATUS" "$ACTION" "$RISK_LEVEL" "$INPUT_HASH" "$OUTPUT_HASH" >> "$AUDIT_DIR/security.jsonl"
printf '{"timestamp":"%s","trace_id":"%s","harness":"H4-security","mode":"%s","status":"%s","action":"%s","risk_level":"%s","matched_categories":%s,"input_hash":"%s","output_hash":"%s"}\n' \
"$TIMESTAMP" "$TRACE_ID" "$MODE" "$STATUS" "$ACTION" "$RISK_LEVEL" "$CATEGORIES_JSON" "$INPUT_HASH" "$OUTPUT_HASH" >> "$AUDIT_DIR/security.jsonl"
if [[ "$STATUS" == "blocked" ]]; then
: > "$OUTPUT_FILE"
@@ -417,4 +438,4 @@ fi
printf '%s\n' "$SAFE_CONTENT" > "$OUTPUT_FILE"
STATUS_UPPER="$(printf '%s' "$STATUS" | tr '[:lower:]' '[:upper:]')"
echo "SECURITY_${STATUS_UPPER} trace_id=$TRACE_ID risk=$RISK_LEVEL action=$ACTION output=$OUTPUT_FILE"
echo "SECURITY_${STATUS_UPPER} trace_id=$TRACE_ID risk=$RISK_LEVEL action=$ACTION categories=$CATEGORIES_CSV output=$OUTPUT_FILE"