Skip to content

Instantly share code, notes, and snippets.

@benkitzelman
Created November 25, 2025 04:22
Show Gist options
  • Select an option

  • Save benkitzelman/dda0ca9f60a927e1116f5fc08a52c218 to your computer and use it in GitHub Desktop.

Select an option

Save benkitzelman/dda0ca9f60a927e1116f5fc08a52c218 to your computer and use it in GitHub Desktop.
Shai-Hulud detection - have I been compromised
#!/bin/bash
# NPM Supply Chain Attack Detector
# Based on GitLab's analysis of "Sha1-Hulud" malware campaign
# Reference: https://about.gitlab.com/blog/gitlab-discovers-widespread-npm-supply-chain-attack/
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
ISSUES_FOUND=0
echo -e "${CYAN}${BOLD}╔════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}${BOLD}║ NPM Supply Chain Attack Detection (Sha1-Hulud) ║${NC}"
echo -e "${CYAN}${BOLD}╚════════════════════════════════════════════════════════╝${NC}"
echo ""
# Function to report findings
report_finding() {
local severity=$1
local message=$2
local path=$3
ISSUES_FOUND=$((ISSUES_FOUND + 1))
case $severity in
CRITICAL)
echo -e "${RED}${BOLD}[CRITICAL]${NC} $message"
;;
HIGH)
echo -e "${RED}[HIGH]${NC} $message"
;;
MEDIUM)
echo -e "${YELLOW}[MEDIUM]${NC} $message"
;;
esac
if [ ! -z "$path" ]; then
echo -e " ${CYAN}→${NC} $path"
fi
echo ""
}
# Pre-scan: Safe package install (optional)
if [ -f "package.json" ]; then
echo -e "${CYAN}[PRE-SCAN]${NC} Optional: Install packages (without running pre / post install scripts) for deeper analysis"
echo "─────────────────────────────────────────────────────────"
echo -e " ${YELLOW}⚠${NC} Found package.json. Install packages with scripts disabled?"
echo -e " This safely installs packages WITHOUT executing any scripts."
echo -e " ${RED}Note:${NC} Will remove existing node_modules before clean install."
echo ""
read -p " Run package install? (y/N): " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo ""
echo -e " Which package manager are you using?"
echo -e " ${BOLD}1)${NC} npm"
echo -e " ${BOLD}2)${NC} yarn"
echo ""
read -p " Select (1 or 2): " -n 1 -r PACKAGE_MANAGER
echo ""
echo ""
if [ -d "./node_modules" ]; then
echo -e " ${YELLOW}⚠${NC} Removing existing node_modules..."
rm -rf ./node_modules
fi
echo -e " ${YELLOW}⚠${NC} Installing packages with scripts disabled for safe analysis..."
if [[ $PACKAGE_MANAGER == "1" ]]; then
npm ci --no-audit --ignore-scripts 2>&1 | tail -n 5
echo -e " ${GREEN}✓${NC} Package installation complete (scripts blocked)"
elif [[ $PACKAGE_MANAGER == "2" ]]; then
YARN_ENABLE_SCRIPTS=false yarn install --frozen-lockfile | tail -n 5
echo -e " ${GREEN}✓${NC} Package installation complete (scripts blocked)"
else
echo -e " ${RED}✗${NC} Invalid selection. Skipping install."
fi
else
echo -e " ${YELLOW}⊘${NC} Skipped package install (scan will only check existing node_modules)"
fi
echo ""
fi
# 1. Check for malicious files in node_modules
echo -e "${CYAN}[1/7]${NC} Scanning for malicious files in node_modules..."
echo "─────────────────────────────────────────────────────────"
if [ -d "./node_modules" ]; then
# Primary malware files
MALICIOUS_FILES=(
"setup_bun.js"
"bun_environment.js"
)
for file in "${MALICIOUS_FILES[@]}"; do
results=$(find ./node_modules -name "$file" 2>/dev/null)
if [ ! -z "$results" ]; then
report_finding "CRITICAL" "Found malware loader/payload: $file" "$results"
fi
done
# Exfiltration data files
EXFIL_FILES=(
"cloud.json"
"contents.json"
"environment.json"
"truffleSecrets.json"
)
for file in "${EXFIL_FILES[@]}"; do
results=$(find ./node_modules -name "$file" 2>/dev/null)
if [ ! -z "$results" ]; then
report_finding "HIGH" "Found potential exfiltration data file: $file" "$results"
fi
done
else
echo -e " ${GREEN}✓${NC} No node_modules directory found"
fi
# 2. Check for Trufflehog cache directories
echo ""
echo -e "${CYAN}[2/7]${NC} Checking for Trufflehog scanner artifacts..."
echo "─────────────────────────────────────────────────────────"
TRUFFLEHOG_PATHS=(
"$HOME/.truffler-cache"
"$HOME/.truffler-cache/extract"
"$HOME/.truffler-cache/trufflehog"
"$HOME/.truffler-cache/trufflehog.exe"
)
for path in "${TRUFFLEHOG_PATHS[@]}"; do
if [ -e "$path" ]; then
report_finding "CRITICAL" "Found Trufflehog cache (malware uses this for credential scanning)" "$path"
fi
done
if [ $ISSUES_FOUND -eq 0 ] || [ ${#TRUFFLEHOG_PATHS[@]} -eq 0 ]; then
echo -e " ${GREEN}✓${NC} No Trufflehog artifacts found"
fi
# 3. Check package.json files for suspicious preinstall scripts
echo ""
echo -e "${CYAN}[3/7]${NC} Analyzing package.json files for suspicious scripts..."
echo "─────────────────────────────────────────────────────────"
if [ -d "./node_modules" ]; then
suspicious_packages=$(find ./node_modules -name "package.json" -type f -exec grep -l "setup_bun.js\|bun_environment.js" {} \; 2>/dev/null)
if [ ! -z "$suspicious_packages" ]; then
while IFS= read -r pkg; do
report_finding "CRITICAL" "Package contains reference to malicious scripts" "$pkg"
done <<< "$suspicious_packages"
else
echo -e " ${GREEN}✓${NC} No suspicious preinstall scripts detected"
fi
else
echo -e " ${GREEN}✓${NC} No node_modules to scan"
fi
# 4. Check for GitHub repositories with "Sha1-Hulud" marker
echo ""
echo -e "${CYAN}[4/7]${NC} Checking for compromised GitHub repositories..."
echo "─────────────────────────────────────────────────────────"
if command -v gh &> /dev/null; then
# Check if authenticated
if gh auth status &> /dev/null; then
repos=$(gh repo list --json name,description --limit 1000 2>/dev/null | grep -i "Sha1-Hulud" || true)
if [ ! -z "$repos" ]; then
report_finding "CRITICAL" "Found repository with Sha1-Hulud marker in your account!" "$repos"
else
echo -e " ${GREEN}✓${NC} No compromised repositories found in your account"
fi
else
echo -e " ${YELLOW}⚠${NC} GitHub CLI not authenticated, skipping repo check"
fi
else
echo -e " ${YELLOW}⚠${NC} GitHub CLI not installed, skipping repo check"
fi
# 5. Check for suspicious environment variables
echo ""
echo -e "${CYAN}[5/7]${NC} Scanning environment for suspicious tokens..."
echo "─────────────────────────────────────────────────────────"
# Check for common token patterns that might have been harvested
suspicious_env=0
if env | grep -q "NPM_CONFIG_TOKEN"; then
echo -e " ${YELLOW}⚠${NC} NPM_CONFIG_TOKEN found in environment (potential target for harvesting)"
suspicious_env=1
fi
if env | grep -E "ghp_|gho_" &> /dev/null; then
echo -e " ${YELLOW}⚠${NC} GitHub token found in environment (potential target for harvesting)"
suspicious_env=1
fi
if [ $suspicious_env -eq 0 ]; then
echo -e " ${GREEN}✓${NC} No obviously exposed tokens in environment"
fi
# 6. Check for large obfuscated JavaScript files
echo ""
echo -e "${CYAN}[6/7]${NC} Searching for suspiciously large obfuscated files..."
echo "─────────────────────────────────────────────────────────"
if [ -d "./node_modules" ]; then
# Find JS files larger than 5MB (bun_environment.js is ~10MB)
large_files=$(find ./node_modules -name "*.js" -type f -size +5M 2>/dev/null)
if [ ! -z "$large_files" ]; then
while IFS= read -r file; do
# Check if file appears obfuscated (very long lines, no whitespace)
first_line=$(head -n 1 "$file" | cut -c1-200)
if [[ ${#first_line} -gt 150 ]] && [[ ! "$first_line" =~ [[:space:]] ]]; then
report_finding "MEDIUM" "Found large, potentially obfuscated JavaScript file" "$file ($(du -h "$file" | cut -f1))"
fi
done <<< "$large_files"
else
echo -e " ${GREEN}✓${NC} No unusually large JavaScript files found"
fi
else
echo -e " ${GREEN}✓${NC} No node_modules to scan"
fi
# 7. Check .github/workflows for suspicious patterns
echo ""
echo -e "${CYAN}[7/7]${NC} Checking GitHub workflows for malicious runners..."
echo "─────────────────────────────────────────────────────────"
if [ -d "./.github/workflows" ]; then
# Look for self-hosted runners or suspicious workflow files
suspicious_workflows=$(grep -r "runs-on: self-hosted\|discussion.yaml\|formatter_" ./.github/workflows/ 2>/dev/null || true)
if [ ! -z "$suspicious_workflows" ]; then
report_finding "MEDIUM" "Found potentially suspicious workflow configuration" "$suspicious_workflows"
else
echo -e " ${GREEN}✓${NC} No suspicious workflow patterns detected"
fi
else
echo -e " ${GREEN}✓${NC} No .github/workflows directory"
fi
# Summary
echo ""
echo -e "${CYAN}${BOLD}╔════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}${BOLD}║ SCAN COMPLETE ║${NC}"
echo -e "${CYAN}${BOLD}╚════════════════════════════════════════════════════════╝${NC}"
echo ""
if [ $ISSUES_FOUND -eq 0 ]; then
echo -e "${GREEN}${BOLD}✓ No indicators of compromise detected${NC}"
echo ""
echo -e "Your system appears clean. However, stay vigilant:"
echo -e " • Regularly audit your npm packages"
echo -e " • Review GitHub repository permissions"
echo -e " • Monitor for unexpected package updates"
exit 0
else
echo -e "${RED}${BOLD}⚠ FOUND $ISSUES_FOUND POTENTIAL INDICATOR(S) OF COMPROMISE${NC}"
echo ""
echo -e "${YELLOW}RECOMMENDED ACTIONS:${NC}"
echo -e " 1. ${BOLD}IMMEDIATELY${NC} rotate all GitHub, npm, and cloud provider tokens"
echo -e " 2. Review all npm packages you maintain for unauthorized updates"
echo -e " 3. Check GitHub repositories for the 'Sha1-Hulud' marker"
echo -e " 4. Remove any infected packages from node_modules"
echo -e " 5. Scan your filesystem with updated antivirus software"
echo -e " 6. ${RED}${BOLD}DO NOT${NC} mass-revoke tokens (may trigger dead man's switch)"
echo ""
echo -e "${YELLOW}IMPORTANT:${NC} This malware contains a destructive payload that triggers"
echo -e "if it loses access to both GitHub and npm. Coordinate remediation carefully."
echo ""
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment