Skip to content

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):

TierToolScopeGate
1govulncheckGo source — reachable code paths onlyInformational (SARIF upload, no failure)
2grypeBuilt binary - dependency CVEs consumers faceFails 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

Terminal window
# 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-lint

grype 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):

  1. Open a PR to upgrade the dependency to a patched version.
  2. Run go mod tidy and verify the finding disappears.
  3. 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.mod creates 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:

Terminal window
# Install vexctl
go 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.json

The --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:

Terminal window
jq '[.matches[] | {id: .vulnerability.id, purl: .artifact.purl}]' build/grype.json

Valid --justification values (from the OpenVEX spec):

JustificationMeaning
component_not_presentThe vulnerable component is not included in the product
vulnerable_code_not_presentThe vulnerable code does not exist in the component version used
vulnerable_code_not_in_execute_pathThe vulnerable code path is never reached at runtime
vulnerable_code_cannot_be_controlled_by_adversaryThe vulnerability exists but cannot be triggered externally
inline_mitigations_already_existControls already in place prevent exploitation

Requirements for every VEX addition:

  • A justification that matches one of the values above.
  • An impact statement 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:

Terminal window
make scan-grype # generates build/grype.json
make vex-lint # reports any orphans (exits 0)