lenucksi/aur-malware-check: scanning an Arch system for the June 2026 AUR campaign
Detection tools for the June 2026 atomic-lockfile AUR supply-chain attack. Consolidated from community Gists.
At a glance
- What is it?
- A Python 3.14 package that checks installed AUR packages, pacman logs and npm-family caches against campaign lists for the atomic-lockfile attack. It reads local state only, so it can tell you what is installed, not what an installer did at runtime.
- Who is it for?
- Adopt it if you run Arch or a derivative and installed AUR packages in the June 2026 window, or if you want the CHAOS RAT and russian-spam lists checked in the same pass. Skip it if you expect runtime detection: the tool reads package names, pacman log lines and cache directories, and an infostealer that already ran leaves no marker there.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 72 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 17, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
What the atomic-lockfile campaign changed about checking an AUR install
The June 2026 attack did not need a malicious binary in a PKGBUILD. According to the README, attackers injected `npm install atomic-lockfile`, `bun install js-digest` or `lockfile-js` into PKGBUILD and install files, and the README puts the number of compromised AUR packages at 1600 or more across two waves. One wave used the npm package atomic-lockfile and the lockfile-js name, tied to the accounts krisztinavarga, franziskaweber, tobiaswesterburg and ellenmyklebust, plus arojas impersonating a legitimate maintainer. The second used js-digest under the bun runtime, tied to custodiatovar and veramagalhaes. Both are described as delivering an infostealer and an eBPF rootkit aimed at developer credentials, browser data and CI/CD secrets.
That shape matters for anyone deciding whether to run this tool. The malicious artefact was a dependency fetched at build or install time, not a file committed to the repository under a recognisable name. A package can look ordinary in the AUR web interface and still have pulled a hostile npm or bun package during the install. This project is for people who want to answer one narrow question on their own machine: did any of the known campaign package names, install events or cache entries land here. It is not a general AUR reputation service and it does not inspect PKGBUILD text for suspicious commands.
How the campaign architecture works and where the data lives
Everything is declared in `data/campaigns.json`, with per-campaign package lists, date windows, environment variable overrides and `campaign_tag` labels. The detection data sits under `data/campaigns/`, one folder per campaign holding its package lists, IOCs and accounts, all resolved relative to the checkout. That layout is the reason the tool can cover the June 2026 waves, the CHAOS RAT packages from July 2025 and the russian-spam list without separate scripts: each is a campaign entry with its own window and its own refresh URL.
The scan itself is a merge-and-compare. The tool queries currently installed foreign packages with a batch `pacman -Qmq` call and matches them exactly against the merged campaign lists. Separately it scans `pacman.log*` for install events and applies the per-campaign date filter, so a package installed inside the June 9 to 12 window is treated differently from the same package installed last year. The CHAOS RAT campaign carries its own window of 2025-07-16 to 2025-07-19 and is scanned automatically against that, not against the June dates. Rotated and compressed logs in `.gz`, `.xz`, `.zst` and `.bz2` form are read, which matters on a machine that has been up long enough to rotate its log. The README states the bundled list holds around 1935 known compromised packages and can be refreshed live.
The optional checks widen the same idea to other places a hostile package leaves traces: systemd units matching `Restart=always` together with `RestartSec=30`, eBPF maps under `/sys/fs/bpf/hidden_*` (root required), and the npm, bun, yarn and pnpm caches. The yarn check covers Yarn Classic v1 and Yarn Berry v2 and later, including fnm per-version globals; the pnpm check covers global installs, metadata cache and dlx cache. Exit codes follow the Nagios-compatible scheme documented in the README, with 0 as `ExitCode.OK` for a clean result, which makes the tool usable from a monitoring wrapper.
Installing and running the AUR malware check script
There is no installation step. The README states the tool is Python-first, needs Python 3.14 or later, uses only the standard library, and runs from a checkout of the repository. The original bash scripts have been removed and their behaviour is covered by the Python implementation. So the first move is to clone the repository and run the package as a module from its directory.
The default invocation checks installed foreign packages and the log against all configured campaigns. You should see a report of matches, or a clean exit with code 0 if nothing matched.
python -m aur_checkThe `--full` flag adds the optional checks: systemd persistence, eBPF maps and the npm, bun, yarn and pnpm caches. This is the run most people want after an incident report, because a package can be uninstalled while its npm or bun cache entry remains.
python -m aur_check --fullIf you would rather compare against current upstream lists than the bundled snapshot, `--refresh` pulls live lists from each campaign's refresh URL, described in the README as including the official Arch Linux HedgeDoc. `--refresh-campaigns` goes further and fetches the latest `campaigns.json` itself, validates it, shows a diff and writes it; adding `--dry-run` shows the diff without writing. Run the dry run first if you want to see what a campaign update would change before it lands.
python -m aur_check --refresh --full
python -m aur_check --refresh-campaigns --dry-runFor scripting, `--json` emits machine-readable output for both the scan and the campaign listing. `--list-campaigns` prints the configured campaigns with their lists, windows, environment variables and refresh URLs, which is the quickest way to confirm that the June 2026 window is what you think it is.
python -m aur_check --json
python -m aur_check --list-campaignsThe date window and the pacman log glob are overridable, either through environment variables or CLI flags. The README gives `START_DATE`, `END_DATE`, `PACMAN_LOG_GLOB`, `CHAOS_START_DATE` and `CHAOS_END_DATE` as the environment variables, and `--start-date` and `--end-date` as the flags. An extra package list can be attached to a named campaign with `-l`, repeatable, which is how you fold in a list from a forum post without editing the repository data.
START_DATE=2026-06-09 END_DATE=2026-06-12 python -m aur_check
python -m aur_check -l aur-infected=/path/to/extra_list.txtOne more flag deserves a warning rather than a tutorial. `--scan-all-homes` walks every user's home directory and the README marks it as root only and opt-in. On a shared machine that is a meaningful amount of filesystem traversal, so decide deliberately before running it under sudo.
The gap between a clean scan and a clean machine
The tool answers a question about artefacts, and the campaign it targets is described as delivering an infostealer and an eBPF rootkit. A package name match, a pacman log line in the window, or a cache entry is evidence. Their absence is not proof that nothing ran. If a hostile npm or bun package executed during a build, harvested credentials and cleaned up, a later scan of package lists and caches has nothing to find. The README does not claim runtime detection, and the checks it lists are all filesystem and log inspections.
Some checks are also conditional in ways that are easy to miss. The eBPF map check reads `/sys/fs/bpf/hidden_*` and requires root, so a non-root run silently cannot perform it. The systemd check looks for a specific combination, `Restart=always` with `RestartSec=30`, which will miss persistence configured with different values or through another mechanism. The date-window filter depends on pacman log retention: on a system whose logs have been rotated away or pruned, an install from June 2026 may simply not be visible, and the compressed-log support only helps if the rotated files still exist. `--all-time` exists for exactly that reason, scanning all installed packages regardless of install date, but it trades precision for coverage and will flag anything that matches a campaign list whenever it was installed.
There is also the question of what the campaign lists cover. The README describes them as consolidated from community sources and a HedgeDoc, refreshed from upstream. A package compromised after your last refresh will not appear. That is an argument for running with `--refresh`, not an argument against the tool.
How it differs from a general rootkit scanner
The obvious alternative is a general-purpose scanner such as chkrootkit or rkhunter, and the difference is one of scope rather than quality. Those tools look for known rootkit binaries, suspicious file permissions and modified system binaries across the whole machine. They have no knowledge of which AUR package names were involved in a specific campaign, no concept of an install date window, and nothing to say about an npm or bun cache directory. Running rkhunter tells you whether common rootkit signatures are present; it will not tell you that a package you installed on 2026-06-10 appears in the atomic-lockfile list.
This project inverts that. It is narrow and current: campaign lists, exact package name matching, install windows, package-manager caches. It is also Arch-specific in a way a general scanner is not, because its primary data source is `pacman -Qmq` and its primary log is `pacman.log`. On a Debian or Fedora machine there is nothing for it to read. The two approaches are complementary, and the README's own design acknowledges the boundary by keeping the scope to AUR and npm-family artefacts rather than attempting host-wide integrity checking.
Licence, maintenance and the cost of keeping campaign data fresh
The project is GPL-3.0. If you run it as a local diagnostic on your own machine, the licence obligations are minimal in practice. If you intend to bundle it into a product, redistribute a modified version, or ship it inside a managed image, the copyleft terms apply to the distributed work and are worth reading in full rather than inferring from this paragraph. Nothing here is legal advice.
The last push to the repository was on 2026-07-07. The repository is not archived, but two months of quiet after an incident-response tool is worth noticing: the bundled campaign lists are a snapshot from that date, and the refresh flags exist precisely because the data goes stale. There are no retrieved releases, so there is no versioned upgrade path to follow; you update by pulling the repository again. The practical cost of ownership is therefore not installation but data currency. Every scan is only as good as the campaign lists it compares against, and the two ways to keep them current are `--refresh` for the package lists and `--refresh-campaigns` for the campaign definitions themselves.
The README points discussion at the repository's Discussions section and reserves issues for bug reports and feature requests, which is a reasonable signal about where the maintainer wants traffic. The SOURCES.md file credits the community forks and gists the detection logic was consolidated from, including the commonsourcecs fork for the installed-package query and date filtering, the Kacper-Kondracki fork for historical log scanning and compressed log support, and PR #8 for the `--refresh` flag.
Editorial conclusion
Adopt it if you run Arch or a derivative and installed AUR packages in the June 2026 window, or if you want the CHAOS RAT and russian-spam lists checked in the same pass. Skip it if you expect runtime detection: the tool reads package names, pacman log lines and cache directories, and an infostealer that already ran leaves no marker there. Before trusting a clean result, run python -m aur_check --list-campaigns to see which lists and date windows are actually active, then python -m aur_check --refresh --full so the comparison uses current upstream data rather than the bundled snapshot.
Frequently asked questions
How do I use aur-malware-check?
Clone the repository, make sure Python 3.14 or later is available, and run python -m aur_check from the checkout. Add --full for the systemd, eBPF and package-cache checks, or --refresh --full to pull current campaign lists before scanning.
How to check if an AUR package is safe?
aur-malware-check compares your installed foreign packages against per-campaign lists of known compromised names and filters by install date, so it can flag a package installed during the June 2026 window. It matches known campaign names only; it does not read PKGBUILD contents to judge an unfamiliar package.
How do I check to see if I have malware?
For this campaign, run python -m aur_check --full, which scans installed packages, pacman logs, systemd units with Restart=always and RestartSec=30, eBPF maps under /sys/fs/bpf/hidden_* and the npm, bun, yarn and pnpm caches. The eBPF check needs root, and a clean result means no matching artefact was found, not that nothing executed.
How can I detect malware on Linux with aur-malware-check?
The tool reads local state: pacman -Qmq output, pacman.log entries including compressed rotated logs, and package-manager cache directories. It uses the Nagios-compatible exit code scheme, so exit code 0 means clean and a non-zero code can drive a monitoring check.
Community notes