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_checkpackage (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, orlockfile-jsinto PKGBUILD/install files. Two attack waves: 1. atomic-lockfile / lockfile-js (npm) — accountskrisztinavarga,franziskaweber,tobiaswesterburg,ellenmyklebust;arojas(impersonated legitimate maintainer — see Impersonation Clarification) 2. js-digest (bun) — accountscustodiatovar,veramagalhaesBoth deliver an infostealer and eBPF rootkit targeting developer credentials, browser data, and CI/CD secrets.
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
| 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.
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: falsekeeps 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.
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.
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
| 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.
This analysis aggregates information from the following sources:
| 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 |
$ claude mcp add aur-malware-check \
-- python -m otcore.mcp_server <graph>