Skip to content

Add Arch Linux package scanning with the Arch security tracker - #9

Merged
Fazmin merged 3 commits into
Fazmin:mainfrom
pkilar:feat/arch-linux-scan
Nov 3, 2026
Merged

Fazmin merged 3 commits into
Fazmin:mainfrom
pkilar:feat/arch-linux-scan

Conversation

@pkilar

@pkilar pkilar commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Why

Host and image scans read dpkg, apk and rpm databases and match them through OSV.dev. OSV has no Arch Linux ecosystem, so an Arch machine or an archlinux image got no OS package coverage at all.

Approach

Arch packages are matched against the Arch Linux Security Tracker, the distribution's own advisory source. The whole list (~1 MB, ~2,400 advisory groups) is downloaded every 6 hours and matched on the machine, so no package names are sent anywhere. Version order follows pacman exactly: vercmp is a port of libalpm's alpm_pkg_vercmp/rpmvercmp, checked against the real vercmp binary on 34 cases.

Decisions worth reviewing:

  • Outdated "Vulnerable" groups. The tracker leaves many advisories open long after upstream fixed them (an OpenSSL 1.1.1o issue still open against 3.6.5, kernel 5.15 issues against 7.2). Matching like arch-audit (every unfixed group, at any version) produced 27 findings on a current Arch desktop, nearly all out of date. An unfixed group now counts as a finding only up to the version the tracker confirmed it in. Past that it is listed as not checked, with the advisory IDs, so nothing is hidden. One consequence: --strict counts those as gaps.
  • Tracker outage fails the scan, like an OSV outage. Otherwise --fail-on would pass with every Arch package unchecked.
  • Derivatives (Manjaro, EndeavourOS, CachyOS, Garuda, Artix, SteamOS) are matched against the same tracker, with a note that packages they build themselves can differ from Arch's.
  • Matched by binary package name (%NAME%), not by pkgbase, because that is how the tracker files advisories (poppler-glib, libcurl-gnutls, kube-apiserver).

How it works

  • Inventory: ospkg.rs reads var/lib/pacman/local/*/desc, using libalpm's section rules, for host scans and for image layers (image/layers.rs keeps only the desc files, and whiteouts of upgraded package dirs apply). Each package database now keeps its own packages and path. pacman packages are matched only under Arch Linux, and dpkg/apk/rpm packages only under OSV ecosystems. A database that fits neither is listed as not checked.
  • Matching: the new archlinux.rs holds vercmp, the tracker index and the matcher. scan_inventory sends Arch packages to it while the OSV batch runs, and its rows have the same shape as OSV rows, so findings, ranking, KEV/EPSS enrichment, SARIF and history work unchanged. Findings carry sources: ["arch_tracker"].
  • Other inputs: pkg:alpm/arch/... purls (not MSYS2's) and name:Arch Linux:version lines are mapped to the Arch ecosystem.
  • Output:
    • One format::advisory_url sends CVEs to NVD, AVG/ASA IDs to security.archlinux.org and everything else to OSV. SARIF, CycloneDX, Markdown, attack chains and the desktop app use it.
    • Text, Markdown and host-preview output now escape control characters and bidi overrides from package databases, image metadata and advisories, using format::clean_text, moved out of tls.rs.
  • Apps: the AUR packages for Chrome, Edge, Brave and VS Code are recognised and checked with NVD.
  • Catalog and docs: arch-security is added to the source catalog (now 41 sources). README and SECURITY.md describe the behaviour and the data flow.

Testing:

  • CI's commands: cargo fmt --check, clippy with -D warnings, 283 core tests plus the server tests, and cargo check -p cve-intel-desktop.
  • New tests cover vercmp, the tracker index and matcher, the undecided rule, a tracker failure, pacman desc parsing, per-database matching, an Arch image with an upgraded package, purls, and terminal escaping. The escaping tests fail with escaping disabled.
  • Live checks:
    • On an Arch host, scan host read all 2,042 installed packages.
    • A simulated tracker outage now exits 1; it used to exit 0.
    • A pasted pam:Arch Linux:1.7.0-2 gives CVE-2025-6020, linked to security.archlinux.org/AVG-2901 in SARIF.
  • Not covered: a real archlinux image (only the unit test above), and the desktop GUI was not opened.

Links

OSV has no Arch Linux ecosystem, so Arch packages were never checked.
Host and image scans now read pacman's local database
(/var/lib/pacman/local/*/desc) and match it against the Arch Linux
Security Tracker. The whole list (~1 MB) is downloaded every 6 hours and
matched locally with a port of pacman's vercmp, so package names never
leave the machine. pkg:alpm/arch purls and `name:Arch Linux:version`
lines work in dependency scans too.

