#!/usr/bin/env bash
set -euo pipefail
# ============================================================
# Configuration — set before output redirection
# ============================================================
LIST_URL="https://md.archlinux.org/s/SxbqukK6IA"
SUSPICIOUS_NPM="atomic-lockfile"
CRED_DIRS=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config/gcloud" "$HOME/.kube")
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
LIST_FILE="${SCRIPT_DIR}/malicious_aur.txt"
LOGFILE="${SCRIPT_DIR}/$(date '+%Y-%m-%d-%H-%M')-aur-check.log"
# All stdout + stderr goes to terminal and log file simultaneously
exec > >(tee -a "$LOGFILE") 2>&1
# ============================================================
# Logging helper — prepends timestamp to every line
# ============================================================
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*"; }
log "Script started. Log: $LOGFILE"
# ============================================================
# Root check
# ============================================================
log "Checking root privileges..."
if [[ $EUID -ne 0 ]]; then
log "ERROR: root required"
log " Needed for: bpftool (eBPF rootkit detection), ss -p (TCP connection PIDs)"
log " Re-run as: sudo bash $0"
exit 1
fi
log "OK: running as root (EUID=0)"
# ============================================================
# Install missing dependencies
# ============================================================
log "Checking required dependencies..."
# Need at least one of: wget, curl
if ! command -v wget &>/dev/null && ! command -v curl &>/dev/null; then
log "Neither wget nor curl found — installing wget..."
pacman -S --noconfirm wget
log "OK: wget installed"
fi
# python3 — stdlib only, no external packages needed
if ! command -v python3 &>/dev/null; then
log "python3 not found — installing python..."
pacman -S --noconfirm python
log "OK: python installed"
fi
# bpftool — needed for eBPF rootkit detection in STEP 3
if ! command -v bpftool &>/dev/null; then
log "bpftool not found — installing bpf-tools..."
pacman -S --noconfirm bpf-tools
log "OK: bpf-tools installed"
fi
log "All required dependencies available."
# ============================================================
# Downloader helper: prefer wget, fall back to curl
# ============================================================
run_downloader() {
if command -v wget &>/dev/null; then
wget -qO- "$1"
else
curl -fsSL "$1"
fi
}
# ============================================================
# Temp files + cleanup on exit
# ============================================================
installed=""
known=""
matches=""
cleanup() {
[[ -n "${installed:-}" ]] && rm -f "$installed"
[[ -n "${known:-}" ]] && rm -f "$known"
[[ -n "${matches:-}" ]] && rm -f "$matches"
}
trap cleanup EXIT
installed="$(mktemp)"
known="$(mktemp)"
matches="$(mktemp)"
# ============================================================
# STEP 0: Download and parse malicious package list
# ============================================================
log "=== STEP 0: Downloading malicious package list ==="
log "Source: $LIST_URL"
log "Target: $LIST_FILE"
# Extract plain text from div#doc, strip all HTML — stdout goes directly to file (not into log)
run_downloader "$LIST_URL" | python3 -c "
import sys
html = sys.stdin.read()
start = html.find('
', start) + 1
end = html.find('
', start)
if end == -1:
sys.stderr.write('ERROR: closing not found\n')
sys.exit(1)
for line in html[start:end].splitlines():
line = line.strip()
if line:
print(line)
" > "$LIST_FILE"
if [[ ! -s "$LIST_FILE" ]]; then
log "ERROR: list is empty after download: $LIST_FILE"
exit 1
fi
log "OK: downloaded $(wc -l < "$LIST_FILE") entries -> $LIST_FILE"
# ============================================================
# STEP 1: AUR package comparison
# ============================================================
log "=== STEP 1: Collecting installed foreign (AUR) packages ==="
# '|| true' prevents pipefail exit when pacman returns non-zero (no foreign packages installed)
{ pacman -Qqm 2>/dev/null || true; } | sort -u > "$installed"
log "Found $(wc -l < "$installed") foreign packages"
log "Parsing malicious package list..."
# Strip bullets/backticks/whitespace, keep only valid Arch package name characters
# '|| true' prevents pipefail exit if grep matches nothing (e.g. list format changed)
{ sed -E \
-e 's/^[[:space:]]*[-*—][[:space:]]*//' \
-e 's/`//g' \
-e 's/[[:space:]]+$//' \
"$LIST_FILE" \
| grep -E '^[-A-Za-z0-9@._+]+$' \
| sort -u > "$known" \
|| true; }
KNOWN_COUNT="$(wc -l < "$known")"
log "Parsed $KNOWN_COUNT entries from malicious list"
# Warn if list is empty — result would be a false-negative
if [[ ! -s "$known" ]]; then
log "WARNING: known list is empty after parsing — result may be false-negative"
fi
log "Comparing lists..."
comm -12 "$installed" "$known" > "$matches" || true
log "Comparison complete"
echo
echo "Foreign packages installed: $(wc -l < "$installed")"
echo "Known malicious list size: $KNOWN_COUNT"
echo
# ============================================================
# STEP 2: Files dropped on disk
# ============================================================
log "=== STEP 2: Scanning files on disk ==="
# Check suspicious npm package — payload confirmed by Sonatype report 2026-003775
log "Checking suspicious npm package: $SUSPICIOUS_NPM"
if command -v npm &>/dev/null; then
if npm list -g --depth=0 2>/dev/null | grep -q "$SUSPICIOUS_NPM"; then
log "ALERT: globally installed npm package found: $SUSPICIOUS_NPM"
else
log "OK: $SUSPICIOUS_NPM not found globally"
fi
else
log "INFO: npm not available — skipping npm check"
fi
# Hidden files in HOME modified in last 7 days — requires manual review
log "Scanning for hidden files in HOME modified in last 7 days..."
HIDDEN_COUNT=0
while IFS= read -r f; do
log " SUSPICIOUS FILE: $f"
HIDDEN_COUNT=$((HIDDEN_COUNT + 1))
done < <(find "$HOME" -maxdepth 3 -name ".*" -mtime -7 -type f 2>/dev/null \
| grep -vE '(\.cache|bash_history|zsh_history|\.lesshst|\.python_history|\.viminfo)' \
| head -30 \
|| true)
if [[ $HIDDEN_COUNT -eq 0 ]]; then
log "OK: no suspicious hidden files found"
else
log "Found $HIDDEN_COUNT suspicious hidden files — require manual review"
fi
echo
# ============================================================
# STEP 3: eBPF rootkit detection
# ============================================================
log "=== STEP 3: eBPF rootkit check ==="
log "Listing loaded BPF programs:"
bpftool prog list 2>/dev/null || log "WARNING: cannot access BPF programs"
log "Checking pinned objects in /sys/fs/bpf/:"
ls -la /sys/fs/bpf/ 2>/dev/null || log "WARNING: /sys/fs/bpf/ not accessible"
echo
# ============================================================
# STEP 4: Credential exfiltration traces
# ============================================================
log "=== STEP 4: Credential exfiltration check ==="
# Active TCP connections with PID — requires manual review of suspicious destinations
log "Checking active TCP connections (ss -tnp)..."
ESTAB_COUNT=0
while IFS= read -r line; do
log " CONNECTION: $line"
ESTAB_COUNT=$((ESTAB_COUNT + 1))
done < <(ss -tnp 2>/dev/null | grep ESTAB || true)
if [[ $ESTAB_COUNT -eq 0 ]]; then
log "OK: no active TCP connections"
else
log "Found $ESTAB_COUNT active TCP connections — require manual review"
fi
# Credential files modified in last 7 days — requires manual review
log "Checking mtime of credential files..."
CRED_COUNT=0
for dir in "${CRED_DIRS[@]}"; do
if [[ -d "$dir" ]]; then
while IFS= read -r f; do
log " MODIFIED: $f"
CRED_COUNT=$((CRED_COUNT + 1))
done < <(find "$dir" -mtime -7 -type f 2>/dev/null || true)
fi
done
if [[ $CRED_COUNT -eq 0 ]]; then
log "OK: no recently modified credential files"
else
log "Found $CRED_COUNT modified credential files — require manual review"
fi
echo
# ============================================================
# STEP 5: Match against malicious package list
# ============================================================
log "=== STEP 5: Matching against malicious package list ==="
if [[ -s "$matches" ]]; then
log "ALERT: the following matches were found:"
cat "$matches"
echo
log "Pacman install history for matched packages:"
while IFS= read -r pkg; do
# Escape regex metacharacters (. and + may appear in Arch package names)
# Full package name + version paren — avoids false positives for short names (e.g. "go")
pkg_regex="${pkg//./\\.}"
pkg_regex="${pkg_regex//+/\\+}"
grep -E "(installed|upgraded) ${pkg_regex} \(" /var/log/pacman.log || true
done < "$matches"
echo
log "WARNING: If any package was installed between 2026-06-10 and 2026-06-12,"
log " assume compromise — rotate SSH keys, browser passwords, API tokens, cloud keys."
log "Scan completed with alerts. Log: $LOGFILE"
exit 2
fi
log "OK: no matches found in malicious package list"
log "Scan completed successfully. Log: $LOGFILE"