feat: appove and go
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user