Two pastes was friction for a check people should run casually. Chain the fetch, the less, a typed-yes prompt and the run together, so nothing executes until the file has been shown and confirmed. The prompt takes a word rather than one keypress: input typed during less is buffered and handed straight to read on exit, which a single-key gate would happily answer itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .gitignore | ||
| aurcheck.py | ||
| fetch.sh | ||
| README.md | ||
aur-malware
Cross-references your installed AUR packages against the aur-audit malware feed and reports any hits.
The feed classifies flagged AUR packages into two tiers:
| Tier | Meaning |
|---|---|
| black | Confirmed malicious (e.g. runs a bundled binary via sudo) |
| red | Suspicious (e.g. bundled ELF with exec + network + obfuscation) |
Each package carries blackFlags / redFlags explaining why it was flagged.
Quick check
aurcheck.py is a single self-contained standard-library script, so checking a
system needs no clone and no pip install — fetch it, read it, then decide:
curl -fsSL https://git.nuke.ie/joe/aur-malware/raw/branch/main/aurcheck.py --output ./aurcheck.py &&
less ./aurcheck.py &&
printf 'Run it? [type yes] ' && read -r a && [ "$a" = yes ] &&
chmod +x ./aurcheck.py && ./aurcheck.py
Piping a downloaded script straight into a shell is exactly the habit that
gets people owned by AUR packages in the first place, so nothing executes
until you've been shown the file and typed yes at it. It's ~430 lines,
stdlib only, no shell-outs beyond pacman. Anything other than yes — enter,
n, Ctrl-C — breaks the chain, leaving the script on disk unexecuted and
still not +x.
The prompt takes a word rather than a single y keypress on purpose:
keystrokes you type while less is open sit in the terminal buffer and get
handed to read the moment it exits, so a one-key gate can answer itself if
you were mashing space to page through. Works in sh, bash and zsh.
Be honest with yourself about what this is: the shell can force you to look
at the file, but only you can decide the code is fine, and a prompt at the end
of a command you already pasted is a weak place to make that call. If you
don't intend to actually read 430 lines, stop after less and run the rest
later, or don't run it at all.
That last step fetches the current feed and reports on the AUR packages you have
installed. Exit status 0 means clean, 2 means something was flagged (see
Exit codes). Needs Python 3 and pacman; the cached
red.json / black.json are written next to the script.
Files
| File | Purpose |
|---|---|
aurcheck.py |
Main tool — fetches the feed and checks it against your packages |
fetch.sh |
Minimal helper that dumps the raw feed JSON to disk |
red.json |
Cached "red" feed (written on each aurcheck.py fetch) |
black.json |
Cached "black" feed (written on each aurcheck.py fetch) |
Usage
./aurcheck.py # check installed AUR pkgs (pacman -Qmq), fetch fresh
./aurcheck.py -c linux-devmgmt # check a specific package (takes several names)
./aurcheck.py --no-fetch # use cached red.json/black.json (offline)
./aurcheck.py -f packages.txt # check a newline-delimited package-name list
./aurcheck.py --all # dump the whole feed, ignore your package list
./aurcheck.py --status # how fresh is the audit data?
./aurcheck.py --json # machine-readable output
By default it checks only the AUR packages you actually have installed
(pacman -Qmq), so you don't wade through hundreds of irrelevant entries.
-c/--check reports on named packages whether or not they're installed —
useful before you install something. Unlike the other modes, it explicitly
prints a CLEAN line for packages that aren't in either feed, so a silent
result never gets mistaken for "not checked".
It also verifies the package exists, via the AUR RPC (rpc/v5/info), so a
typo can't come back looking clean:
NOT FOUND linux-devmgt
no such package in the AUR or your sync repos — check the spelling
CLEAN firefox
not listed in the red or black feed
? official repo package (extra), not from the AUR
BLACK lib32-sdl2_image-bin 2.8.12-1
■ Executes bundled binary 'preprocessor' with sudo privileges ...
? no longer in the AUR — package may have been deleted
A name not in the AUR is checked against your local sync db (pacman -Si,
no network) before being called missing, so official-repo packages are
identified as such rather than reported as typos. --no-fetch skips the
lookup entirely and says so (? AUR existence not verified).
In --json mode every entry carries an existence field — aur, repo
(plus repo: <name>), missing, or unverified — orthogonal to tier,
which stays the feed verdict:
[{"tier": "CLEAN", "packageName": "linux-devmgt", "existence": "missing"},
{"tier": "CLEAN", "packageName": "firefox", "existence": "repo", "repo": "extra"}]
Feed freshness
A clean result is only as good as the last time the scanner ran, so every run
prints the newest analysis date, with a warning past STALE_HOURS (24h):
feed: 101 black + 325 red | newest analysis 2026-08-01 14:19 UTC (4d 3h ago)
warning: last flagged-package analysis was 4d 3h ago — anything published
since may be unaudited ...
--status gives the full picture, including the service-wide freshness that
the flagged feeds alone can't show:
black 101 pkg(s)
published 2026-07-30 15:34 UTC -> 2026-08-01 11:51 UTC
analysed 2026-07-30 16:20 UTC -> 2026-08-01 13:59 UTC (4d 3h ago)
red 325 pkg(s)
...
all scanned
newest analysis anywhere in the feed: 2026-08-02 20:13 UTC (2d 21h ago)
STALE scanner last ran 2d 21h ago (2026-08-02 20:13 UTC)
The two dates differ on purpose. The inline note uses the red/black feeds
already in memory (no extra request), and the newest flagged package can
easily be older than the newest scan — a quiet week isn't an outage.
--status additionally fetches one page of filter=scanned (newest-first,
server caps pages at 500, so one page suffices) to get the real
"is the scanner alive?" answer. With --no-fetch it reports from cache and
says so.
Health baseline: when running, the scanner analyses a package a median 47–100 min after it hits the AUR (p90 ~140 min). Silence much beyond that means it has stopped, not that nothing was published.
Exit codes
| Code | Meaning |
|---|---|
0 |
No flagged packages found (clean); --status: feed is fresh |
1 |
-c: a named package doesn't exist anywhere; --status: feed is stale |
2 |
One or more flagged packages found |
The non-zero exit on hits makes it drop-in usable for cron/systemd timers or a pacman hook — it fails loudly only when something is actually wrong.
How it works
- Fetches
filter=blackandfilter=redfrom the API, followingnextCursorpagination, and caches each to disk. - If the network is unavailable, falls back to the cached JSON automatically.
- Builds a name→tier lookup (black wins over red if a package is in both).
- Intersects that with your installed AUR packages and prints the hits, black first, with every flag reason.
Only the Python standard library is used — no pip dependencies.
API reference
GET https://aur-audit.wtako.net/packages?filter=<black|red>&limit=<n>&cursor=<c>
Returns {"packages": [...], "nextCursor": <str|null>}. Each package object:
{
"packageName": "lib32-sdl2_image-bin",
"version": "2.8.12-1",
"aurUrl": "https://aur.archlinux.org/packages/lib32-sdl2_image-bin",
"blackFlags": ["Executes bundled binary 'preprocessor' with sudo ..."],
"redFlags": ["Binary executable found (ELF): preprocessor"],
"yellowFlags": []
}