Created
April 2, 2026 06:01
-
-
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)
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 | |
| # ============================================================================= | |
| # 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