- Arch derivatives (Manjaro, EndeavourOS, CachyOS, Garuda, Artix,
  SteamOS) are matched with a note that their own builds can differ.
- An unfixed advisory confirmed only in an older version than the one
  installed is listed as not checked, not as a finding: the tracker
  leaves many open long after upstream fixes them.
- An unreadable tracker fails the scan, like an OSV outage, so a gate
  cannot pass with every Arch package unchecked.
- Each package database is matched only under its own ecosystem and
  reports its own path.
- Text, Markdown and host-preview output escape control characters and
  bidi overrides from package databases, images and advisories.
- AVG/ASA advisories link to security.archlinux.org in SARIF, CycloneDX,
  Markdown and the desktop app.
- AUR names for Chrome, Edge, Brave and VS Code are checked with NVD.

@Fazmin Fazmin left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Paul, this is a great addition! Arch users had no OS package coverage at all, and doing the matching locally against Arch's own tracker is exactly the right call. I really like the attention to detail: the vercmp port matches libalpm on every tricky case I could think of (epochs, 1.0a vs 1.0, leading zeros, huge numbers), the layer handling and whiteouts look solid, and the row shape means KEV/EPSS, SARIF and history just work. The write-up of the "undecided" decision was really helpful too.

A few things I'd like sorted before merging:

1. rpm -qa is now skipped on non-Arch hosts too (host/linux.rs ~L50). The new !rpm_ecosystems.is_empty() check is meant for Arch, but scan.ecosystems is also empty on any rpm distro OSV doesn't cover (SLES/SLED 15, for example, which hits the rpm_unread path with its NDB database). Before this PR, rpm -qa still ran there, so third_party_apps found Chrome, Edge and VS Code and checked them against NVD. Now they quietly drop out of the scan. Gating on "not Arch" (!scan.ecosystems.iter().any(|e| e == archlinux::ECOSYSTEM)) would match what the comment says.

2. An empty tracker response fails open (clients.rs ~L1100). The only check is is_array(), so a [] (or a schema change, like a renamed packages key) becomes an empty index that's cached for 6 hours. Every Arch package then scans clean and --fail-on passes, which goes against the PR's own "an outage fails the scan" rule. Could we refuse an empty or implausibly small index (the tracker has ~2,400 groups) before caching it? It'd also be worth versioning the cache key (arch:tracker:v1, like kev:catalog:v2), since it stores the derived index and a later format change would otherwise read the old shape.

3. A pkgrel-only rebuild hides a confirmed finding (archlinux.rs ~L260). The undecided test compares the full version-release, so if AVG-xxxx was confirmed in pam 1.7.0-2 and Arch rebuilds it as 1.7.0-3 without a patch (a soname mass rebuild, say), the finding moves to "not checked" and history shows it as removed. Comparing only epoch:pkgver for that test would keep it, and still handle the cases you were after (1.1.1o vs 3.6.5, 5.15 vs 7.2).

4. Use the large-download path for the tracker (clients.rs ~L1099). It goes through get_json, so it gets the normal per-request timeout (30 s, or as low as 5 s with REQUEST_TIMEOUT), and the 10 MB cap is only checked after the whole body is buffered. Now that a tracker outage fails the whole scan, a slow link would fail it too. get_large_bytes (streaming cap, 180 s) is what the other whole-feed downloads use.

5. The image preview still says "checked in OSV as Arch Linux" (image/mod.rs ~L501 and index.html ~L1968). The host preview got the new wording, this one didn't.

Smaller things, none blocking:

  • image::preview_text (scan image --list) doesn't go through clean_text, so the image name, PRETTY_NAME, the new derivative note and the reasons are printed raw. The PR says image metadata is now escaped.
  • The tracker's AVG name and ASA IDs aren't checked with is_advisory_id (CVE IDs are), and the AVG name becomes vuln_id and ends up in URLs and Markdown links. It would need a compromised tracker, but it's an easy filter.
  • tokio::join! in scan_inventory waits for the whole OSV batch even when the tracker has already failed. try_join! would fail fast.
  • --strict will exit 1 on almost every current Arch host, because the stale "Vulnerable" groups land in not_checked. You flagged this already. I wonder if a separate "undecided" bucket would keep --strict meaning "couldn't check". Happy to leave it as a follow-up.
  • For pasted or SBOM input only: a versionless pkg:alpm/arch/x compares as older than everything, so every group ever filed for that package shows up as a finding, fixed ones included. And a version without pkgrel compares equal to a pkgrel-only fix (a false negative). Host scans aren't affected, since desc always has a pkgrel.
  • IDs are sorted as strings, so AVG-1000 comes before AVG-999.
  • I'm not 100% sure, but I think SteamOS 3 keeps its database under /usr/lib/holo/pacmandb. If so, nothing gets read there and there's no not-checked row, even though the README lists SteamOS.
  • The README still says "lists all 40 sources" in one place, and there's a "1 packages" plural.
  • Not from this PR, but worth knowing: the MCP tools check_package_vulns and find_fixed_version with ecosystem "Arch Linux" still query OSV and come back with 0, which looks clean. That could be a nice follow-up now that the tracker is wired in.

Since #8 and #5 have landed, the next push will run CI against the current main as well. There are no conflicts, it just needs a fresh run.

Really nice work. Looking forward to the next round!

- Refuse a tracker list with under 1,000 usable groups before caching
  it, so an empty or renamed-field response fails the scan instead of
  passing every package. Version the cache key (arch:tracker:v1).
- Download the tracker through the streaming large-feed path (16 MB
  cap, long timeout) instead of get_json.
- Keep a finding when only the pkgrel moved past the confirmed
  version; only a newer pkgver is undecided.
- Pasted or SBOM input: a version without a pkgrel counts as older
  than a fix in a later release of that pkgver, and a package with no
  version is listed as not checked instead of matching every group.
- Accept only AVG/ASA-shaped IDs from the tracker; sort IDs by number.
- Fail fast with try_join! when either OSV or the tracker fails.
- Run rpm -qa again on rpm hosts OSV does not cover (SLES), skipping
  it only on Arch, so third-party apps are still found there.
- Read SteamOS's pacman database (/usr/lib/holo/pacmandb/local) on
  hosts and in images; list an Arch-family OS with no database as not
  checked.
- Image preview: tracker wording for Arch, escape all dynamic text.
- Fix "1 packages" and the stale source count in the README.
@pkilar

pkilar commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review! All five are fixed in 13414dd, along with most of the smaller notes.

  1. rpm -qa is skipped only on Arch now, so SLES and other rpm hosts OSV doesn't cover get their third-party apps back. The decision is a small tested helper.
  2. tracker_index refuses a list with fewer than 1,000 usable groups (a valid AVG ID plus packages; the live list has 2,444) before anything is cached, so [] or a renamed packages/name field fails the scan. The cache key is arch:tracker:v1.
  3. The undecided test compares epoch:pkgver only, so a pkgrel-only rebuild stays a finding. On my machine this turned lua51 5.1.5-13 (AVG-1302, confirmed in 5.1.5-9) into a finding.
  4. The tracker goes through get_large_text (16 MB streaming cap, long timeout).
  5. The image preview, CLI and desktop, now names the Arch tracker.

Smaller ones: image::preview_text escapes everything; tracker names must be AVG-n and advisories ASA-yyyymm-n; try_join! fails fast; AVG IDs sort by number; a versionless alpm package is listed as not checked; a version without a pkgrel counts as older than a fix in a later pkgrel of the same pkgver. You were right about SteamOS (DBPath = /usr/lib/holo/pacmandb/): host and image scans read it now, and an Arch-family OS with no pacman database gets a not-checked row. The README count and the "1 packages" plural are fixed.

I left the --strict "undecided" bucket and Arch support in check_package_vulns/find_fixed_version for follow-ups, as you suggested. I also merged main, so CI runs against it.

@Fazmin
Fazmin merged commit 8690257 into Fazmin:main Nov 3, 2026
5 checks passed
@Fazmin

Fazmin commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Merged, thanks Paul! That was a really quick and thorough turnaround. Every point is covered and there are tests for all of it. The lua51 one you found on your own machine is a nice proof that the rebuild change was worth making, and thanks for checking the SteamOS path too.

Arch users finally have proper OS package coverage now, which is great to have. The --strict bucket and the MCP tools are both good follow-ups whenever you get to them, no rush.

Looking forward to the OAuth PR for the CLI!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants