Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 245 additions & 0 deletions site/src/content/docs/best-practices/vulnerability-scanning.mdx
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
Comment thread
brandtkeller marked this conversation as resolved.
Outdated

| 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
Comment thread
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
Loading