-
Notifications
You must be signed in to change notification settings - Fork 256
chore(docs): add documentation for Zarf package vuln scanning #4960
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
brandtkeller
wants to merge
3
commits into
main
Choose a base branch
from
chore_package_vuln_scanning
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+237
−0
Open
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
245 changes: 245 additions & 0 deletions
245
site/src/content/docs/best-practices/vulnerability-scanning.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,245 @@ | ||
| --- | ||
| title: "Vulnerability Scanning with Grype" | ||
| --- | ||
|
|
||
| import { Tabs, TabItem } from "@astrojs/starlight/components"; | ||
|
|
||
| [Grype](https://github.com/anchore/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](/ref/sboms) from the package and reports vulnerabilities for every image and component artifact inside it. | ||
|
|
||
| ```bash | ||
| grype zarf:/path/to/package.tar.zst | ||
| ``` | ||
|
|
||
| ## How 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. | ||
|
|
||
| :::note | ||
|
|
||
| SBOMs are generated at package creation time. If a package was created with `--skip-sbom`, Grype has nothing to scan. Always generate SBOMs when creating packages intended for security gating. | ||
|
|
||
| ::: | ||
|
|
||
| ## Scanning a Package | ||
|
|
||
| ```bash | ||
| # Scan and print a table of findings (default output) | ||
| grype zarf:/path/to/package.tar.zst | ||
|
|
||
| # Write JSON output to a file for further analysis | ||
| grype zarf:/path/to/package.tar.zst -o json --file findings.json | ||
| ``` | ||
|
|
||
| The `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`. | ||
|
|
||
| <Tabs> | ||
| <TabItem label="GitHub Actions"> | ||
|
|
||
| ```yaml | ||
| - 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.json | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| <TabItem label="GitLab CI"> | ||
|
|
||
| ```yaml | ||
| scan-package: | ||
| script: | ||
| - grype zarf:/path/to/package.tar.zst | ||
| --fail-on high | ||
| -o json | ||
| --file findings.json | ||
| artifacts: | ||
| when: always | ||
| paths: | ||
| - findings.json | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| <TabItem label="Shell"> | ||
|
|
||
| ```bash | ||
| grype zarf:/path/to/package.tar.zst --fail-on critical -o json --file findings.json | ||
| if [ $? -eq 2 ]; then | ||
| echo "Package failed vulnerability gate" | ||
| exit 1 | ||
| fi | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| ### Recommended CI Thresholds | ||
|
|
||
| | Environment | Suggested `--fail-on` | Rationale | | ||
| |---|---|---| | ||
| | Pre-merge / PR gate | `high` | Catch high-impact issues before they land | | ||
| | Release candidate | `medium` | Tighten the bar before publishing | | ||
| | Production deploy | `critical` | Hard block on exploitable vulns with active fixes | | ||
|
|
||
| ### Ignoring Known Acceptable Findings | ||
|
|
||
| For findings that are accepted risk (e.g., no fix available, vendor advisory disputed), create a [Grype ignore file](https://github.com/anchore/grype#specifying-matches-to-ignore) rather than lowering your threshold: | ||
|
|
||
| ```yaml | ||
| # .grype.yaml | ||
| ignore: | ||
| - vulnerability: CVE-2024-12345 | ||
| reason: "No fix available; mitigated at network boundary" | ||
| - fix-state: "wont-fix" | ||
| ``` | ||
|
|
||
| ```bash | ||
| grype zarf:/path/to/package.tar.zst --fail-on high --config .grype.yaml | ||
| ``` | ||
|
|
||
| ## Isolating Findings by Component Artifact | ||
|
brandtkeller marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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. | ||
|
|
||
| :::tip | ||
|
|
||
| The `annotations` field only appears in `-o json` output, not in the default `table` format. | ||
|
|
||
| ::: | ||
|
|
||
| :::note | ||
|
|
||
| Source names are recorded at SBOM generation time and **do not include image tags**. When filtering by image, use just the registry and image name (e.g., `ghcr.io/go-gitea/gitea`) rather than the full tagged ref. | ||
|
|
||
| ::: | ||
|
|
||
| ### List All Source Images in the Findings | ||
|
|
||
| ```bash | ||
| jq -r '[.matches[].artifact.annotations["zarf-sbom-source"][]?] | unique[]' findings.json | ||
| ``` | ||
|
|
||
| Example output: | ||
|
|
||
| ``` | ||
| ghcr.io/go-gitea/gitea | ||
| ghcr.io/zarf-dev/agent | ||
| registry1.dso.mil/ironbank/redhat/ubi/ubi9-minimal | ||
| ``` | ||
|
|
||
| ### Count Findings per Image | ||
|
|
||
| ```bash | ||
| jq -r ' | ||
| [.matches[] | .artifact.annotations["zarf-sbom-source"][]?] | ||
| | group_by(.) | ||
| | map({source: .[0], count: length}) | ||
| | sort_by(-.count)[] | ||
| | "\(.count)\t\(.source)" | ||
| ' findings.json | ||
| ``` | ||
|
|
||
| ### Filter Findings for a Specific Image | ||
|
|
||
| `zarf-sbom-source` is a JSON array and `index` does exact element matching, so omit the tag: | ||
|
|
||
| ```bash | ||
| # 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.json | ||
| ``` | ||
|
|
||
| ### Show Critical and High Findings Only | ||
|
|
||
| ```bash | ||
| jq '[.matches[] | select(.vulnerability.severity | test("Critical|High"))]' findings.json | ||
| ``` | ||
|
|
||
| ### Show Critical/High Findings per Image | ||
|
|
||
| Combine severity filtering with source attribution to get a targeted remediation list: | ||
|
|
||
| ```bash | ||
| 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 -t | ||
| ``` | ||
|
|
||
| Example output: | ||
|
|
||
| ``` | ||
| ghcr.io/go-gitea/gitea Critical CVE-2024-56337 tomcat-embed-core 10.1.31 | ||
| ghcr.io/go-gitea/gitea High CVE-2025-24813 tomcat-embed-core 10.1.31 | ||
| registry1.dso.mil/.../ubi9-minimal High CVE-2024-50602 expat 2.5.0-2 | ||
| ``` | ||
|
|
||
| ### Extract a Per-Image Summary Report | ||
|
|
||
| ```bash | ||
| 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.json | ||
| ``` | ||
|
|
||
| ## Relating 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. | ||
|
|
||
| ```bash | ||
| # 1. List every image ref present in the findings | ||
| jq -r '[.matches[].artifact.annotations["zarf-sbom-source"][]?] | unique[]' findings.json | ||
|
|
||
| # 2. Cross-reference with the component that owns the image | ||
| zarf package inspect definition /path/to/package.tar.zst 2>/dev/null | grep -B10 "your-image-name" | ||
|
|
||
| # 3. Count findings for that image | ||
| IMAGE="ghcr.io/your-org/your-image" # use an image from step 1 | ||
| jq --arg img "$IMAGE" ' | ||
| [.matches[] | select((.artifact.annotations["zarf-sbom-source"] // []) | index($img))] | ||
| | length | ||
| ' findings.json | ||
| ``` | ||
|
|
||
| ## Further Reading | ||
|
|
||
| - [SBOMs in Zarf](/ref/sboms) — how SBOMs are generated and embedded in packages | ||
| - [Grype documentation](https://github.com/anchore/grype) — full flag reference and ignore file format | ||
| - [Grype v0.114.0 release](https://github.com/anchore/grype/releases/tag/v0.114.0) — `zarf:` scan target release notes | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.