Skip to content

Instantly share code, notes, and snippets.

@joeskeen
Forked from phxgg/check-npm-cache.sh
Last active September 24, 2025 06:10
Show Gist options
  • Save joeskeen/202fe9f6d7a2f624097962507c5ab681 to your computer and use it in GitHub Desktop.
Save joeskeen/202fe9f6d7a2f624097962507c5ab681 to your computer and use it in GitHub Desktop.
This script will check your npm cache and find if any of the affected packages was pulled in your machine. `chmod +x check-npm-cache.sh` before usage. No dependencies - should work on any system with Bash. (Only tested by me on Linux)

🔍 NPM Compromised Package Scanner (Dependency-Free)

This script scans your system and project directories for known compromised versions of popular NPM packages, as reported by Aikido Security.

It works without installing any dependencies (no jq, awk, sed, or node) and is compatible with projects using npm, Yarn, pnpm, and nvm. It also inspects Dockerfiles, CI configs, and vendored folders for embedded vulnerabilities.


🚨 What It Detects

Package Version
ansi-regex 6.2.1
ansi-styles 6.2.2
backslash 0.2.1
chalk 5.6.1
chalk-template 1.1.1
color-convert 3.1.1
color-name 2.0.1
color-string 2.1.1
debug 4.4.2
error-ex 1.3.3
has-ansi 6.0.1
is-arrayish 0.3.3
proto-tinker-wc 0.1.87
simple-swizzle 0.2.3
slice-ansi 7.1.1
strip-ansi 7.1.1
supports-color 10.2.1
supports-hyperlinks 4.1.1
wrap-ansi 9.0.1

📁 Where It Scans

