Skip to content

Instantly share code, notes, and snippets.

@rkalkani
Created April 2, 2026 06:01
Show Gist options
  • Select an option

  • Save rkalkani/00f519cf5df7b1242f49b5abf6dbc6d1 to your computer and use it in GitHub Desktop.

Select an option

Save rkalkani/00f519cf5df7b1242f49b5abf6dbc6d1 to your computer and use it in GitHub Desktop.
Scans project subdirectories AND global package manager caches/stores for compromised axios versions (SNYK-JS-AXIOS-15850650)
#!/usr/bin/env bash
# =============================================================================
# check_axios_compromise.sh
# Scans project subdirectories AND global package manager caches/stores for
# compromised axios versions (SNYK-JS-AXIOS-15850650)
#
# Affected versions : axios@1.14.1 | axios@0.30.4
# Malicious dep : plain-crypto-js@4.2.1
# Supported PMs : npm · yarn (v1 classic & v2/v3 Berry) · pnpm · bun
# Node version mgrs : nvm · fnm
# Compatible with : macOS (10.15+) and Linux
#
# Usage:
# ./check_axios_compromise.sh # scans current directory (e.g. $HOME)
# ./check_axios_compromise.sh /path # scans a specific directory
#
# Reference: https://snyk.io/blog/axios-npm-package-compromised-supply-chain-attack-delivers-cross-platform/
# =============================================================================
set -euo pipefail
# ── Colours ───────────────────────────────────────────────────────────────────
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'
CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m'
# ── Constants ─────────────────────────────────────────────────────────────────
readonly BAD_AXIOS_VERSIONS=("1.14.1" "0.30.4")
readonly MALICIOUS_DEP="plain-crypto-js"
readonly MALICIOUS_DEP_VER="4.2.1"
readonly C2_IP="142.11.206.73"
FOUND_ISSUES=0
SCANNED_PROJECTS=0
# ── macOS-safe realpath ───────────────────────────────────────────────────────
# macOS ships without GNU coreutils realpath by default
safe_realpath() {
python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$1" 2>/dev/null \
|| { cd "$1" 2>/dev/null && pwd; }
}
# ── OS detection ──────────────────────────────────────────────────────────────
IS_MACOS=false
[[ "$(uname -s)" == "Darwin" ]] && IS_MACOS=true
# =============================================================================
# Python helper — parse lockfiles in a single pass (avoids spawning python3
# once per version per format; also handles deeply-nested transitive deps)
# =============================================================================
# Writes findings to stdout, one line each:
# AXIOS_HIT <format> <version>
# MAL_HIT <package>@<version>
parse_npm_lockfile() {
local lock_path="$1"
python3 - "$lock_path" << 'PYEOF'
import json, sys
BAD = {"1.14.1", "0.30.4"}
MALDEP = "plain-crypto-js"
MALVER = {"4.2.1"}
try:
with open(sys.argv[1]) as f:
d = json.load(f)
except Exception:
sys.exit(0)
# ── npm v2/v3: packages{} ─────────────────────────────────────────────────────
packages = d.get("packages", {})
ax_v2 = packages.get("node_modules/axios", {}).get("version", "")
if ax_v2 in BAD:
print(f"AXIOS_HIT npm-v2/v3-packages {ax_v2}")
for key, meta in packages.items():
if ("/" + MALDEP + "@") in key or key.endswith("/" + MALDEP):
v = meta.get("version", "")
if v in MALVER:
print(f"MAL_HIT {MALDEP}@{v} (packages)")
# ── npm v1: dependencies{} (recursive) ────────────────────────────────────────
def walk_deps(deps, depth=0):
for name, meta in deps.items():
ver = meta.get("version", "")
if name == "axios" and ver in BAD:
print(f"AXIOS_HIT npm-v1-dependencies {ver}")
if name == MALDEP and ver in MALVER:
print(f"MAL_HIT {MALDEP}@{ver} (dependencies)")
if "dependencies" in meta:
walk_deps(meta["dependencies"], depth + 1)
walk_deps(d.get("dependencies", {}))
PYEOF
}
# =============================================================================
# SECTION 1 — Per-project scan
# =============================================================================
check_project() {
local dir="$1"
local issues=0
# ── package.json ─────────────────────────────────────────────────────────────
local pkg="$dir/package.json"
if [[ -f "$pkg" ]]; then
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
# Matches "axios": "1.14.1" / "~1.14.1" / "^1.14.1" etc.
if grep -qE "\"axios\"[[:space:]]*:[[:space:]]*\"[^\"]*${ver}[^\"]*\"" "$pkg" 2>/dev/null; then
echo -e " ${RED}[VULN]${RESET} package.json declares axios ${BOLD}${ver}${RESET}"
((issues++)) || true
fi
done
if grep -q "\"${MALICIOUS_DEP}\"" "$pkg" 2>/dev/null; then
echo -e " ${RED}[VULN]${RESET} package.json references ${BOLD}${MALICIOUS_DEP}${RESET} (malicious dep)"
((issues++)) || true
fi
fi
# ── package-lock.json (npm v1 / v2 / v3) — single python3 pass ───────────────
local npm_lock="$dir/package-lock.json"
if [[ -f "$npm_lock" ]]; then
while IFS= read -r line; do
case "$line" in
AXIOS_HIT*)
fmt="${line#AXIOS_HIT }"; fmt="${fmt% *}"
ver="${line##* }"
echo -e " ${RED}[VULN]${RESET} package-lock.json (${fmt}) → axios ${BOLD}${ver}${RESET}"
((issues++)) || true
;;
MAL_HIT*)
info="${line#MAL_HIT }"
echo -e " ${RED}[VULN]${RESET} package-lock.json contains ${BOLD}${info}${RESET}"
((issues++)) || true
;;
esac
done < <(parse_npm_lockfile "$npm_lock")
fi
# ── yarn.lock — detect version once, then check ───────────────────────────────
local yarn_lock="$dir/yarn.lock"
if [[ -f "$yarn_lock" ]]; then
# Read only first 10 lines to detect Berry vs classic (fast)
local yarn_header
yarn_header=$(head -10 "$yarn_lock" 2>/dev/null)
if echo "$yarn_header" | grep -q "__metadata"; then
# ── Yarn v2/v3 Berry: entry looks like "axios@npm:1.14.1":
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
if grep -qE "^\"?axios@npm:${ver}" "$yarn_lock" 2>/dev/null; then
echo -e " ${RED}[VULN]${RESET} yarn.lock (Berry v2/v3) → axios ${BOLD}${ver}${RESET}"
((issues++)) || true
fi
done
if grep -qE "\"?${MALICIOUS_DEP}@npm:" "$yarn_lock" 2>/dev/null; then
echo -e " ${RED}[VULN]${RESET} yarn.lock (Berry) contains ${BOLD}${MALICIOUS_DEP}${RESET}"
((issues++)) || true
fi
else
# ── Yarn v1 classic: block format, version line follows header
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
if grep -A5 -E "^axios@|^\"axios@" "$yarn_lock" 2>/dev/null \
| grep -qE "^[[:space:]]+version[[:space:]]+\"${ver}\""; then
echo -e " ${RED}[VULN]${RESET} yarn.lock (v1 classic) → axios ${BOLD}${ver}${RESET}"
((issues++)) || true
fi
done
if grep -qE "^${MALICIOUS_DEP}@|^\"${MALICIOUS_DEP}@" "$yarn_lock" 2>/dev/null; then
echo -e " ${RED}[VULN]${RESET} yarn.lock (v1) contains ${BOLD}${MALICIOUS_DEP}${RESET}"
((issues++)) || true
fi
fi
fi
# ── pnpm-lock.yaml ────────────────────────────────────────────────────────────
local pnpm_lock="$dir/pnpm-lock.yaml"
if [[ -f "$pnpm_lock" ]]; then
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
# lockfileVersion 6+: /axios@1.14.1: or axios@1.14.1:
# lockfileVersion 5 : /axios/1.14.1:
if grep -qE "^[[:space:]]+(/?axios[@/]${ver}[_:'\" ]|/?axios[@/]${ver}$)" "$pnpm_lock" 2>/dev/null; then
echo -e " ${RED}[VULN]${RESET} pnpm-lock.yaml → axios ${BOLD}${ver}${RESET}"
((issues++)) || true
fi
done
if grep -q "${MALICIOUS_DEP}" "$pnpm_lock" 2>/dev/null; then
echo -e " ${RED}[VULN]${RESET} pnpm-lock.yaml contains ${BOLD}${MALICIOUS_DEP}${RESET}"
((issues++)) || true
fi
fi
# ── bun.lock (text, Bun ≥ v1.1) ──────────────────────────────────────────────
local bun_lock="$dir/bun.lock"
if [[ -f "$bun_lock" ]]; then
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
if grep -qE "\"axios@[^\"]*${ver}" "$bun_lock" 2>/dev/null; then
echo -e " ${RED}[VULN]${RESET} bun.lock → axios ${BOLD}${ver}${RESET}"
((issues++)) || true
fi
done
if grep -q "\"${MALICIOUS_DEP}@" "$bun_lock" 2>/dev/null; then
echo -e " ${RED}[VULN]${RESET} bun.lock contains ${BOLD}${MALICIOUS_DEP}${RESET}"
((issues++)) || true
fi
fi
# ── node_modules — flat layout (npm / yarn v1 / bun) ─────────────────────────
local axios_nm="$dir/node_modules/axios/package.json"
if [[ -f "$axios_nm" ]]; then
local inst_ver
inst_ver=$(python3 -c "import json; print(json.load(open('$axios_nm')).get('version',''))" 2>/dev/null || true)
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
if [[ "$inst_ver" == "$ver" ]]; then
echo -e " ${RED}[CRITICAL]${RESET} node_modules/axios INSTALLED at ${BOLD}${ver}${RESET} — machine may be compromised!"
((issues++)) || true
fi
done
fi
if [[ -d "$dir/node_modules/${MALICIOUS_DEP}" ]]; then
echo -e " ${RED}[CRITICAL]${RESET} node_modules/${MALICIOUS_DEP} PRESENT — malicious dep is installed!"
((issues++)) || true
fi
# ── node_modules/.pnpm — pnpm virtual store ───────────────────────────────────
# Layout: node_modules/.pnpm/axios@<ver>[_<hash>]/node_modules/axios/package.json
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
local pnpm_virtual
for pnpm_virtual in \
"$dir/node_modules/.pnpm/axios@${ver}/node_modules/axios/package.json" \
"$dir/node_modules/.pnpm/axios@${ver}_"*/node_modules/axios/package.json; do
[[ -f "$pnpm_virtual" ]] || continue
echo -e " ${RED}[CRITICAL]${RESET} pnpm virtual store → axios ${BOLD}${ver}${RESET}"
echo -e " ${DIM}${pnpm_virtual}${RESET}"
((issues++)) || true
done
done
for pnpm_mal in "$dir/node_modules/.pnpm/${MALICIOUS_DEP}"*/; do
[[ -d "$pnpm_mal" ]] || continue
echo -e " ${RED}[CRITICAL]${RESET} pnpm virtual store contains ${BOLD}${MALICIOUS_DEP}${RESET}!"
((issues++)) || true
done
echo "$issues"
}
# =============================================================================
# SECTION 2 — Global cache / store scans
# =============================================================================
# ── npm ───────────────────────────────────────────────────────────────────────
check_npm_global_cache() {
echo -e "${BOLD}${CYAN}── npm: cache & global install ─────────────────────────────────────────${RESET}"
local found=0
# Cache directory
local npm_cache=""
command -v npm &>/dev/null && npm_cache=$(npm config get cache 2>/dev/null || true)
[[ -z "$npm_cache" || "$npm_cache" == "undefined" ]] && npm_cache="$HOME/.npm"
echo -e " ${DIM}cache : ${npm_cache}${RESET}"
if [[ -d "$npm_cache" ]]; then
# npm ≤ 4 legacy layout: <cache>/axios/<version>/
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
[[ -d "$npm_cache/axios/$ver" ]] || continue
echo -e " ${RED}[VULN]${RESET} npm legacy cache entry: axios ${BOLD}${ver}${RESET}"
echo -e " ${DIM}${npm_cache}/axios/${ver}${RESET}"
((found++)) || true
done
# npm 5+ content-addressed index: _cacache/index-v5/**
# Only grep *.json files; stop at 5 hits to avoid huge output
local index_dir="$npm_cache/_cacache/index-v5"
if [[ -d "$index_dir" ]]; then
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
local hits
hits=$(grep -rl --include="*" "\"axios\"" "$index_dir" 2>/dev/null \
| xargs grep -l "\"${ver}\"" 2>/dev/null | head -5 || true)
if [[ -n "$hits" ]]; then
echo -e " ${RED}[VULN]${RESET} npm cache index entry for axios ${BOLD}${ver}${RESET}:"
while IFS= read -r f; do echo -e " ${DIM}${f}${RESET}"; done <<< "$hits"
((found++)) || true
fi
done
# Malicious dep
local mal_hits
mal_hits=$(grep -rl --include="*" "\"${MALICIOUS_DEP}\"" "$index_dir" 2>/dev/null | head -3 || true)
if [[ -n "$mal_hits" ]]; then
echo -e " ${RED}[VULN]${RESET} npm cache index entry for ${BOLD}${MALICIOUS_DEP}${RESET}:"
while IFS= read -r f; do echo -e " ${DIM}${f}${RESET}"; done <<< "$mal_hits"
((found++)) || true
fi
fi
else
echo -e " ${YELLOW}[SKIP]${RESET} cache dir not found."
fi
# npm global installs
local npm_global=""
command -v npm &>/dev/null && npm_global=$(npm root -g 2>/dev/null || true)
[[ -z "$npm_global" ]] && npm_global="/usr/local/lib/node_modules"
echo -e " ${DIM}global: ${npm_global}${RESET}"
if [[ -f "$npm_global/axios/package.json" ]]; then
local gv
gv=$(python3 -c "import json; print(json.load(open('$npm_global/axios/package.json')).get('version',''))" 2>/dev/null || true)
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
[[ "$gv" == "$ver" ]] || continue
echo -e " ${RED}[CRITICAL]${RESET} npm global axios INSTALLED at ${BOLD}${ver}${RESET}"
((found++)) || true
done
fi
[[ $found -eq 0 ]] && echo -e " ${GREEN}[OK]${RESET} clean."
FOUND_ISSUES=$((FOUND_ISSUES + found))
}
# ── yarn ──────────────────────────────────────────────────────────────────────
check_yarn_global_cache() {
echo -e "${BOLD}${CYAN}── yarn: cache & global install ────────────────────────────────────────${RESET}"
local found=0
# ── Yarn v1 classic ──────────────────────────────────────────────────────────
local yarn_v1_cache=""
command -v yarn &>/dev/null && yarn_v1_cache=$(yarn cache dir 2>/dev/null || true)
[[ -z "$yarn_v1_cache" ]] && yarn_v1_cache="$HOME/.yarn/cache"
echo -e " ${DIM}yarn v1 cache: ${yarn_v1_cache}${RESET}"
if [[ -d "$yarn_v1_cache" ]]; then
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
# Yarn v1 tarball names: npm-axios-<ver>-<hash>.tgz (maxdepth 2 is sufficient)
local tarballs
tarballs=$(find "$yarn_v1_cache" -maxdepth 2 \
-name "npm-axios-${ver}-*.tgz" 2>/dev/null || true)
if [[ -n "$tarballs" ]]; then
echo -e " ${RED}[VULN]${RESET} Yarn v1 cache tarball for axios ${BOLD}${ver}${RESET}:"
while IFS= read -r t; do echo -e " ${DIM}${t}${RESET}"; done <<< "$tarballs"
((found++)) || true
fi
done
local mal_tarballs
mal_tarballs=$(find "$yarn_v1_cache" -maxdepth 2 \
-name "npm-${MALICIOUS_DEP}-*.tgz" 2>/dev/null || true)
if [[ -n "$mal_tarballs" ]]; then
echo -e " ${RED}[VULN]${RESET} Yarn v1 cache tarball for ${BOLD}${MALICIOUS_DEP}${RESET}:"
while IFS= read -r t; do echo -e " ${DIM}${t}${RESET}"; done <<< "$mal_tarballs"
((found++)) || true
fi
else
echo -e " ${YELLOW}[SKIP]${RESET} Yarn v1 cache not found."
fi
# Yarn v1 global installs
local yarn_global_dir=""
command -v yarn &>/dev/null && yarn_global_dir=$(yarn global dir 2>/dev/null || true)
if [[ -n "$yarn_global_dir" && -f "$yarn_global_dir/node_modules/axios/package.json" ]]; then
local gv
gv=$(python3 -c "import json; print(json.load(open('$yarn_global_dir/node_modules/axios/package.json')).get('version',''))" 2>/dev/null || true)
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
[[ "$gv" == "$ver" ]] || continue
echo -e " ${RED}[CRITICAL]${RESET} Yarn v1 global axios INSTALLED at ${BOLD}${ver}${RESET}"
((found++)) || true
done
fi
# ── Yarn v2/v3 Berry zip cache ────────────────────────────────────────────────
# Default locations differ by OS; check both
local berry_caches=()
if $IS_MACOS; then
berry_caches+=("$HOME/Library/Caches/yarn" "$HOME/.yarn/berry/cache")
else
berry_caches+=("${XDG_CACHE_HOME:-$HOME/.cache}/yarn" "$HOME/.yarn/berry/cache")
fi
for berry_cache in "${berry_caches[@]}"; do
[[ -d "$berry_cache" ]] || continue
echo -e " ${DIM}Yarn Berry cache: ${berry_cache}${RESET}"
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
# Berry zip name: axios-npm-<ver>-<hash>-<checksum>.zip
local zips
zips=$(find "$berry_cache" -maxdepth 2 \
-name "axios-npm-${ver}-*.zip" 2>/dev/null || true)
if [[ -n "$zips" ]]; then
echo -e " ${RED}[VULN]${RESET} Yarn Berry cache zip for axios ${BOLD}${ver}${RESET}:"
while IFS= read -r z; do echo -e " ${DIM}${z}${RESET}"; done <<< "$zips"
((found++)) || true
fi
done
local mal_zips
mal_zips=$(find "$berry_cache" -maxdepth 2 \
-name "${MALICIOUS_DEP}-npm-*.zip" 2>/dev/null || true)
if [[ -n "$mal_zips" ]]; then
echo -e " ${RED}[VULN]${RESET} Yarn Berry cache zip for ${BOLD}${MALICIOUS_DEP}${RESET}:"
while IFS= read -r z; do echo -e " ${DIM}${z}${RESET}"; done <<< "$zips"
((found++)) || true
fi
done
[[ $found -eq 0 ]] && echo -e " ${GREEN}[OK]${RESET} clean."
FOUND_ISSUES=$((FOUND_ISSUES + found))
}
# ── pnpm ──────────────────────────────────────────────────────────────────────
check_pnpm_global_cache() {
echo -e "${BOLD}${CYAN}── pnpm: store & global install ────────────────────────────────────────${RESET}"
local found=0
# pnpm content-addressable store
local pnpm_store=""
command -v pnpm &>/dev/null && pnpm_store=$(pnpm store path 2>/dev/null || true)
if [[ -z "$pnpm_store" ]]; then
if $IS_MACOS; then
pnpm_store="$HOME/Library/pnpm/store/v3"
else
pnpm_store="${XDG_DATA_HOME:-$HOME/.local/share}/pnpm/store/v3"
# Older pnpm fallback
[[ -d "$pnpm_store" ]] || pnpm_store="$HOME/.pnpm-store"
fi
fi
echo -e " ${DIM}store : ${pnpm_store}${RESET}"
if [[ -d "$pnpm_store" ]]; then
# pnpm v3 store: small JSON metadata files sit alongside content blobs.
# grep only files likely to be metadata (no extension or .json) and cap results.
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
local meta_hits
meta_hits=$(grep -rl "\"version\":\"${ver}\"" "$pnpm_store" 2>/dev/null \
| xargs -I{} grep -l "\"name\":\"axios\"" {} 2>/dev/null \
| head -5 || true)
if [[ -n "$meta_hits" ]]; then
echo -e " ${RED}[VULN]${RESET} pnpm store metadata for axios ${BOLD}${ver}${RESET}:"
while IFS= read -r f; do echo -e " ${DIM}${f}${RESET}"; done <<< "$meta_hits"
((found++)) || true
fi
done
local mal_meta
mal_meta=$(grep -rl "\"name\":\"${MALICIOUS_DEP}\"" "$pnpm_store" 2>/dev/null | head -3 || true)
if [[ -n "$mal_meta" ]]; then
echo -e " ${RED}[VULN]${RESET} pnpm store metadata for ${BOLD}${MALICIOUS_DEP}${RESET}:"
while IFS= read -r f; do echo -e " ${DIM}${f}${RESET}"; done <<< "$mal_meta"
((found++)) || true
fi
else
echo -e " ${YELLOW}[SKIP]${RESET} pnpm store not found."
fi
# pnpm global installs
local pnpm_global=""
command -v pnpm &>/dev/null && pnpm_global=$(pnpm root -g 2>/dev/null || true)
if [[ -z "$pnpm_global" ]]; then
# pnpm global store version number can vary (4, 5, 6); try a glob
for d in "${XDG_DATA_HOME:-$HOME/.local/share}/pnpm/global"/*/node_modules \
"$HOME/.local/share/pnpm/global"/*/node_modules; do
[[ -d "$d" ]] && pnpm_global="$d" && break
done
fi
if [[ -n "$pnpm_global" ]]; then
echo -e " ${DIM}global: ${pnpm_global}${RESET}"
if [[ -f "$pnpm_global/axios/package.json" ]]; then
local gv
gv=$(python3 -c "import json; print(json.load(open('$pnpm_global/axios/package.json')).get('version',''))" 2>/dev/null || true)
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
[[ "$gv" == "$ver" ]] || continue
echo -e " ${RED}[CRITICAL]${RESET} pnpm global axios INSTALLED at ${BOLD}${ver}${RESET}"
((found++)) || true
done
fi
fi
[[ $found -eq 0 ]] && echo -e " ${GREEN}[OK]${RESET} clean."
FOUND_ISSUES=$((FOUND_ISSUES + found))
}
# ── bun ───────────────────────────────────────────────────────────────────────
check_bun_global_cache() {
echo -e "${BOLD}${CYAN}── bun: install cache ──────────────────────────────────────────────────${RESET}"
local found=0
local bun_cache=""
if command -v bun &>/dev/null; then
bun_cache=$(bun pm cache 2>/dev/null | awk '{print $NF}' || true)
fi
if [[ -z "$bun_cache" || ! -d "$bun_cache" ]]; then
if $IS_MACOS; then
bun_cache="$HOME/Library/Caches/bun/install/cache"
else
bun_cache="${XDG_CACHE_HOME:-$HOME/.cache}/bun/install/cache"
fi
fi
echo -e " ${DIM}cache : ${bun_cache}${RESET}"
if [[ -d "$bun_cache" ]]; then
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
# Bun 1.x cache layout: <cache>/axios@<ver>/ (directory)
local bun_dir="$bun_cache/axios@${ver}"
if [[ -d "$bun_dir" ]]; then
echo -e " ${RED}[VULN]${RESET} Bun cache dir for axios ${BOLD}${ver}${RESET}:"
echo -e " ${DIM}${bun_dir}${RESET}"
((found++)) || true
fi
done
# Malicious dep
for bun_mal in "$bun_cache/${MALICIOUS_DEP}"*/; do
[[ -d "$bun_mal" ]] || continue
echo -e " ${RED}[VULN]${RESET} Bun cache dir for ${BOLD}${MALICIOUS_DEP}${RESET}:"
echo -e " ${DIM}${bun_mal}${RESET}"
((found++)) || true
done
else
echo -e " ${YELLOW}[SKIP]${RESET} Bun cache not found."
fi
[[ $found -eq 0 ]] && echo -e " ${GREEN}[OK]${RESET} clean."
FOUND_ISSUES=$((FOUND_ISSUES + found))
}
# ── nvm ───────────────────────────────────────────────────────────────────────
# nvm installs Node versions under $NVM_DIR (default: $HOME/.nvm).
# Each version has its own npm prefix:
# $NVM_DIR/versions/node/<ver>/lib/node_modules/
# npm caches are typically shared at $HOME/.npm, already covered above.
# Here we scan for axios in every per-version global node_modules.
check_nvm_installs() {
echo -e "${BOLD}${CYAN}── nvm: per-version global node_modules ────────────────────────────────${RESET}"
local found=0
local nvm_dir="${NVM_DIR:-$HOME/.nvm}"
local versions_dir="$nvm_dir/versions/node"
if [[ ! -d "$versions_dir" ]]; then
echo -e " ${YELLOW}[SKIP]${RESET} nvm versions dir not found (${versions_dir})."
FOUND_ISSUES=$((FOUND_ISSUES + found))
return
fi
echo -e " ${DIM}nvm dir: ${nvm_dir}${RESET}"
local checked=0
for node_ver_dir in "$versions_dir"/*/; do
[[ -d "$node_ver_dir" ]] || continue
local node_ver
node_ver="$(basename "$node_ver_dir")"
local global_nm="$node_ver_dir/lib/node_modules"
[[ -d "$global_nm" ]] || continue
((checked++)) || true
# axios global install for this Node version
local axios_pkg="$global_nm/axios/package.json"
if [[ -f "$axios_pkg" ]]; then
local inst_ver
inst_ver=$(python3 -c "import json; print(json.load(open('$axios_pkg')).get('version',''))" 2>/dev/null || true)
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
if [[ "$inst_ver" == "$ver" ]]; then
echo -e " ${RED}[CRITICAL]${RESET} nvm Node ${BOLD}${node_ver}${RESET} global axios INSTALLED at ${BOLD}${ver}${RESET}"
echo -e " ${DIM}${axios_pkg}${RESET}"
((found++)) || true
fi
done
fi
# Malicious dep global install for this Node version
if [[ -d "$global_nm/${MALICIOUS_DEP}" ]]; then
echo -e " ${RED}[CRITICAL]${RESET} nvm Node ${BOLD}${node_ver}${RESET} global node_modules contains ${BOLD}${MALICIOUS_DEP}${RESET}!"
((found++)) || true
fi
done
if [[ $checked -eq 0 ]]; then
echo -e " ${YELLOW}[SKIP]${RESET} No nvm-managed Node versions found."
elif [[ $found -eq 0 ]]; then
echo -e " ${GREEN}[OK]${RESET} clean (checked ${checked} Node version(s))."
fi
FOUND_ISSUES=$((FOUND_ISSUES + found))
}
# ── fnm ───────────────────────────────────────────────────────────────────────
# fnm (Fast Node Manager) stores versions under:
# macOS/Linux : $FNM_DIR (default: $HOME/Library/Application Support/fnm on macOS,
# $HOME/.local/share/fnm on Linux)
# Each version layout: <fnm_dir>/node-versions/<ver>/installation/lib/node_modules/
# npm caches are shared (covered by check_npm_global_cache), so we only scan
# per-version global node_modules here.
check_fnm_installs() {
echo -e "${BOLD}${CYAN}── fnm: per-version global node_modules ────────────────────────────────${RESET}"
local found=0
# Resolve fnm data directory
local fnm_dir="${FNM_DIR:-}"
if [[ -z "$fnm_dir" ]]; then
if $IS_MACOS; then
fnm_dir="$HOME/Library/Application Support/fnm"
else
fnm_dir="${XDG_DATA_HOME:-$HOME/.local/share}/fnm"
fi
fi
local versions_dir="$fnm_dir/node-versions"
if [[ ! -d "$versions_dir" ]]; then
echo -e " ${YELLOW}[SKIP]${RESET} fnm versions dir not found (${versions_dir})."
FOUND_ISSUES=$((FOUND_ISSUES + found))
return
fi
echo -e " ${DIM}fnm dir: ${fnm_dir}${RESET}"
local checked=0
for node_ver_dir in "$versions_dir"/*/; do
[[ -d "$node_ver_dir" ]] || continue
local node_ver
node_ver="$(basename "$node_ver_dir")"
# fnm layout: <ver>/installation/lib/node_modules
local global_nm="$node_ver_dir/installation/lib/node_modules"
[[ -d "$global_nm" ]] || continue
((checked++)) || true
# axios global install for this Node version
local axios_pkg="$global_nm/axios/package.json"
if [[ -f "$axios_pkg" ]]; then
local inst_ver
inst_ver=$(python3 -c "import json; print(json.load(open('$axios_pkg')).get('version',''))" 2>/dev/null || true)
for ver in "${BAD_AXIOS_VERSIONS[@]}"; do
if [[ "$inst_ver" == "$ver" ]]; then
echo -e " ${RED}[CRITICAL]${RESET} fnm Node ${BOLD}${node_ver}${RESET} global axios INSTALLED at ${BOLD}${ver}${RESET}"
echo -e " ${DIM}${axios_pkg}${RESET}"
((found++)) || true
fi
done
fi
# Malicious dep global install for this Node version
if [[ -d "$global_nm/${MALICIOUS_DEP}" ]]; then
echo -e " ${RED}[CRITICAL]${RESET} fnm Node ${BOLD}${node_ver}${RESET} global node_modules contains ${BOLD}${MALICIOUS_DEP}${RESET}!"
((found++)) || true
fi
done
if [[ $checked -eq 0 ]]; then
echo -e " ${YELLOW}[SKIP]${RESET} No fnm-managed Node versions found."
elif [[ $found -eq 0 ]]; then
echo -e " ${GREEN}[OK]${RESET} clean (checked ${checked} Node version(s))."
fi
FOUND_ISSUES=$((FOUND_ISSUES + found))
}
# =============================================================================
# SECTION 3 — System IOC check
# =============================================================================
check_iocs() {
echo -e "${BOLD}${CYAN}── System-level IOC artifacts ──────────────────────────────────────────${RESET}"
local found=0
# Known RAT drop paths
local ioc_paths=(
"/Library/Caches/com.apple.act.mond" # macOS system-wide RAT binary
"$HOME/Library/Caches/com.apple.act.mond" # macOS per-user RAT binary
"/tmp/ld.py" # Linux Python RAT script
)
for ioc in "${ioc_paths[@]}"; do
[[ -e "$ioc" ]] || continue
echo -e " ${RED}[IOC]${RESET} Found RAT artifact: ${BOLD}${ioc}${RESET}"
((found++)) || true
done
# Active/recent outbound connection to C2 IP
# macOS: netstat -an (no -p flag needed for basic check)
# Linux: prefer ss (iproute2), fall back to netstat
local c2_active=false
if command -v ss &>/dev/null; then
ss -tn 2>/dev/null | grep -q "${C2_IP}" && c2_active=true || true
fi
if ! $c2_active && command -v netstat &>/dev/null; then
netstat -an 2>/dev/null | grep -q "${C2_IP}" && c2_active=true || true
fi
if $c2_active; then
echo -e " ${RED}[IOC]${RESET} Active connection to C2 IP ${BOLD}${C2_IP}:8000${RESET} detected!"
((found++)) || true
fi
[[ $found -eq 0 ]] && echo -e " ${GREEN}[OK]${RESET} No IOC artifacts found."
FOUND_ISSUES=$((FOUND_ISSUES + found))
}
# =============================================================================
# MAIN
# =============================================================================
ROOT="${1:-.}"
ROOT="$(safe_realpath "$ROOT")"
echo ""
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════════════════${RESET}"
echo -e "${BOLD}${CYAN} Axios Supply-Chain Compromise Scanner${RESET}"
echo -e "${BOLD}${CYAN} Affected : axios@1.14.1 | axios@0.30.4 | plain-crypto-js@4.2.1${RESET}"
echo -e "${BOLD}${CYAN} Checks : npm · yarn (v1 + Berry) · pnpm · bun · nvm · fnm${RESET}"
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════════════════${RESET}"
echo -e " OS : $(uname -s) $(uname -m)"
echo -e " Scan root : ${BOLD}${ROOT}${RESET}"
echo ""
# ── Per-project scans ─────────────────────────────────────────────────────────
echo -e "${BOLD}${CYAN}════ Project Scans ═════════════════════════════════════════════════════${RESET}"
echo ""
# ── Smart pruning for $HOME runs ─────────────────────────────────────────────
# We prune directories that will never contain JS projects to keep this fast.
# Categories pruned:
# • Version control metadata (.git, .svn, .hg)
# • Package manager stores/caches (node_modules, .npm, .yarn, .pnpm-store,
# .cache, Library/Caches on macOS)
# • Node version manager dirs (.nvm, .fnm — scanned separately below)
# • OS / language build artifacts (.Trash, go, vendor, __pycache__, venv)
# • Binary/media asset dirs (Pictures, Music, Movies, Applications)
# • Other large non-project dirs (Downloads is intentionally NOT pruned
# in case you have project zips unpacked there)
PRUNE_DIRS=(
# VCS
".git" ".svn" ".hg"
# package manager internal dirs — never contain top-level projects
"node_modules" ".npm" ".yarn" ".pnpm-store" ".pnpm" ".bun"
# Node version managers — scanned via dedicated functions below
".nvm" ".fnm"
# OS caches & large dirs
".cache" ".Trash" "Trash"
# macOS media / system
"Library" "Pictures" "Music" "Movies" "Applications"
# Language build / env dirs
"go" "vendor" "__pycache__" ".venv" "venv" ".tox"
".gradle" ".m2" ".ivy2" ".sbt"
# IDE / editor dirs
".idea" ".vscode" ".eclipse"
)
# Build the find -prune expression dynamically
PRUNE_EXPR=()
for d in "${PRUNE_DIRS[@]}"; do
PRUNE_EXPR+=(-o -name "$d" -prune)
done
# find syntax: \( <prune-conditions> \) -o -name package.json -print0
# The leading -false starts the OR chain cleanly.
while IFS= read -r -d '' pkg_path; do
project_dir="$(dirname "$pkg_path")"
# Extra guard: skip anything that somehow still has node_modules in path
[[ "$project_dir" == */node_modules/* ]] && continue
[[ "$project_dir" == */node_modules"" ]] && continue
((SCANNED_PROJECTS++)) || true
echo -e "${BOLD}${CYAN}── Project: ${project_dir}${RESET}"
result=$(check_project "$project_dir")
issue_count=$(echo "$result" | tail -1)
if [[ "$issue_count" -eq 0 ]]; then
echo -e " ${GREEN}[OK]${RESET} Clean."
else
FOUND_ISSUES=$((FOUND_ISSUES + issue_count))
fi
echo ""
done < <(
find "$ROOT" \
\( -false \
"${PRUNE_EXPR[@]}" \
\) \
-o -name "package.json" -print0 \
2>/dev/null
)
# ── Global cache / store scans ────────────────────────────────────────────────
echo -e "${BOLD}${CYAN}════ Global Cache / Store Scans ════════════════════════════════════════${RESET}"
echo ""
check_npm_global_cache
echo ""
check_yarn_global_cache
echo ""
check_pnpm_global_cache
echo ""
check_bun_global_cache
echo ""
check_nvm_installs
echo ""
check_fnm_installs
# ── System IOC check ──────────────────────────────────────────────────────────
echo ""
echo -e "${BOLD}${CYAN}════ System IOC Check ══════════════════════════════════════════════════${RESET}"
echo ""
check_iocs
# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════════════════${RESET}"
echo -e " Projects scanned : ${BOLD}${SCANNED_PROJECTS}${RESET}"
if [[ $FOUND_ISSUES -gt 0 ]]; then
echo -e " Status : ${RED}${BOLD}VULNERABLE — ${FOUND_ISSUES} issue(s) found${RESET}"
echo ""
echo -e "${YELLOW}${BOLD} Immediate Actions Required:${RESET}"
echo -e " 1. ${BOLD}Isolate${RESET} affected machines immediately."
echo -e " 2. ${BOLD}Rotate ALL secrets${RESET}: API keys, SSH keys, cloud creds, npm/GitHub tokens."
echo -e " 3. ${BOLD}Audit outbound logs${RESET} for connections to sfrclak[.]com / ${C2_IP}:8000."
echo -e " 4. ${BOLD}Rebuild${RESET} from a clean snapshot — do NOT try to sanitise in-place."
echo -e " 5. ${BOLD}Pin axios${RESET} to a safe version (anything except 1.14.1 or 0.30.4)."
echo -e " 6. ${BOLD}Purge caches${RESET} on ALL machines that installed in the risk window:"
echo -e " npm : npm cache clean --force"
echo -e " yarn : yarn cache clean"
echo -e " pnpm : pnpm store prune"
echo -e " bun : bun pm cache rm"
echo -e " 7. Use ${BOLD}npm ci --ignore-scripts${RESET} in CI to block postinstall hooks."
else
echo -e " Status : ${GREEN}${BOLD}CLEAN — no compromised versions found${RESET}"
fi
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════════════════${RESET}"
echo ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment