Vulnerability Management
This page describes how the Zarf maintainers identify, triage, and resolve CVEs detected in Zarf’s Go dependencies and init package.
Nightly Scanning Pipeline
Zarf scans main nightly using a two-tier model
(.github/workflows/scan-nightly-cve.yml):
| Tier | Tool | Scope | Gate |
|---|---|---|---|
| 1 | govulncheck | Go source — reachable code paths only | Informational (SARIF upload, no failure) |
| 2 | grype | Built binary - dependency CVEs consumers face | Fails on High or Critical (un-suppressed) |
Tier 1 surfaces vulnerabilities in code actually reached at runtime, with very few false positives. It has no suppression mechanism; every finding must be addressed upstream.
Tier 2 scans the compiled binary to reflect what consumers would see when they scan Zarf. Findings that are not applicable to Zarf are suppressed via OpenVEX statements (see below); un-suppressed High/Critical findings fail the nightly run.
Running locally
# Tier 1 — govulncheck (version pinned via go.mod tool directive)make scan-govulncheck
# Tier 2 — grype on binary, fails on High+make scan-grype
# Check for orphaned VEX statements (advisory, always exits 0)make vex-lintgrype must be installed separately (brew install grype).
govulncheck is fetched automatically via go tool govulncheck.
Vulnerability Triage Policy
Direct dependencies
When grype or govulncheck reports a CVE in a direct dependency (imported by Zarf’s own
code, no // indirect annotation in go.mod):
- Open a PR to upgrade the dependency to a patched version.
- Run
go mod tidyand verify the finding disappears. - If no patch exists upstream, open a tracking issue and consider a VEX suppression (see below) until a fix is available.
Indirect dependencies
When a CVE appears in an indirect dependency (marked // indirect in go.mod — present
only because a direct dependency requires it):
- Do not open a PR that bumps the indirect dependency solely to resolve the finding.
Bumping an indirect dep in
go.modcreates drift from true dependency resolution and typically pulls in unrelated changes from the upstream module. - Instead, evaluate whether the finding applies to Zarf. If the vulnerable code is not present in or not reachable from Zarf’s execution paths, document this with a VEX statement (see below). This creates a reviewed, auditable record of the determination.
- If govulncheck also reports the vulnerability (meaning the code path is reachable through the call graph), it is applicable regardless of how the dependency is classified — work toward a patch in the owning package or a direct-dep upgrade path.
This policy applies equally to the CLI/SDK Go code and the init package components.
OpenVEX Suppressions
Findings that are not applicable to Zarf are recorded in
.vex/zarf.cli.openvex.json
using the OpenVEX format. Grype is configured in
.grype.yaml to consume this
file; not_affected and fixed statements move matching results to the suppressed set.
Adding a VEX statement
Use vexctl to author statements:
# Install vexctlgo install github.com/openvex/vexctl@latest
# Add a not_affected statement (modifies the document in-place)vexctl add \ --in-place \ --product "pkg:golang/<module>@<version>" \ --vuln "GHSA-XXXX-XXXX-XXXX" \ --status not_affected \ --justification vulnerable_code_not_in_execute_path \ --impact-statement "The vulnerable function in <dep> is never called from Zarf's execution paths." \ .vex/zarf.cli.openvex.jsonThe --product PURL must match the artifact PURL that grype reports for the affected dependency
(e.g. pkg:golang/github.com/sigstore/fulcio@v1.8.5) — not Zarf’s own PURL. Use the purl field
from build/grype.json matches to get the exact value:
jq '[.matches[] | {id: .vulnerability.id, purl: .artifact.purl}]' build/grype.jsonValid --justification values (from the
OpenVEX spec):
| Justification | Meaning |
|---|---|
component_not_present | The vulnerable component is not included in the product |
vulnerable_code_not_present | The vulnerable code does not exist in the component version used |
vulnerable_code_not_in_execute_path | The vulnerable code path is never reached at runtime |
vulnerable_code_cannot_be_controlled_by_adversary | The vulnerability exists but cannot be triggered externally |
inline_mitigations_already_exist | Controls already in place prevent exploitation |
Requirements for every VEX addition:
- A
justificationthat matches one of the values above. - An
impactstatement with enough context for a future reviewer to independently verify the determination (e.g., which function, which code path, why it is not reachable). - A PR with at least one maintainer review before merging.
Orphaned VEX statements
The nightly pipeline runs make vex-lint after the grype scan to detect orphaned
statements — suppressions whose vulnerability ID no longer appears in grype’s results
(meaning the dependency was patched or removed). Orphans are reported as warnings in the
uploaded scan artifact and should be removed to keep the VEX document accurate.
To check locally:
make scan-grype # generates build/grype.jsonmake vex-lint # reports any orphans (exits 0)