#!/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"