#!/usr/bin/env bash set -uo pipefail # CASAN Track C-MVP — Supply-chain gate for generated dependencies (C2, V18). # # An agent that writes code can also add a dependency. This gate diffs a # dependency manifest against a baseline and decides: # BLOCK malicious/denylisted package, typosquat of a known package, # or a dangerous lifecycle script (postinstall/preinstall/install) -> exit 2 # REQUIRE_APPROVAL a genuinely new dependency was added -> exit 3 # (ALLOW+audit when CASAN_ACTION_APPROVER is set) # ALLOW no new dependencies -> exit 0 # # Supports: package.json, requirements.txt, pom.xml, build.gradle(.kts). # When npm audit / pip-audit / osv-scanner are installed they are recorded as # available; otherwise the local denylist (malicious-packages.txt) is authoritative. # The diff/scan logic lives in supply-chain-scan.py (deterministic, no network). # # Usage: supply-chain-gate.sh [baseline-manifest] [report.json] # If baseline is omitted, the manifest's committed git version is used. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" SEC_DIR="$PROJECT_ROOT/.specify/security" # shellcheck source=casan-log.sh source "$SCRIPT_DIR/casan-log.sh" MANIFEST="${1:-}" BASELINE="${2:-}" REPORT="${3:-$PROJECT_ROOT/.specify/logs/level5/supply-chain-report.json}" mkdir -p "$(dirname "$REPORT")" if [[ -z "$MANIFEST" || ! -f "$MANIFEST" ]]; then echo "Usage: supply-chain-gate.sh [baseline-manifest] [report.json]" >&2 exit 64 fi # Resolve baseline: explicit file, else the manifest's committed git version, # else empty (treat every dependency as new). BASELINE_TMP="" if [[ -z "$BASELINE" ]]; then BASELINE_TMP="$(mktemp)" REL="${MANIFEST#"$PROJECT_ROOT"/}" if git -C "$PROJECT_ROOT" show "HEAD:$REL" > "$BASELINE_TMP" 2>/dev/null; then BASELINE="$BASELINE_TMP" else : > "$BASELINE_TMP"; BASELINE="$BASELINE_TMP" fi fi # Record which live scanners are available (honest capability reporting). SCANNERS="" for pair in "npm:npm" "pip-audit:pip-audit" "osv-scanner:osv-scanner"; do command -v "${pair##*:}" >/dev/null 2>&1 && SCANNERS="${SCANNERS:+$SCANNERS }${pair%%:*}" done RESULT="$(python "$SCRIPT_DIR/supply-chain-scan.py" \ "$MANIFEST" "$BASELINE" "$SEC_DIR/known-packages.txt" "$SEC_DIR/malicious-packages.txt" \ "$REPORT" "$SCANNERS")" RC=$? [[ -n "$BASELINE_TMP" ]] && rm -f "$BASELINE_TMP" [[ "$RC" -ne 0 ]] && { echo "SUPPLY_CHAIN_SCAN_ERROR rc=$RC" >&2; exit 2; } OUTCOME="${RESULT%%|*}" REASON="${RESULT#*|}" APPROVER="${CASAN_ACTION_APPROVER:-}" case "$OUTCOME" in BLOCK) casan_log error supply-chain "SUPPLY_CHAIN_BLOCKED $REASON" echo "SUPPLY_CHAIN_BLOCKED reason=$REASON report=$REPORT" >&2 exit 2 ;; REQUIRE_APPROVAL) if [[ -n "$APPROVER" ]]; then echo "SUPPLY_CHAIN_APPROVED by=$APPROVER new=$REASON report=$REPORT" exit 0 fi casan_log warn supply-chain "SUPPLY_CHAIN_REQUIRES_APPROVAL new=$REASON (set CASAN_ACTION_APPROVER=)" echo "SUPPLY_CHAIN_REQUIRES_APPROVAL new=$REASON report=$REPORT" >&2 exit 3 ;; *) echo "SUPPLY_CHAIN_CLEAN reason=$REASON report=$REPORT" exit 0 ;; esac