Library / SDK
lenucksi/aur-malware-check avatar
lenucksi/aur-malware-check

aur-malware-check: a supply-chain incident turned into a data-driven scanner

Detection tools for the June 2026 atomic-lockfile AUR supply-chain attack. Consolidated from community Gists.

2,194 stars48 forksPythonGPL-3.0

At a glance

What is it?
This repository began as a pile of someone else's Gist links about a compromised AUR campaign, and it has since become a Python tool that models attacks as data: a campaigns index, one directory per campaign, package lists that refresh from upstream, and scanners for packages, logs, systemd units, eBPF and language package caches. The honest detail is in the structure, including which files the scanner actually reads.
Who is it for?
Adopt this tool if you run Arch-derived systems and want to answer a specific question, whether a package you installed is on a published compromised list, whether a systemd unit or eBPF artifact from the campaign is present, or whether your language package caches hold one of the malicious names.
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 90 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 October 4, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The author says they only collected other people's work

The provenance is stated plainly, and it matters for how you read the rest. The repository is described as a collection of scattered resources, especially the ones in the detection scripts Gist, and the author says in the first person that others made this and they collected it into a repository so it would be in one place, with the hope that people would file pull requests instead of trading Gist links across posts.

The code has since moved under their own roof, and the README says so directly: the project is Python-first, the detection tool is the aur_check package, it needs Python 3.14 or newer, uses only the standard library, and is typed and tested. The original bash scripts have been removed, and the README claims their behaviour is fully covered by the Python implementation.

That claim is the one thing to check before you trust the port, and it is not checkable from the repository, because the removed scripts are gone from the tree rather than preserved under a directory. Credits live in a SOURCES.md at the root and in a per-campaign SOURCES.md inside each data package.

There is a workflow hint in the README as well: questions and discussion belong in Discussions, and issues are reserved for bug reports and feature requests.

The tool runs as a module from a checkout, with no install and no dependencies

The quick start is a set of invocations rather than an installation:

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

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

The text above them says there is no installation and no dependencies, just Python 3.14 or newer and a checkout of the repository.

The pyproject.toml supports that description and explains an omission. It contains a ruff section and a mypy section and nothing else. There is no project table, so there is no package name, no version, no dependency list and no console script entry point. The tool works because you are inside the checkout, which also means it cannot be installed into an environment and cannot be pinned as a dependency.

The configuration is strict where it can be. Ruff targets py314 with a line length of 120 and excludes a worktrees directory, implying contributors keep parallel checkouts that way. Mypy runs on Python 3.14 with strict enabled. The lint selection includes the bandit rules and the flake8-bugbear set, with two per-file exceptions: assertions and hardcoded temporary paths are allowed in the test directory, and subprocess calls with unvalidated input are allowed in the scanner modules.

Attacks are data: an index file and one directory per campaign

The architecture is the campaign, and a campaign is a directory. The index is data/campaigns.json, which declares each campaign with its own lists, date windows, environment overrides and a campaign_tag label, plus the paths into the campaign directories.

Three campaigns are present in the tree. aur-infected is the June 2026 one and it is the only one with a full data set: a package list, a supplementary package list, a file of malicious npm package names used by the cache checks, an annotated sources file, a timeline document covering the incident, attack vectors and capabilities, structured indicators with hashes, command and control and persistence information, the same indicators as prose, and a file of attacker accounts with tracking status.

chaos-rat and russian-spam are deliberately thin. Each has a package list, sources and two files whose contents are documented as empty: indicators with no known IOCs, and accounts with no known accounts.

Three commands operate on that data. --list-campaigns prints the campaigns with their lists, windows, environment variables and refresh URLs. --refresh-campaigns fetches the latest campaigns.json from upstream, validates it, shows a diff and writes it. --dry-run applies to that last one and shows the diff without writing.

So a new campaign is added by writing a directory and a line in the index, not by writing a detector.

The package list refreshes from a web document, and a second file survives the refresh

This is the part of the design that deserves the most attention, because it decides what a clean scan means.

The AUR package list for the main campaign is not static. It is fetched from an upstream source, and the repository is explicit about which one: the official HedgeDoc list for the aur-infected campaign. The merging module is described as handling list fetching and merging from HedgeDoc plus custom lists, which means the authoritative list is an editable document on somebody's web host.

A second file exists precisely to survive that. There is a packages-extra.txt described as supplementary packages that survives a refresh, and npm-packages.txt holding the malicious npm names the cache checks look for. The first campaign also keeps an iocs.json with hashes, command and control endpoints and persistence artifacts, and an accounts.json tracking the attackers.

The index file itself is described as static in the repository and used as a fallback when the upstream fetch is not available, which is the right default for a tool people may run offline.

What none of this says is where the signature on the list comes from. The refresh path fetches, validates and diffs, and the validation is a parse. A list that changes shape is caught. A list that changes content quietly is not.

Two of the three campaigns have no indicators and no accounts

The repository draws a line between files the scanner reads and files that only document the incident, and it publishes that line in a table.

