Skip to content

Instantly share code, notes, and snippets.

@maxgfr
Last active November 28, 2025 18:02
Show Gist options
  • Select an option

  • Save maxgfr/ba6d5bc6008b3a42bc808c3a46a7069f to your computer and use it in GitHub Desktop.

Select an option

Save maxgfr/ba6d5bc6008b3a42bc808c3a46a7069f to your computer and use it in GitHub Desktop.
Shai-Hulud Second Coming Vulnerability Checker

Shai-Hulud Second Coming Vulnerability Checker

A lightweight shell script to detect vulnerable npm packages related to the Shai-Hulud "Second Coming" supply chain attack.

What it does

This script recursively scans your entire Node.js monorepo or project for packages that match the vulnerable versions listed in:

  • Tenable's official database (JSON format)
  • Datadog's Indicators of Compromise (CSV format)

It:

  • Searches for ALL lockfiles in your project:
    • package-lock.json (npm)
    • npm-shrinkwrap.json (npm)
    • yarn.lock (Yarn v1/v2)
    • pnpm-lock.yaml (pnpm)
    • bun.lock (Bun)
  • Searches for ALL package.json files in your project (for declared dependencies)
  • Respects .gitignore to avoid analyzing ignored files
  • Shows which folder each file is in for easy location
  • Lists all found files before analysis for transparency

Prerequisites

  • jq (required): JSON parser

    • macOS: brew install jq
    • Ubuntu/Debian: apt-get install jq
  • yq (optional): Better YAML parsing for pnpm-lock.yaml

    • macOS: brew install yq
    • Ubuntu/Debian: snap install yq

Usage

Local scan

  1. Download the script:
curl -O https://gist.githubusercontent.com/maxgfr/ba6d5bc6008b3a42bc808c3a46a7069f/raw/f63402cb7b7d95a2d6fb2dd17d497a9031dbf8d2/check-shai-hulud.sh
chmod +x check-shai-hulud.sh
  1. Run in your project directory:
./check-shai-hulud.sh

One-liner (direct execution)

curl -sS https://gist.githubusercontent.com/maxgfr/ba6d5bc6008b3a42bc808c3a46a7069f/raw/f63402cb7b7d95a2d6fb2dd17d497a9031dbf8d2/check-shai-hulud.sh | bash

Features

  • πŸ” Recursive search: Finds all lockfiles and package.json files in subdirectories
  • πŸ“‹ Comprehensive listing: Shows all found files before analysis
  • πŸ“‚ Folder context: Clearly indicates which folder each file is in
  • 🚫 Respects .gitignore: Automatically ignores files listed in .gitignore (when in a git repo)
  • 🎨 Color-coded output: Easy-to-read results with color highlighting
  • 🏒 Monorepo-friendly: Perfect for projects with multiple packages
  • πŸ“Š Summary report: Lists all affected files at the end with their vulnerabilities
  • ⏭️ Complete scan: Continues analysis even after finding vulnerabilities

Output

The script will display:

  • βœ… Green: No vulnerable packages detected
  • ⚠️ Red: Vulnerable packages found (exact version match in lockfiles)
  • ⚠️ Yellow: Package is in the vulnerable list (check lockfile for exact version)

Example output

πŸ” Downloading Tenable Shai-Hulud 'Second Coming' list...
➑️  3 packages present in list.json

πŸ” Searching for all lockfiles in the project (respecting .gitignore)...
βœ… 5 lockfile(s) found

πŸ“‹ List of found lockfiles:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  πŸ“ ./
     └─ package-lock.json
  πŸ“ ./packages/frontend
     └─ yarn.lock
  πŸ“ ./packages/backend
     └─ pnpm-lock.yaml
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

πŸ”Ž Starting lockfiles analysis...

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
πŸ“‚ Folder: ./packages/frontend
πŸ“„ File: ./packages/frontend/yarn.lock
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

πŸ“¦ Analyzing ./packages/frontend/yarn.lock...
⚠️  [./packages/frontend/yarn.lock] @ahmedhfarag/[email protected] (vulnerable)

[... continues analyzing all other lockfiles ...]

πŸ” Searching for all package.json files in the project (respecting .gitignore)...
[... analyzes package.json files ...]

=============================
⚠️  WARNING: Vulnerable packages have been detected

Vulnerable packages found:

πŸ“„ File: ./packages/frontend/yarn.lock
   └─ @ahmedhfarag/[email protected]

πŸ“„ File: ./packages/backend/pnpm-lock.yaml
   └─ @ahmedhfarag/[email protected]

