Nixploit is a Nix vulnerability scanner. It checks packages in a Nix closure against NVD, OSV, and optional VulnCheck data.
It reads NVD's CPE records alongside affected-version data from CVE Numbering Authorities (CNAs), so advisories can be matched before they receive CPE enrichment. Updates build a local database, and scans run offline.
From a checkout, build the scanner and scan the running NixOS system.
nix build
./result/bin/nixploit update
./result/bin/nixploit scan --systemThe remaining examples assume nixploit is on your PATH. You can use
nix shell . from the checkout to get a shell with it available.
The first update imports every NVD archive from 2002 through the current year.
Unchanged archives are skipped on later updates. Use --from-year for a smaller
download at the cost of older advisory coverage.
If the current year's archive is not published yet, an update warns and keeps the available years. It retries that year on the next update. This exception only covers a missing or empty archive that has never been cached. Older years and previously cached archives must still be available.
nixploit update --from-year 2026
nixploit statsstats shows database coverage and feed check times. The cache lives in
$XDG_CACHE_HOME/nixploit, or ~/.cache/nixploit if unset. Pass --cache-dir to
use a different directory.
Scan a build result, include its build dependencies, or check a package by name and version without querying Nix.
nixploit scan ./result
nixploit scan --build-deps ./result
nixploit scan --package libheif@1.21.2Store outputs include their runtime closure by default. --no-requisites limits
collection to the named paths. Explicit .drv inputs inspect those derivations,
with dependencies included only when --build-deps is set.
You can also save an inventory and scan it elsewhere.
nixploit inventory --system > inventory.json
nixploit scan --inventory inventory.json --json > report.jsonGenerated inventories leave system unset because a derivation records its
build platform. Set a package's system in a saved inventory only when its
output platform is known, for example aarch64-linux.
affectedmeans the advisory's rules include the installed version.unknownmeans the product matched, but its identity, version range, platform, or configuration requirements couldn't be established. Vulnerabilities that NVD tags as disputed also land here, markedDisputed.suppressedholds findings covered by patches or ignore rules.
A match still needs review against your build and configuration. A clean report
only covers the advisories you've imported. JSON includes all three arrays,
feed coverage, missing derivations, and paths without usable versions. Text
output hides suppressed findings unless you pass --show-suppressed.
The exit code is 1 when there are unsuppressed affected findings, 0 when
there aren't, and 2 for an error. Unknown findings don't change the exit code.
Use --kev-only to limit results to CVEs marked as known exploited by CISA in
the feed.
NVD is the default provider. To use VulnCheck NVD++, set VULNCHECK_API_TOKEN in
the environment and update that provider.
nixploit update --provider vulncheckVulnCheck's generated CPEs only count for products NVD's analysis of the CVE doesn't place, and are dropped when the assigning CNA excludes the version.
The OSV provider downloads OSV's public per-ecosystem dumps for PyPI, npm,
crates.io, and Go. These carry GHSA, PYSEC, RUSTSEC, and Go advisories with
upstream package versions, including ones that never received a CVE. It also
downloads the GIT dump, which gives the commits introducing and fixing a CVE in
its upstream repository. Pass --ecosystem to refresh only some of them.
nixploit update --provider osv
nixploit update --provider osv --ecosystem PyPI --ecosystem GITOSV claims only match packages built by the matching nixpkgs language builder,
or Python packages named with a python3.x- prefix. Go modules match through
the package's GitHub, GitLab, Codeberg, or Bitbucket source URL.
Each provider's records are stored separately, so updating one doesn't remove another's claims. A scan merges every record that names the same vulnerability, so a CVE's NVD, VulnCheck, and OSV claims appear in one finding. Local JSON, gzip, and ZIP feeds can be imported without network access.
nixploit import nvdcve-2.0-2026.json.gz
nixploit import --provider vulncheck nist-nvd2.zip
nixploit import --provider osv PyPI-all.zipPass a TOML file with nixploit scan --config config.toml. It accepts package
aliases and ignore rules.
Matching normalizes case, spaces, hyphens, and underscores, then compares exact product names. Nix package names sometimes need an alias to match the upstream product. A vendor can also be supplied to distinguish unrelated products with the same name.
Names inferred from Nix store paths also match without a Python or Perl version
prefix. For example, python3.13-cryptography matches cryptography, while the
report retains the original package name.
[aliases]
my-openldap = [{ product = "openldap", vendor = "openldap" }]
my-linux = [
{ product = "linux", vendor = "linux" },
{ product = "linux_kernel", vendor = "linux" },
]Aliases for linux, linux-bunker, linux-hardened, linux-zen, linux-rt,
linux-libre, linux-xanmod, and linux-lqx are built in. Other kernel package
names need both upstream product aliases, as above. Chromium, ungoogled-chromium,
and google-chrome match NVD's chrome product, whose versions follow Chromium's.
An alias with a vendor drops claims from other named vendors for that product,
so Red Hat Linux advisories never reach the kernel. Unresolved vendor collisions
stay in unknown.
Claims about distribution packages, such as Red Hat's per-release entries, only
count when nothing describes the upstream release. When the assigning CNA's own
version data settles the installed version, contributor claims such as CISA's
ADP restatements are ignored, and NVD CPE matches it contradicts move to
unknown. When NVD places a product only in another language collection,
same-named CNA claims are read as that product too. Records that give each
release line its own upper bound count a release past its own line's bound as
fixed.
Ignore rules need a package, vulnerability id, bucket, and review reason. The id
is the finding's CVE, or its OSV identifier when no CVE exists. For an unknown
finding, copy its fingerprint from the report into the rule. Replace the
placeholder below with the full 64-character value.
[[ignore]]
package = "example-package"
id = "CVE-2026-12345"
bucket = "unknown"
reason = "Reviewed the advisory against our package configuration"
fingerprint = "paste the fingerprint from the finding"
until = "2026-12-01"The fingerprint ties the rule to the reviewed evidence. If that evidence
changes, the finding appears again. Affected rules can pin a fingerprint too.
Use version to restrict a rule to one installed version, and until to expire
it at the start of that date in UTC. Optional ranges must match the report's raw_ranges.
Patch filenames containing a CVE identifier suppress that CVE for the
derivation, following vulnix's convention. So do CVE identifiers inside patches
present in the store, which covers release-branch patches such as glibc's.
These findings remain in the report's suppressed array.
Given the kernel's Kbuild files and .config, nixploit suppresses kernel
findings whose fix touches only files the configuration never compiles.
[kernel]
output = "/nix/store/...-linux-7.2.0"
build = "/nix/store/...-linux-kbuild-7.2.0"output is the kernel store path the build applies to, and build is a
directory holding the patched tree's Kbuild and Makefile files plus
.config. The NixOS module fills this in for the running kernel.
Advisory ranges often name only the release that first shipped a fix, while a
release branch took it earlier. nixploit history takes the same arguments as
scan and keeps a commit-only clone of each upstream repository that the GIT
dump names for an affected finding. It needs git and network access.
nixploit history --system
nixploit scan --systemA scan then looks up the tag naming the installed version and suppresses the
finding when a fixing commit is in that tag, when the tag's branch carries a
commit with the fix's author and subject or a cherry picked from trailer
naming it, or when no introducing commit reaches the tag. Scans never fetch.
Kernel repositories are skipped because kernel records already list stable
backports. A repository that fails to sync leaves its findings unsuppressed and
makes nixploit history exit with 1.
Add this flake to your configuration as inputs.nixploit, then import its
module to schedule scans.
{ inputs, ... }:
{
imports = [ inputs.nixploit.nixosModules.default ];
services.nixploit = {
enable = true;
calendar = "daily";
};
}The service updates the feeds and scans the running system's runtime closure.
Set buildDependencies = true to include build dependencies, fromYear to
limit NVD coverage, nvdMirror and osvMirror to fetch NVD archives and OSV
dumps from mirrors, and settings for the aliases and ignore rules above.
kbuild.enable turns on the kernel configuration filter, and history.enable
runs nixploit history before each scan. These options live under
services.nixploit. The timer persists across reboots and adds up to 15
minutes of jitter.
Start a scan immediately with sudo systemctl start nixploit. The last completed
report is saved at /var/lib/nixploit/report.json, readable by root and the
nixploit user. Failed updates still allow a scan against the cache. Failed
scans preserve the last completed report and its finding metrics.
Set providers to refresh more than NVD, for example [ "nvd" "osv" ], and
ecosystems to limit which OSV dumps are downloaded. For VulnCheck, add
"vulncheck" to services.nixploit.providers and set
services.nixploit.tokenFile = "/run/secrets/vulncheck-token". The file contains
only the API token. Systemd loads it as a credential, and the service passes it
to the update process. Keep the token out of Nix expressions, which become
readable store files.
The module enables node_exporter's textfile collector and writes metrics to
/var/lib/nixploit/metrics. Have Prometheus scrape that node_exporter instance.
For a local Prometheus server, the configuration can look like this.
services.prometheus.scrapeConfigs = [
{
job_name = "node";
static_configs = [{ targets = [ "127.0.0.1:9100" ]; }];
}
];On the Grafana host, import the module and enable
services.nixploit.dashboard.enable alongside your Grafana service. The scanner
doesn't need to run on that host. You can also import the
dashboard manually.
Alert rules cover failed jobs, stale data, and affected known-exploited CVEs. Load them on the Prometheus host and adjust the freshness thresholds if you change the daily schedule.
services.prometheus.ruleFiles = [ "${inputs.nixploit}/monitoring/alerts.yml" ];Outside the module, --prometheus-file writes metrics for node_exporter after a
completed scan, including scans that return 1. The destination directory must
exist. An error leaves the previous metrics file intact.
Invalid CVSS scores are ignored when choosing a score. Findings without a valid
score remain in their original bucket and appear as unscored in metrics.
nixploit scan --system --prometheus-file /var/lib/nixploit/metrics/scan.promnix develop --command cargo build --release
nix develop --command cargo clippy --all-targets -- -D warnings