MCPcopy
hub / github.com/lenucksi/aur-malware-check

github.com/lenucksi/aur-malware-check @5.1.0 sqlite

repository ↗ · DeepWiki ↗ · release 5.1.0 ↗
237 symbols 772 edges 14 files 9 documented · 4%
README

AUR Malware Check — June 2026 Campaign & Cross-Campaign Detection

Detection and analysis tools for the atomic-lockfile supply-chain attack on the Arch User Repository (AUR), generalized to a campaign-based architecture that handles multiple concurrent and historical attack waves (CHAOS RAT 2025, Russian spam packages, and future campaigns declared via campaigns.json).

This is a collection of all the scattered resources, especially the ones in the detection scripts Gist - they made this, I just collected this to a repo so I have it all in one place and possibly people could put up PR's instead of Gist links across multiple posts. Certainly see the source section for details on the sources!

[!TIP] Questions, support, or general discussion? Head over to Discussions. Issues are reserved for bug reports and feature requests only.

[!NOTE] This project is Python-first. The detection tool is the aur_check package (Python 3.14+, standard library only, typed, tested). The original bash scripts have been removed; their behaviour is fully covered by the Python implementation. See the source section for credits.

1600+ AUR packages compromised by attackers who injected npm install atomic-lockfile, bun install js-digest, or lockfile-js into PKGBUILD/install files. Two attack waves: 1. atomic-lockfile / lockfile-js (npm) — accounts krisztinavarga, franziskaweber, tobiaswesterburg, ellenmyklebust; arojas (impersonated legitimate maintainer — see Impersonation Clarification) 2. js-digest (bun) — accounts custodiatovar, veramagalhaes

Both deliver an infostealer and eBPF rootkit targeting developer credentials, browser data, and CI/CD secrets.

Quick Start

No installation, no dependencies — just Python 3.14+ and a checkout of this repo. Run the package directly with python -m aur_check:

# Check if you have any infected packages (all campaigns)
python -m aur_check

# List configured campaigns with their lists, windows, env vars, and refresh URLs
python -m aur_check --list-campaigns

# List campaigns in machine-readable JSON
python -m aur_check --list-campaigns --json

# Fetch latest campaigns.json from upstream, validate, show diff, write
python -m aur_check --refresh-campaigns

# Same, but only show diff without writing
python -m aur_check --refresh-campaigns --dry-run

# Add an extra package list to a specific campaign
python -m aur_check -l aur-infected=/path/to/extra_list.txt

# Check bun cache specifically (for js-digest / atomic-lockfile)
python -m aur_check --check-bun-cache

# Check yarn cache specifically (Yarn Classic v1 + Yarn Berry v2+)
python -m aur_check --check-yarn-cache

# Check pnpm store/cache specifically (global installs + metadata + dlx)
python -m aur_check --check-pnpm-cache

# Full scan with all optional checks (systemd, eBPF, npm + bun + yarn + pnpm cache)
python -m aur_check --full

# Cross-campaign: scan all installed packages regardless of install date
python -m aur_check --all-time

# CHAOS RAT (July 2025) packages are scanned automatically against their own
# window (2025-07-16..19). Override it via env vars if needed:
#   CHAOS_START_DATE=2025-07-15 CHAOS_END_DATE=2025-07-20 python -m aur_check

# Refresh all campaign package lists from their upstream sources
python -m aur_check --refresh --full

# Output scan results as JSON
python -m aur_check --json

The date window and pacman log glob can be overridden via environment variables:

