Skip to content

Instantly share code, notes, and snippets.

@arbaes
Last active June 13, 2026 02:29
Show Gist options
  • Select an option

  • Save arbaes/e29e68d9ed1513ddd80ae9cc4a6c9f0e to your computer and use it in GitHub Desktop.

Select an option

Save arbaes/e29e68d9ed1513ddd80ae9cc4a6c9f0e to your computer and use it in GitHub Desktop.
Atomic Arch vulnerability scan (atomic-lockfile injection checker)
#!/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