πŸ“„ File: ./package-lock.json
   └─ @ahmedhfarag/[email protected]

Recommendations:
   - Update to versions not listed in Tenable's database
   - Also check your CI/CD pipeline and generated artifacts
=============================

Key points:

  • The script analyzes ALL files before reporting
  • Vulnerabilities are detected during analysis (red warnings)
  • A final summary lists ALL affected files grouped by location
  • Exit code 1 is returned ONLY after the complete scan

Exit codes

  • 0: No vulnerabilities found
  • 1: Vulnerabilities detected

This makes it easy to integrate into CI/CD pipelines:

./check-shai-hulud.sh || exit 1

What to do if vulnerabilities are found

  1. Update immediately: Upgrade to versions not listed as vulnerable
  2. Check your build artifacts: Ensure your CI/CD hasn't been compromised
  3. Review your infrastructure: Check for any suspicious activity
  4. Follow Tenable's recommendations: https://www.tenable.com/security/research/tra-2024-44

CI/CD Integration

GitHub Actions

- name: Check for Shai-Hulud vulnerabilities
  run: |
    curl -sS https://raw.githubusercontent.com/.../check-shai-hulud.sh | bash

GitLab CI

shai-hulud-check:
  script:
    - curl -sS https://raw.githubusercontent.com/.../check-shai-hulud.sh | bash

How it works

  1. Lockfile Discovery: Recursively finds all lockfiles (package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml, bun.lock) in the project
  2. Gitignore Filtering: Automatically excludes files listed in .gitignore when in a git repository
  3. File Listing: Shows all discovered files with their folder paths for transparency
  4. Analysis: Analyzes each lockfile for exact version matches against Tenable's vulnerability list
  5. package.json Check: Also checks all package.json files for vulnerable dependencies (warns to check lockfiles)

Notes

  • The script reads from two official sources:

    • Tenable: https://raw.githubusercontent.com/tenable/shai-hulud-second-coming-affected-packages/refs/heads/main/list.json
    • Datadog IOC: https://raw.githubusercontent.com/DataDog/indicators-of-compromise/refs/heads/main/shai-hulud-2.0/consolidated_iocs.csv
  • It performs exact version matching for lockfiles (high confidence)

  • For package.json, it only warns if a package is in the vulnerable list (you need to check your lockfile)

  • Automatically respects .gitignore to avoid scanning build artifacts or cached files

  • Perfect for monorepos with multiple packages and lockfiles

License

MIT

Credits

Based on:

