Last active
June 17, 2026 07:12
-
-
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
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 | |
| # | |
| # 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 |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
π 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.
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.js-digestto the threat-signature regex patterns, addressing the latest wave of the campaign..installhooks 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.installhook 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!--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 withpacman -Syuremains safe.^[a-z0-9][a-z0-9_.+-]*$. This ensures edge-case legitimate packages with trailing special characters (likels++) are preserved and verified instead of silently dropped.-type d) under validnode_modulespaths. This prevents the script from accidentally flagging its own temporary string artifacts generated bycurlor process substitutions in/tmp.set -uo pipefail, ensuring seamless execution even on legacy Bash deployments (<4.4).