Vulnerability Management
Zarf maintainers use automated scans to find vulnerabilities in the Go source and in the compiled CLI. This guide explains how to review a finding, choose a remediation, and record a reviewed exception when Zarf is not affected.
Scanning for Vulnerabilities
Zarf scans main nightly in the
CVE scan workflow.
The scans answer different questions:
| Scan | Tool | What it checks | Result |
|---|---|---|---|
| Source reachability | govulncheck | Vulnerable Go functions reached from Zarf source | Uploads SARIF to GitHub Code Scanning |
| Binary inventory | grype | Dependencies included in the built CLI | Fails on un-suppressed High and Critical findings |
govulncheck reports call paths that reach vulnerable functions. grype scans the complete
binary dependency inventory, so it can report a dependency even when Zarf does not execute the
affected code.
Running Scans Locally
Run the same checks before changing dependency or VEX data:
# Find reachable vulnerable functions. govulncheck is pinned in go.mod.make scan-govulncheck
# Build Zarf, scan the binary, and fail on un-suppressed High or Critical findings.# Install Grype first: brew install grypemake scan-grype
# Report VEX statements that no longer match the latest Grype result.make vex-lintmake scan-grype writes its report to build/grype.json. Keep that report when reviewing a
finding; it contains the affected artifact version and package URL (PURL).
Triage a Finding
Start by identifying how the vulnerable module reaches Zarf:
go mod why -m <module-path>Then read the advisory and, when available, use the govulncheck trace to identify the vulnerable
function and the conditions required to reach it. A dependency listed as // indirect is still
part of the built product; the comment only means that Zarf does not directly import a package
from that module.
Direct Dependencies
When Zarf directly imports the vulnerable module, upgrade it to a patched compatible version. Build, vendor, and test the resulting dependency graph before submitting the change:
go get <module-path>@<patched-version>make buildmake test-unit-quickIf the module has no patch, track the upstream fix and evaluate whether the vulnerable code is reachable. Do not use a VEX statement to hide an affected path.
Indirect Dependencies
For an indirect dependency, first determine whether Zarf reaches the affected code. When it is
not reachable, do not upgrade the indirect dependency solely to clear the scanner finding.
Adding or bumping an indirect require overrides the dependency map that should be resolved by
Zarf’s direct dependencies. Record the reviewed determination in a VEX statement instead.
Typical evidence includes a govulncheck trace, the dependency’s source path, and the Zarf feature
that does or does not activate the vulnerable component.
When the affected code is reachable, first look for an update to the direct dependency that
selects a patched version. When an indirect override is necessary, use a scoped replace
directive instead of adding or updating an indirect require directive. Document why the
override is necessary, validate the complete graph, and remove the replacement once a direct
dependency selects the patched version:
go mod edit -replace=<module-path>@<selected-version>=<module-path>@<patched-version>make buildmake test-unit-quickRecording a Non-Affected Finding
Zarf records reviewed exceptions in
.vex/zarf.cli.openvex.json
using the OpenVEX format. Grype reads this document through
.grype.yaml and moves matching
not_affected and fixed findings to its suppressed results.
Find the Artifact PURL
The VEX product must be the affected dependency PURL from the Grype report, not Zarf’s PURL:
jq -r ' .matches[] | select(.vulnerability.id == "GHSA-XXXX-XXXX-XXXX") | .artifact.purl' build/grype.jsonFor example, a result might be
pkg:golang/google.golang.org/grpc@v1.81.1. If the finding is already suppressed, inspect
ignoredMatches[].artifact.purl instead.
Add a Statement
Use vexctl to create VEX additions. It updates the
document version and timestamp automatically:
# Install vexctl when it is not already available.go install github.com/openvex/vexctl@latest
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 "Describe the vulnerable function, the required trigger, and why Zarf cannot reach it." \ .vex/zarf.cli.openvex.jsonChoose a justification that matches the evidence:
| Justification | Use when |
|---|---|
component_not_present | The component is not included in the product |
vulnerable_code_not_present | The affected code does not exist in the version Zarf uses |
vulnerable_code_not_in_execute_path | Zarf includes the code but does not execute it |
vulnerable_code_cannot_be_controlled_by_adversary | The code can run but an attacker cannot trigger it |
inline_mitigations_already_exist | Existing controls prevent exploitation |
Every statement needs an impact statement that lets a future reviewer independently reproduce the conclusion. Name the affected function or component, the relevant Zarf path, and the condition that makes the advisory inapplicable. Include the VEX change in a reviewed pull request.
Reviewing VEX Statements
Run the binary scan and orphan check after adding, updating, or removing a statement:
make scan-grypemake vex-lintmake vex-lint reports an orphaned statement when its vulnerability ID no longer appears
in either active or suppressed Grype findings. This is advisory, but remove or update the
statement after confirming that the dependency was patched or removed. Keeping the document
current prevents an old exception from obscuring a future finding with the same identifier.