pre-install-scan.sh 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. #!/bin/bash
  2. # hooks/pre-install-scan.sh
  3. # PreToolUse hook — surfaces supply-chain hygiene before a dependency install runs.
  4. # Matcher: Bash
  5. #
  6. # The 2026 worm campaign (Shai-Hulud / Mini Shai-Hulud) executes via package
  7. # lifecycle scripts (postinstall, sdist setup.py) the moment you install, and
  8. # poisons brand-new releases that are pulled before any advisory exists. This hook
  9. # recognises install/add verbs and reminds you to scan + respect the release-age
  10. # cooldown, routing through the Socket CLI when available.
  11. #
  12. # Configuration in .claude/settings.json:
  13. # {
  14. # "hooks": {
  15. # "PreToolUse": [{
  16. # "matcher": "Bash",
  17. # "hooks": ["bash hooks/pre-install-scan.sh $TOOL_INPUT"]
  18. # }]
  19. # }
  20. # }
  21. #
  22. # Behaviour:
  23. # Default → ADVISORY. Prints guidance, exits 0 (command proceeds).
  24. # SUPPLY_CHAIN_BLOCK=1 → HARD GATE. Exits 2 (command blocked) so you scan first.
  25. #
  26. # Exit codes:
  27. # 0 = allow (not an install, already wrapped, or advisory mode)
  28. # 2 = block with message (install verb matched AND SUPPLY_CHAIN_BLOCK=1)
  29. INPUT="$1"
  30. # Modern Claude Code delivers the tool call as JSON on stdin
  31. # ({"tool_input":{"command":"..."}}); older configs pass it as $TOOL_INPUT/$1.
  32. # Support both so the hook works regardless of harness version.
  33. if [[ -z "$INPUT" && ! -t 0 ]]; then
  34. RAW="$(cat 2>/dev/null)"
  35. if [[ -n "$RAW" ]] && command -v jq >/dev/null 2>&1; then
  36. INPUT="$(printf '%s' "$RAW" | jq -r '.tool_input.command // .tool_input // empty' 2>/dev/null)"
  37. fi
  38. [[ -z "$INPUT" ]] && INPUT="$RAW"
  39. fi
  40. [[ -z "$INPUT" ]] && exit 0
  41. # Already routed through the behavioural scanner — let it through silently.
  42. echo "$INPUT" | grep -qE '\bsocket\s+(npm|npx|scan|ci|package)\b' && exit 0
  43. # Lockfile-pinned installs are the safer path we recommend — don't nag them.
  44. echo "$INPUT" | grep -qE '\bnpm\s+ci\b|--frozen-lockfile|--locked\b' && exit 0
  45. # ─── Recognise ecosystem install/add verbs ─────────────────────────────────
  46. ECO=""
  47. SAFE=""
  48. if echo "$INPUT" | grep -qE '\b(npm|pnpm)\s+(install|i|add)\b'; then
  49. ECO="npm"; SAFE="socket npm install <pkg> # or: socket wrapper on"
  50. elif echo "$INPUT" | grep -qE '\byarn\s+(add|install)\b'; then
  51. ECO="npm"; SAFE="socket npm install <pkg> # yarn has no socket wrapper"
  52. elif echo "$INPUT" | grep -qE '\bbun\s+(add|install)\b'; then
  53. ECO="npm"; SAFE="socket scan create . # bun has no socket wrapper"
  54. elif echo "$INPUT" | grep -qE '\b(pip|pip3)\s+install\b'; then
  55. ECO="pypi"; SAFE="socket scan create . # no socket pip wrapper exists"
  56. elif echo "$INPUT" | grep -qE '\buv\s+(add|pip\s+install)\b'; then
  57. ECO="pypi"; SAFE="socket scan create . # scan the manifest after uv add"
  58. elif echo "$INPUT" | grep -qE '\bpoetry\s+add\b'; then
  59. ECO="pypi"; SAFE="socket scan create ."
  60. elif echo "$INPUT" | grep -qE '\bcomposer\s+(require|install|update)\b'; then
  61. ECO="composer"; SAFE="socket scan create . # composer update re-resolves tags — tag-rewrite risk (Laravel-Lang)"
  62. elif echo "$INPUT" | grep -qE '\bgem\s+install\b'; then
  63. ECO="rubygems"; SAFE="socket scan create ."
  64. elif echo "$INPUT" | grep -qE '\bcargo\s+(add|install)\b'; then
  65. ECO="cargo"; SAFE="socket scan create ."
  66. else
  67. exit 0
  68. fi
  69. # ─── Compose the advisory ──────────────────────────────────────────────────
  70. HAS_SOCKET=0; command -v socket >/dev/null 2>&1 && HAS_SOCKET=1
  71. echo "SUPPLY CHAIN: dependency install detected (${ECO})."
  72. echo "Lifecycle scripts run on install — the 2026 worm vector. Before proceeding:"
  73. echo " 1. Behavioural scan (not just npm audit / pip-audit — those miss fresh malware)."
  74. echo " 2. Respect the 7-day release-age cooldown for anything that hits prod/CI."
  75. if [[ "$HAS_SOCKET" -eq 1 ]]; then
  76. echo " Route it through Socket: ${SAFE}"
  77. else
  78. echo " Socket CLI not installed (free): npm install -g socket"
  79. echo " Or add depscore MCP (no key): claude mcp add --transport http socket-mcp https://mcp.socket.dev/"
  80. fi
  81. echo " Cooldown check: bash skills/supply-chain-defense/scripts/preinstall-check.sh <pkg>"
  82. if [[ "${SUPPLY_CHAIN_BLOCK:-0}" == "1" ]]; then
  83. echo ""
  84. echo "Blocked (SUPPLY_CHAIN_BLOCK=1). Scan the package, then re-run via the Socket"
  85. echo "wrapper or unset SUPPLY_CHAIN_BLOCK after you've confirmed it's safe."
  86. exit 2
  87. fi
  88. exit 0