preinstall-check.sh 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. #!/usr/bin/env bash
  2. # Release-age pre-check for dependencies — enforce the cooldown policy.
  3. #
  4. # Flags any package whose target version was published inside the cooldown window
  5. # (default 7 days), because the 2026 worm campaign poisons brand-new releases that
  6. # are removed within hours. Routes to `socket` for a behavioural verdict when the
  7. # CLI is installed. Queries public registries (npm registry / PyPI JSON API) — no
  8. # auth, no install, read-only.
  9. #
  10. # Usage: preinstall-check.sh [--pip|--npm] [--json] [-q] <pkg>[@version] ...
  11. # Input: one or more package specs as positionals; --pip selects the PyPI ecosystem
  12. # Output: stdout = per-package records (tab-separated, or JSON with --json)
  13. # Stderr: headers, socket suggestions, progress, errors
  14. # Exit: 0 all outside cooldown, 2 usage, 5 missing-dep, 7 registry-unavailable,
  15. # 10 at-least-one-inside-cooldown
  16. #
  17. # Examples:
  18. # preinstall-check.sh axios react@19.0.0
  19. # preinstall-check.sh --pip requests fastapi@0.110.0
  20. # preinstall-check.sh --json axios | jq '.data[] | select(.inside_cooldown)'
  21. # COOLDOWN_DAYS=14 preinstall-check.sh left-pad
  22. set -uo pipefail
  23. EXIT_OK=0; EXIT_USAGE=2; EXIT_MISSING_DEP=5; EXIT_UNAVAILABLE=7; EXIT_INSIDE=10
  24. ECOSYSTEM="npm"; COOLDOWN_DAYS="${COOLDOWN_DAYS:-7}"; JSON=0; QUIET=0; PKGS=()
  25. while [[ $# -gt 0 ]]; do
  26. case "$1" in
  27. --pip|--pypi) ECOSYSTEM="pypi" ;;
  28. --npm) ECOSYSTEM="npm" ;;
  29. --json) JSON=1 ;;
  30. -q|--quiet) QUIET=1 ;;
  31. -h|--help) sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//'; exit "$EXIT_OK" ;;
  32. -*) echo "ERROR: unknown flag: $1 (try --help)" >&2; exit "$EXIT_USAGE" ;;
  33. *) PKGS+=("$1") ;;
  34. esac
  35. shift
  36. done
  37. [[ ${#PKGS[@]} -eq 0 ]] && { echo "ERROR: no package specs given (try --help)" >&2; exit "$EXIT_USAGE"; }
  38. command -v curl >/dev/null 2>&1 || { echo "ERROR: curl required" >&2; exit "$EXIT_MISSING_DEP"; }
  39. HAS_JQ=0; command -v jq >/dev/null 2>&1 && HAS_JQ=1
  40. [[ "$JSON" -eq 1 && "$HAS_JQ" -eq 0 ]] && {
  41. echo '{"error":{"code":"MISSING_DEPENDENCY","message":"jq required for --json"}}'
  42. echo "ERROR: jq required for --json" >&2; exit "$EXIT_MISSING_DEP"; }
  43. HAS_SOCKET=0; command -v socket >/dev/null 2>&1 && HAS_SOCKET=1
  44. emit() { [[ "$QUIET" -eq 1 ]] && return; printf '%s\n' "$1" >&2; }
  45. now_epoch=$(date +%s); inside=0; unavailable=0
  46. JSON_OBJS=()
  47. iso_to_epoch() {
  48. local ts=$1
  49. date -d "$ts" +%s 2>/dev/null && return 0
  50. ts="${ts%%.*}"; ts="${ts%Z}"
  51. date -j -f "%Y-%m-%dT%H:%M:%S" "$ts" +%s 2>/dev/null && return 0
  52. echo ""
  53. }
  54. result() { # name version published
  55. local name=$1 version=$2 published=$3 days=-1 ic=false
  56. if [[ -n "$version" && -n "$published" ]]; then
  57. local epoch; epoch=$(iso_to_epoch "$published")
  58. if [[ -n "$epoch" ]]; then
  59. days=$(( (now_epoch - epoch) / 86400 ))
  60. if [[ "$days" -lt "$COOLDOWN_DAYS" ]]; then ic=true; inside=1; fi
  61. fi
  62. fi
  63. # data record → stdout (non-json mode)
  64. if [[ "$JSON" -eq 0 ]]; then
  65. printf '%s\t%s\t%s\t%s\t%s\n' "$ECOSYSTEM" "$name" "${version:-?}" "${days}" "$ic"
  66. fi
  67. [[ "$HAS_JQ" -eq 1 ]] && JSON_OBJS+=("$(jq -cn \
  68. --arg e "$ECOSYSTEM" --arg n "$name" --arg v "$version" \
  69. --arg p "$published" --argjson d "$days" --argjson ic "$ic" \
  70. '{ecosystem:$e, name:$n, version:($v|select(length>0)), published:($p|select(length>0)), age_days:(if $d<0 then null else $d end), inside_cooldown:$ic}')")
  71. # human framing → stderr
  72. if [[ "$ic" == "true" ]]; then
  73. emit " [INSIDE COOLDOWN] ${name}@${version} — ${days}d ago (< ${COOLDOWN_DAYS}d). Hold off."
  74. elif [[ "$days" -ge 0 ]]; then
  75. emit " [ok] ${name}@${version} — ${days}d ago (>= ${COOLDOWN_DAYS}d)."
  76. else
  77. emit " [?] ${name} — version/publish time not found or registry unreachable."
  78. fi
  79. }
  80. fetch() { curl -fsSL "$1" 2>/dev/null || { unavailable=1; echo ""; }; }
  81. check_npm() {
  82. local spec=$1 name version json
  83. name="${spec%@*}"; version=""
  84. [[ "$spec" == *"@"* && "$spec" != @*/* ]] && version="${spec#*@}"
  85. json=$(fetch "https://registry.npmjs.org/${name}")
  86. [[ -z "$json" || "$HAS_JQ" -eq 0 ]] && { result "$name" "" ""; return; }
  87. [[ -z "$version" ]] && version=$(jq -r '."dist-tags".latest // empty' <<<"$json")
  88. result "$name" "$version" "$(jq -r --arg v "$version" '.time[$v] // empty' <<<"$json")"
  89. }
  90. check_pypi() {
  91. local spec=$1 name version url json
  92. name="${spec%==*}"; version=""
  93. [[ "$spec" == *"=="* ]] && version="${spec#*==}"
  94. [[ "$spec" == *"@"* ]] && { name="${spec%@*}"; version="${spec#*@}"; }
  95. url="https://pypi.org/pypi/${name}/json"; [[ -n "$version" ]] && url="https://pypi.org/pypi/${name}/${version}/json"
  96. json=$(fetch "$url")
  97. [[ -z "$json" || "$HAS_JQ" -eq 0 ]] && { result "$name" "" ""; return; }
  98. [[ -z "$version" ]] && version=$(jq -r '.info.version // empty' <<<"$json")
  99. result "$name" "$version" "$(jq -r --arg v "$version" \
  100. '(.releases[$v]//[])[0].upload_time_iso_8601 // .urls[0].upload_time_iso_8601 // empty' <<<"$json")"
  101. }
  102. emit "=== Pre-install check (${ECOSYSTEM}, cooldown ${COOLDOWN_DAYS}d) ==="
  103. for spec in "${PKGS[@]}"; do
  104. case "$ECOSYSTEM" in npm) check_npm "$spec" ;; pypi) check_pypi "$spec" ;; esac
  105. done
  106. if [[ "$JSON" -eq 1 ]]; then
  107. printf '%s\n' "${JSON_OBJS[@]:-}" | jq -s \
  108. --argjson cd "$COOLDOWN_DAYS" --arg eco "$ECOSYSTEM" \
  109. '{data: map(select(length>0)), meta:{ecosystem:$eco, cooldown_days:$cd, count:(map(select(length>0))|length), schema:"axiom.tool.preinstall-check.report/v1"}}'
  110. fi
  111. if [[ "$QUIET" -eq 0 ]]; then
  112. if [[ "$HAS_SOCKET" -eq 1 ]]; then
  113. emit ""; emit "Behavioural verdict:"
  114. for spec in "${PKGS[@]}"; do n="${spec%@*}"; n="${n%==*}"; emit " socket package score ${ECOSYSTEM} ${n}"; done
  115. else
  116. emit ""; emit "Behavioural scan (free): npm install -g socket # then: socket package score ${ECOSYSTEM} <pkg>"
  117. emit "Or depscore MCP (no key): claude mcp add --transport http socket-mcp https://mcp.socket.dev/"
  118. fi
  119. fi
  120. [[ "$inside" -eq 1 ]] && exit "$EXIT_INSIDE"
  121. [[ "$unavailable" -eq 1 ]] && exit "$EXIT_UNAVAILABLE"
  122. exit "$EXIT_OK"