The consumed files are the campaigns index, the AUR package lists, and the malicious npm package names. Everything else is documentary: the timeline, the prose indicator file, the annotated sources and the accounts files.

That distinction explains the empty files. For chaos-rat and russian-spam, the indicators file and the accounts file are recorded as having no known content, which means the campaigns exist in this tool as package lists and nothing else. There is no hash to check, no endpoint to check and no account to trace for those two.

It also explains what a scan can and cannot say. A hit on a package list is a name match against a list that came from the internet. A hit on an indicator file is a content match, and that path only has content for one of the three campaigns.

The scanner module that reads indicators is one function, check_ioc_files, sitting beside check_systemd and check_ebpf in the same file. So a machine with no files for two campaigns will report nothing from that check, and that absence is indistinguishable in the output from a clean machine.

The campaign it was built for: 1600 packages, two waves, six accounts

The incident summary in the README is the reason the tool exists, and it is specific. The June 2026 aur-infected campaign compromised more than 1600 AUR packages, with attackers injecting one of three npm or bun packages, atomic-lockfile, js-digest or lockfile-js, into PKGBUILDs and install files.

There are two waves. The first covers the npm packages and lists four accounts, plus a fifth, arojas, which the README flags separately as impersonating a legitimate maintainer under an Impersonation Clarification section. The second wave covers the bun package js-digest and lists two further accounts.

Both waves deliver an infostealer and an eBPF rootkit, and the targets are developer credentials, browser data and CI and CD secrets. That combination is why the tool checks language package caches as well as the package database, since a developer machine is the asset being targeted.

Two environment variables shape the scan window rather than the scan logic: start and end dates narrow the log window, and a log glob points the pacman log parser at rotated or compressed logs. Both appear in the quick start as one-line prefixes, which is a good way to make an override visible.

The scanner is split into focused modules and attached by monkey patch

The package layout tells you how the checks are organised, and one line in it is unusual.

The entry point is a __main__ module, and the support modules are conventional: a campaign module holding the configuration dataclass and load, refresh and print helpers, a constants module with an exit code enum, environment variable names, defaults, thresholds and paths, a merging module for list fetching, a models module for the result dataclasses, and a log utilities module that parses the pacman log with support for compressed files.

The scanner itself is a directory, and its __init__ module is described as holding the scanner class plus monkey-patched attachments. So the focused submodules are attached to the class by patching rather than by a normal import graph, and each one holds a small set of functions: package checks for the current database and the logs, system checks for systemd, eBPF and indicator files, and cache checks for npm, bun, yarn and pnpm.

The constants module owning the exit codes and thresholds is worth noting for automation, because it means the exit status is a defined value rather than a bare failure.

The test suite is a standard library unittest run, and the README states it runs without an Arch system, pacman, npm or bun, which is what makes the tool testable on a developer machine that is not the thing it is built to check.

Editorial conclusion

Adopt this tool if you run Arch-derived systems and want to answer a specific question, whether a package you installed is on a published compromised list, whether a systemd unit or eBPF artifact from the campaign is present, or whether your language package caches hold one of the malicious names. Do not treat it as a general anti-malware suite: two of its three campaigns have no indicators and no accounts recorded at all, and the package list it checks against is refreshed from an upstream web document rather than from a signed source. Before you rely on a clean result, read which data files the scanner consumes, because the repository deliberately keeps documentary files next to consumed ones, and remember the floor is Python 3.14 with no dependencies at all.

Frequently asked questions

how to use aur malware check

Clone the repository and run python -m aur_check with no installation and no dependencies beyond Python 3.14 or newer. Add --full for the optional systemd, eBPF and npm, bun, yarn and pnpm cache checks, --json for machine-readable output, and --list-campaigns to see the configured campaigns.

How does aur-malware-check decide a package is compromised?

It checks against campaign package lists held in the repository, one list per campaign, with the main June 2026 list refreshed from an upstream HedgeDoc document and a supplementary list that survives a refresh. The index file stays in the repository as a fallback when the fetch is not available.

Which campaigns does aur-malware-check know about?

Three are shipped in data/campaigns: aur-infected for the June 2026 atomic-lockfile campaign, chaos-rat for 2025, and russian-spam. The last two have empty indicator and account files, recorded as having no known IOCs and no known accounts.

Can aur-malware-check be installed as a package?

Not as written. The pyproject.toml contains only ruff and mypy configuration, with no project table, so there is no package name, version or console script entry point. The tool is run as a module from a checkout of the repository.

What did the June 2026 AUR campaign install?

The README describes two attack waves injecting npm install atomic-lockfile, bun install js-digest or lockfile-js into PKGBUILDs and install files, affecting more than 1600 packages and six listed accounts. Both waves deliver an infostealer and an eBPF rootkit aimed at developer credentials, browser data and CI and CD secrets.

Official sources

  1. Issues
  2. lenucksi/aur-malware-check on GitHub
  3. License: GPL-3.0
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/lenucksi-aur-malware-check.svg)](https://hysenlabs.com/projects/lenucksi-aur-malware-check)