Vulnerability scanning
Two complementary scanners are installed via modules/hardening.nix: vulnix and trivy.
The module also defines the sec-scan, trivy-scan, and sec-update shell aliases, and the Justfile provides vuln-nix, vuln-repo, and vuln recipes.
vulnix — Nix-closure CVE scanner
vulnix walks the Nix store closure of a derivation (or the current system profile) and cross-references every package against the NVD CVE database.
Because it understands Nix derivations it reports the exact store path where a vulnerable package lives.
# scan the current home-manager profile
vulnix --profile ~/.nix-profile
# scan the runtime closure of a built flake output (e.g. the philipp configuration)
vulnix -C "$(nix build --no-link --print-out-paths .#homeConfigurations.philipp-linux.activationPackage)"
vulnix fetches and caches the NVD archive automatically on first use; there is no separate update command.
To force a refresh, clear ~/.cache/vulnix.
There is no --home flag: use -p/--profile for a profile or -C/--closure for a built output path.
Whitelisting a known-acceptable CVE (e.g. when there is no fix yet):
# append a rule to the repo's whitelist.toml
cat >> whitelist.toml <<'EOF'
["somepackage"]
cve = ["CVE-2099-12345"]
comment = "no upstream fix available"
until = "2027-10-01"
EOF
vulnix --whitelist whitelist.toml --profile ~/.nix-profile
The quoted section header is the selector: ["pname"] matches every version of that package, and the rule masks only the CVEs it lists, so an unlisted CVE is still reported.
Omit cve to mask every advisory for the selector.
whitelist.toml records the triaged entries and is passed by both just vuln-nix and the Crow job below.
trivy — filesystem / repo scanner
trivy scans source trees, lock files, and container images for known CVEs, misconfigurations, and secret leaks.
It covers language-specific manifests (package-lock.json, requirements.txt, Cargo.lock, etc.) that vulnix does not.
# scan this repo's source tree for CVEs and secrets
trivy fs .
# same, but only report HIGH and CRITICAL findings
trivy fs --severity HIGH,CRITICAL .
# scan for misconfigurations (e.g. overly permissive file permissions)
trivy fs --scanners misconfig .
# scan a container image pulled from a registry
trivy image nixos/nix:latest
Automated scanning (Crow)
.crow/vulnerability-scanning.yaml runs both scanners automatically — on a daily cron and on every push to the default branch.
- vulnix scans the
philipp-linuxhome-manager closure (.#homeConfigurations.philipp-linux.activationPackage). The step runsvulnix -w whitelist.toml. vulnix exits 0 when clean, 2 when advisories remain, and 3 on error; the step treats 0, 1, and 2 as success so the dashboard still updates, failing only on 3 (error). Exit 1 requires--show-whitelisted, which the step does not pass. If the closure fails to build, the report records the build log and the pipeline still publishes the trivy results. - trivy scans the repository filesystem (
trivy fs) using the custom template atassets/trivy-markdown.tpl, which renders vulnerabilities and misconfigurations as Markdown tables. - Both reports are concatenated and published to a single Vulnerability dashboard issue — the pipeline creates the issue if none exists, or patches the body of the existing open issue otherwise.
The step writes the JSON body to
payload.jsonand sends it withcurl --data-binary @payload.json. That lets reports larger than the ~128 KiB Linux exec argument limit publish, where inline-d/--datatrippedcurl: Argument list too long.
Validate the dashboard step without contacting a forge with scripts/tests/test-vulnerability-dashboard-payload.sh.
It extracts the live step, runs it against curl stubs with a report over 200 KiB, and asserts curl receives a file reference rather than an inline body.
Creating the dashboard cron job
The cron job cannot be defined in the pipeline YAML, so create it once in the repository’s Crow dashboard under the Crons settings. Add a job with these exact values:
| Field | Value |
|---|---|
| Name | vulnerability-scanning |
| Branch | leave empty (defaults to the repository default branch) |
| Schedule (UTC) | @daily |
The Name must match cron: vulnerability-scanning in .crow/vulnerability-scanning.yaml exactly, otherwise the daily run never fires.
The push trigger works regardless of this job.
Schedule expressions use the gdgvda/cron format and are interpreted in UTC, not local time.
Suggested workflow
- The automated pipeline covers daily
vulnixclosure scans andtrivy fson every push to the default branch, so manual runs are for ad-hoc checks between scheduled scans. - Run
trivy fs --severity HIGH,CRITICAL .before committing dependency bumps for faster feedback than waiting for the pipeline. - Add
trivy fs --exit-code 1 --severity CRITICAL .as aprekhook if you want CI-style blocking on critical findings.