Vulnerability Scanning with Grype
Grype v0.114.0 introduced a native zarf: scan target that lets you scan a Zarf package archive directly — without extracting it first. Grype reads the embedded SBOMs from the package and reports vulnerabilities for every image and component artifact inside it.
grype zarf:/path/to/package.tar.zstHow It Works
When Grype receives a zarf: source, it opens the outer .tar.zst archive, locates the sboms.tar bundle inside, and parses each Syft SBOM it finds. Packages that appear across multiple images (e.g., shared base-layer content) are deduplicated, and every matching vulnerability record tracks which image(s) the affected package originated from via the artifact.annotations["zarf-sbom-source"] field in JSON output.
This means a single scan covers the entire package — all component images and file artifacts — and you can trace any finding back to its source.
Scanning a Package
# Scan and print a table of findings (default output)grype zarf:/path/to/package.tar.zst
# Write JSON output to a file for further analysisgrype zarf:/path/to/package.tar.zst -o json --file findings.jsonThe table output gives a quick human-readable summary. Use -o json when you need to query findings programmatically or preserve results as a CI artifact.
Gating CI on Severity with --fail-on
Use the -f / --fail-on flag to fail a pipeline when any unignored vulnerability at or above a given severity is found. Grype exits with code 2 when the threshold is met, and 0 when no findings exceed it.
Accepted severity values (lowest to highest): negligible, low, medium, high, critical.
- name: Scan Zarf package run: | grype zarf:/path/to/package.tar.zst \ --fail-on high \ -o json \ --file findings.json
- name: Upload scan results if: always() uses: actions/upload-artifact@v4 with: name: grype-findings path: findings.jsonscan-package: script: - grype zarf:/path/to/package.tar.zst --fail-on high -o json --file findings.json artifacts: when: always paths: - findings.jsongrype zarf:/path/to/package.tar.zst --fail-on critical -o json --file findings.jsonif [ $? -eq 2 ]; then echo "Package failed vulnerability gate" exit 1fiIgnoring Known Acceptable Findings
For findings that are accepted risk (e.g., no fix available, vendor advisory disputed), create a Grype ignore file rather than lowering your threshold:
ignore: - vulnerability: CVE-2024-12345 reason: "No fix available; mitigated at network boundary" - fix-state: "wont-fix"grype zarf:/path/to/package.tar.zst --fail-on high --config .grype.yamlIsolating Findings by Artifact
JSON output includes an artifact.annotations["zarf-sbom-source"] field on each match — a JSON array of image refs or file paths within the package that contain the vulnerable package. Use this to drill into which component is responsible for a finding.
List All Source Images in the Findings
jq -r '[.matches[].artifact.annotations["zarf-sbom-source"][]?] | unique[]' findings.jsonExample output:
ghcr.io/go-gitea/giteaghcr.io/zarf-dev/agentregistry1.dso.mil/ironbank/redhat/ubi/ubi9-minimalCount Findings per Image
jq -r ' [.matches[] | .artifact.annotations["zarf-sbom-source"][]?] | group_by(.) | map({source: .[0], count: length}) | sort_by(-.count)[] | "\(.count)\t\(.source)"' findings.jsonFilter Findings for a Specific Image
zarf-sbom-source is a JSON array and index does exact element matching, so omit the tag:
# Strip tag if starting from a full image ref: IMAGE="${FULL_IMAGE%%:*}"IMAGE="ghcr.io/go-gitea/gitea"jq --arg img "$IMAGE" '[ .matches[] | select( (.artifact.annotations["zarf-sbom-source"] // []) | index($img) )]' findings.jsonShow Critical and High Findings Only
jq '[.matches[] | select(.vulnerability.severity | test("Critical|High"))]' findings.jsonShow Critical/High Findings per Image
Combine severity filtering with source attribution to get a targeted remediation list:
jq -r ' .matches[] | select(.vulnerability.severity | test("Critical|High")) | .artifact.annotations["zarf-sbom-source"][]? as $src | [$src, .vulnerability.severity, .vulnerability.id, .artifact.name, .artifact.version] | @tsv' findings.json | sort | column -tExample output:
ghcr.io/go-gitea/gitea Critical CVE-2024-56337 tomcat-embed-core 10.1.31ghcr.io/go-gitea/gitea High CVE-2025-24813 tomcat-embed-core 10.1.31registry1.dso.mil/.../ubi9-minimal High CVE-2024-50602 expat 2.5.0-2Extract a Per-Image Summary Report
jq -r ' [ .matches[] | .vulnerability as $vuln | .artifact.annotations["zarf-sbom-source"][]? as $src | {source: $src, severity: $vuln.severity, id: $vuln.id, package: .artifact.name} ] | group_by(.source) | map({ source: .[0].source, critical: (map(select(.severity == "Critical")) | length), high: (map(select(.severity == "High")) | length), medium: (map(select(.severity == "Medium")) | length) }) | sort_by(-.critical, -.high)[] | "\(.source) critical=\(.critical) high=\(.high) medium=\(.medium)"' findings.jsonRelating Findings Back to Zarf Components
Grype reports findings at the image level. To understand which Zarf component owns a given image, first identify which image refs appear in your scan output, then cross-reference with the package definition. Docker Hub (docker.io) images may be stored without the registry prefix in the definition output, so if a match isn’t found try just the image name.
# 1. List every image ref present in the findingsjq -r '[.matches[].artifact.annotations["zarf-sbom-source"][]?] | unique[]' findings.json
# 2. Cross-reference with the component that owns the imagezarf package inspect definition /path/to/package.tar.zst 2>/dev/null | grep -B10 "your-image-name"
# 3. Count findings for that imageIMAGE="ghcr.io/your-org/your-image" # use an image from step 1jq --arg img "$IMAGE" ' [.matches[] | select((.artifact.annotations["zarf-sbom-source"] // []) | index($img))] | length' findings.jsonFurther Reading
- SBOMs in Zarf — how SBOMs are generated and embedded in packages
- Grype documentation — full flag reference and ignore file format
- Grype v0.114.0 release —
zarf:scan target release notes