#!/bin/bash
# Shai-Hulud "Second Coming" Checker
# Analyzes package.json and lockfiles to detect vulnerable packages
set -e
AFFECTED_URL="https://raw.githubusercontent.com/tenable/shai-hulud-second-coming-affected-packages/refs/heads/main/list.json"
DATADOG_IOC_URL="https://raw.githubusercontent.com/DataDog/indicators-of-compromise/refs/heads/main/shai-hulud-2.0/consolidated_iocs.csv"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo "πŸ” Downloading Tenable Shai-Hulud 'Second Coming' list..."
# Download vulnerable packages list
if ! VULN_LIST=$(curl -sS "$AFFECTED_URL"); then
echo "❌ Error: Unable to download vulnerable packages list"
exit 1
fi
# Check that jq is installed
if ! command -v jq &> /dev/null; then
echo "❌ Error: 'jq' must be installed to run this script"
echo " Installation: brew install jq (macOS) or apt-get install jq (Linux)"
exit 1
fi
PACKAGES_COUNT=$(echo "$VULN_LIST" | jq 'length')
echo "➑️ $PACKAGES_COUNT packages present in Tenable list.json"
echo ""
echo "πŸ” Downloading Datadog Indicators of Compromise list..."
# Download Datadog IOC CSV
if ! DATADOG_CSV=$(curl -sS "$DATADOG_IOC_URL"); then
echo "⚠️ Warning: Unable to download Datadog IOC list (continuing with Tenable list only)"
DATADOG_CSV=""
else
DATADOG_PACKAGES_COUNT=$(echo "$DATADOG_CSV" | tail -n +2 | wc -l | tr -d ' ')
echo "➑️ $DATADOG_PACKAGES_COUNT packages present in Datadog consolidated_iocs.csv"
fi
echo ""
FOUND_VULNERABLE=0
VULNERABLE_PACKAGES=()
# Function to extract package name from a lockfile line
extract_package_name() {
local line="$1"
# For yarn.lock and npm (format: "package@version" or "@scope/package@version")
if [[ "$line" =~ ^\"?(@?[^@\"]+)@.*\"?:?$ ]]; then
echo "${BASH_REMATCH[1]}"
fi
}
# Function to check if a package+version is vulnerable
check_vulnerability() {
local package_name="$1"
local version="$2"
local source="$3"
local found_in=""
# Check in Tenable list
local vuln_versions=$(echo "$VULN_LIST" | jq -r --arg pkg "$package_name" '.[$pkg].vuln_vers // [] | .[]')
if [ -n "$vuln_versions" ]; then
while IFS= read -r vuln_ver; do
if [ "$version" = "$vuln_ver" ]; then
found_in="Tenable"
break
fi
done <<< "$vuln_versions"
fi
# Check in Datadog CSV if not already found and CSV is available
if [ -z "$found_in" ] && [ -n "$DATADOG_CSV" ]; then
# CSV format: package_name,package_versions,sources
# Search for exact package name and version match
if echo "$DATADOG_CSV" | grep -q "^${package_name},${version},"; then
found_in="Datadog"
fi
fi
if [ -n "$found_in" ]; then
echo -e "${RED}⚠️ [$source] $package_name@$version (vulnerable - source: $found_in)${NC}"
FOUND_VULNERABLE=1
VULNERABLE_PACKAGES+=("$source|$package_name@$version")
return 0
fi
return 1
}
# Function to analyze a package-lock.json file
analyze_package_lock() {
local lockfile="$1"
local lockdir=$(dirname "$lockfile")
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "${GREEN}πŸ“‚ Folder: $lockdir${NC}"
echo -e "${GREEN}πŸ“„ File: $lockfile${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "πŸ“¦ Analyzing $lockfile..."
PACKAGES=$(jq -r '
((.dependencies // {}) | to_entries[] | "\(.key)@\(.value.version)"),
((.packages // {}) | to_entries[] |
select(.key | startswith("node_modules/")) |
"\(.key | split("node_modules/")[-1])@\(.value.version)")
' "$lockfile" 2>/dev/null | sort -u)
if [ -n "$PACKAGES" ]; then
while IFS= read -r pkg_line; do
if [[ "$pkg_line" =~ ^(.+)@(.+)$ ]]; then
pkg_name="${BASH_REMATCH[1]}"
pkg_version="${BASH_REMATCH[2]}"
check_vulnerability "$pkg_name" "$pkg_version" "$lockfile" || true
fi
done <<< "$PACKAGES"
fi
echo ""
}
# Function to analyze a yarn.lock file
analyze_yarn_lock() {
local lockfile="$1"
local lockdir=$(dirname "$lockfile")
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "${GREEN}πŸ“‚ Folder: $lockdir${NC}"
echo -e "${GREEN}πŸ“„ File: $lockfile${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "πŸ“¦ Analyzing $lockfile..."
current_package=""
while IFS= read -r line; do
if [[ "$line" =~ ^[^\s].*:$ ]] && [[ ! "$line" =~ ^[[:space:]] ]]; then
pkg_line=$(echo "$line" | sed 's/:$//' | tr -d '"')
if [[ "$pkg_line" =~ ^(@[^@]+)@.*$ ]]; then
current_package="${BASH_REMATCH[1]}"
elif [[ "$pkg_line" =~ ^([^@]+)@.*$ ]]; then
current_package="${BASH_REMATCH[1]}"
fi
elif [[ "$line" =~ ^[[:space:]]+version[[:space:]]+\"?([^\"]+)\"?$ ]] && [ -n "$current_package" ]; then
pkg_version="${BASH_REMATCH[1]}"
check_vulnerability "$current_package" "$pkg_version" "$lockfile" || true
current_package=""
fi
done < "$lockfile"
echo ""
}
# Function to analyze a pnpm-lock.yaml file
analyze_pnpm_lock() {
local lockfile="$1"
local lockdir=$(dirname "$lockfile")
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "${GREEN}πŸ“‚ Folder: $lockdir${NC}"
echo -e "${GREEN}πŸ“„ File: $lockfile${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "πŸ“¦ Analyzing $lockfile..."
if command -v yq &> /dev/null; then
PACKAGES=$(yq eval '.packages | keys | .[]' "$lockfile" 2>/dev/null | grep '^/' | sed 's|^/||')
while IFS= read -r pkg_line; do
if [[ "$pkg_line" =~ ^(@?[^@]+)@(.+)$ ]]; then
pkg_name="${BASH_REMATCH[1]}"
pkg_version="${BASH_REMATCH[2]}"
check_vulnerability "$pkg_name" "$pkg_version" "$lockfile" || true
fi
done <<< "$PACKAGES"
else
# Fallback: parsing basique avec grep - avoid pipe to preserve array
PACKAGES=$(grep -E "^\s+/" "$lockfile" 2>/dev/null | sed 's/^[[:space:]]*//' | sed 's/:$//' | sed 's|^/||')
while IFS= read -r pkg_line; do
if [[ "$pkg_line" =~ ^(@?[^@]+)@(.+)$ ]]; then
pkg_name="${BASH_REMATCH[1]}"
pkg_version="${BASH_REMATCH[2]}"
check_vulnerability "$pkg_name" "$pkg_version" "$lockfile" || true
fi
done <<< "$PACKAGES"
fi
echo ""
}
# Function to analyze a bun.lock file (JSON format)
analyze_bun_lock() {
local lockfile="$1"
local lockdir=$(dirname "$lockfile")
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "${GREEN}πŸ“‚ Folder: $lockdir${NC}"
echo -e "${GREEN}πŸ“„ File: $lockfile${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "πŸ“¦ Analyzing $lockfile..."
# Bun lockfiles are JSON format but may have trailing commas
# Use grep to extract package@version strings directly (more robust than jq for malformed JSON)
PACKAGES=$(grep -E '^\s*"@?[^"]+@[^"]+",?$' "$lockfile" | sed 's/[",]//g' | sed 's/^[[:space:]]*//')
if [ -n "$PACKAGES" ]; then
while IFS= read -r pkg_line; do
if [[ "$pkg_line" =~ ^(.+)@(.+)$ ]]; then
pkg_name="${BASH_REMATCH[1]}"
pkg_version="${BASH_REMATCH[2]}"
check_vulnerability "$pkg_name" "$pkg_version" "$lockfile" || true
fi
done <<< "$PACKAGES"
fi
echo ""
}
# Search for all lockfiles in the project
echo "πŸ” Searching for all lockfiles in the project (respecting .gitignore)..."
TEMP_LOCKFILES=$(find . \( -name "package-lock.json" -o -name "npm-shrinkwrap.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" -o -name "bun.lock" \) -type f ! -path "*/node_modules/*" ! -path "*/.yarn/*" ! -path "*/.git/*")
# Filter using git check-ignore if in a git repo
if git rev-parse --git-dir > /dev/null 2>&1; then
LOCKFILES=""
while IFS= read -r file; do
if ! git check-ignore -q "$file" 2>/dev/null; then
if [ -z "$LOCKFILES" ]; then
LOCKFILES="$file"
else
LOCKFILES="$LOCKFILES
$file"
fi
fi
done <<< "$TEMP_LOCKFILES"
else
LOCKFILES="$TEMP_LOCKFILES"
fi
if [ -z "$LOCKFILES" ]; then
echo "❌ No lockfiles found"
else
LOCKFILE_COUNT=$(echo "$LOCKFILES" | wc -l | tr -d ' ')
echo "βœ… $LOCKFILE_COUNT lockfile(s) found"
echo ""
# Display list of all found lockfiles
echo "πŸ“‹ List of found lockfiles:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
while IFS= read -r lockfile; do
lockdir=$(dirname "$lockfile")
lockname=$(basename "$lockfile")
echo " πŸ“ $lockdir/"
echo " └─ $lockname"
done <<< "$LOCKFILES"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Analyze each lockfile
echo "πŸ”Ž Starting lockfiles analysis..."
echo ""
while IFS= read -r lockfile; do
lockname=$(basename "$lockfile")
case "$lockname" in
"package-lock.json"|"npm-shrinkwrap.json")
analyze_package_lock "$lockfile"
;;
"yarn.lock")
analyze_yarn_lock "$lockfile"
;;
"pnpm-lock.yaml")
analyze_pnpm_lock "$lockfile"
;;
"bun.lock")
analyze_bun_lock "$lockfile"
;;
esac
done <<< "$LOCKFILES"
fi
echo ""
# Search for all package.json files in the project
echo "πŸ” Searching for all package.json files in the project (respecting .gitignore)..."
# Find all package.json and filter those in .gitignore
TEMP_FILES=$(find . -name "package.json" -type f ! -path "*/node_modules/*" ! -path "*/.yarn/*" ! -path "*/.git/*")
# Filter using git check-ignore if in a git repo
if git rev-parse --git-dir > /dev/null 2>&1; then
PACKAGE_JSON_FILES=""
while IFS= read -r file; do
if ! git check-ignore -q "$file" 2>/dev/null; then
if [ -z "$PACKAGE_JSON_FILES" ]; then
PACKAGE_JSON_FILES="$file"
else
PACKAGE_JSON_FILES="$PACKAGE_JSON_FILES
$file"
fi
fi
done <<< "$TEMP_FILES"
else
# If not in a git repo, use the complete list
PACKAGE_JSON_FILES="$TEMP_FILES"
fi
if [ -z "$PACKAGE_JSON_FILES" ]; then
echo "❌ No package.json files found"
else
PACKAGE_COUNT=$(echo "$PACKAGE_JSON_FILES" | wc -l | tr -d ' ')
echo "βœ… $PACKAGE_COUNT package.json file(s) found"
echo ""
# Display list of all found package.json files
echo "πŸ“‹ List of found package.json files:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
while IFS= read -r package_file; do
package_dir=$(dirname "$package_file")
echo " πŸ“ $package_dir/"
echo " └─ package.json"
done <<< "$PACKAGE_JSON_FILES"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Analyze each found package.json
echo "πŸ”Ž Starting dependencies analysis..."
echo ""
while IFS= read -r package_file; do
# Determine the package.json folder
package_dir=$(dirname "$package_file")
# Display folder being analyzed
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "${GREEN}πŸ“‚ Folder being analyzed: $package_dir${NC}"
echo -e "${GREEN}πŸ“„ File: $package_file${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "πŸ“¦ Analyzing declared dependencies in $package_file..."
# Extract all dependencies
ALL_DEPS=$(jq -r '
[
(.dependencies // {} | keys[]),
(.devDependencies // {} | keys[]),
(.optionalDependencies // {} | keys[]),
(.peerDependencies // {} | keys[])
] | unique | .[]
' "$package_file" 2>/dev/null)
if [ -n "$ALL_DEPS" ]; then
while IFS= read -r pkg_name; do
found_source=""
vuln_versions=""
# Check if this package is in Tenable's list
if echo "$VULN_LIST" | jq -e --arg pkg "$pkg_name" '.[$pkg]' > /dev/null 2>&1; then
vuln_versions=$(echo "$VULN_LIST" | jq -r --arg pkg "$pkg_name" '.[$pkg].vuln_vers | join(", ")')
found_source="Tenable"
fi
# Check if this package is in Datadog's list
if [ -n "$DATADOG_CSV" ]; then
datadog_match=$(echo "$DATADOG_CSV" | grep "^${pkg_name}," | head -1)
if [ -n "$datadog_match" ]; then
datadog_version=$(echo "$datadog_match" | cut -d',' -f2)
if [ -z "$found_source" ]; then
found_source="Datadog"
vuln_versions="$datadog_version"
else
found_source="Tenable+Datadog"
vuln_versions="$vuln_versions (Datadog: $datadog_version)"
fi
fi
fi
if [ -n "$found_source" ]; then
echo -e "${YELLOW}⚠️ [$package_file] $pkg_name is in the list - source: $found_source (vuln. versions: $vuln_versions)${NC}"
echo " β†’ Check your lockfile for the exact installed version"
FOUND_VULNERABLE=1
fi
done <<< "$ALL_DEPS"
else
echo " ℹ️ No dependencies found in this package.json"
fi
echo ""
done <<< "$PACKAGE_JSON_FILES"
fi
echo ""
echo "============================="
if [ $FOUND_VULNERABLE -eq 0 ]; then
echo -e "${GREEN}βœ… No vulnerable packages detected${NC}"
else
echo -e "${RED}⚠️ WARNING: Vulnerable packages have been detected${NC}"
echo ""
echo "Vulnerable packages found:"
echo ""
# Display vulnerabilities grouped by file (simple approach without associative arrays)
current_file=""
for vuln in "${VULNERABLE_PACKAGES[@]}"; do
IFS='|' read -r file pkg <<< "$vuln"
if [ "$file" != "$current_file" ]; then
if [ -n "$current_file" ]; then
echo ""
fi
echo -e "${RED}πŸ“„ File: $file${NC}"
current_file="$file"
fi
echo -e "${RED} └─ $pkg${NC}"
done
echo ""
echo -e "${YELLOW}Recommendations:${NC}"
echo " - Update to versions not listed in Tenable's database"
echo " - Also check your CI/CD pipeline and generated artifacts"
fi
echo "============================="
echo ""
exit $FOUND_VULNERABLE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment