Skip to content

Instantly share code, notes, and snippets.

@sreejithbnaick
Created April 2, 2026 16:33
Show Gist options
  • Select an option

  • Save sreejithbnaick/2f9457f46965d41167ce68cccd9f393d to your computer and use it in GitHub Desktop.

Select an option

Save sreejithbnaick/2f9457f46965d41167ce68cccd9f393d to your computer and use it in GitHub Desktop.
Check axios compromise on your system or local workspace folders
#!/usr/bin/env bash
# =============================================================================
# check-axios-compromise.sh
# Detects systems/projects affected by the axios npm supply chain attack
# (axios@1.14.1 / axios@0.30.4 — published 2026-03-31, malicious versions
# injecting plain-crypto-js@4.2.1 which drops a cross-platform RAT)
#
# Reference: https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan
#
# Usage:
# ./check-axios-compromise.sh # system-wide check only
# ./check-axios-compromise.sh --workspace /path/to # system + all projects under path
# ./check-axios-compromise.sh --help
# =============================================================================
set -euo pipefail
# ---------------------------------------------------------------------------
# Colour helpers (disabled automatically when not a tty)
# ---------------------------------------------------------------------------
if [ -t 1 ]; then
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
else
RED=''; YELLOW=''; GREEN=''; CYAN=''; BOLD=''; RESET=''
fi
# ---------------------------------------------------------------------------
# Globals / constants
# ---------------------------------------------------------------------------
SCRIPT_VERSION="1.0.0"
REPORT_FILE="/tmp/axios-compromise-report-$(date +%Y%m%d-%H%M%S).txt"
MALICIOUS_AXIOS_VERSIONS=("1.14.1" "0.30.4")
MALICIOUS_AXIOS_SHASUMS=(
"2553649f2322049666871cea80a5d0d6adc700ca" # axios@1.14.1
"d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71" # axios@0.30.4
)
MALICIOUS_PLAIN_CRYPTO_SHASUM="07d889e2dadce6f3910dcbc253317d28ca61c766"
C2_DOMAIN="sfrclak.com"
C2_IP="142.11.206.73"
C2_PORT="8000"
C2_CAMPAIGN_ID="6202033"
WORKSPACE_PATH=""
FOUND_ISSUES=0
WARNINGS=0
CHECKS_RUN=0
# ---------------------------------------------------------------------------
# Utility functions
# ---------------------------------------------------------------------------
log() { echo -e "${BOLD}[*]${RESET} $*" | tee -a "$REPORT_FILE"; }
ok() { echo -e "${GREEN}[✔]${RESET} $*" | tee -a "$REPORT_FILE"; }
warn() { echo -e "${YELLOW}[!]${RESET} $*" | tee -a "$REPORT_FILE"; ((WARNINGS++)) || true; }
alert() { echo -e "${RED}[✘ COMPROMISED]${RESET} $*" | tee -a "$REPORT_FILE"; ((FOUND_ISSUES++)) || true; }
info() { echo -e "${CYAN}${RESET} $*" | tee -a "$REPORT_FILE"; }
banner() { echo -e "\n${BOLD}${CYAN}══════════════════════════════════════════════${RESET}" | tee -a "$REPORT_FILE"
echo -e "${BOLD}${CYAN} $*${RESET}" | tee -a "$REPORT_FILE"
echo -e "${BOLD}${CYAN}══════════════════════════════════════════════${RESET}" | tee -a "$REPORT_FILE"; }
check_run() { ((CHECKS_RUN++)) || true; }
usage() {
cat <<EOF
${BOLD}check-axios-compromise.sh v${SCRIPT_VERSION}${RESET}
Detects compromise from the axios npm supply chain attack
(axios@1.14.1 / axios@0.30.4 injecting plain-crypto-js RAT dropper).
${BOLD}Usage:${RESET}
$0 System-wide check only
$0 --workspace <path> System check + scan all Node projects under <path>
$0 --workspace <path> --depth N Max folder depth for project discovery (default: 5)
$0 --help Show this help
${BOLD}Examples:${RESET}
$0
$0 --workspace ~/projects
$0 --workspace /srv/apps --depth 3
${BOLD}Exit codes:${RESET}
0 — No indicators of compromise found
1 — One or more indicators of compromise detected
2 — Warnings / inconclusive findings
EOF
}
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
FIND_DEPTH=5
while [[ $# -gt 0 ]]; do
case "$1" in
--workspace|-w) WORKSPACE_PATH="${2:-}"; shift 2 ;;
--depth|-d) FIND_DEPTH="${2:-5}"; shift 2 ;;
--help|-h) usage; exit 0 ;;
*) echo "Unknown argument: $1"; usage; exit 1 ;;
esac
done
# ---------------------------------------------------------------------------
# Print header
# ---------------------------------------------------------------------------
{
echo "====================================================="
echo " axios npm Compromise Detection Script v${SCRIPT_VERSION}"
echo " Reference: stepsecurity.io/blog/axios-compromised-on-npm..."
echo " Run at: $(date)"
echo " Host: $(hostname)"
echo " OS: $(uname -s) $(uname -r)"
echo "====================================================="
} | tee -a "$REPORT_FILE"
echo ""
# ---------------------------------------------------------------------------
# Helper: check a single node_modules dir for plain-crypto-js
# ---------------------------------------------------------------------------
check_plain_crypto_in_dir() {
local nm_dir="$1" # path to node_modules
local label="$2" # human-readable label for display
local pcp_dir="${nm_dir}/plain-crypto-js"
check_run
if [ -d "$pcp_dir" ]; then
alert "plain-crypto-js found in ${label}"
info "Path: ${pcp_dir}"
info "This package is NOT a dependency of any legitimate axios version."
info "Its presence means the RAT dropper has already executed."
# The dropper replaces package.json with a clean stub reporting v4.2.0.
# Check version field — if it says 4.2.0 the swap already happened.
local pj="${pcp_dir}/package.json"
if [ -f "$pj" ]; then
local ver
ver=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$pj" 2>/dev/null \
| grep -o '"[0-9][^"]*"' | tr -d '"' || true)
if [ "$ver" = "4.2.0" ]; then
alert "package.json inside plain-crypto-js reports v4.2.0 — this is the anti-forensics stub."
info "The real malicious version was 4.2.1. The dropper already swapped package.json."
fi
# setup.js should be deleted by the dropper; if still present, dropper didn't finish
if [ -f "${pcp_dir}/setup.js" ]; then
alert "setup.js still present in ${pcp_dir} — dropper may not have completed!"
info "This IS the malicious postinstall script. Do not run it."
fi
fi
return 0
fi
# Check npm list (may report 4.2.0 due to version spoofing, so grep both)
if command -v npm &>/dev/null 2>&1; then
local npm_out
npm_out=$(cd "${nm_dir}/.." 2>/dev/null && npm list plain-crypto-js 2>/dev/null || true)
if echo "$npm_out" | grep -qE "plain-crypto-js@(4\.2\.0|4\.2\.1)"; then
alert "npm list reports plain-crypto-js in ${label} (version spoofing may show 4.2.0)"
info "Output: $npm_out"
fi
fi
}
# ---------------------------------------------------------------------------
# Helper: check package-lock.json for compromised versions
# ---------------------------------------------------------------------------
check_lockfile() {
local lockfile="$1"
local label="$2"
check_run
if [ ! -f "$lockfile" ]; then return 0; fi
local hit=0
for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do
# Ensure "axios" and the version appear near each other (within 10 lines)
# to avoid false positives from unrelated packages with the same version.
if grep -A 10 '"axios"' "$lockfile" 2>/dev/null | grep -q "\"${v}\""; then
alert "Compromised axios@${v} found in ${label}"
info "File: ${lockfile}"
hit=1
fi
done
if grep -q '"plain-crypto-js"' "$lockfile" 2>/dev/null; then
alert "plain-crypto-js found in lockfile ${label}"
info "File: ${lockfile}"
hit=1
fi
[ "$hit" -eq 0 ] && ok "Lockfile clean: ${label}"
}
# ---------------------------------------------------------------------------
# Helper: check package.json for compromised axios version range
# ---------------------------------------------------------------------------
check_package_json() {
local pj="$1"
local label="$2"
check_run
if [ ! -f "$pj" ]; then return 0; fi
local hit=0
for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do
# Match "axios" and the version on the SAME line to avoid false positives
# when another package coincidentally shares the same version string.
if grep -qE "\"axios\"[[:space:]]*:[[:space:]]*\"[~^]?${v}\"" "$pj" 2>/dev/null; then
alert "Compromised axios@${v} pinned in package.json — ${label}"
info "File: ${pj}"
hit=1
fi
done
[ "$hit" -eq 0 ] && ok "package.json axios version safe: ${label}"
}
# ---------------------------------------------------------------------------
# Helper: verify installed axios shasum
# ---------------------------------------------------------------------------
check_axios_shasum_in_dir() {
local nm_dir="$1"
local label="$2"
local axios_pj="${nm_dir}/axios/package.json"
#log "2.4.1 Starting axios shasum check for ${axios_pj}"
check_run
[ -f "$axios_pj" ] || return 0
#log "2.4.2 Found axios package.json at ${axios_pj}"
local installed_ver
installed_ver=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$axios_pj" 2>/dev/null \
| grep -o '"[0-9][^"]*"' | tr -d '"' || true)
# Only warn on malicious version numbers
#log "2.4.3 Checking if installed axios version matches malicious release..."
for i in "${!MALICIOUS_AXIOS_VERSIONS[@]}"; do
if [ "$installed_ver" = "${MALICIOUS_AXIOS_VERSIONS[$i]}" ]; then
alert "Installed axios version matches malicious release: ${installed_ver}${label}"
info "Expected shasum: ${MALICIOUS_AXIOS_SHASUMS[$i]}"
# If shasum command available, compute the tarball shasum
local axios_dir="${nm_dir}/axios"
if command -v sha1sum &>/dev/null || command -v shasum &>/dev/null; then
local actual_shasum=""
if command -v sha1sum &>/dev/null; then
actual_shasum=$(find "$axios_dir" -type f | sort | xargs sha1sum 2>/dev/null | sha1sum | awk '{print $1}')
else
actual_shasum=$(find "$axios_dir" -type f | sort | xargs shasum -a 1 2>/dev/null | shasum -a 1 | awk '{print $1}')
fi
info "Computed directory shasum (may differ from npm tarball shasum): ${actual_shasum}"
fi
fi
done
}
# =============================================================================
# SECTION 1 — SYSTEM-WIDE CHECKS
# =============================================================================
banner "SECTION 1: System-Level Checks"
# -------------------------------------------------------------------
# 1.1 — Global npm axios version
# -------------------------------------------------------------------
log "1.1 Checking global npm axios version..."
check_run
if command -v npm &>/dev/null; then
global_axios=$(npm list -g axios 2>/dev/null | grep axios || true)
if [ -n "$global_axios" ]; then
for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do
if echo "$global_axios" | grep -q "$v"; then
alert "GLOBAL npm axios@${v} installed! — $global_axios"
fi
done
if ! echo "$global_axios" | grep -qE "1\.14\.1|0\.30\.4"; then
ok "Global npm axios appears safe: $global_axios"
fi
else
ok "axios not installed globally via npm"
fi
else
warn "npm not found in PATH — skipping npm global check"
fi
# -------------------------------------------------------------------
# 1.2 — Global npm plain-crypto-js
# -------------------------------------------------------------------
log "1.2 Checking global npm for plain-crypto-js..."
check_run
if command -v npm &>/dev/null; then
global_pcp=$(npm list -g plain-crypto-js 2>/dev/null || true)
if echo "$global_pcp" | grep -q "plain-crypto-js"; then
alert "plain-crypto-js found in GLOBAL npm packages!"
info "$global_pcp"
else
ok "plain-crypto-js not installed globally"
fi
fi
# -------------------------------------------------------------------
# 1.3 — Global node_modules plain-crypto-js directory
# -------------------------------------------------------------------
log "1.3 Scanning global node_modules for plain-crypto-js directory..."
check_run
if command -v npm &>/dev/null; then
global_nm=$(npm root -g 2>/dev/null || true)
if [ -d "$global_nm" ]; then
check_plain_crypto_in_dir "$global_nm" "global node_modules"
if [ ! -d "${global_nm}/plain-crypto-js" ]; then
ok "plain-crypto-js directory absent from global node_modules"
fi
fi
fi
# -------------------------------------------------------------------
# 1.4 — RAT artifact: macOS /Library/Caches/com.apple.act.mond
# -------------------------------------------------------------------
log "1.4 Checking for macOS RAT artifact..."
check_run
if [ "$(uname -s)" = "Darwin" ]; then
if [ -f "/Library/Caches/com.apple.act.mond" ]; then
alert "macOS RAT binary found: /Library/Caches/com.apple.act.mond"
info "This file was placed by the plain-crypto-js dropper."
info "Remove it and treat this machine as COMPROMISED."
ls -la "/Library/Caches/com.apple.act.mond" 2>/dev/null | tee -a "$REPORT_FILE" || true
else
ok "macOS RAT artifact not found: /Library/Caches/com.apple.act.mond"
fi
else
info "Not macOS — skipping macOS artifact check"
fi
# -------------------------------------------------------------------
# 1.5 — RAT artifact: Linux /tmp/ld.py
# -------------------------------------------------------------------
log "1.5 Checking for Linux RAT artifact (/tmp/ld.py)..."
check_run
if [ -f "/tmp/ld.py" ]; then
alert "Suspicious file found: /tmp/ld.py"
info "This is the Python RAT script dropped by plain-crypto-js on Linux."
info "File details:"
ls -la /tmp/ld.py 2>/dev/null | tee -a "$REPORT_FILE" || true
info "First 5 lines (do NOT execute):"
head -5 /tmp/ld.py 2>/dev/null | tee -a "$REPORT_FILE" || true
else
ok "/tmp/ld.py not found"
fi
# -------------------------------------------------------------------
# 1.6 — Temp script artifact: /tmp/6202033 (macOS AppleScript temp)
# -------------------------------------------------------------------
log "1.6 Checking for dropper temp file (/tmp/${C2_CAMPAIGN_ID})..."
check_run
if [ -f "/tmp/${C2_CAMPAIGN_ID}" ]; then
alert "Dropper temp file found: /tmp/${C2_CAMPAIGN_ID}"
info "This file was written by the plain-crypto-js dropper (AppleScript wrapper)."
ls -la "/tmp/${C2_CAMPAIGN_ID}" 2>/dev/null | tee -a "$REPORT_FILE" || true
else
ok "/tmp/${C2_CAMPAIGN_ID} not found"
fi
# -------------------------------------------------------------------
# 1.7 — Suspicious running processes (nohup, ld.py, osascript dropper)
# -------------------------------------------------------------------
log "1.7 Checking for suspicious running processes..."
check_run
PROC_FOUND=0
if command -v ps &>/dev/null; then
# nohup python3 /tmp/ld.py
if ps aux 2>/dev/null | grep -v grep | grep -q "ld\.py"; then
alert "Suspicious process: python3 running ld.py (RAT payload)"
ps aux 2>/dev/null | grep -v grep | grep "ld\.py" | tee -a "$REPORT_FILE" || true
PROC_FOUND=1
fi
# setup.js still running
if ps aux 2>/dev/null | grep -v grep | grep -q "setup\.js"; then
alert "Suspicious process: node running setup.js (dropper may still be active)"
ps aux 2>/dev/null | grep -v grep | grep "setup\.js" | tee -a "$REPORT_FILE" || true
PROC_FOUND=1
fi
# macOS osascript running campaign ID script
if ps aux 2>/dev/null | grep -v grep | grep -q "osascript.*${C2_CAMPAIGN_ID}"; then
alert "Suspicious osascript process referencing campaign ID ${C2_CAMPAIGN_ID}"
ps aux 2>/dev/null | grep -v grep | grep "osascript" | tee -a "$REPORT_FILE" || true
PROC_FOUND=1
fi
fi
[ "$PROC_FOUND" -eq 0 ] && ok "No suspicious dropper/RAT processes found"
# -------------------------------------------------------------------
# 1.8 — Network: active connections to C2
# -------------------------------------------------------------------
log "1.8 Checking active network connections to C2 (${C2_IP}:${C2_PORT} / ${C2_DOMAIN})..."
check_run
NET_FOUND=0
for tool in ss netstat lsof; do
if command -v "$tool" &>/dev/null; then
case "$tool" in
ss)
conn=$(ss -tnp 2>/dev/null | grep -E "${C2_IP}|${C2_DOMAIN}" || true)
[ -n "$conn" ] && { alert "Active C2 connection via ss: $conn"; NET_FOUND=1; }
;;
netstat)
conn=$(netstat -tn 2>/dev/null | grep -E "${C2_IP}" || true)
[ -n "$conn" ] && { alert "Active C2 connection via netstat: $conn"; NET_FOUND=1; }
;;
lsof)
conn=$(lsof -i "@${C2_IP}:${C2_PORT}" 2>/dev/null || true)
[ -n "$conn" ] && { alert "Active C2 connection via lsof: $conn"; NET_FOUND=1; }
;;
esac
break
fi
done
[ "$NET_FOUND" -eq 0 ] && ok "No active connections to C2 IP ${C2_IP}"
# -------------------------------------------------------------------
# 1.9 — DNS / hosts: C2 domain resolution
# -------------------------------------------------------------------
log "1.9 Checking if C2 domain ${C2_DOMAIN} resolves (should NOT unless already blocked)..."
check_run
if command -v host &>/dev/null 2>&1; then
c2_resolve=$(host "$C2_DOMAIN" 2>/dev/null | grep -v "NXDOMAIN\|not found\|has no" || true)
if [ -n "$c2_resolve" ]; then
warn "C2 domain ${C2_DOMAIN} still resolves — consider blocking it in /etc/hosts or firewall."
info "$c2_resolve"
else
ok "C2 domain ${C2_DOMAIN} does not resolve (may be taken down or already blocked)"
fi
elif command -v nslookup &>/dev/null 2>&1; then
c2_resolve=$(nslookup "$C2_DOMAIN" 2>/dev/null | grep -i "address" | grep -v "127.0.0.1\|::1" || true)
if [ -n "$c2_resolve" ]; then
warn "C2 domain ${C2_DOMAIN} still resolves. Consider blocking."
info "$c2_resolve"
else
ok "C2 domain ${C2_DOMAIN} does not resolve"
fi
else
warn "Neither 'host' nor 'nslookup' available — cannot test C2 DNS resolution"
fi
# -------------------------------------------------------------------
# 1.10 — /etc/hosts: check if C2 already blocked
# -------------------------------------------------------------------
log "1.10 Checking /etc/hosts for C2 block entry..."
check_run
if grep -q "${C2_DOMAIN}" /etc/hosts 2>/dev/null; then
ok "C2 domain ${C2_DOMAIN} is already in /etc/hosts (blocked or redirected)"
grep "${C2_DOMAIN}" /etc/hosts | tee -a "$REPORT_FILE" || true
else
warn "C2 domain ${C2_DOMAIN} is NOT blocked in /etc/hosts"
info "Consider adding: echo '0.0.0.0 ${C2_DOMAIN}' >> /etc/hosts"
fi
# -------------------------------------------------------------------
# 1.11 — npm cache: check for cached malicious tarballs
# -------------------------------------------------------------------
log "1.11 Checking npm cache for malicious package tarballs..."
check_run
if command -v npm &>/dev/null; then
npm_cache_dir=$(npm config get cache 2>/dev/null || true)
if [ -d "$npm_cache_dir" ]; then
CACHE_HIT=0
for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do
cached=$(find "$npm_cache_dir" -type f -name "*.tgz" 2>/dev/null \
| xargs grep -l "axios" 2>/dev/null \
| grep "$v" 2>/dev/null || true)
# simpler path check:
if find "$npm_cache_dir" -path "*/axios/${v}*" -type f 2>/dev/null | grep -q .; then
warn "npm cache may contain axios@${v} tarball in: ${npm_cache_dir}"
info "Run: npm cache clean --force (after resolving any active projects)"
CACHE_HIT=1
fi
done
if find "$npm_cache_dir" -path "*/plain-crypto-js*" -type f 2>/dev/null | grep -q .; then
warn "npm cache may contain plain-crypto-js tarball in: ${npm_cache_dir}"
info "Run: npm cache clean --force"
CACHE_HIT=1
fi
[ "$CACHE_HIT" -eq 0 ] && ok "npm cache appears clean of malicious packages"
else
warn "npm cache directory not found at: ${npm_cache_dir}"
fi
fi
# -------------------------------------------------------------------
# 1.12 — Shell history: npm install of compromised versions
# -------------------------------------------------------------------
log "1.12 Scanning shell history for installs of compromised versions..."
check_run
HISTORY_FOUND=0
for hist_file in ~/.bash_history ~/.zsh_history ~/.sh_history ~/.history; do
if [ -f "$hist_file" ]; then
for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do
if grep -q "axios@${v}\|axios.*${v}" "$hist_file" 2>/dev/null; then
warn "Shell history references axios@${v} in ${hist_file}"
grep -n "axios.*${v}\|${v}.*axios" "$hist_file" 2>/dev/null | head -5 | tee -a "$REPORT_FILE" || true
HISTORY_FOUND=1
fi
done
if grep -q "plain-crypto-js" "$hist_file" 2>/dev/null; then
warn "Shell history references plain-crypto-js in ${hist_file}"
HISTORY_FOUND=1
fi
fi
done
[ "$HISTORY_FOUND" -eq 0 ] && ok "No compromised version references in shell history"
# -------------------------------------------------------------------
# 1.13 — ~/.npmrc: check for compromised tokens or overrides
# -------------------------------------------------------------------
log "1.13 Checking ~/.npmrc for suspicious overrides..."
check_run
if [ -f ~/.npmrc ]; then
if grep -qiE "sfrclak|ifstap|nrwise" ~/.npmrc 2>/dev/null; then
alert "~/.npmrc contains C2/attacker references!"
grep -iE "sfrclak|ifstap|nrwise" ~/.npmrc | tee -a "$REPORT_FILE" || true
else
ok "~/.npmrc looks clean"
fi
else
ok "~/.npmrc not found (no overrides configured)"
fi
# =============================================================================
# SECTION 2 — PROJECT-LEVEL CHECKS (system-wide home directory scan)
# =============================================================================
banner "SECTION 2: Home Directory Project Scan"
log "Scanning home directory for Node.js projects..."
# Collect all node_modules under home (respecting depth)
HOME_RESULTS=$(find ~ \
-maxdepth "$FIND_DEPTH" \
-type d -name "node_modules" \
! -path "*/node_modules/*/node_modules" \
2>/dev/null || true)
if [ -z "$HOME_RESULTS" ]; then
ok "No node_modules directories found under home directory"
else
#log "Scanning ${HOME_RESULTS}"
echo ""
echo "$HOME_RESULTS" | while IFS= read -r nm; do
project_dir=$(dirname "$nm")
label="${project_dir}"
log "2.1 Checking ${project_dir} for compromised axios..."
# 2.1 Check for plain-crypto-js
#log "2.1 Checking ${project_dir} for plain-crypto-js..."
check_plain_crypto_in_dir "$nm" "$label"
# 2.2 Check lockfile
lock="${project_dir}/package-lock.json"
yarn_lock="${project_dir}/yarn.lock"
pnpm_lock="${project_dir}/pnpm-lock.yaml"
if [ -f "$lock" ]; then
#log "2.2 Checking ${project_dir} for package-lock.json..."
check_lockfile "$lock" "$label"
fi
# yarn.lock
if [ -f "$yarn_lock" ]; then
#log "2.2 Checking ${project_dir} for yarn.lock..."
check_run
for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do
if grep -q "axios@" "$yarn_lock" 2>/dev/null && \
grep -A5 "axios@" "$yarn_lock" 2>/dev/null | grep -q "\"${v}\""; then
alert "Compromised axios@${v} found in yarn.lock — ${label}"
info "File: ${yarn_lock}"
fi
done
if grep -q "plain-crypto-js" "$yarn_lock" 2>/dev/null; then
alert "plain-crypto-js in yarn.lock — ${label}"
fi
fi
# pnpm-lock.yaml
if [ -f "$pnpm_lock" ]; then
#log "2.2 Checking ${project_dir} for pnpm-lock.yaml..."
check_run
for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do
# Match axios and version together (pnpm uses /axios/version or axios@version format)
if grep -qE "axios[@/]${v}" "$pnpm_lock" 2>/dev/null; then
alert "Possible compromised axios@${v} in pnpm-lock.yaml — ${label}"
info "File: ${pnpm_lock}"
fi
done
if grep -q "plain-crypto-js" "$pnpm_lock" 2>/dev/null; then
alert "plain-crypto-js in pnpm-lock.yaml — ${label}"
fi
fi
# 2.3 Check package.json dependency declarations
#log "2.3 Checking ${project_dir} for package.json..."
check_package_json "${project_dir}/package.json" "$label"
# 2.4 Verify axios shasum
#log "2.4 Checking ${project_dir} for axios shasum..."
check_axios_shasum_in_dir "$nm" "$label"
done
ok "Home directory scanning done"
fi
# =============================================================================
# SECTION 3 — WORKSPACE SCAN
# =============================================================================
echo $WORKSPACE_PATH
if [ -n "$WORKSPACE_PATH" ]; then
banner "SECTION 3: Workspace Scan — ${WORKSPACE_PATH}"
if [ ! -d "$WORKSPACE_PATH" ]; then
warn "Workspace path does not exist: ${WORKSPACE_PATH}"
else
log "Discovering Node.js projects in: ${WORKSPACE_PATH} (depth ${FIND_DEPTH})..."
# Find package.json files that aren't inside node_modules
PROJECT_ROOTS=$(find "$WORKSPACE_PATH" \
-maxdepth "$FIND_DEPTH" \
-name "package.json" \
! -path "*/node_modules/*" \
2>/dev/null | xargs -I{} dirname {} | sort -u || true)
if [ -z "$PROJECT_ROOTS" ]; then
ok "No Node.js projects found under ${WORKSPACE_PATH}"
else
PROJECT_COUNT=$(echo "$PROJECT_ROOTS" | wc -l | tr -d ' ')
log "Found ${PROJECT_COUNT} project(s). Scanning..."
echo "$PROJECT_ROOTS" | while IFS= read -r proj; do
nm_dir="${proj}/node_modules"
label="$(basename "$proj") [${proj}]"
echo "" | tee -a "$REPORT_FILE"
log "── Project: ${label}"
# 3.1 Check package.json
check_package_json "${proj}/package.json" "$label"
# 3.2 Check lockfile
check_lockfile "${proj}/package-lock.json" "$label"
# 3.3 yarn.lock
if [ -f "${proj}/yarn.lock" ]; then
check_run
YARN_HIT=0
for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do
# Check version appears within context of an axios entry
if grep -A 5 "axios@" "${proj}/yarn.lock" 2>/dev/null | grep -q "\"${v}\""; then
alert "Compromised axios@${v} in yarn.lock — ${label}"
YARN_HIT=1
fi
done
if grep -q "plain-crypto-js" "${proj}/yarn.lock" 2>/dev/null; then
alert "plain-crypto-js in yarn.lock — ${label}"
YARN_HIT=1
fi
[ "$YARN_HIT" -eq 0 ] && ok "yarn.lock clean — ${label}"
fi
# 3.4 pnpm-lock
if [ -f "${proj}/pnpm-lock.yaml" ]; then
check_run
PNPM_HIT=0
for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do
# Match axios and version together (pnpm uses /axios/version or axios@version format)
if grep -qE "axios[@/]${v}" "${proj}/pnpm-lock.yaml" 2>/dev/null; then
alert "Possible compromised axios@${v} in pnpm-lock.yaml — ${label}"
PNPM_HIT=1
fi
done
if grep -q "plain-crypto-js" "${proj}/pnpm-lock.yaml" 2>/dev/null; then
alert "plain-crypto-js in pnpm-lock.yaml — ${label}"
PNPM_HIT=1
fi
[ "$PNPM_HIT" -eq 0 ] && ok "pnpm-lock.yaml clean — ${label}"
fi
# 3.5 node_modules checks (only if installed)
if [ -d "$nm_dir" ]; then
check_plain_crypto_in_dir "$nm_dir" "$label"
check_axios_shasum_in_dir "$nm_dir" "$label"
# 3.6 Check axios package.json inside node_modules for missing husky prepare
# (the attacker removed "prepare": "husky" from the malicious release)
axios_pj="${nm_dir}/axios/package.json"
if [ -f "$axios_pj" ]; then
check_run
ax_ver=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$axios_pj" 2>/dev/null \
| grep -o '"[0-9][^"]*"' | tr -d '"' || true)
has_prepare=$(grep '"prepare"' "$axios_pj" 2>/dev/null || true)
has_plain_crypto=$(grep '"plain-crypto-js"' "$axios_pj" 2>/dev/null || true)
has_githead=$(grep '"gitHead"' "$axios_pj" 2>/dev/null || true)
if [ -n "$has_plain_crypto" ]; then
alert "axios package.json contains plain-crypto-js dependency! — ${label}"
info "Installed version: ${ax_ver}"
fi
# For v1.x: missing prepare script + no gitHead = malicious publish signal
if echo "$ax_ver" | grep -q "^1\."; then
if [ -z "$has_prepare" ] && [ -z "$has_githead" ]; then
warn "axios@${ax_ver} is missing 'prepare' script and 'gitHead' — possible malicious publish in ${label}"
info "Legitimate 1.x releases are published via GitHub Actions OIDC and include gitHead"
fi
fi
fi
# 3.7 Check for setup.js still present (dropper didn't finish cleanup)
pcp_dir="${nm_dir}/plain-crypto-js"
if [ -f "${pcp_dir}/setup.js" ]; then
alert "setup.js still present in ${pcp_dir} — dropper active/incomplete!"
fi
else
info "node_modules not installed for ${label} — only manifest/lockfile checked"
fi
done
fi
fi
else
echo "" | tee -a "$REPORT_FILE"
info "No --workspace path given. Pass --workspace <path> to also scan local projects."
fi
# =============================================================================
# SECTION 4 — REMEDIATION GUIDANCE
# =============================================================================
banner "SECTION 4: Remediation Reference"
cat <<'REMEDIATION' | tee -a "$REPORT_FILE"
If any issues were found above, take these steps:
IMMEDIATE (any compromised project):
─────────────────────────────────────
1. Downgrade axios in affected projects:
npm install axios@1.14.0 # for 1.x users
npm install axios@0.30.3 # for 0.x users
2. Remove plain-crypto-js and reinstall with scripts disabled:
rm -rf node_modules/plain-crypto-js
npm install --ignore-scripts
3. Add overrides to prevent re-resolution of malicious versions:
In package.json:
"overrides": { "axios": "1.14.0" }
"resolutions": { "axios": "1.14.0" }
COMPROMISED MACHINE (RAT artifacts found / plain-crypto-js executed):
────────────────────────────────────────────────────────────────────
1. Isolate machine from the network immediately.
2. Rotate ALL credentials (npm tokens, SSH keys, AWS, GCP, Azure,
GitHub PATs, Docker credentials, .env secrets, browser passwords).
3. Remove RAT artifacts:
macOS: sudo rm -f /Library/Caches/com.apple.act.mond
Linux: rm -f /tmp/ld.py
4. Block C2:
echo "0.0.0.0 sfrclak.com" | sudo tee -a /etc/hosts
# Linux firewall:
sudo iptables -A OUTPUT -d 142.11.206.73 -j DROP
5. Reformat and rebuild from a known-good image.
CI/CD (ephemeral runners):
──────────────────────────
• Rotate all secrets injected into any workflow that ran during the
compromise window (2026-03-31 00:21 UTC – 03:15 UTC).
• Add --ignore-scripts to CI install commands:
npm ci --ignore-scripts
PREVENTION (going forward):
───────────────────────────
• Pin exact axios versions in package.json and commit lockfiles.
• Configure npm release age gate in .npmrc:
min-release-age=7d
• Use npm ci --ignore-scripts in CI/CD.
• Enable Dependabot cooldown (3 days minimum).
IOCs to block:
──────────────
Domain : sfrclak.com
IP : 142.11.206.73
Port : 8000
REMEDIATION
# =============================================================================
# SUMMARY
# =============================================================================
banner "SUMMARY"
echo "" | tee -a "$REPORT_FILE"
echo -e " Checks run : ${BOLD}${CHECKS_RUN}${RESET}" | tee -a "$REPORT_FILE"
echo -e " Warnings : ${BOLD}${YELLOW}${WARNINGS}${RESET}" | tee -a "$REPORT_FILE"
if [ "$FOUND_ISSUES" -gt 0 ]; then
echo -e " COMPROMISE INDICATORS FOUND : ${BOLD}${RED}${FOUND_ISSUES}${RESET}" | tee -a "$REPORT_FILE"
echo "" | tee -a "$REPORT_FILE"
echo -e "${RED}${BOLD} !! SYSTEM MAY BE COMPROMISED — See remediation steps above !!${RESET}" | tee -a "$REPORT_FILE"
elif [ "$WARNINGS" -gt 0 ]; then
echo -e " Compromise indicators : ${BOLD}${GREEN}None detected${RESET}" | tee -a "$REPORT_FILE"
echo "" | tee -a "$REPORT_FILE"
echo -e "${YELLOW}${BOLD} Warnings found — review items marked [!] above.${RESET}" | tee -a "$REPORT_FILE"
else
echo -e " Compromise indicators : ${BOLD}${GREEN}None detected${RESET}" | tee -a "$REPORT_FILE"
echo "" | tee -a "$REPORT_FILE"
echo -e "${GREEN}${BOLD} No indicators of compromise found. Stay vigilant.${RESET}" | tee -a "$REPORT_FILE"
fi
echo "" | tee -a "$REPORT_FILE"
echo -e " Full report saved to: ${BOLD}${REPORT_FILE}${RESET}" | tee -a "$REPORT_FILE"
echo "" | tee -a "$REPORT_FILE"
# Exit code: 1 = compromised, 2 = warnings only, 0 = clean
if [ "$FOUND_ISSUES" -gt 0 ]; then
exit 1
elif [ "$WARNINGS" -gt 0 ]; then
exit 2
else
exit 0
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment