Vulnerability scanning decision and operations¶
Provider decision¶
Grype is the selected scanner for the current pre-promotion gate. The policy in scripts/vulnerability.py consumes Grype JSON, while scripts/scan_sources.py and scripts/evidence_gate.py independently bind each result to the intended image architecture and OCI archive. This keeps the admission decision separate from Grype's finding format. Trivy is also a viable local scanner; this choice reflects the tested Grype source modes and the repository's existing Syft use, not a claim that other providers lack them.
| Selection criterion | Evidence and limit |
|---|---|
| OCI and multiple architectures | The action scans the built oci-archive: with --platform linux/$ARCHITECTURE and the selected registry architecture manifest by digest. Source validation checks the archive, registry index, manifest, config architecture, and source revision. An AMD64 OCI archive was exercised locally; the configured native jobs cover each declared architecture. |
| Existing SBOMs | Grype accepts Syft SBOMs through sbom: input. The current Syft SBOM is made by the publish job after the pre-promotion scan, so no SBOM exists for this gate to consume. The gate scans the archive directly; its JSON and the later published SBOM are separate evidence. Moving Syft earlier would enable SBOM reuse without changing the policy adapter. |
| Machine-readable output | Both scans use Grype JSON; the policy adapter has deterministic tests for new, fixed, remaining, severity, fix availability, and exceptions. The comparison report is retained as a seven-day workflow artifact. Raw Grype JSON and source metadata are retained in a separate one-day IMAGE-ARCH-scan-debug artifact for diagnosis; credential files live outside that artifact directory. |
| Database cadence and provenance | Each scan job runs grype db update once, disables implicit updates for both scans, and records scanner version and database status in the report. Database availability is an external dependency; an update or source failure stops publication. The database is not vendored or digest-pinned here. |
| Alpine and compiled languages | Grype documents Alpine and language packages including Go, Java, PHP, Rust, and .NET. The native image matrix exercises the repository's actual OS and application mix. Statically linked or manually copied binaries may expose no package metadata; a clean Grype result is not proof that their compiled dependencies are free of vulnerabilities. Package detection and false positives still require review of the raw findings. |
| License and hosted service | Grype is Apache-2.0 licensed. Scanning runs on the GitHub runner; image content is not sent to a scanner SaaS. Jobs download the Grype release, vulnerability database, and authenticated GHCR baseline. |
| Maintenance, performance, and false positives | The action pins each release archive by SHA-256, and Renovate groups the AMD64/ARM64 release and checksum changes for review. One database update and two scans per architecture add work to native builds; there is no controlled timing benchmark yet. Candidate-versus-baseline comparison avoids blocking unchanged findings, and exact, expiring exceptions document accepted false positives or temporary risks. |
The admission rule blocks newly introduced High/Critical findings with a known fix for production, unless a scoped unexpired exception applies. isolated-test results remain visible without granting production admission. Scanner updates are reviewed rather than auto-merged because JSON shape and database behavior can change. .github/renovate.json uses github-release-attachments: it identifies each pinned archive by its current checksum in the official release checksum asset and maps that filename to the new release. scripts.grype_pins requires both architecture release tags to match. The runtime action verifies each downloaded archive with sha256sum before executing it.
Reproduce a scan locally¶
Use the exact OCI archive and source revision from a failed build. Pull-request jobs do not upload the archive, so reproduce from a locally built archive or use the scan-debug artifact for policy replay. Install the pinned Grype release and checksum from the scanner action, plus skopeo, Python dependencies from uv.lock, and authenticated read access to GHCR. A registry authorization error is not evidence that a baseline is absent. The example below covers an existing latest baseline; use the image's declared admission from container.yaml.
image=ghcr.io/strukturpiloten/IMAGE
arch=amd64
archive=/path/to/IMAGE-amd64.tar
admission=isolated-test
mkdir -p /tmp/grype-triage
skopeo login ghcr.io
uv run --frozen --python 3.14 python -m scripts.scan_sources candidate \
--archive "$archive" --architecture "$arch" \
--output /tmp/grype-triage/candidate-source.json
uv run --frozen --python 3.14 python -m scripts.scan_sources baseline \
--image "$image" --reference "$image:latest" --architecture "$arch" \
--output /tmp/grype-triage/baseline-source.json
candidate_digest=$(python3 -c 'import json; print(json.load(open("/tmp/grype-triage/candidate-source.json"))["manifestDigest"])')
baseline_digest=$(python3 -c 'import json; print(json.load(open("/tmp/grype-triage/baseline-source.json"))["manifestDigest"])')
source_revision=$(python3 -c 'import json; print(json.load(open("/tmp/grype-triage/candidate-source.json"))["sourceRevision"])')
grype db update
export GRYPE_DB_AUTO_UPDATE=false
grype version -o json > /tmp/grype-triage/grype-version.json
grype db status -o json > /tmp/grype-triage/grype-db.json
grype "oci-archive:$archive" --platform "linux/$arch" -o json > /tmp/grype-triage/candidate-grype.json
grype "registry:$image@$baseline_digest" --platform "linux/$arch" -o json > /tmp/grype-triage/baseline-grype.json
uv run --frozen --python 3.14 python -m scripts.vulnerability \
--image "$image" --architecture "$arch" --admission "$admission" \
--candidate-digest "$candidate_digest" --baseline-digest "$baseline_digest" \
--source-revision "$source_revision" --run-id 1 --run-attempt 1 \
--candidate-source "oci-archive:$archive" \
--candidate-evidence /tmp/grype-triage/candidate-source.json \
--baseline-evidence /tmp/grype-triage/baseline-source.json \
--candidate-scan /tmp/grype-triage/candidate-grype.json \
--baseline-scan /tmp/grype-triage/baseline-grype.json \
--scanner /tmp/grype-triage/grype-version.json \
--database /tmp/grype-triage/grype-db.json \
--exceptions security/vulnerability-exceptions.json \
--json-output /tmp/grype-triage/vulnerability-report.json \
--summary-output /tmp/grype-triage/vulnerability-summary.md
For a first publication, the workflow accepts an absent latest only after an authenticated GitHub Packages 404. To reproduce that case, set GITHUB_TOKEN and GITHUB_REPOSITORY, add --allow-missing to baseline source resolution, omit the baseline Grype scan and both --baseline-* arguments, and keep the generated absence evidence. A 403, timeout, or other lookup failure must remain an error.
If the comparison reports blocked, read the blocking, new, and remaining sections; reproduce the raw Grype records when needed before deciding whether to fix the package, wait for a fix, or propose a scoped exception. The exception file requires exact image, architecture, candidate digest, vulnerability and package identity, owner, reason, and expiry. A changed image digest needs a new exception. Source-identity, database, or authentication failures are operational failures and cannot be waived by a policy exception. Retrieve the normalized report with gh run download RUN_ID --name IMAGE-ARCH-vulnerability-report. When produced by a failed or fork pull-request scan, download IMAGE-ARCH-scan-debug within one day to inspect the raw scanner JSON, verified source identities, scanner version, and database status. The archive is not present in that debug artifact, but the saved JSON inputs can be passed directly to the scripts.vulnerability command above after replacing the /tmp/grype-triage paths. Release evidence is durable after successful finalization. A blocked candidate has not moved maintained tags; if an earlier published image must be reverted, follow the guarded rollback procedure.