Last active
June 13, 2026 02:29
-
-
Save arbaes/e29e68d9ed1513ddd80ae9cc4a6c9f0e to your computer and use it in GitHub Desktop.
Atomic Arch vulnerability scan (atomic-lockfile injection checker)
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 | |
| # Atomic Arch / atomic-lockfile AUR campaign check | |
| # Sources: | |
| # - https://lists.archlinux.org/archives/list/aur-general@lists.archlinux.org/thread/FGXPCB3ZVCJIV7FX323SBAX2JHYB7ZS4/ | |
| # - https://www.sonatype.com/blog/atomic-arch-npm-campaign-adds-malicious-dependency | |
| # - https://ioctl.fail/preliminary-analysis-of-aur-malware/ | |
| set -uo pipefail | |
| # Known IOC strings: the malicious npm dep names this campaign rotates through, | |
| # plus the payload's path inside the npm package ("preinstall": "./src/hooks/deps"). | |
| IOC_NAMES='atomic-lockfile|js-digest|lockfile-js|src/hooks/deps' | |
| # Delivery invariant: a pacman INSTALL SCRIPTLET or HOOK that runs a JS package | |
| # manager is almost certainly malicious. | |
| # The campaign has already switched npm->bun and rotated dep names, so we match the | |
| # manager, not the name: npm/npx/pnpm/yarn/bun/bunx. | |
| PM_ACTION='(^|[^[:alnum:]_/-])(npm|npx|pnpm|yarn|bun|bunx)[[:space:]]' | |
| nb_hits=0 | |
| ebpf_ran=0 # set to 1 only if the (root-only) eBPF rootkit scan actually runs | |
| CAMPAIGN_START='2026-06-09' # Sources vary but this is the earliest observed date of activity I found | |
| # URL of the community-maintained reported-compromised package list (Arch HedgeDoc). | |
| # Override with the ATOMIC_LIST_URL env var if it moves or you mirror it. | |
| # Source: https://lists.archlinux.org/archives/list/aur-general@lists.archlinux.org/message/FCH7TT6IOVT7D477JKSVJALBKADAARSW/ | |
| LIST_URL="${ATOMIC_LIST_URL:-https://md.archlinux.org/s/SxbqukK6IA/download}" | |
| echo "Atomic Arch / atomic-lockfile AUR check" | |
| echo | |
| # --- Setup: find the AUR helper clone caches ---- | |
| # Honor XDG dirs (helpers fall back to these when XDG_* is unset). | |
| echo "Locating AUR helper caches..." | |
| xch="${XDG_CACHE_HOME:-$HOME/.cache}" | |
| xdh="${XDG_DATA_HOME:-$HOME/.local/share}" | |
| caches=() | |
| for d in "$xch/yay" "$xch/paru" \ | |
| "$xdh/pikaur/aur_repos" "$xch/pikaur/aur_repos" \ | |
| "$xch/trizen" "$xch/aurutils" "$HOME/aur"; do | |
| [ -d "$d" ] && caches+=("$d") | |
| done | |
| if [ ${#caches[@]} -eq 0 ]; then | |
| echo " None found - the AUR-cache part of the scan will be skipped." | |
| echo " This does NOT mean you are safe: installed packages are still checked" | |
| echo " below via pacman's database and hooks." | |
| else | |
| for c in "${caches[@]}"; do echo " using: $c"; done | |
| fi | |
| echo | |
| # --- Informational: any AUR install/upgrade since the campaign began? ----------------- | |
| # No AUR activity in the window is a good sign and activity in it is worth a look. | |
| window_hits="" | |
| log=/var/log/pacman.log | |
| echo "Note: AUR activity since the campaign began ($CAMPAIGN_START):" | |
| if [ -r "$log" ]; then | |
| events=$(awk '/\[ALPM\] (installed|upgraded)/{ | |
| ts=$1; gsub(/[][]/,"",ts); d=substr(ts,1,10) | |
| for(i=1;i<=NF;i++) if($i=="installed"||$i=="upgraded") print d, $(i+1) | |
| }' "$log" 2>/dev/null) | |
| foreign=$(printf '%s\n' "$events" | grep -Fwf <(pacman -Qmq 2>/dev/null) 2>/dev/null) | |
| last=$(printf '%s\n' "$foreign" | awk 'NF' | sort | tail -1) | |
| window_hits=$(printf '%s\n' "$foreign" | awk -v s="$CAMPAIGN_START" 'NF && $1 >= s' | sort -u) | |
| if [ -n "$window_hits" ]; then | |
| echo " Installed/upgraded AUR package(s) in the campaign window (Worth checking) :" | |
| printf '%s\n' "$window_hits" | sed 's/^/ /' | |
| elif [ -n "$last" ]; then | |
| echo " Last AUR install/upgrade: $last" | |
| echo " (Before $CAMPAIGN_START: Risk is lower but not zero (not a guarantee))" | |
| else | |
| echo " no AUR install/upgrade events found in the log." | |
| fi | |
| else | |
| echo " (cannot read $log - Skipping...)" | |
| fi | |
| echo | |
| # --- Check 1: Find the malicious dependency / its delivery mechanism ---------- | |
| echo "[1/2] Looking for the malicious dependency or its delivery..." | |
| hits="" | |
| label=" - Local scan (scriptlets, hooks, AUR caches)..." | |
| printf '%s ' "$label" | |
| phase=$( | |
| grep -rlEI "$IOC_NAMES" /var/lib/pacman/local /usr/share/libalpm/hooks /etc/pacman.d/hooks 2>/dev/null | |
| grep -rlEI "$PM_ACTION" --include=install /var/lib/pacman/local 2>/dev/null | |
| grep -rlEI "$PM_ACTION" --include='*.hook' /usr/share/libalpm/hooks /etc/pacman.d/hooks 2>/dev/null | |
| ) | |
| [ -n "$phase" ] && hits+="$phase"$'\n' | |
| if [ ${#caches[@]} -gt 0 ]; then | |
| repos=() | |
| for cache in "${caches[@]}"; do | |
| for d in "$cache"/*/; do [ -d "$d" ] && repos+=("$d"); done | |
| done | |
| total=${#repos[@]} | |
| i=0 | |
| for d in "${repos[@]}"; do | |
| i=$((i+1)) | |
| [ -t 1 ] && printf '\r%s %d/%d ' "$label" "$i" "$total" | |
| phase=$( | |
| grep -rlEI "$IOC_NAMES" "$d" 2>/dev/null | |
| grep -rlEI "$PM_ACTION" --include='*.install' --include='*.hook' "$d" 2>/dev/null | |
| ) | |
| [ -n "$phase" ] && hits+="$phase"$'\n' | |
| done | |
| [ -t 1 ] && printf '\r%s ' "$label" | |
| fi | |
| hits=$(printf '%s\n' "$hits" | sed '/^$/d' | sort -u) | |
| if [ -n "$hits" ]; then | |
| echo "FOUND (COMPROMISED):" | |
| printf '%s\n' "$hits" | sed 's/^/ /' | |
| nb_hits=$((nb_hits+1)) | |
| else | |
| echo "NOT FOUND" | |
| fi | |
| # Probably not exhaustive but nice to have, sourced from the AUR mailing list | |
| printf " - Check against known impacted AUR packages list... (Uses curl|wget, but can be skipped)" | |
| fetch="" | |
| if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then | |
| echo "SKIPPED (no curl/wget)" | |
| elif [ -t 0 ]; then | |
| echo | |
| printf ' Download from %s ? [y/N]: ' "$LIST_URL" | |
| read -r ans | |
| case "$ans" in | |
| [yY]*) | |
| if command -v curl >/dev/null 2>&1; then | |
| fetch=$(curl -fsSL --max-time 20 "$LIST_URL" 2>/dev/null) | |
| else | |
| fetch=$(wget -qO- --timeout=20 "$LIST_URL" 2>/dev/null) | |
| fi ;; | |
| *) echo " SKIPPED" ;; | |
| esac | |
| else | |
| echo "SKIPPED (non-interactive; would download $LIST_URL)" | |
| fi | |
| if [ -n "$fetch" ]; then | |
| reported=$(printf '%s' "$fetch" | tr -s ' \t\r\n' '\n' \ | |
| | grep -E '^[a-zA-Z0-9][a-zA-Z0-9@._+-]+$' | sort -u) | |
| match=$(comm -12 <(pacman -Qmq 2>/dev/null | sort -u) <(printf '%s\n' "$reported")) | |
| if [ -n "$match" ]; then | |
| echo " FOUND:" | |
| printf '%s\n' "$match" | sed 's/^/ /' | |
| echo " ^ installed AND on the reported list - you may have built a poisoned version." | |
| echo " Check the timeline note above and verify with: pacman -Qkk <pkg>" | |
| nb_hits=$((nb_hits+1)) | |
| else | |
| echo " $(printf '%s\n' "$reported" | grep -c .) names checked, no matches with installed packages." | |
| fi | |
| fi | |
| echo | |
| # --- Check 2: Look for leftovers if the payload actually ran (ioctl.fail, unverified) --- | |
| echo "[2/2] Checking for payload leftovers..." | |
| # systemd persistence fingerprint (Restart=always + RestartSec=30). | |
| printf " - Scan for systemd persistence fingerprint... " | |
| review="" | |
| for unit in /etc/systemd/system/*.service ~/.config/systemd/user/*.service; do | |
| [ -f "$unit" ] || continue | |
| if grep -q '^Restart=always' "$unit" 2>/dev/null \ | |
| && grep -q '^RestartSec=30' "$unit" 2>/dev/null; then | |
| review+=" $unit -> $(grep -m1 '^ExecStart=' "$unit" 2>/dev/null)"$'\n' | |
| fi | |
| done | |
| if [ -n "$review" ]; then | |
| echo "TO REVIEW (confirm each ExecStart is something you installed):" | |
| printf '%s' "$review" | |
| else | |
| echo "NOT FOUND" | |
| fi | |
| # Staging target: the payload fetches a cryptominer and references this path. | |
| # A file here that NO package owns is a red flag; an owned one can be verified. | |
| mwg=/usr/bin/monero-wallet-gui | |
| printf " - Check %s (cryptominer staging target)... " "$mwg" | |
| if [ -e "$mwg" ]; then | |
| if pacman -Qo "$mwg" >/dev/null 2>&1; then | |
| echo "present, package-owned (expected if you use monero-gui; verify: pacman -Qkk monero-gui)" | |
| else | |
| echo "FOUND - exists but owned by NO package (suspicious)" | |
| nb_hits=$((nb_hits+1)) | |
| fi | |
| else | |
| echo "NOT PRESENT" | |
| fi | |
| # eBPF rootkit maps, bpffs is root-only, so this can't run unprivileged. | |
| # Kept last because it is the only interactive (sudo prompt) step. | |
| ebpf_cmd="sudo sh -c 'ls -d /sys/fs/bpf/hidden_* 2>/dev/null'" | |
| printf " - Scan '/sys/fs/bpf/hidden_*' for eBPF rootkit maps... " | |
| if [ -t 0 ]; then | |
| echo "(needs root)" | |
| echo | |
| printf ' Command: %s\n' "$ebpf_cmd" | |
| printf " Run it now? (You'll be prompted for your password, but you can skip and run it yourself) [y/N]: " | |
| read -r ans | |
| case "$ans" in | |
| [yY]*) | |
| ebpf_ran=1 | |
| maps=$(sudo sh -c 'ls -d /sys/fs/bpf/hidden_* 2>/dev/null') | |
| if [ -n "$maps" ]; then | |
| printf '%s\n' "$maps" | sed 's/^/ FOUND - rootkit map: /' | |
| nb_hits=$((nb_hits+1)) | |
| else | |
| echo " NOT FOUND (checked as root)" | |
| fi ;; | |
| *) | |
| echo " SKIPPED" ;; | |
| esac | |
| else | |
| echo "SKIPPED (needs root)" | |
| fi | |
| echo | |
| echo "=============================================================" | |
| if [ "$nb_hits" -eq 0 ]; then | |
| echo "LIKELY CLEAN: no Atomic Arch payload found." | |
| [ "$ebpf_ran" -eq 0 ] && echo " NOTE: eBPF rootkit check did NOT run." | |
| [ -n "$review" ] && echo " Confirm the 'TO REVIEW' unit(s) above are yours." | |
| [ -n "$window_hits" ] && echo " Note: you updated AUR package(s) in the campaign window (see top) - stay alert." | |
| else | |
| echo "/!\\ IMPACTED /!\\: $nb_hits finding(s) above. See the FOUND lines." | |
| echo | |
| echo "This payload is a credential stealer. If it ran, act now:" | |
| echo " 1. Disconnect this machine from the network." | |
| echo " 2. Assume ALL secrets readable by you were exfiltrated. Rotate them from a" | |
| echo " DIFFERENT, clean machine: SSH/GPG keys, cloud/CI/API tokens, npm/Docker/" | |
| echo " Vault creds, browser & app sessions, and any password in shell history." | |
| echo " 3. Remove the package(s) above (sudo pacman -Rns <pkg>); verify others with" | |
| echo " 'pacman -Qkk <pkg>' and reinstall anything that fails." | |
| echo " 4. If a rootkit map or systemd unit was found, treat the host as fully" | |
| echo " compromised: back up DATA only and reinstall; a live-USB scan is safest." | |
| echo " 5. Going forward: review PKGBUILD/.install diffs before building, and" | |
| echo " prefer 'npm install --ignore-scripts' for any manual npm/bun use." | |
| fi | |
| echo "=============================================================" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment