feat: appove and go

This commit is contained in:
thanhnv
2026-07-19 09:37:16 +07:00
parent 13fae3e6c3
commit 709b6cccd6
24 changed files with 1245 additions and 70 deletions
@@ -25,6 +25,22 @@ TRACE_DIR="$LOG_DIR/trace"
AUDIT_DIR="$LOG_DIR/audit"
SECURITY_DIR="$CASAN_HARNESS_ROOT/security"
# Prefer the OS Python over framework/shim installations that may exist in a
# developer shell but cannot execute. The runtime image also exposes this path.
PYTHON_BIN="${CASAN_PYTHON_BIN:-}"
if [[ -z "$PYTHON_BIN" ]]; then
for candidate in /usr/bin/python3 python3 python; do
if command -v "$candidate" >/dev/null 2>&1 && "$candidate" --version >/dev/null 2>&1; then
PYTHON_BIN="$candidate"
break
fi
done
fi
if [[ -z "$PYTHON_BIN" ]]; then
echo "SECURITY_RUNTIME_UNAVAILABLE: working Python 3 interpreter not found" >&2
exit 69
fi
# Shared log taxonomy (error<warn<info<debug<trace via CASAN_LOG_LEVEL). Used to
# make semantic skips loud (never silent) — stderr only, stdout contract intact.
# shellcheck source=casan-log.sh
@@ -59,7 +75,7 @@ new_trace_id() {
}
json_escape() {
python -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null || sed 's/\\/\\\\/g; s/"/\\"/g'
"$PYTHON_BIN" -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null || sed 's/\\/\\\\/g; s/"/\\"/g'
}
hash_text() {
@@ -85,7 +101,7 @@ load_yaml_values() {
local file="$1"
local key="$2"
[[ -f "$file" ]] || return 0
python - "$file" "$key" <<'PY'
"$PYTHON_BIN" - "$file" "$key" <<'PY'
import re
import sys
path, key = sys.argv[1], sys.argv[2]
@@ -98,6 +114,45 @@ with open(path, encoding="utf-8") as f:
PY
}
# Load only rule patterns whose declared action matches the requested action.
# The previous generic loader returned every `pattern:` in prompt-filter.yaml,
# which accidentally promoted `require_approval`, `alert`, and `log` rules to
# hard blocks. That made ordinary source code containing methods such as
# `delete()` fail the workspace-context scan as prompt injection.
load_yaml_rule_patterns() {
local file="$1"
local requested_action="$2"
[[ -f "$file" ]] || return 0
"$PYTHON_BIN" - "$file" "$requested_action" <<'PY'
import re
import sys
path, requested_action = sys.argv[1], sys.argv[2]
pattern = None
action = None
def flush():
if pattern is not None and action == requested_action:
print(pattern)
with open(path, encoding="utf-8") as handle:
for line in handle:
if re.match(r"^\s*-\s+id:\s*", line):
flush()
pattern = None
action = None
continue
pattern_match = re.match(r'^\s*pattern:\s*"(.*)"\s*$', line)
if pattern_match:
pattern = pattern_match.group(1)
continue
action_match = re.match(r"^\s*action:\s*([A-Za-z_]+)\s*$", line)
if action_match:
action = action_match.group(1)
flush()
PY
}
TRACE_ID="$(new_trace_id)"
TIMESTAMP="$(timestamp)"
CONTENT="$(cat "$INPUT_FILE")"
@@ -126,7 +181,7 @@ BLOCK_PATTERNS=(
while IFS= read -r pattern; do
[[ -n "$pattern" ]] && BLOCK_PATTERNS+=("$pattern")
done < <(load_yaml_values "$SECURITY_DIR/prompt-filter.yaml" "pattern")
done < <(load_yaml_rule_patterns "$SECURITY_DIR/prompt-filter.yaml" "block")
APPROVAL_PATTERNS=(
"delete[[:space:]].*"
@@ -169,8 +224,8 @@ NORM_CONTENT="$(normalize_for_match "$CONTENT")"
# fullwidth/zero-width/Cyrillic-lookalike obfuscation cannot split or disguise
# a blocked phrase (V3). Falls back to the raw content if python is missing.
UNI_CONTENT="$CONTENT"
if command -v python >/dev/null 2>&1; then
UNI_CONTENT="$(printf '%s' "$CONTENT" | python "$SCRIPT_DIR/unicode-normalize.py" 2>/dev/null)"
if [[ -n "$PYTHON_BIN" ]]; then
UNI_CONTENT="$(printf '%s' "$CONTENT" | "$PYTHON_BIN" "$SCRIPT_DIR/unicode-normalize.py" 2>/dev/null)"
[[ -n "$UNI_CONTENT" ]] || UNI_CONTENT="$CONTENT"
fi
UNI_NORM_CONTENT="$(normalize_for_match "$UNI_CONTENT")"
@@ -180,8 +235,8 @@ UNI_NORM_CONTENT="$(normalize_for_match "$UNI_CONTENT")"
# Only mostly-printable decodes survive, so random base64-looking words never
# create a false positive.
DECODED_CONTENT=""
if command -v python >/dev/null 2>&1; then
DECODED_CONTENT="$(printf '%s' "$CONTENT" | python "$SCRIPT_DIR/decode-suspicious.py" 2>/dev/null || true)"
if [[ -n "$PYTHON_BIN" ]]; then
DECODED_CONTENT="$(printf '%s' "$CONTENT" | "$PYTHON_BIN" "$SCRIPT_DIR/decode-suspicious.py" 2>/dev/null || true)"
fi
# Matches a pattern against the raw (case-insensitive), leetspeak-folded,
@@ -279,7 +334,7 @@ if [[ "$MODE" == "input" ]]; then
SEM_JSON="$TRACE_DIR/semantic-$TRACE_ID.json"
"$SCRIPT_DIR/model-router.sh" "$INPUT_FILE" "$SEM_JSON" --role classify >/dev/null 2>&1 || true
if [[ -f "$SEM_JSON" ]]; then
SEM_VERDICT="$(python -c "import json;print(json.load(open('$SEM_JSON')).get('verdict',''))" 2>/dev/null || echo "")"
SEM_VERDICT="$("$PYTHON_BIN" -c "import json;print(json.load(open('$SEM_JSON')).get('verdict',''))" 2>/dev/null || echo "")"
fi
fi
if [[ "$SEM_VERDICT" == "INJECTION" ]]; then
@@ -302,8 +357,8 @@ fi
SAFE_CONTENT="$CONTENT"
# Policy-driven PII masking (source of truth: pii-rules.yaml). Built-in sed
# masking below remains as defense-in-depth if the policy file is unavailable.
if [[ -f "$SECURITY_DIR/pii-rules.yaml" ]] && command -v python >/dev/null 2>&1; then
SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | python "$SCRIPT_DIR/pii-mask.py" "$SECURITY_DIR/pii-rules.yaml")"
if [[ -f "$SECURITY_DIR/pii-rules.yaml" ]] && [[ -n "$PYTHON_BIN" ]]; then
SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | "$PYTHON_BIN" "$SCRIPT_DIR/pii-mask.py" "$SECURITY_DIR/pii-rules.yaml")"
fi
SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | sed -E "s/$EMAIL_REGEX/***MASKED_EMAIL***/g")"
SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | sed -E "s/$PHONE_REGEX/***MASKED_PHONE***/g")"
@@ -333,7 +388,7 @@ fi
INPUT_HASH="$(printf '%s' "$CONTENT" | hash_text)"
OUTPUT_HASH="$(printf '%s' "$SAFE_CONTENT" | hash_text)"
RULES_JSON="$(printf '%s\n' "${MATCHED_RULES[@]:-}" | python -c 'import json,sys; print(json.dumps([x for x in sys.stdin.read().splitlines() if x]))')"
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]))')"
TRACE_FILE="$TRACE_DIR/security-$TRACE_ID.json"
cat > "$TRACE_FILE" <<EOF