Skip to content

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:

ScanToolWhat it checksResult
Source reachabilitygovulncheckVulnerable Go functions reached from Zarf sourceUploads SARIF to GitHub Code Scanning
Binary inventorygrypeDependencies included in the built CLIFails 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:

Terminal window
# 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 grype
make scan-grype
# Report VEX statements that no longer match the latest Grype result.
make vex-lint

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

Terminal window
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:

Terminal window
go get <module-path>@<patched-version>
make build
make test-unit-quick

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

Terminal window
go mod edit -replace=<module-path>@<selected-version>=<module-path>@<patched-version>
make build
make test-unit-quick

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

Terminal window
jq -r '
.matches[]
| select(.vulnerability.id == "GHSA-XXXX-XXXX-XXXX")
| .artifact.purl
' build/grype.json

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

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

Choose a justification that matches the evidence:

JustificationUse when
component_not_presentThe component is not included in the product
vulnerable_code_not_presentThe affected code does not exist in the version Zarf uses
vulnerable_code_not_in_execute_pathZarf includes the code but does not execute it
vulnerable_code_cannot_be_controlled_by_adversaryThe code can run but an attacker cannot trigger it
inline_mitigations_already_existExisting 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:

Terminal window
make scan-grype
make vex-lint

make 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.