Skip to content

Instantly share code, notes, and snippets.

@ericboehs
Last active November 26, 2025 21:28
Show Gist options
  • Select an option

  • Save ericboehs/dfb10ea79a4083f79930d8f467a50846 to your computer and use it in GitHub Desktop.

Select an option

Save ericboehs/dfb10ea79a4083f79930d8f467a50846 to your computer and use it in GitHub Desktop.
Shai-Hulud 2.0 Full Machine Scanner - Scans for npm supply chain attack indicators
#!/bin/bash
#
# Shai-Hulud 2.0 Full Machine Scanner
# ====================================
#
# Scans your machine for signs of the Shai-Hulud npm supply chain attack
# (November 2025). This attack compromised 796+ npm packages affecting
# 20+ million weekly downloads.
#
# WHAT THIS SCRIPT DOES:
#
# Phase 1 - Quick Critical Checks (fast, excludes node_modules):
# - Malware files: setup_bun.js, bun_environment.js
# - Backdoor workflows: .github/workflows/discussion.yaml
# - Exfiltration artifacts: cloud.json, truffleSecrets.json
# - Unexpected Bun runtime installation
# - Unexpected Trufflehog installation
#
# Phase 2 - Find Node.js Projects:
# - Searches for package.json files
# - Excludes node_modules, .git, .cache
#
# Phase 3 - Full Project Scans (checks node_modules via lockfiles):
# - Runs shai-hulud-detector on each project
# - Checks lockfiles (package-lock.json, yarn.lock) for 1600+
# compromised package versions - this catches malware in
# node_modules without scanning every file
# - Reports HIGH/MEDIUM/CLEAN for each
#
# USAGE:
#
# ./scan-machine-shai-hulud.sh
#
# The script will prompt to download the detector if not found.
#
# REQUIREMENTS:
#
# - bash or zsh
# - curl (for downloading detector)
# - Standard Unix tools (find, grep, shasum)
#
# EXIT CODES:
#
# 0 - All clean
# 1 - High risk findings (likely compromise)
# 2 - Medium risk findings (review recommended, often false positives)
#
# CUSTOMIZATION:
#
# Edit SCAN_DIRS below to add/remove directories to scan.
#
# MORE INFO:
#
# - Wiz.io writeup: https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack
# - Datadog writeup: https://securitylabs.datadoghq.com/articles/shai-hulud-2.0-npm-worm/
# - Detector repo: https://github.com/Cobenian/shai-hulud-detect
#
# Author: Eric Boehs (with Claude Code)
# Date: November 2025
#
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DETECTOR="$SCRIPT_DIR/shai-hulud-detector"
RESULTS_DIR="$SCRIPT_DIR/shai-hulud-results"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
RESULTS_FILE="$RESULTS_DIR/scan-$TIMESTAMP.txt"
# Detector version - update these to use a newer version
# Get latest from: https://github.com/Cobenian/shai-hulud-detect/commits/main
DETECTOR_COMMIT="e78331a301bc2fd958e0092c6279c7ad561dbdd4"
DETECTOR_SHA256="8d799bb265827ed5f0e9b5883024a38eccdc16355727817ea461d9c1567ef2a2"
# Directories to scan (add more as needed)
SCAN_DIRS=(
"$HOME/Code"
"$HOME/Projects"
"$HOME/Developer"
"$HOME/dev"
"$HOME/src"
"$HOME/work"
"$HOME/repos"
)
# Directories to skip
SKIP_PATTERNS=(
"node_modules"
".git"
".cache"
"vendor"
".npm"
".yarn/cache"
"dist"
"build"
".next"
)
echo "=========================================="
echo " Shai-Hulud Full Machine Scanner"
echo " $(date)"
echo "=========================================="
echo ""
# Create results directory
mkdir -p "$RESULTS_DIR"
# Check if detector exists, offer to download if not
if [ ! -f "$DETECTOR" ]; then
echo "Detector not found at $DETECTOR"
echo ""
read -p "Download from Cobenian/shai-hulud-detect? [y/N] " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Downloading detector..."
# Download to temp files first
TEMP_DETECTOR=$(mktemp)
TEMP_PACKAGES=$(mktemp)
# Detector: pinned to specific commit for security
curl -sL "https://raw.githubusercontent.com/Cobenian/shai-hulud-detect/${DETECTOR_COMMIT}/shai-hulud-detector.sh" -o "$TEMP_DETECTOR"
# Package list: always get latest (new compromised packages are added frequently)
curl -sL "https://raw.githubusercontent.com/Cobenian/shai-hulud-detect/main/compromised-packages.txt" -o "$TEMP_PACKAGES"
# Verify detector checksum
ACTUAL_SHA256=$(shasum -a 256 "$TEMP_DETECTOR" | cut -d' ' -f1)
if [ "$ACTUAL_SHA256" != "$DETECTOR_SHA256" ]; then
echo "ERROR: Detector checksum mismatch!"
echo " Expected: $DETECTOR_SHA256"
echo " Got: $ACTUAL_SHA256"
echo " This could indicate tampering. Aborting."
rm -f "$TEMP_DETECTOR" "$TEMP_PACKAGES"
exit 1
fi
echo " Detector checksum verified: $DETECTOR_SHA256"
# Verify package list looks legitimate (can't checksum - it changes frequently)
if ! grep -q "^# Shai-Hulud NPM Supply Chain Attack" "$TEMP_PACKAGES"; then
echo "ERROR: Downloaded package list missing expected header"
rm -f "$TEMP_DETECTOR" "$TEMP_PACKAGES"
exit 1
fi
PKG_COUNT=$(grep -v "^#" "$TEMP_PACKAGES" | grep -c ":" || echo "0")
if [ "$PKG_COUNT" -lt 500 ]; then
echo "ERROR: Package list seems too small ($PKG_COUNT packages, expected 500+)"
rm -f "$TEMP_DETECTOR" "$TEMP_PACKAGES"
exit 1
fi
echo " Package list verified ($PKG_COUNT compromised packages)"
# All checks passed, install
mv "$TEMP_DETECTOR" "$SCRIPT_DIR/shai-hulud-detector"
mv "$TEMP_PACKAGES" "$SCRIPT_DIR/compromised-packages.txt"
chmod +x "$SCRIPT_DIR/shai-hulud-detector"
echo ""
echo "Downloaded successfully!"
echo " Detector: ./shai-hulud-detector (locked to commit $DETECTOR_COMMIT)"
echo " Packages: ./compromised-packages.txt ($PKG_COUNT packages)"
echo ""
else
echo "Aborted. Install manually from: https://github.com/Cobenian/shai-hulud-detect"
exit 1
fi
fi
# Build find exclusion pattern
EXCLUDES=""
for pattern in "${SKIP_PATTERNS[@]}"; do
EXCLUDES="$EXCLUDES -path '*/$pattern' -prune -o -path '*/$pattern/*' -prune -o"
done
# Initialize results
echo "Shai-Hulud Full Machine Scan - $(date)" > "$RESULTS_FILE"
echo "==========================================" >> "$RESULTS_FILE"
echo "" >> "$RESULTS_FILE"
CRITICAL_FINDINGS=()
HIGH_RISK_PROJECTS=()
MEDIUM_RISK_PROJECTS=()
CLEAN_PROJECTS=()
###########################################
# PHASE 1: Quick Critical Checks
###########################################
echo "=== PHASE 1: Quick Critical Checks (Machine-Wide) ==="
echo ""
# Find all directories that exist from our scan list
EXISTING_DIRS=()
for dir in "${SCAN_DIRS[@]}"; do
if [ -d "$dir" ]; then
EXISTING_DIRS+=("$dir")
fi
done
if [ ${#EXISTING_DIRS[@]} -eq 0 ]; then
echo "No standard code directories found. Scanning $HOME..."
EXISTING_DIRS=("$HOME")
fi
echo "Scanning directories:"
for dir in "${EXISTING_DIRS[@]}"; do echo " - $dir"; done
echo ""
# 1. Check for malware payload files
echo "[1/5] Checking for malware files (setup_bun.js, bun_environment.js)..."
MALWARE_FILES=""
for dir in "${EXISTING_DIRS[@]}"; do
FOUND=$(find "$dir" -type d -name "node_modules" -prune -o -type d -name ".git" -prune -o \
\( -name "setup_bun.js" -o -name "bun_environment.js" \) -type f -print 2>/dev/null || true)
if [ -n "$FOUND" ]; then
MALWARE_FILES="$MALWARE_FILES$FOUND"$'\n'
fi
done
if [ -n "$MALWARE_FILES" ]; then
echo ""
echo " !!! CRITICAL: MALWARE FILES FOUND !!!"
echo "$MALWARE_FILES" | grep -v '^$' | while read -r f; do echo " $f"; done
CRITICAL_FINDINGS+=("MALWARE FILES: $MALWARE_FILES")
echo "CRITICAL - Malware files:" >> "$RESULTS_FILE"
echo "$MALWARE_FILES" >> "$RESULTS_FILE"
else
echo " None found"
fi
# 2. Check for backdoor workflows
echo "[2/5] Checking for backdoor workflows (discussion.yaml in .github/workflows)..."
BACKDOOR_FILES=""
for dir in "${EXISTING_DIRS[@]}"; do
FOUND=$(find "$dir" -type d -name "node_modules" -prune -o \
-path "*/.github/workflows/discussion.yaml" -print -o \
-path "*/.github/workflows/discussion.yml" -print 2>/dev/null || true)
if [ -n "$FOUND" ]; then
BACKDOOR_FILES="$BACKDOOR_FILES$FOUND"$'\n'
fi
done
if [ -n "$BACKDOOR_FILES" ]; then
echo ""
echo " !!! CRITICAL: BACKDOOR WORKFLOWS FOUND !!!"
echo "$BACKDOOR_FILES" | grep -v '^$' | while read -r f; do echo " $f"; done
CRITICAL_FINDINGS+=("BACKDOOR WORKFLOWS: $BACKDOOR_FILES")
echo "CRITICAL - Backdoor workflows:" >> "$RESULTS_FILE"
echo "$BACKDOOR_FILES" >> "$RESULTS_FILE"
else
echo " None found"
fi
# 3. Check for exfiltration artifacts
echo "[3/5] Checking for exfiltration artifacts (cloud.json, truffleSecrets.json)..."
EXFIL_FILES=""
for dir in "${EXISTING_DIRS[@]}"; do
FOUND=$(find "$dir" -type d -name "node_modules" -prune -o -type d -name ".git" -prune -o \
\( -name "cloud.json" -o -name "truffleSecrets.json" -o -name "environment.json" \) \
-type f -print 2>/dev/null | grep -v "node_modules" || true)
if [ -n "$FOUND" ]; then
EXFIL_FILES="$EXFIL_FILES$FOUND"$'\n'
fi
done
if [ -n "$EXFIL_FILES" ]; then
echo ""
echo " !!! WARNING: Potential exfiltration files found !!!"
echo "$EXFIL_FILES" | grep -v '^$' | while read -r f; do echo " $f"; done
echo " (Review these - could be legitimate config files)"
echo "POTENTIAL - Exfiltration artifacts:" >> "$RESULTS_FILE"
echo "$EXFIL_FILES" >> "$RESULTS_FILE"
else
echo " None found"
fi
# 4. Check for unexpected Bun installation
echo "[4/5] Checking for unexpected Bun runtime..."
if command -v bun &> /dev/null; then
BUN_PATH=$(which bun)
echo " WARNING: Bun is installed at $BUN_PATH"
echo " If you didn't install this intentionally, investigate!"
echo "WARNING - Bun installed at: $BUN_PATH" >> "$RESULTS_FILE"
else
echo " Bun not found in PATH (good)"
fi
# 5. Check for trufflehog (malware downloads this)
echo "[5/5] Checking for unexpected Trufflehog installation..."
if command -v trufflehog &> /dev/null; then
TH_PATH=$(which trufflehog)
echo " WARNING: Trufflehog is installed at $TH_PATH"
echo " If you didn't install this intentionally, investigate!"
echo "WARNING - Trufflehog installed at: $TH_PATH" >> "$RESULTS_FILE"
else
echo " Trufflehog not found in PATH (good)"
fi
echo ""
# Early exit if critical findings
if [ ${#CRITICAL_FINDINGS[@]} -gt 0 ]; then
echo "=========================================="
echo " !!! CRITICAL FINDINGS - STOP HERE !!!"
echo "=========================================="
echo ""
echo "Critical malware indicators were found on your machine."
echo "DISCONNECT FROM NETWORK and investigate immediately."
echo ""
echo "Files to investigate:"
for finding in "${CRITICAL_FINDINGS[@]}"; do
echo " $finding"
done
echo ""
echo "Results saved to: $RESULTS_FILE"
exit 1
fi
###########################################
# PHASE 2: Find Node.js Projects
###########################################
echo "=== PHASE 2: Finding Node.js Projects ==="
echo ""
echo "Searching for package.json files (excluding node_modules)..."
PROJECT_DIRS=()
for dir in "${EXISTING_DIRS[@]}"; do
while IFS= read -r pkg_json; do
if [ -n "$pkg_json" ]; then
PROJECT_DIR=$(dirname "$pkg_json")
# Skip if it's inside node_modules or other excluded dirs
if [[ ! "$PROJECT_DIR" =~ node_modules ]] && \
[[ ! "$PROJECT_DIR" =~ \.git ]] && \
[[ ! "$PROJECT_DIR" =~ \.cache ]]; then
PROJECT_DIRS+=("$PROJECT_DIR")
fi
fi
done < <(find "$dir" -type d -name "node_modules" -prune -o \
-type d -name ".git" -prune -o \
-type d -name ".cache" -prune -o \
-name "package.json" -type f -print 2>/dev/null)
done
# Remove duplicates and sort (portable - works with zsh and bash)
UNIQUE_DIRS=$(printf '%s\n' "${PROJECT_DIRS[@]}" | sort -u)
PROJECT_DIRS=()
while IFS= read -r dir; do
[ -n "$dir" ] && PROJECT_DIRS+=("$dir")
done <<< "$UNIQUE_DIRS"
echo "Found ${#PROJECT_DIRS[@]} Node.js projects"
echo ""
if [ ${#PROJECT_DIRS[@]} -eq 0 ]; then
echo "No Node.js projects found to scan."
echo "All quick checks passed!"
exit 0
fi
###########################################
# PHASE 3: Full Project Scans
###########################################
echo "=== PHASE 3: Full Project Scans ==="
echo ""
echo "This may take a while depending on project count..."
echo ""
SCANNED=0
TOTAL=${#PROJECT_DIRS[@]}
for project in "${PROJECT_DIRS[@]}"; do
SCANNED=$((SCANNED + 1))
PROJECT_NAME=$(basename "$project")
printf "[%d/%d] Scanning: %s... " "$SCANNED" "$TOTAL" "$PROJECT_NAME"
# Run detector and capture output
OUTPUT=$("$DETECTOR" "$project" 2>&1 || true)
# Determine result
if echo "$OUTPUT" | grep -q "HIGH RISK"; then
echo "!!! HIGH RISK !!!"
HIGH_RISK_PROJECTS+=("$project")
echo "" >> "$RESULTS_FILE"
echo "HIGH RISK: $project" >> "$RESULTS_FILE"
echo "$OUTPUT" | grep -E "(HIGH RISK|CRITICAL|🚨)" >> "$RESULTS_FILE" || true
elif echo "$OUTPUT" | grep -q "MEDIUM RISK"; then
echo "MEDIUM RISK"
MEDIUM_RISK_PROJECTS+=("$project")
echo "" >> "$RESULTS_FILE"
echo "MEDIUM RISK: $project" >> "$RESULTS_FILE"
echo "$OUTPUT" | grep -E "(MEDIUM RISK|⚠️)" >> "$RESULTS_FILE" || true
elif echo "$OUTPUT" | grep -q "No indicators"; then
echo "CLEAN"
CLEAN_PROJECTS+=("$project")
else
echo "CLEAN"
CLEAN_PROJECTS+=("$project")
fi
done
echo ""
###########################################
# SUMMARY
###########################################
echo "=========================================="
echo " SCAN COMPLETE - SUMMARY"
echo "=========================================="
echo ""
echo "Scanned ${#PROJECT_DIRS[@]} Node.js projects"
echo ""
if [ ${#HIGH_RISK_PROJECTS[@]} -gt 0 ]; then
echo "!!! HIGH RISK PROJECTS (${#HIGH_RISK_PROJECTS[@]}) !!!"
for project in "${HIGH_RISK_PROJECTS[@]}"; do
echo " - $project"
done
echo ""
fi
if [ ${#MEDIUM_RISK_PROJECTS[@]} -gt 0 ]; then
echo "MEDIUM RISK PROJECTS (${#MEDIUM_RISK_PROJECTS[@]}):"
for project in "${MEDIUM_RISK_PROJECTS[@]}"; do
echo " - $project"
done
echo ""
fi
echo "CLEAN PROJECTS: ${#CLEAN_PROJECTS[@]}"
echo ""
# Summary to file
echo "" >> "$RESULTS_FILE"
echo "==========================================" >> "$RESULTS_FILE"
echo "SUMMARY" >> "$RESULTS_FILE"
echo "==========================================" >> "$RESULTS_FILE"
echo "Total projects scanned: ${#PROJECT_DIRS[@]}" >> "$RESULTS_FILE"
echo "High risk: ${#HIGH_RISK_PROJECTS[@]}" >> "$RESULTS_FILE"
echo "Medium risk: ${#MEDIUM_RISK_PROJECTS[@]}" >> "$RESULTS_FILE"
echo "Clean: ${#CLEAN_PROJECTS[@]}" >> "$RESULTS_FILE"
echo "Full results saved to: $RESULTS_FILE"
echo ""
# Exit code
if [ ${#HIGH_RISK_PROJECTS[@]} -gt 0 ]; then
echo "!!! ACTION REQUIRED: High risk projects detected !!!"
exit 1
elif [ ${#MEDIUM_RISK_PROJECTS[@]} -gt 0 ]; then
echo "Review recommended for medium risk projects"
echo "(Medium risk often includes false positives from legitimate tools)"
exit 2
else
echo "All projects appear clean from Shai-Hulud indicators"
exit 0
fi
@ericboehs
Copy link
Author

ericboehs commented Nov 26, 2025

Shai-Hulud 2.0 Full Machine Scanner

Scans your machine for signs of the Shai-Hulud npm supply chain attack (November 2025). This attack compromised 796+ npm packages affecting 20+ million weekly downloads.

Quick Start

# Download
curl -sLO https://gist.githubusercontent.com/ericboehs/dfb10ea79a4083f79930d8f467a50846/raw/scan-machine-shai-hulud.sh

# Verify checksum
echo "bc202922781ebbe32ec3d15798ef1f0acd1ac79edc906649f569c9014635e30c  scan-machine-shai-hulud.sh" | shasum -a 256 -c

# Run
chmod +x scan-machine-shai-hulud.sh
./scan-machine-shai-hulud.sh

The script will prompt to download the shai-hulud-detector if not found.

What It Does

Phase Description
Phase 1 Quick checks for malware files, backdoor workflows, exfiltration artifacts
Phase 2 Finds all Node.js projects on your machine
Phase 3 Runs shai-hulud-detector on each, checking lockfiles for 1600+ compromised packages

Runtime Warning

This script can take a long time depending on how many Node.js projects you have.

Projects Estimated Time
~50 30-60 minutes
~150 2-4 hours
~300 5-10 hours

Phase 1 and 2 are fast. Phase 3 (full project scans) takes ~1-2 minutes per project (or longer for large repos with multiple package.json files).

Consider running overnight or in a background terminal:

nohup ./scan-machine-shai-hulud.sh > scan-output.log 2>&1 &
tail -f scan-output.log

Exit Codes

  • 0 - All clean
  • 1 - High risk (likely compromise)
  • 2 - Medium risk (review recommended, often false positives)

More Info

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment