Skip to content

Instantly share code, notes, and snippets.

@ericallam
Created November 25, 2025 18:48
Show Gist options
  • Select an option

  • Save ericallam/e2ae497d99a21d500fd9fbb01e1bfa02 to your computer and use it in GitHub Desktop.

Select an option

Save ericallam/e2ae497d99a21d500fd9fbb01e1bfa02 to your computer and use it in GitHub Desktop.
Detects indicators of the Shai-Hulud ("bun") npm supply chain malware
#!/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 "$@"
@ericallam
Copy link
Author

Run this like so:

chmod +x ./detect_npm_bun_malware.sh
./detect_npm_bun_malware.sh machine
./detect_npm_bun_malware.sh repo /path/to/repo

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