```bash
START_DATE=2026-06-09 END_DATE=2026-06-12 python -m aur_check
PACMAN_LOG_GLOB='/var/log/pacman.log*' python -m aur_check

What it checks

Check / Feature Detail Source
Campaign-based architecture All campaigns declared in data/campaigns.json with per-campaign lists, date windows, env overrides, and campaign_tag labels Original addition
--list-campaigns Show configured campaigns with lists, windows, env vars, and refresh URLs Original addition
--refresh-campaigns Fetch latest campaigns.json from upstream, validate, show diff, write Original addition
--dry-run With --refresh-campaigns, show diff without writing Original addition
--json Machine-readable output for --list-campaigns and scan results Original addition
-l [CAMPAIGN_ID=]PATH Add an extra package list to a specific campaign (repeatable) Original addition
Currently installed foreign packages Batch pacman -Qmq query against the merged campaign lists, exact-match commonsourcecs fork
Date window filtering (Jun 9-12) Per-campaign install-date / log-date recency filters (override via env vars or CLI) commonsourcecs fork
Historical pacman.log scanning Scans pacman.log* for install events Kacper-Kondracki fork
Compressed log support (.gz / .xz / .zst / .bz2) Reads rotated/compressed logs Kacper-Kondracki fork
~1935 known compromised packages (live via --refresh) Bundled list per campaign, refreshable from upstream per campaign Consolidated from all sources + HedgeDoc
CHAOS RAT (July 2025) campaign chaos-rat campaign with own window and campaign_tag labels SOURCES.md (CHAOS RAT)
Russian spam packages russian-spam campaign — static list, no date window Original addition
systemd persistence check *.service units with Restart=always + RestartSec=30 Original addition
eBPF rootkit check /sys/fs/bpf/hidden_* maps (requires root) Original addition
npm cache check Packages in malicious_npm_packages.txt in npm cache / global node_modules (incl. fnm per-version globals) Original addition
bun cache check Same packages in the bun cache Original addition
yarn cache check Yarn Classic v1 + Yarn Berry v2+, incl. fnm per-version globals Original addition
pnpm store check global installs + metadata cache + dlx cache Original addition
--refresh flag Pulls live lists from each campaign's refresh URL (e.g., official Arch Linux HedgeDoc) PR #8 (drbbgh)
Configurable date window via env vars or CLI START_DATE / END_DATE / CHAOS_START_DATE / CHAOS_END_DATE env vars or --start-date / --end-date CLI flags Kacper-Kondracki fork
Color output ANSI colors in all print functions, gated by stdout.isatty() Original addition

The detection data lives under data/campaigns/ — one folder per campaign with its package lists, IOCs, and accounts. Everything is referenced by data/campaigns.json and resolved relative to the checkout.

Exit Codes

  • 0: Clean - no indicators found
  • 1: Warnings (log scan issues, missing files)
  • 2: Infected packages or artifacts detected

Package-manager cache checks

The malware is delivered via npm install atomic-lockfile, bun install js-digest, or lockfile-js. These optional flags scan each package manager's global cache and global installs for the names in malicious_npm_packages.txt:

Flag Covers
--check-npm-cache npm cache (npm cache ls) + global node_modules (npm root -g)
--check-bun-cache bun cache (bun pm cache)
--check-yarn-cache Yarn Classic v1 (yarn cache dir, yarn global dir) and Yarn Berry v2+ (global cache ~/.yarn/berry/cache, the default since Yarn 4, plus ~/.cache/yarn)
--check-pnpm-cache pnpm global installs (pnpm root -g + the global store under $PNPM_HOME/~/.local/share/pnpm), the metadata cache (~/.cache/pnpm/metadata*/<registry>/[@scope/]<pkg>.json — the metadata* version suffix varies across pnpm releases, all are scanned), and the dlx cache (~/.cache/pnpm/dlx)

--full enables all four (plus the systemd and eBPF checks).

Scope: like the npm/bun checks, this inspects the global cache only — not per-project caches. A repo opting into Berry's enableGlobalCache: false keeps its cache in a local .yarn/cache/; scan that project root directly if needed.

fnm note: fnm installs a separate Node — with its own global node_modules — per version. A malicious global install under an inactive Node version is invisible to a plain npm root -g / yarn global dir. The npm and yarn checks therefore also walk every installed version's global prefix (<fnm-dir>/node-versions/<version>/installation/lib/node_modules), honoring $FNM_DIR and falling back to ~/.local/share/fnm then ~/.fnm. (bun and pnpm are unaffected — bun keeps globals in ~/.bun and pnpm in $PNPM_HOME, both independent of fnm.)

pnpm note: pnpm's content-addressable store (<store>/v*/files, <store>/v*/index) is hash-named and does not preserve package names, so it cannot be matched by name and is deliberately not scanned (doing so would yield nothing useful). The check instead targets the name-preserving locations: global installs, the metadata cache (a hit means the package was at least resolved/fetched), and the dlx cache.

Tests

python -m unittest discover -s aur_check/tests/ -t .

Standard library only — the suite runs without an Arch system, pacman, npm or bun.

See DEVELOPING.md for the development guide: how to run, test, lint, and type-check the tool, plus the code conventions.

Repository Structure

aur-malware-check/
├── README.md                  # This file
├── DEVELOPING.md              # Development guide (running, testing, conventions)
├── pyproject.toml             # Tooling config (ruff, mypy)
├── CHANGELOG.md               # Version history
├── data/
│   ├── campaigns.json         # Campaign definitions (index — paths into campaigns/)
│   └── campaigns/             # Self-contained campaign packages
│       ├── aur-infected/
│       │   ├── packages.txt           # Compromised AUR packages (--refresh)
│       │   ├── npm-packages.txt       # Malicious npm package names (cache checks)
│       │   ├── iocs.json              # Structured IOCs (hashes, C2, persistence, eBPF)
│       │   ├── iocs.txt               # Indicators of Compromise (prose)
│       │   └── accounts.json          # Attacker accounts (tracking status)
│       ├── chaos-rat/
│       │   ├── packages.txt           # CHAOS RAT packages
│       │   ├── iocs.json              # {} — no known IOCs
│       │   └── accounts.json          # {} — no known accounts
│       └── russian-spam/
│           ├── packages.txt           # Russian spam packages
│           ├── iocs.json              # {} — no known IOCs
│           └── accounts.json          # {} — no known accounts
├── aur_check/                 # Python package (the detection tool)
│   ├── __main__.py            # CLI entry point (python -m aur_check)
│   ├── campaign.py            # CampaignConfig dataclass, load/refresh/print helpers
│   ├── scanner.py             # AurScanner: the individual checks
│   ├── merger.py              # List fetching/merging (HedgeDoc + custom lists)
│   └── tests/                 # unittest suite
├── sources/                   # Original community scripts (historical reference)
└── SOURCES.md                 # Numbered, sectioned source references

Data files: what's scanned vs. documentary

File Campaign Consumed by scanner? How it's kept up to date
data/campaigns.json — (index) ✅ Declares campaigns, their lists, windows, refresh URLs, sources --refresh-campaigns fetches from upstream; static in repo as fallback
data/campaigns/aur-infected/packages.txt aur-infected ✅ AUR package checks --refresh fetches the official HedgeDoc list
data/campaigns/aur-infected/npm-packages.txt aur-infected (as npm_lists) ✅ npm/bun/yarn/pnpm cache checks Hand-curated — no machine-readable feed exists for this campaign (see below)
data/campaigns/aur-infected/iocs.json aur-infected ❌ documentary Structured from iocs.txt on 2026-06-24
data/campaigns/aur-infected/iocs.txt aur-infected ❌ documentary Hand-curated from SOURCES.md
data/campaigns/aur-infected/accounts.json aur-infected ❌ documentary Hand-curated from SOURCES.md
data/campaigns/chaos-rat/packages.txt chaos-rat ✅ AUR package checks (own date window) Static — historical July 2025 campaign
data/campaigns/russian-spam/packages.txt russian-spam ✅ AUR package checks (no date window) Static — maintained by hand

Why the npm list isn't auto-fetched: the malicious npm package names (atomic-lockfile, js-digest, lockfile-js, …) were pulled from npm and are documented only in prose on Socket.dev / Sonatype. They are not present in OSV.dev or the GitHub Advisory Database under these names, so there is no structured feed to fetch. The list is maintained by hand with provenance in SOURCES.md §2. Re-check those sources when adding names.

Sources

This analysis aggregates information from the following sources:

Primary Reports

Source URL Content Used
IFIN Discourse https://discourse.ifin.network/t/400-aur-packages-compromised-with-infostealer-and-rootkit/577 Attack summary, links, bun/js-digest wave update (Jun 12)
ioctl.fail Analysis https://ioctl.fail/preliminary-analysis-of-aur-malware/ Detailed technical analysis, IOCs, eBPF rootkit details, C2 extraction
Arch ML: Main Thread https://lists.archlinux.org/archives/list/aur-general@lists.archlinux.org/thread/FGXPCB3ZVCJIV7FX323SBAX2JHYB7ZS4/ Master list of ~408 packages by Andre Herbst, additional reports by Rafal Lichwala, Nicolas Boichat, Damien
Arch ML: HedgeDoc Package List https://lists.archlinux.org/archives/list/aur-general@lists.archlinux.org/message/FCH7TT6IOVT7D477JKSVJALBKADAARSW/ Jonathan Grotel

Core symbols most depended-on inside this repo

_c
called by 39
aur_check/campaign.py
_in_window
called by 16
aur_check/scanners/__init__.py
exit_code
called by 14
aur_check/models.py
create_parser
called by 13
aur_check/__main__.py
_ok
called by 11
aur_check/__main__.py
_alert
called by 10
aur_check/__main__.py
extract_package_names
called by 10
aur_check/merger.py
print_section
called by 9
aur_check/__main__.py

Shape

Method 116
Function 54
Class 41
Route 26

Languages

Python100%

Modules by API surface

aur_check/tests/test_scanner.py120 symbols
aur_check/__main__.py27 symbols
aur_check/tests/test_main.py25 symbols
aur_check/tests/test_merger.py22 symbols
aur_check/models.py10 symbols
aur_check/campaign.py7 symbols
aur_check/scanners/__init__.py6 symbols
aur_check/log_utils.py6 symbols
aur_check/scanners/cache.py5 symbols
aur_check/merger.py4 symbols
aur_check/scanners/system.py3 symbols
aur_check/scanners/package.py2 symbols

For agents

$ claude mcp add aur-malware-check \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact