User guide
Checking packages
amivuln nginx # installed package by name
amivuln /usr/sbin/nginx # the package that owns this file
amivuln openssl@3.5.1-1 # a specific version, without reading the host
amivuln pkg:rpm/fedora/nginx@1.26.2-1.fc44?arch=x86_64 # a package URL
amivuln --all # every installed package
--file PATH, --package NAME[@VERSION] and --purl PURL are explicit forms
of the same targets and can be repeated. When a name is ambiguous (for
example several kernel versions installed) amivuln fails with exit code 3 and
tells you which selector to use. It never guesses.
Reading the result
The summary prints, per package, the number of affected findings by severity, how many are in the CISA KEV catalog, the highest EPSS score, and the fixed version with the command you would run to apply it. amivuln never runs that command.
Findings the distribution could not confirm are listed apart as unverified
and are not counted with the affected ones. --strict makes them count for
--fail-on.
The last lines list every source that was consulted, its status
(ok, stale, failed, skipped), its age, and whether the result is
complete. A required source that failed makes the result incomplete: the
output says so loudly and the exit code is 2, so an outage can never look like
a clean scan.
Use in CI
amivuln --all --fail-on high # fail the job on HIGH or worse
amivuln --all --fail-on kev # fail only on actively exploited CVEs
amivuln --all --fail-on-epss 0.5 # fail on high exploitation probability
amivuln --all -f json > amivuln.json # keep a machine-readable record
To check a version for another distribution, for example a Fedora package in a
Debian-based runner, pass --distro fedora:44 or --distro arch together
with name@version. Only the ecosystem, package name and version are sent over
the network.
Offline and caching
Responses are cached in $XDG_CACHE_HOME/amivuln/cache.sqlite (default
~/.cache/amivuln). A response younger than cache_ttl (6 h) is used without
asking the network; when the network fails, one up to stale_max_age (72 h)
old is used and reported as stale. --offline never touches the network and
fails closed (exit 2) when the cache has nothing usable. amivuln cache info
and amivuln cache clear manage the cache; --no-cache skips the disk cache.
Configuration
amivuln config init writes a commented default file; amivuln config show
prints the effective configuration and amivuln config validate checks it.
Unknown keys are rejected so typos do not pass silently.
Prometheus and the node_exporter textfile collector
amivuln --all -f prometheus writes text metrics: amivuln_findings{severity,kev},
amivuln_unverified_findings, amivuln_source_up{source},
amivuln_source_age_seconds{source}, amivuln_run_complete and
amivuln_last_run_timestamp_seconds. Labels come from fixed vocabularies, so
no package name, hostname or path reaches the metrics store. Write the output
to a .prom file in the textfile directory through a temporary file and a
rename so the collector never reads a partial file.
Running on a timer
contrib/systemd/amivuln.service and amivuln.timer scan the host every six
hours and write amivuln.prom for the textfile collector, through --output
so the file is replaced atomically. The service runs as a dynamic user with no
capabilities, a read-only file system and a system call filter;
systemd-analyze security amivuln.service reports an exposure of 1.2.
sudo install -m 0644 contrib/systemd/amivuln.{service,timer} /etc/systemd/system/
sudo systemctl enable --now amivuln.timer
Adjust ExecStart and ReadWritePaths when the textfile directory differs.
Exit codes 1 (policy), 2 (incomplete) and 3 (unresolved) still write a valid
file, so the unit only fails on a usage or internal error.
Behind a proxy or a corporate CA
Set the [network] table in the configuration file:
[network]
proxy = "http://proxy.corp.example:3128" # default: HTTPS_PROXY and NO_PROXY
ca_file = "/etc/pki/ca-trust/source/anchors/corp.pem"
user_agent_suffix = "team-sre"
ca_file adds trusted roots to the built-in ones (PEM, at most 1 MiB).
user_agent_suffix is appended to the User-Agent and is the only
host-specific text that can leave the machine, only when you set it. A proxy
URL may carry credentials; amivuln never prints it. To use an internal mirror
instead, set sources.<name>.url.
For AI agents: the MCP server
amivuln mcp speaks the Model Context Protocol over stdio (newline-delimited
JSON-RPC 2.0, protocol revisions 2025-06-18, 2025-03-26 and 2024-11-05). Register
it in a client such as:
{ "mcpServers": { "amivuln": { "command": "amivuln", "args": ["mcp"] } } }
The tools are read-only and each one runs a fixed command line with -f json;
the result is the same versioned report the command line prints.
check_target:targets(1 to 20 package names,name@versionorpkg:URLs) and an optionaldistro.check_cve:idsuch asCVE-2024-6387; scans every installed package.pending_updates,needs_restartandlist_services: no arguments.
There is no shell, no file path input and no way to pass options: a target that
starts with - or holds control characters is refused. The server updates the
local cache like any other run; pass --offline or --no-cache to
amivuln mcp to restrict that. Always read complete and sources in a
result before treating an empty list as clean.
Checking an SBOM
amivuln --sbom bom.cdx.json --distro fedora:44 --fail-on high reads a
CycloneDX JSON document (any 1.x), checks every component whose purl is an
rpm, deb or alpm package and leaves the host alone. A purl whose
namespace disagrees with --distro is an error. Components that are not OS
packages (npm, PyPI, Maven and so on) are not dropped silently: they appear as
unresolved entries, one per purl type with a count, and the run exits with 3.
To gate only on OS packages, filter the SBOM first, for example with
jq '.components |= map(select(.purl // "" | startswith("pkg:rpm/")))'.
Resilient networks
Sources can have mirrors. A source that fails on its primary endpoint is asked
again on each mirror in fallback, in the order written, before it counts as
failed. Every failing call is retried with exponential backoff and jitter
(retries, default 2, at most 10); a Retry-After the server sends is honored
inside the retry budget. An endpoint that fails repeatedly trips a per-endpoint
circuit breaker and is skipped for the remainder of a run (cooldown, 30 s by
default), so a dead mirror does not stall the scan. A slow host can be slowed
down deliberately with min_interval, which spaces requests to the same host
(for example "100ms").
The fallback order is: primary endpoint, then mirrors, then the cache, then
the installed offline database (see the offline database section), then failure. sources in
every output says which of them answered, their age, and whether the result is
complete.
Webhook notifications
A [notify] section sends a compact summary after a check run: host facts,
completeness, score, totals, finding ids. It is opt-in and carries nothing
beyond what the JSON report already states.
[notify]
webhook = "https://ntfy.sh/example-topic" # any endpoint that accepts a POST
bearer_file = "/etc/amivuln/notify-token" # optional; sent as Authorization: Bearer
on = "violation" # violation (default) | incomplete | always
A failed delivery prints a warning and never changes the exit code: the policy decision was made from the scan, not from the notification.
The offline database
amivuln db moves advisory data between hosts that may reach the network and
hosts that may not. The archive is a tar.zst holding the SQLite cache and a
manifest with a SHA-256 of the database; --key and --sig add a minisign
signature over the manifest.
amivuln --all -f json # warm the cache (needs the network once)
amivuln db build --archive db.tar.zst
amivuln db verify db.tar.zst --key amivuln.pub --sig db.tar.zst.minisig
amivuln db pull db.tar.zst # verify and install for this user
amivuln db info db.tar.zst
db pull installs under $XDG_DATA_HOME/amivuln/offline and refuses an
archive older than the installed one (anti-rollback); --force overrides.
Once installed, a run with --offline answers from it and reports those
sources as stale, so an air-gapped host produces the same report shapes with
exit codes that still respect --fail-on. amivuln db verify exits 65 when
an archive is tampered; a regressive db pull also exits 65.
Signing is deliberately not built in: generate the project key with the
minisign tool and sign outside amivuln
(minisign -S -s key.sec -m manifest.json -x db.tar.zst.minisig), so the
secret key never runs through this binary.
Files no package owns
A path target that the package manager does not know (a binary copied into a
container, vendored software) is identified by its own systemd
.note.package when it has one. The package, version and packaging system
come from the file, and the report marks the package with the provenance
embedded_note. A note for another packaging system than the host’s (a deb
note on an RPM host) is refused as unresolved instead of guessed, and the file
is never executed. On Debian and Ubuntu hosts, which have no package database
to ask, the note is the only way to identify a file.
Go and Rust programs nothing owns
When a path target is not owned by a package and has no package note, amivuln
reads the Go modules (build information) or Rust crates (cargo-auditable data)
the program declares and asks OSV about each exact version. Findings show the
lowest fixed version above the installed one, and update_hint says the program
has to be rebuilt, because no package manager can update a statically linked
dependency. Files that a package owns are deliberately not expanded this way:
a distribution backports fixes into vendored code without changing versions, so
the module versions of an owned binary would produce false positives.
The OSV endpoint for this pass is [sources.osv] (required by default: a
failed pass leaves those modules unjudged, which must not look clean).
Textual severity comes from the advisory when it states one (GitHub advisories
do; RUSTSEC entries do not and report unknown).
The project minisign key
contrib/offline-db/amivuln.pub is the project minisign public key. It is
also what the nightly workflow uses to verify the database before it is
published on the release. The project’s private key lives only in the
repository secrets (MINISIGN_SECRET_KEY plus MINISIGN_PASSWORD) and never
on a developer machine, so a compromised mirror cannot fake freshness or
replay an old database past the anti-rollback timestamp.