#!/usr/bin/env bash set -uo pipefail # CASAN Plan-16 SEC-15 — low-cluster hardening. # * supply-chain typosquat: catch distance<=2 (was distance==1), no false positives # (levenshtein length sentinel fixed so it no longer collides with the threshold), # * tool-exec: fail CLOSED in enforced mode when no timeout backend exists, # * validate-tool-input: recurse into nested objects/arrays (was one level deep). # # Deterministic; hermetic. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" S="$PROJECT_ROOT/.specify/scripts/bash" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT PASS=0; FAIL=0 pass() { echo "PASS: $1"; PASS=$((PASS + 1)); } fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); } rc_of() { set +e; "$@" >/dev/null 2>&1; echo $?; set -e 2>/dev/null || true; } echo "===== Plan-16 SEC-15: low-cluster hardening =====" # ---- typosquat (distance<=2) ---- printf '{"dependencies":{}}' > "$WORK/base.json" printf 'requests\nnumpy\nexpress\n' > "$WORK/known.txt"; : > "$WORK/deny.txt" scan() { python3 "$S/supply-chain-scan.py" "$1" "$WORK/base.json" "$WORK/known.txt" "$WORK/deny.txt" "$WORK/r.json" "" >/dev/null 2>&1; } printf '{"dependencies":{"reqeusts":"1.0.0"}}' > "$WORK/m1.json"; scan "$WORK/m1.json" grep -q "typosquat_of:requests" "$WORK/r.json" \ && pass "2-char typosquat (reqeusts→requests) flagged" || fail "2-char typosquat missed" printf '{"dependencies":{"fastapi":"1.0.0"}}' > "$WORK/m2.json"; scan "$WORK/m2.json" grep -q "typosquat" "$WORK/r.json" \ && fail "legit package fastapi false-flagged as typosquat" \ || pass "legit distant package not false-flagged (levenshtein sentinel fixed)" # ---- tool-exec fail-closed when no timeout backend ---- # Hermetic: build a PATH that contains ONLY bash (no timeout, no perl) so the # no-backend branch is reached regardless of the host's /usr layout. On # merged-/usr systems /bin is a symlink to /usr/bin, so PATH=/bin would still # find timeout/perl — the old heuristic only worked on split-/usr (e.g. macOS). ONLYBIN="$WORK/onlybin"; mkdir -p "$ONLYBIN" BASH_BIN="$(command -v bash)" ln -sf "$BASH_BIN" "$ONLYBIN/bash" [[ "$(set +e; PATH="$ONLYBIN" CASAN_TOOL_EXEC_STRICT=1 "$BASH_BIN" "$S/tool-exec.sh" 2 -- echo hi >/dev/null 2>&1; echo $?)" -eq 2 ]] \ && pass "tool-exec refuses (fail-closed) with no timeout backend in enforced mode" \ || fail "tool-exec did not fail closed without a timeout backend" [[ "$(set +e; PATH="$ONLYBIN" "$BASH_BIN" "$S/tool-exec.sh" 2 -- echo hi >/dev/null 2>&1; echo $?)" -eq 0 ]] \ && pass "tool-exec dev: runs without backend (backward compatible)" \ || fail "tool-exec dev mode broke" # ---- validate-tool-input recursion ---- cat > "$WORK/schema.json" <<'J' {"type":"object","additionalProperties":false,"properties":{ "cfg":{"type":"object","additionalProperties":false,"properties":{"port":{"type":"integer"}}}}} J printf '{"cfg":{"port":8080}}' > "$WORK/ok.json" printf '{"cfg":{"port":"NOPE"}}' > "$WORK/badtype.json" printf '{"cfg":{"port":80,"evil":"x"}}' > "$WORK/badextra.json" [[ "$(rc_of bash "$S/validate-tool-input.sh" "$WORK/schema.json" "$WORK/ok.json")" -eq 0 ]] \ && pass "valid nested object accepted" || fail "valid nested object rejected" [[ "$(rc_of bash "$S/validate-tool-input.sh" "$WORK/schema.json" "$WORK/badtype.json")" -eq 2 ]] \ && pass "nested wrong type rejected (recursion)" || fail "nested wrong type slipped through" [[ "$(rc_of bash "$S/validate-tool-input.sh" "$WORK/schema.json" "$WORK/badextra.json")" -eq 2 ]] \ && pass "nested unexpected field rejected (recursion)" || fail "nested unexpected field slipped through" echo "" echo "===== SEC-15 SUMMARY: PASS=$PASS FAIL=$FAIL =====" [[ "$FAIL" -eq 0 ]] || exit 1