-
-
Save ericallam/e2ae497d99a21d500fd9fbb01e1bfa02 to your computer and use it in GitHub Desktop.
Detects indicators of the Shai-Hulud ("bun") npm supply chain malware
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # Detects indicators of the Shai-Hulud ("bun") npm supply chain malware | |
| # described by GitLab (Nov 2025). | |
| # | |
| # Modes: | |
| # 1) repo – scan all node_modules under a specific repo path | |
| # 2) machine – scan the current machine for non-node_modules IoCs | |
| # | |
| # Usage: | |
| # ./detect_npm_bun_malware.sh repo /path/to/repo | |
| # ./detect_npm_bun_malware.sh machine | |
| # | |
| # Exit codes: | |
| # 0 - Script ran successfully | |
| # 1 - Usage / argument error | |
| set -o errexit | |
| set -o pipefail | |
| set -o nounset | |
| SCRIPT_NAME="$(basename "$0")" | |
| print_usage() { | |
| cat <<EOF | |
| Usage: | |
| $SCRIPT_NAME repo /path/to/repo | |
| $SCRIPT_NAME machine | |
| Modes: | |
| repo Scan a specific repo for infected node_modules (package.json + bun_environment.js). | |
| machine Scan the current machine (user home) for non-node_modules indicators of compromise. | |
| This script is read-only and will not execute any npm scripts or JavaScript. | |
| EOF | |
| } | |
| log() { | |
| # Simple log helper | |
| printf '%s\n' "$*" >&2 | |
| } | |
| scan_repo() { | |
| local repo_path="$1" | |
| if [[ ! -d "$repo_path" ]]; then | |
| log "ERROR: Repo path does not exist or is not a directory: $repo_path" | |
| exit 1 | |
| fi | |
| log "=== Repo mode: scanning node_modules under: $repo_path ===" | |
| local infected=0 | |
| # 1) Look for suspicious package.json files inside node_modules | |
| # We look for: | |
| # - references to setup_bun.js | |
| # - references to bun_environment.js | |
| # - suspicious Bun installer curl (bun.sh/install) | |
| # | |
| # These patterns are taken from GitLab's analysis of the malware family. | |
| while IFS= read -r pkg; do | |
| if grep -Eq 'setup_bun\.js|bun_environment\.js|bun\.sh/install' "$pkg"; then | |
| infected=1 | |
| local rel="${pkg#$repo_path/}" | |
| printf '[SUSPICIOUS package.json] %s\n' "${rel}" | |
| fi | |
| done < <( | |
| find "$repo_path" \ | |
| -type f -name package.json \ | |
| -path "*/node_modules/*" 2>/dev/null | |
| ) | |
| # 2) Look for bun_environment.js files under node_modules | |
| while IFS= read -r bunfile; do | |
| infected=1 | |
| local rel="${bunfile#$repo_path/}" | |
| printf '[SUSPICIOUS bun_environment.js in node_modules] %s\n' "${rel}" | |
| done < <( | |
| find "$repo_path" \ | |
| -type f -name "bun_environment.js" \ | |
| -path "*/node_modules/*" 2>/dev/null | |
| ) | |
| # 3) (Optional extra) look for setup_bun.js files under node_modules | |
| while IFS= read -r setupfile; do | |
| infected=1 | |
| local rel="${setupfile#$repo_path/}" | |
| printf '[SUSPICIOUS setup_bun.js in node_modules] %s\n' "${rel}" | |
| done < <( | |
| find "$repo_path" \ | |
| -type f -name "setup_bun.js" \ | |
| -path "*/node_modules/*" 2>/dev/null | |
| ) | |
| if [[ "$infected" -eq 0 ]]; then | |
| log "=== Result: No indicators of this npm malware found in node_modules under: $repo_path ===" | |
| else | |
| log "=== Result: One or more suspicious files were found. Treat this repo as potentially compromised. ===" | |
| fi | |
| } | |
| scan_machine() { | |
| log "=== Machine mode: scanning for machine-wide indicators (excluding node_modules) ===" | |
| local infected=0 | |
| local home_dir="$HOME" | |
| # 1) .truffler-cache in the user's home (used to store Trufflehog binary) | |
| # IoCs: | |
| # ~/.truffler-cache/ | |
| # ~/.truffler-cache/extract/ | |
| # ~/.truffler-cache/trufflehog | |
| # ~/.truffler-cache/trufflehog.exe | |
| local truffler_dir="$home_dir/.truffler-cache" | |
| if [[ -d "$truffler_dir" ]]; then | |
| infected=1 | |
| printf '[SUSPICIOUS DIR] %s\n' "$truffler_dir" | |
| [[ -d "$truffler_dir/extract" ]] && printf '[SUSPICIOUS DIR] %s\n' "$truffler_dir/extract" | |
| [[ -f "$truffler_dir/trufflehog" ]] && printf '[SUSPICIOUS FILE] %s\n' "$truffler_dir/trufflehog" | |
| [[ -f "$truffler_dir/trufflehog.exe" ]] && printf '[SUSPICIOUS FILE] %s\n' "$truffler_dir/trufflehog.exe" | |
| fi | |
| # 4) Suspicious Bun installer curl command in shell history: | |
| # curl -fsSL https://bun.sh/install | bash | |
| # This is not inherently evil, but the malware uses exactly this to bootstrap. | |
| for hist in "$home_dir/.bash_history" "$home_dir/.zsh_history" "$home_dir/.zhistory"; do | |
| if [[ -f "$hist" ]] && grep -Fq 'curl -fsSL https://bun.sh/install | bash' "$hist"; then | |
| infected=1 | |
| printf '[SUSPICIOUS COMMAND IN HISTORY] Found Bun installer curl in %s\n' "$hist" | |
| fi | |
| done | |
| # 5) Check if the destructive shred command appears to be running: | |
| # shred -uvz -n 1 | |
| if command -v ps >/dev/null 2>&1; then | |
| if ps aux | grep -E 'shred -uvz -n 1' | grep -v grep >/dev/null 2>&1; then | |
| infected=1 | |
| printf '[SUSPICIOUS PROCESS] "shred -uvz -n 1" appears to be running right now!\n' | |
| fi | |
| fi | |
| if [[ "$infected" -eq 0 ]]; then | |
| log "=== Result: No machine-wide indicators of this malware were found under $home_dir ===" | |
| else | |
| log "=== Result: One or more machine-wide indicators were found. This system may be compromised. ===" | |
| fi | |
| } | |
| main() { | |
| if [[ $# -lt 1 ]]; then | |
| print_usage | |
| exit 1 | |
| fi | |
| local mode="$1" | |
| shift || true | |
| case "$mode" in | |
| repo) | |
| if [[ $# -ne 1 ]]; then | |
| print_usage | |
| exit 1 | |
| fi | |
| scan_repo "$1" | |
| ;; | |
| machine) | |
| if [[ $# -ne 0 ]]; then | |
| print_usage | |
| exit 1 | |
| fi | |
| scan_machine | |
| ;; | |
| -h|--help|help) | |
| print_usage | |
| ;; | |
| *) | |
| log "ERROR: Unknown mode: $mode" | |
| print_usage | |
| exit 1 | |
| ;; | |
| esac | |
| } | |
| main "$@" |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Run this like so: