Search Documentation

Search for pages and headings in the documentation

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.

  1. vulnix scans the philipp-linux home-manager closure (.#homeConfigurations.philipp-linux.activationPackage). The step runs vulnix -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.
  2. trivy scans the repository filesystem (trivy fs) using the custom template at assets/trivy-markdown.tpl, which renders vulnerabilities and misconfigurations as Markdown tables.
  3. 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.json and sends it with curl --data-binary @payload.json. That lets reports larger than the ~128 KiB Linux exec argument limit publish, where inline -d/--data tripped curl: 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

  1. The automated pipeline covers daily vulnix closure scans and trivy fs on every push to the default branch, so manual runs are for ad-hoc checks between scheduled scans.
  2. Run trivy fs --severity HIGH,CRITICAL . before committing dependency bumps for faster feedback than waiting for the pipeline.
  3. Add trivy fs --exit-code 1 --severity CRITICAL . as a prek hook if you want CI-style blocking on critical findings.