🔒 Project-Specific (Relative to Current Working Directory)

  • package-lock.json, package.json, yarn.lock, pnpm-lock.yaml
  • Dockerfiles (Dockerfile)
  • CI/CD config files (.github/workflows/*.yml, .gitlab-ci.yml)
  • Vendored folders: vendor/, third_party/, static/, assets/

📦 Global Caches and Install Locations

NPM

  • $HOME/.npm/_cacache
  • $HOME/.npm-packages

Yarn

  • Auto-detected via yarn cache dir
  • Typically ~/.cache/yarn or ~/.yarn/cache

pnpm

  • Auto-detected via pnpm store path
  • Typically ~/.pnpm-store or ~/.local/share/pnpm/store

NVM

  • $NVM_DIR/versions/node/*/lib/node_modules
  • $NVM_DIR/versions/node/*/.npm
  • $NVM_DIR/versions/node/*/node_modules

You can override the NVM path by setting the NVM_DIR environment variable.


✅ How to Use

  1. Save the script as scan-npm.sh
  2. Make it executable:
    chmod +x scan-npm.sh
  3. Run it from your project root or home directory:
    ./scan-npm.sh

🧠 Why This Script?

  • No dependencies — works on minimal systems
  • Cross-manager support — npm, Yarn, pnpm, nvm
  • Fast and portable — uses only POSIX shell and grep
  • Expanded coverage — scans Dockerfiles, CI configs, and vendored folders
  • Quiet but informative — logs progress periodically and summarizes findings

📊 Output

At the end of the scan, you'll see a summary like:

📁 /code/project-a
🚨 Found [email protected] in /code/project-a/package-lock.json
🚨 Found [email protected] in /code/project-a/package-lock.json

📁 /code/project-b
🚨 Found [email protected] in /code/project-b/yarn.lock

🛠️ Suggested Remediation Commands:
💡 cd "/code/project-a" && npm install
💡 cd "/code/project-b" && yarn install

🔢 Total findings: 3
#!/usr/bin/env bash
echo "🔍 Scanning for compromised NPM packages..."
# Define compromised packages and versions
declare -A compromised=(
[ansi-regex]="6.2.1"
[ansi-styles]="6.2.2"
[backslash]="0.2.1"
[chalk]="5.6.1"
[chalk-template]="1.1.1"
[color-convert]="3.1.1"
[color-name]="2.0.1"
[color-string]="2.1.1"
[debug]="4.4.2"
[error-ex]="1.3.3"
[has-ansi]="6.0.1"
[is-arrayish]="0.3.3"
[proto-tinker-wc]="0.1.87"
[simple-swizzle]="0.2.3"
[slice-ansi]="7.1.1"
[strip-ansi]="7.1.1"
[supports-color]="10.2.1"
[supports-hyperlinks]="4.1.1"
[wrap-ansi]="9.0.1"
)
declare -a findings=()
scan_file() {
local file="$1"
for pkg in "${!compromised[@]}"; do
version="${compromised[$pkg]}"
if grep -q "$pkg" "$file" && grep -q "$version" "$file"; then
findings+=("Found $pkg@$version in $file")
fi
done
}
scan_npm_cache() {
local cache_dir="$1"
for pkg in "${!compromised[@]}"; do
version="${compromised[$pkg]}"
while IFS= read -r match; do
findings+=("Found $pkg@$version in cached file: $match")
done < <(find "$cache_dir" -type f -exec grep -l "$pkg@$version" {} + 2>/dev/null)
done
}
scan_nvm_versions() {
local nvm_dir="${NVM_DIR:-$HOME/.nvm}"
if [ ! -d "$nvm_dir" ]; then return; fi
echo "🧠 Scanning NVM-managed Node versions..."
count=0
find "$nvm_dir/versions/node" -type d \( -name "node_modules" -o -name ".npm" \) | while read -r dir; do
scan_npm_cache "$dir"
count=$((count + 1))
if (( count % 5 == 0 )); then
echo " ...scanned $count NVM directories"
fi
done
}
scan_dockerfiles() {
echo "🐳 Scanning Dockerfiles..."
count=0
while IFS= read -r file; do
scan_file "$file"
count=$((count + 1))
if (( count % 5 == 0 )); then
echo " ...scanned $count Dockerfiles"
fi
done < <(find . -type f -iname "Dockerfile")
}
scan_ci_configs() {
echo "⚙️ Scanning CI/CD config files..."
count=0
while IFS= read -r file; do
scan_file "$file"
count=$((count + 1))
if (( count % 5 == 0 )); then
echo " ...scanned $count CI config files"
fi
done < <(find . -type f \( -name "*.yml" -o -name "*.yaml" \) -path "*/.github/*" -o -path "*/.gitlab/*")
}
scan_vendored_dirs() {
echo "📁 Scanning vendored folders..."
for dir in vendor third_party static assets; do
[ -d "$dir" ] || continue
while IFS= read -r file; do
scan_file "$file"
done < <(find "$dir" -type f \( -name "*.js" -o -name "*.json" -o -name "*.tgz" \))
done
}
scan_lockfile_resolved() {
local file="$1"
for pkg in "${!compromised[@]}"; do
version="${compromised[$pkg]}"
# Match resolved tarball URLs for compromised versions
if grep -q "$pkg/-/$pkg-$version.tgz" "$file"; then
findings+=("Resolved $pkg@$version in $file")
fi
done
}
echo "🔒 Scanning project lockfiles and package.json..."
count=0
while IFS= read -r file; do
scan_lockfile_resolved "$file" # new accurate match
count=$((count + 1))
if (( count % 10 == 0 )); then
echo " ...scanned $count files"
fi
done < <(find . -type f \( -name "package-lock.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" \))
# Global caches
echo "📦 Scanning global npm caches..."
[ -d "$HOME/.npm/_cacache" ] && scan_npm_cache "$HOME/.npm/_cacache"
[ -d "$HOME/.npm-packages" ] && scan_npm_cache "$HOME/.npm-packages"
echo "📦 Scanning Yarn global cache..."
if command -v yarn >/dev/null 2>&1; then
yarn_cache=$(yarn cache dir 2>/dev/null)
[ -n "$yarn_cache" ] && [ -d "$yarn_cache" ] && scan_npm_cache "$yarn_cache"
fi
echo "📦 Scanning pnpm global store..."
if command -v pnpm >/dev/null 2>&1; then
pnpm_cache=$(pnpm store path 2>/dev/null)
[ -n "$pnpm_cache" ] && [ -d "$pnpm_cache" ] && scan_npm_cache "$pnpm_cache"
fi
scan_nvm_versions
scan_dockerfiles
scan_ci_configs
scan_vendored_dirs
echo ""
echo "📊 Summary of Findings:"
if [ ${#findings[@]} -eq 0 ]; then
echo "✅ No compromised packages found."
else
declare -A grouped
declare -A remediation
# Group findings by remediation directory
for line in "${findings[@]}"; do
file=$(echo "$line" | awk -F'in ' '{print $2}')
dir=$(dirname "$file")
# Trim path before node_modules if present
if [[ "$dir" == *"/node_modules/"* ]]; then
dir="${dir%%/node_modules/*}"
elif [[ "$dir" == *"/node_modules" ]]; then
dir="${dir%/node_modules}"
fi
# Walk up to find the remediation root
while [ "$dir" != "/" ]; do
if [ -f "$dir/package-lock.json" ]; then
remediation["$dir"]="npm"
break
elif [ -f "$dir/yarn.lock" ]; then
remediation["$dir"]="yarn"
break
elif [ -f "$dir/pnpm-lock.yaml" ]; then
remediation["$dir"]="pnpm"
break
fi
dir=$(dirname "$dir")
done
grouped["$dir"]+="$line"$'\n'
done
# Print grouped findings
for dir in "${!grouped[@]}"; do
echo "📁 $dir"
echo "${grouped[$dir]}"
done
echo ""
echo "🛠️ Suggested Remediation Commands:"
for dir in "${!remediation[@]}"; do
tool="${remediation[$dir]}"
echo "💡 cd \"$dir\" && rm -rf node_modules ${tool}-lock.yaml yarn.lock package-lock.json && $tool install"
done
fi
@joeskeen
Copy link
Author

joeskeen commented Sep 9, 2025

I ran this on my /code/ directory, under which I have all my cloned repositories...

It found 235 problems! 😱

Edit: it turned out all of those issues were false positives - packages were requesting the bad version, but since those versions were unpublished by npm, they did not resolve to that version, but the good version instead. So I updated my script to find only resolved packages that were bad.

@djt-auditor
Copy link

Hi, thanks for sharing this so quickly! Just a note for macOS users: the default system Bash is too old and doesn’t support declare -A. You’ll need Bash 4.x or higher from Homebrew, and make sure the shebang points to that newer Bash or that your environment is set up to use it so the script runs correctly.

@qsniyg
Copy link

qsniyg commented Sep 17, 2025

@alexkli
Copy link

alexkli commented Sep 20, 2025

Thanks for sharing this! Cursor and me rewrote this into a Node.js version that is much faster and can deal with the huge list of the Shai Hulud attack: https://gist.github.com/alexkli/47435174f5bdfa92fe66803cb8d463e7

@alexkli
Copy link

alexkli commented Sep 23, 2025

@joeskeen Note your script has a bug (that I inadvertently replicated in my version too) for scoped packages like @art-ws/di-node and would never find them in lockfiles.

I know the qix attack you included doesn't have any scoped packages, but the shai hulud has so putting it out there just in case...

The problem is that scoped packages have tgz urls where the filename only includes the "local" name, not the @scope part:

https://registry.npmjs.org/@art-ws/di-node/-/di-node-2.2.0.tgz

Hence this check in scan_lockfile_resolved is not working for scoped packages:

    # Match resolved tarball URLs for compromised versions
    if grep -q "$pkg/-/$pkg-$version.tgz" "$file"; then

and has to be changed to something like this (haven't tested this well):

    # Match resolved tarball URLs for compromised versions
    pkgLocalName=$(basename "$pkg")
    if grep -q "$pkg/-/$pkgLocalName-$version.tgz" "$file"; then

HTH

@alexkli
Copy link

alexkli commented Sep 23, 2025

Also the current list in this gist for the qix is outdated, here is an updated one with more dependencies (including scoped ones): https://snyk.io/blog/npm-supply-chain-attack-via-open-source-maintainer-compromise/

@djt-auditor
Copy link

@alexkli for the next npm

scan_lockfile_resolved() {
  local file="$1"
  for pkg in "${!compromised[@]}"; do
    version="${compromised[$pkg]}"
    # Match resolved tarball URLs for compromised versions
    if [[ $pkg == @* ]]; then
      if grep -q "$pkg/-/${pkg##*/}-$version.tgz" "$file"; then
        findings+=("Resolved $pkg@$version in $file")
      fi
    elif grep -q "$pkg/-/$pkg-$version.tgz" "$file"; then
      findings+=("Resolved $pkg@$version in $file")
    fi
  done
}

or for this one look the
rg -uu --max-columns=80 --glob '*.js' _0x112fa8

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