---
title: "Vulnerability scanning"
description: "vulnix and trivy vulnerability scanning for this configuration."
---

# 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`](https://github.com/nix-community/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.

```sh
# 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):

```sh
# 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`](../whitelist.toml) records the triaged entries and is passed by both `just vuln-nix` and the Crow job below.

## `trivy` — filesystem / repo scanner

[`trivy`](https://trivy.dev) 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.

```sh
# 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`](../.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`](../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`](../.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`](https://pkg.go.dev/github.com/gdgvda/cron#hdr-CRON_Expression_Format) 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.
