Scripts
shai-hulud-2-detector (NPM Supply Chain Attack)
Valida se seu package.json (e lock-file) possui algum pacote infectado pelo ataque Shai-Hulud 2.0 (24 Nov de 2025); Funciona com npm, yarn e pnpm. Leia mais sobre o ataque
Como usar (Bash)
- 1.Baixe o script para a pasta do seu projeto (mesmo diretório do package.json)
- 2.Torne-o executável: chmod +x {scriptPath}
- 3.Execute: ./{scriptPath}
1#!/usr/bin/env bash
2
3# Shai-Hulud 2.0 Supply Chain Attack Detector
4#
5# Checks your project dependencies against known compromised packages from the
6# Shai-Hulud 2.0 supply chain attack that targeted npm packages.
7#
8# This script checks:
9# - package.json dependencies
10# - package-lock.json (npm)
11# - pnpm-lock.yaml (pnpm)
12# - yarn.lock (yarn)
13#
14# References:
15# - https://www.wiz.io/blog/shai-hulud-2-0-ongoing-supply-chain-attack
16# - https://socket.dev/blog/supply-chain-attack-shai-hulud-2-0
17#
18# Usage:
19# ./check-deps.sh
20# (Automatically finds package.json in current or parent directories)
21
22set -euo pipefail
23
24# Full list of compromised packages from Shai-Hulud 2.0 supply chain attack
25# Format: "package@version"
26COMPROMISED_LIST=(
27 "@accordproject/concerto-analysis@3.24.1"
28 "@accordproject/concerto-metamodel@3.12.5"
29 "@accordproject/concerto-types@3.24.1"
30 "@accordproject/markdown-it-cicero@0.16.26"
31 "@asyncapi/studio@1.0.3"
32 "@asyncapi/studio@1.0.2"
33 "@ensdomains/address-encoder@1.1.5"
34 "@ensdomains/content-hash@3.0.1"
35 "@ensdomains/dnsprovejs@0.5.3"
36 "@ensdomains/ens-validation@0.1.1"
37 "@ensdomains/ensjs@4.0.3"
38 "@ensdomains/eth-ens-namehash@2.0.16"
39 "@posthog/agent@1.24.1"
40 "@posthog/ai@7.1.2"
41 "@posthog/cli@0.5.15"
42 "@posthog/clickhouse@1.7.1"
43 "@posthog/core@1.5.6"
44 "@posthog/hedgehog-mode@0.0.42"
45 "@posthog/icons@0.36.1"
46 "@posthog/lemon-ui@0.0.1"
47 "@posthog/nextjs-config@1.5.1"
48 "@posthog/nuxt@1.2.9"
49 "@posthog/piscina@3.2.1"
50 "@posthog/plugin-contrib@0.0.6"
51 "@posthog/react-rrweb-player@1.1.4"
52 "@posthog/rrdom@0.0.31"
53 "@posthog/rrweb@0.0.31"
54 "@posthog/rrweb-player@0.0.31"
55 "@posthog/rrweb-record@0.0.31"
56 "@posthog/rrweb-replay@0.0.19"
57 "@posthog/rrweb-snapshot@0.0.31"
58 "@posthog/rrweb-utils@0.0.31"
59 "@posthog/siphash@1.1.2"
60 "@posthog/wizard@1.18.1"
61 "@postman/aether-icons@2.23.3"
62 "@postman/aether-icons@2.23.4"
63 "@postman/aether-icons@2.23.2"
64 "@postman/csv-parse@4.0.5"
65 "@postman/csv-parse@4.0.3"
66 "@postman/csv-parse@4.0.4"
67 "@postman/node-keytar@7.9.5"
68 "@postman/node-keytar@7.9.6"
69 "@postman/node-keytar@7.9.4"
70 "@postman/tunnel-agent@0.6.7"
71 "@postman/tunnel-agent@0.6.6"
72 "@postman/tunnel-agent@0.6.5"
73 "@voiceflow/common@8.9.2"
74 "@voiceflow/common@8.9.1"
75 "@zapier/ai-actions@0.1.19"
76 "@zapier/ai-actions@0.1.20"
77 "@zapier/ai-actions@0.1.18"
78 "@zapier/babel-preset-zapier@6.4.2"
79 "@zapier/babel-preset-zapier@6.4.3"
80 "@zapier/babel-preset-zapier@6.4.1"
81 "@zapier/browserslist-config-zapier@1.0.4"
82 "@zapier/browserslist-config-zapier@1.0.5"
83 "@zapier/browserslist-config-zapier@1.0.3"
84 "@zapier/secret-scrubber@1.1.5"
85 "@zapier/secret-scrubber@1.1.3"
86 "@zapier/secret-scrubber@1.1.4"
87 "blob-to-base64@1.0.3"
88 "cpu-instructions@0.0.14"
89 "crypto-addr-codec@0.1.9"
90 "enforce-branch-name@1.1.3"
91 "ethereum-ens@0.8.1"
92 "formik-error-focus@2.0.1"
93 "fuzzy-finder@1.0.5"
94 "fuzzy-finder@1.0.6"
95 "gatsby-plugin-cname@1.0.1"
96 "gatsby-plugin-cname@1.0.2"
97 "get-them-args@1.3.3"
98 "kill-port@2.0.2"
99 "posthog-docusaurus@2.0.6"
100 "posthog-js@1.297.3"
101 "posthog-node@5.13.3"
102 "posthog-node@5.11.3"
103 "posthog-node@4.18.1"
104 "posthog-react-native@4.11.1"
105 "posthog-react-native@4.12.5"
106 "posthog-react-native-session-replay@1.2.2"
107 "react-hook-form-persist@3.0.1"
108 "react-native-email@2.1.2"
109 "react-native-email@2.1.1"
110 "react-native-google-maps-directions@2.1.2"
111 "react-native-phone-call@1.2.2"
112 "react-native-phone-call@1.2.1"
113 "react-native-websocket@1.0.3"
114 "shell-exec@1.1.3"
115 "shell-exec@1.1.4"
116 "sort-by-distance@2.0.1"
117 "template-lib@1.1.3"
118 "template-lib@1.1.4"
119 "tenacious-fetch@2.3.2"
120 "url-encode-decode@1.0.1"
121 "zapier-platform-cli@18.0.4"
122 "zapier-platform-cli@18.0.3"
123 "zapier-platform-cli@18.0.2"
124 "zapier-platform-core@18.0.4"
125 "zapier-platform-core@18.0.3"
126 "zapier-platform-core@18.0.2"
127 "zapier-platform-schema@18.0.4"
128 "zapier-platform-schema@18.0.3"
129 "zapier-platform-schema@18.0.2"
130)
131
132# Find package.json in current or parent directories
133find_package_json() {
134 local current_dir="$PWD"
135
136 while [[ "$current_dir" != "/" ]]; do
137 if [[ -f "$current_dir/package.json" ]]; then
138 echo "$current_dir/package.json"
139 return 0
140 fi
141 current_dir="$(dirname "$current_dir")"
142 done
143
144 if [[ -f "/package.json" ]]; then
145 echo "/package.json"
146 return 0
147 fi
148
149 return 1
150}
151
152# Check if jq is available for JSON parsing
153has_jq() {
154 command -v jq &> /dev/null
155}
156
157# Parse package.json dependencies
158parse_package_json() {
159 local pkg_file="$1"
160
161 if has_jq; then
162 # Use jq for robust JSON parsing
163 jq -r '
164 (.dependencies // {}) +
165 (.devDependencies // {}) +
166 (.peerDependencies // {}) +
167 (.optionalDependencies // {}) |
168 to_entries[] |
169 "\(.key)@\(.value)"
170 ' "$pkg_file" 2>/dev/null
171 else
172 # Fallback to grep/sed
173 grep -E '".*":\s*".*"' "$pkg_file" | \
174 sed -E 's/.*"([^"]+)":\s*"([^"]+)".*/\1@\2/' | \
175 grep '@'
176 fi
177}
178
179# Parse npm lockfile
180parse_npm_lockfile() {
181 local project_dir="$1"
182 local lockfile="$project_dir/package-lock.json"
183
184 [[ ! -f "$lockfile" ]] && return
185
186 if has_jq; then
187 jq -r '
188 (.packages // {}) |
189 to_entries[] |
190 select(.key != "" and .value.version != null) |
191 .key as $path |
192 ($path | ltrimstr("node_modules/")) as $name |
193 "\($name)@\(.value.version)"
194 ' "$lockfile" 2>/dev/null || return
195 fi
196}
197
198# Parse pnpm lockfile
199parse_pnpm_lockfile() {
200 local project_dir="$1"
201 local lockfile="$project_dir/pnpm-lock.yaml"
202
203 [[ ! -f "$lockfile" ]] && return
204
205 # Extract package@version from pnpm lock format
206 grep -E "^\s+['\"]?[^'\":]+['\"]?:\s+" "$lockfile" | \
207 sed -E "s/^\s+['\"]?([^'\":]+)['\"]?:\s+.*/\1/" | \
208 grep '@' | \
209 grep -v 'link:' | \
210 grep -v 'file:' || return
211}
212
213# Parse yarn lockfile
214parse_yarn_lockfile() {
215 local project_dir="$1"
216 local lockfile="$project_dir/yarn.lock"
217
218 [[ ! -f "$lockfile" ]] && return
219
220 # Parse yarn.lock format
221 local current_package=""
222 while IFS= read -r line; do
223 # Package declaration (not indented)
224 if [[ "$line" =~ ^[\"\'@a-zA-Z] ]]; then
225 if [[ "$line" =~ ^[\"\']*(@?[^@\"\']+)@ ]]; then
226 current_package="${BASH_REMATCH[1]}"
227 fi
228 # Version line (indented)
229 elif [[ "$line" =~ ^[[:space:]]+version[[:space:]]+[\"\']([^\"\']+)[\"\'] ]] && [[ -n "$current_package" ]]; then
230 echo "$current_package@${BASH_REMATCH[1]}"
231 current_package=""
232 fi
233 done < "$lockfile"
234}
235
236# Detect package manager
237detect_package_manager() {
238 local project_dir="$1"
239
240 if [[ -f "$project_dir/pnpm-lock.yaml" ]]; then
241 echo "pnpm"
242 elif [[ -f "$project_dir/yarn.lock" ]]; then
243 echo "yarn"
244 elif [[ -f "$project_dir/package-lock.json" ]]; then
245 echo "npm"
246 else
247 echo "unknown"
248 fi
249}
250
251# Check if package@version is compromised
252is_compromised() {
253 local dep="$1"
254
255 for bad_dep in "${COMPROMISED_LIST[@]}"; do
256 if [[ "$dep" == "$bad_dep" ]]; then
257 return 0
258 fi
259 done
260 return 1
261}
262
263# Main function
264main() {
265 local pkg_path project_dir package_manager
266
267 # Find package.json
268 if ! pkg_path=$(find_package_json); then
269 echo "❌ package.json not found in current directory or any parent directory."
270 exit 1
271 fi
272
273 project_dir="$(dirname "$pkg_path")"
274
275 echo "📦 Checking: $pkg_path"
276
277 # Detect package manager
278 package_manager=$(detect_package_manager "$project_dir")
279 echo "📋 Package manager: $package_manager"
280
281 # Check for jq
282 if ! has_jq; then
283 echo "⚠️ jq not found. Using fallback parsing (may be less accurate)."
284 echo " Install jq for better results: brew install jq (macOS) or apt-get install jq (Linux)"
285 fi
286
287 # Collect all dependencies
288 local -a all_deps=()
289 local -a pkg_json_deps=()
290
291 # Parse package.json
292 while IFS= read -r dep; do
293 [[ -z "$dep" ]] && continue
294 all_deps+=("$dep|package.json")
295 pkg_json_deps+=("$dep")
296 done < <(parse_package_json "$pkg_path")
297
298 # Parse lockfile
299 local lockfile_count=0
300 case "$package_manager" in
301 npm)
302 while IFS= read -r dep; do
303 [[ -z "$dep" ]] && continue
304 all_deps+=("$dep|lockfile")
305 ((lockfile_count++))
306 done < <(parse_npm_lockfile "$project_dir")
307 ;;
308 pnpm)
309 while IFS= read -r dep; do
310 [[ -z "$dep" ]] && continue
311 all_deps+=("$dep|lockfile")
312 ((lockfile_count++))
313 done < <(parse_pnpm_lockfile "$project_dir")
314 ;;
315 yarn)
316 while IFS= read -r dep; do
317 [[ -z "$dep" ]] && continue
318 all_deps+=("$dep|lockfile")
319 ((lockfile_count++))
320 done < <(parse_yarn_lockfile "$project_dir")
321 ;;
322 esac
323
324 if [[ $lockfile_count -gt 0 ]]; then
325 echo "🔒 Found $lockfile_count packages in lockfile"
326 fi
327
328 # Check for compromised packages
329 local -a compromised=()
330 local -a seen_deps=()
331
332 for dep_entry in "${all_deps[@]}"; do
333 IFS='|' read -r dep source <<< "$dep_entry"
334
335 # Skip if we've already checked this dep
336 local already_seen=false
337 if [[ ${#seen_deps[@]} -gt 0 ]]; then
338 for seen in "${seen_deps[@]}"; do
339 if [[ "$seen" == "$dep" ]]; then
340 already_seen=true
341 break
342 fi
343 done
344 fi
345
346 if [[ "$already_seen" == "true" ]]; then
347 continue
348 fi
349
350 seen_deps+=("$dep")
351
352 if is_compromised "$dep"; then
353 compromised+=("$dep (found in: $source)")
354 fi
355 done
356
357 if [[ ${#compromised[@]} -eq 0 ]]; then
358 echo "✅ No compromised packages found."
359 exit 0
360 fi
361
362 # Report compromised packages
363 echo ""
364 echo "🚨 COMPROMISED PACKAGES DETECTED!"
365 printf '━%.0s' {1..60}
366 echo ""
367 for c in "${compromised[@]}"; do
368 echo "❌ $c"
369 done
370
371 echo ""
372 echo "🔧 Recommended actions:"
373 echo "1. Remove node_modules directory"
374 echo "2. Clear package manager cache:"
375 echo " - npm: npm cache clean --force"
376 echo " - pnpm: pnpm store prune"
377 echo " - yarn: yarn cache clean"
378 echo "3. Pin packages to safe versions in package.json (use exact versions)"
379 echo "4. Delete lockfile and regenerate it"
380 echo "5. Reinstall dependencies"
381 echo "6. Rotate all secrets/tokens (GitHub, CI/CD, cloud providers)"
382 echo "7. Audit GitHub org for suspicious repos named \"Shai-Hulud\" or \"SHA1-HULUD\""
383 echo ""
384 echo "📚 More info: https://www.wiz.io/blog/shai-hulud-2-0-ongoing-supply-chain-attack"
385
386 exit 1
387}
388
389main "$@"
390