Skip to content

Instantly share code, notes, and snippets.

@l33tm4st3r
Last active June 17, 2026 07:12
Show Gist options
  • Select an option

  • Save l33tm4st3r/f6895a62167bd3d83f298d27ffd46741 to your computer and use it in GitHub Desktop.

Select an option

Save l33tm4st3r/f6895a62167bd3d83f298d27ffd46741 to your computer and use it in GitHub Desktop.
AUR supply-chain malware checker (atomic-lockfile / lockfile-js / nextfile-js campaign, June 2026) - behavior-based detection
#!/usr/bin/env bash
#
# aur_malware_check.sh - official-list + BEHAVIOR-based detection
#
# AUR malware campaign, June 2026
# (atomic-lockfile / lockfile-js / nextfile-js / js-digest):
# https://lists.archlinux.org/archives/list/aur-general@lists.archlinux.org/thread/FGXPCB3ZVCJIV7FX323SBAX2JHYB7ZS4/
#
# Two complementary strategies:
# - Cross-check installed AUR packages against Arch's authoritative live list.
# - Behavior detection: the package list keeps growing (>1900) and the newest
# wave obfuscates its commands (shell escapes + "bun add"), so we also
# inspect the real install hooks for the malicious behavior: running a JS
# package manager (npm/bun/yarn/pnpm) from a .install hook, obfuscation, or
# "cd /tmp" right before installing packages.
#
# Source of truth: /var/lib/pacman/local/<pkg>/install (what ACTUALLY ran here)
#
# Usage:
# aur_malware_check.sh full system scan (installed + cache + disk)
# aur_malware_check.sh --vet <dir> vet ONE AUR build dir before you build it
#
# Note: only the AUR is affected; updating official repos with `pacman -Syu` is
# safe. Use --vet to keep updating AUR packages safely instead of holding off.
#
# Read-only. Does not delete or modify anything.
set -uo pipefail
RED=$'\033[1;31m'; GRN=$'\033[1;32m'; YEL=$'\033[1;33m'; CYN=$'\033[1;36m'; RST=$'\033[0m'
ALERTS=0
# --- IOC patterns -------------------------------------------------------------
# Any language package-manager call. Legit in build(), but a major red flag in
# .install hooks: no .install hook should ever invoke an external package
# manager (deps belong to pacman). Covers JS plus Rust/PHP/Python/Ruby.
PM_ANY_REGEX='(npm|pnpm|yarn|bun|cargo|composer|pip|pipx|gem)[[:space:]]+(install|add|i|exec|dlx|require)([[:space:]]|$)'
# JS package manager installing NAMED packages (what the malware does):
# npm install atomic-lockfile ... | bun add ansi-colors ...
# Excludes flags (-x) and a bare "install"/"ci" (a project's own deps).
PM_WITHPKG_REGEX='(npm|pnpm|yarn)[[:space:]]+(install|add|i)[[:space:]]+[@a-z0-9]|bun[[:space:]]+add[[:space:]]+[@a-z0-9'\''"$]'
# Known malicious npm package names
NAME_REGEX='atomic-lockfile|lockfile-js|nextfile-js|js-digest'
# Authoritative live list of affected AUR packages, maintained by Arch Linux.
# The /download endpoint returns clean markdown.
LIST_URL='https://md.archlinux.org/s/SxbqukK6IA/download'
# Shell-escape obfuscation (the new wave) $'\x63' $'\141'
OBF_REGEX="\\\$'\\\\x[0-9a-fA-F]|\\\$'\\\\[0-7]{2,3}"
# Switching to /tmp (payloads are staged there before installing)
TMP_REGEX='cd[[:space:]]+["'\'']?/?["'\'']?t["'\'']?m["'\'']?p'
# scan_file <label> <path> <mode>
# mode=hook -> ANY JS package manager is suspicious (.install never builds)
# mode=pkgbuild -> only flag JS manager installing named packages / obfuscation
# Reads the file directly with grep (no $(cat ...)): safe for big/binary files,
# avoids null-byte truncation, and is far more efficient. Increments ALERTS here
# as the single source of truth and returns 0 when something is found.
scan_file() {
local label="$1" file="$2" mode="${3:-hook}" hit=0 reasons=()
# -a: treat data as text so patterns are searched even in odd/binary files
if [[ "$mode" == "hook" ]]; then
grep -qaiE "$PM_ANY_REGEX" "$file" && { reasons+=("package manager in .install hook"); hit=1; }
fi
grep -qaiE "$PM_WITHPKG_REGEX" "$file" && { reasons+=("installs named JS packages"); hit=1; }
grep -qaiE "$NAME_REGEX" "$file" && { reasons+=("known malicious npm name"); hit=1; }
grep -qaE "$OBF_REGEX" "$file" && { reasons+=("shell obfuscation"); hit=1; }
grep -qaiE "$TMP_REGEX" "$file" && grep -qaiE "$PM_ANY_REGEX" "$file" \
&& { reasons+=("cd /tmp + install"); hit=1; }
if [[ $hit -eq 1 ]]; then
ALERTS=$((ALERTS+1))
echo " ${RED}ALERT:${RST} $label"
printf ' -> %s\n' "${reasons[@]}"
return 0 # 0 = something was found (success for the 'if')
fi
return 1 # 1 = clean
}
# --- pre-install vet mode -----------------------------------------------------
# Usage: aur_malware_check.sh --vet <dir>
# Scans an AUR build directory (PKGBUILD/.install/.sh) BEFORE you build it, so
# you can keep updating safely instead of holding off entirely. Point it at the
# clone your helper prepared, e.g. ~/.cache/paru/clone/<pkg>.
if [[ "${1:-}" == "--vet" ]]; then
dir="${2:-}"
[[ -n "$dir" && -d "$dir" ]] || { echo "usage: $0 --vet <dir>"; exit 2; }
echo "${CYN}== Pre-install vet: $dir ==${RST}"
echo
clean=1; files=()
mapfile -d '' -t files < <(find "$dir" -type f \
\( -name 'PKGBUILD' -o -name '*.install' -o -name '*.sh' \) -print0 2>/dev/null)
if [[ ${#files[@]} -eq 0 ]]; then
echo " ${YEL}No PKGBUILD/.install/.sh files found here.${RST}"; exit 0
fi
for file in "${files[@]}"; do
case "$file" in *.install) m=hook ;; *) m=pkgbuild ;; esac
if scan_file "$file" "$file" "$m"; then clean=0; fi
done
echo
if [[ $clean -eq 1 ]]; then
echo "${GRN}===> OK: no malicious behavior found. Looks safe to build.${RST}"
exit 0
else
echo "${RED}===> DO NOT build this package. $ALERTS indicator(s) found above.${RST}"
exit 1
fi
fi
echo "${CYN}== AUR malware check (atomic-lockfile / lockfile-js / nextfile-js / js-digest) ==${RST}"
echo
MY_AUR=() # explicit init: safe under `set -u` even on older Bash (<4.4)
mapfile -t MY_AUR < <(pacman -Qmq 2>/dev/null) || MY_AUR=()
echo "Installed AUR/foreign packages: ${#MY_AUR[@]}"
echo
# --- 1) AUTHORITATIVE: cross-check installed AUR pkgs vs the official list -----
echo "${CYN}[1/4]${RST} Cross-checking installed AUR packages against Arch's live list..."
raw=$(curl -fsSL --max-time 15 "$LIST_URL" 2>/dev/null) || raw=""
if [[ -z "$raw" ]]; then
echo " ${YEL}SKIP${RST}: could not fetch the live list ($LIST_URL). Offline? Behavior checks below still run."
else
INFECTED_LIST=()
# strip HTML, keep valid pkgnames (incl. names ending in + or - like ls++)
mapfile -t INFECTED_LIST < <(sed 's/<[^>]*>//g' <<<"$raw" \
| grep -E '^[a-z0-9][a-z0-9_.+-]*$' | sort -u)
if [[ ${#INFECTED_LIST[@]} -eq 0 ]]; then
echo " ${YEL}SKIP${RST}: fetched list but parsed 0 packages (format change?)."
else
found=()
mapfile -t found < <(comm -12 \
<(printf '%s\n' "${MY_AUR[@]}" | sort -u) \
<(printf '%s\n' "${INFECTED_LIST[@]}"))
if [[ ${#found[@]} -eq 0 ]]; then
echo " ${GRN}OK${RST}: none of your ${#MY_AUR[@]} AUR packages are on the official list (${#INFECTED_LIST[@]} entries)."
else
for pkg in "${found[@]}"; do
ALERTS=$((ALERTS+1))
echo " ${RED}ALERT:${RST} $pkg is on Arch's affected-packages list"
done
fi
fi
fi
echo
# --- 2) DEFINITIVE: install scripts pacman actually ran on this system ---------
echo "${CYN}[2/4]${RST} Inspecting real install hooks (/var/lib/pacman/local)..."
clean=1
for f in /var/lib/pacman/local/*/install; do
[[ -f "$f" ]] || continue
pkg=$(basename "$(dirname "$f")")
if scan_file "$pkg ($f)" "$f" hook; then clean=0; fi
done
[[ $clean -eq 1 ]] && echo " ${GRN}OK${RST}: no install hook runs a package manager or is obfuscated."
echo
# --- 2) paru/yay clones (PKGBUILD + *.install before installing) ---------------
echo "${CYN}[3/4]${RST} Scanning paru/yay clones (PKGBUILD, *.install, *.sh)..."
clean=1
CLONE_DIRS=()
for d in "$HOME/.cache/paru/clone" "$HOME/.cache/yay"; do
[[ -d "$d" ]] && CLONE_DIRS+=("$d")
done
if [[ ${#CLONE_DIRS[@]} -gt 0 ]]; then
# -print0 / read -d '' so paths with spaces or newlines never break
while IFS= read -r -d '' file; do
case "$file" in
*.install) m=hook ;;
*) m=pkgbuild ;;
esac
if scan_file "$file" "$file" "$m"; then clean=0; fi
done < <(find "${CLONE_DIRS[@]}" -type f \
\( -name 'PKGBUILD' -o -name '*.install' -o -name '*.sh' \) -print0 2>/dev/null)
fi
[[ $clean -eq 1 ]] && echo " ${GRN}OK${RST}: no malicious behavior in the helper's clones."
echo
# --- 3) malicious npm package already deployed on disk ------------------------
echo "${CYN}[4/4]${RST} Searching for the malicious npm packages on disk..."
clean=1
# Include global node paths (root installs) in addition to $HOME and /tmp
SEARCH_ROOTS=()
for r in "$HOME" /tmp /usr/lib/node_modules /usr/local/lib/node_modules; do
[[ -d "$r" ]] && SEARCH_ROOTS+=("$r")
done
if [[ ${#SEARCH_ROOTS[@]} -gt 0 ]]; then
while IFS= read -r -d '' hit; do
ALERTS=$((ALERTS+1)); clean=0
echo " ${RED}ALERT:${RST} $hit"
done < <(find "${SEARCH_ROOTS[@]}" -type d \
\( -name 'atomic-lockfile' -o -name 'lockfile-js' -o -name 'nextfile-js' \) \
-path '*node_modules*' -print0 2>/dev/null)
fi
[[ $clean -eq 1 ]] && echo " ${GRN}OK${RST}: malicious npm packages not found installed."
echo
# --- summary ------------------------------------------------------------------
if [[ $ALERTS -eq 0 ]]; then
echo "${GRN}===> CLEAN: no check (official list + behavior) detected the malware.${RST}"
exit 0
else
echo "${RED}===> WARNING: $ALERTS indicator(s) found. Review above.${RST}"
echo "${YEL}Actions: uninstall the package, remove ~/.npm and suspicious node_modules,${RST}"
echo "${YEL}rotate credentials (npm, git, SSH, wallets) and check ~/.config and crontab.${RST}"
exit 1
fi
@l33tm4st3r

l33tm4st3r commented Jun 14, 2026

Copy link
Copy Markdown
Author

πŸ”„ Update Notes: Hybrid Detection Engine

The script has been overhauled into a comprehensive, 4-layer hybrid audit tool combining both live threat intelligence and proactive behavioral analysis.

  • New Layer [1/4] (Live Threat Intel): Now dynamically cross-references your foreign packages (pacman -Qmq) against Arch Linux's official live authoritative IOC registry (md.archlinux.org). Implements an elegant fallback to offline mode if the server is unreachable.
  • New IOC Tracking: Added js-digest to the threat-signature regex patterns, addressing the latest wave of the campaign.
  • Ecosystem Expansion (Mailing List Feedback): Expanded the behavioral heuristics in .install hooks to flag unauthorized runtime calls from other language package managers, including Rust (cargo), PHP (composer), Python (pip/pipx), and Ruby (gem), alongside the existing JS managers. No legitimate .install hook should ever invoke an external package manager, so any such call is a strong red flag. Thanks to Keon Cachia from the aur-general list for the suggestion!
  • Pre-install Vet Mode: Added --vet <dir> so you can audit a single AUR build directory before building it, instead of holding off on updates entirely. Note that only the AUR is affected β€” updating the official repos with pacman -Syu remains safe.
  • Upstream Parsing Fix: Refined the package-name sanitization regex to ^[a-z0-9][a-z0-9_.+-]*$. This ensures edge-case legitimate packages with trailing special characters (like ls++) are preserved and verified instead of silently dropped.
  • Zero /tmp False-Positives: Hardened the file-system discovery loop by restricting matching strictly to directory targets (-type d) under valid node_modules paths. This prevents the script from accidentally flagging its own temporary string artifacts generated by curl or process substitutions in /tmp.
  • Strict Environment Fortification: Safeguarded array expansions against "unbound variable" exceptions under set -uo pipefail, ensuring seamless execution even on legacy Bash deployments (<4.4).

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