Skip to content

fix(ci): scan platform manifests, not the index, in the image gate (#2266) - #2272

Merged
Gregory91G merged 2 commits into
mainfrom
fix/2266-image-cve-gate-index
Aug 6, 2026
Merged

fix(ci): scan platform manifests, not the index, in the image gate (#2266)#2272
Gregory91G merged 2 commits into
mainfrom
fix/2266-image-cve-gate-index

Conversation

@Gregory91G

@Gregory91G Gregory91G commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #2266 and supersedes #2267 — with this in, the gate no longer needs to be non-blocking.

What actually breaks

Not attestation manifests. I pulled both digests from the failing job and looked them up in the registry:

52b63ee7…  oci.image.index   children: linux/amd64  +  unknown/unknown
818f534b…  oci.image.index   children: linux/arm64  +  unknown/unknown

Export digest writes steps.build.outputs.digest, and with provenance on, buildx returns the digest of an index, not of a manifest. There is one such index per architecture, and each already contains an attestation child.

Trivy runs on an amd64 runner and resolves an index against the host platform. The amd64 index has the child it wants and scanned clean — that is the "first digest scanned clean" in the issue. The arm64 index has no linux/amd64 child, hence the fatal. The attestation child sits in both indexes, including the one that scanned fine, so it is not what trivy chokes on.

The fix

Expand each index and scan the platform manifests by their own digest. No --platform to derive, and the attestation children fall out of the same filter:

.manifests[]
| select(.platform.os != "unknown")
| select(.annotations["vnd.docker.reference.type"] != "attestation-manifest")
| .digest

Both predicates are kept deliberately. On our images they are equivalent, but they fail differently: the platform one covers an attestation child that ever declares a real platform, the annotation one covers a platform child that ever loses its platform. Media type is not usable as a discriminator here — the image child and the attestation child are both application/vnd.oci.image.manifest.v1+json.

A digest that is already a plain manifest falls back to being scanned as-is, so a build with provenance disabled keeps working. A scanned counter fails the step if expansion yields nothing, extending the existing "a gate that scanned nothing is not a pass" guard from the empty-directory case to the empty-expansion one.

Test plan

Ran the action's script verbatim, against the live registry.

  • The two digests from the failing run (52b63ee7…, 818f534b…): both scan, debian 12.15 detected, 91 packages each, exit 0. Previously the second one fatalled.
  • Multi-arch index (insight-front:latest, 2 platform + 2 attestation children): 2 scans, 2 OS detections, attestation children skipped.
  • Findings still block: alpine:3.10 index at HIGH,CRITICAL → exit 1.
  • Fallback: the same image's amd64 child digest, which has no .manifests → scanned as-is, exit 1.
  • Empty digests directory still errors and exits 1.
  • insight-toolbox (2 platforms, 10 fixable HIGH after fix(images): bump the toolbox to Node 24 and upgrade npm in place (#2166) #2168): CRITICAL → exit 0 on both, HIGH → exit 1 on both.
  • Merge order: simulated ci: make image-cve-gate non-blocking until #2266 is fixed #2267 landing first. Two conflict hunks, both with its temporary wording wholly on one side; taking this branch's side yields a file byte-identical to it — no non-blocking text survives and --exit-code 1 is back.
  • actionlint: 7 findings on the base, 7 on the branch, identical set.
  • First main build after merge tags images again.

Cost note

Scanning insight-toolbox pulls Trivy's Java DB (~1.4 GB) on top of the vulnerability DB, because the image carries Java artifacts and the Java analyzer runs even under --scanners vuln. Each merge job is a fresh runner, so merge-toolbox pays that download every build — roughly a minute beyond the 30–45 s estimated in #2238. The other six jobs are unaffected.

Sequencing

#2267 makes the gate non-blocking as a stop-gap and is in the merge queue. If it lands first I will rebase and drop its || echo ::warning:: line; if this lands first, #2267 can be closed unmerged. Either order works, they just should not both end up on main — the enforcement has to come back on.

Summary by CodeRabbit

  • Bug Fixes
    • Improved container image security scans to inspect each supported platform image individually.
    • Excluded non-runnable and attestation-only manifests from vulnerability scanning.
    • Added a fallback scan when platform details are unavailable.
    • Builds now fail clearly when no image manifest can be scanned, helping prevent incomplete security checks.

@Gregory91G
Gregory91G requested a review from a team as a code owner August 6, 2026 07:49
@Gregory91G Gregory91G self-assigned this Aug 6, 2026
@Gregory91G Gregory91G added github_actions Pull requests that update GitHub Actions code security review labels Aug 6, 2026
@Gregory91G
Gregory91G requested a review from cyberantonz August 6, 2026 07:50
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Gregory91G, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 55ffa8f9-d6cf-4b07-87da-7549b93c5b49

📥 Commits

Reviewing files that changed from the base of the PR and between 849a40c and 4dfc325.

📒 Files selected for processing (1)
  • .github/actions/image-cve-gate/action.yml
📝 Walkthrough

Walkthrough

The image CVE gate now resolves pushed image indexes into platform manifests, skips unknown-platform and attestation manifests, scans each valid digest with Trivy, falls back to index scanning when needed, and fails when no manifest is scanned.

Changes

Image CVE gate

Layer / File(s) Summary
Manifest resolution and scanning
.github/actions/image-cve-gate/action.yml
The action uses docker buildx imagetools inspect and jq to select platform manifests, skips attestation and unknown-platform manifests, scans each selected digest with Trivy, falls back to the original digest when no platform manifests exist, and fails when the scan count is zero.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: ktursunov

Sequence Diagram(s)

sequenceDiagram
  participant DigestList
  participant ImageTools
  participant ManifestFilter
  participant Trivy
  DigestList->>ImageTools: Inspect image digest
  ImageTools->>ManifestFilter: Return manifest metadata
  ManifestFilter->>Trivy: Scan each valid platform digest
  Trivy-->>ManifestFilter: Return scan result
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #2266 by skipping non-platform manifests, scanning platform digests, handling empty results, and restoring vulnerability blocking.
Out of Scope Changes check ✅ Passed The changes are limited to image digest filtering and scanning behavior required by issue #2266.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: scanning platform manifests instead of the image index in the CVE gate.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/2266-image-cve-gate-index

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/actions/image-cve-gate/action.yml:
- Around line 51-58: Update the jq selection in the targets assignment to
require a non-null platform object before excluding unknown operating systems,
and distinguish plain manifests from indexes when choosing the fallback. Use the
original digest only when the inspected document is a plain manifest; leave
targets empty for indexes with no eligible children so the existing scanned == 0
failure path runs.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 526c6459-3908-4978-9979-49324011dd5c

📥 Commits

Reviewing files that changed from the base of the PR and between 27cdfbb and 51901c613cfc9639921dc342bf4326e58e26b7d2.

📒 Files selected for processing (1)
  • .github/actions/image-cve-gate/action.yml

Comment on lines +51 to +58
targets="$(docker buildx imagetools inspect --raw "${IMAGE}@sha256:$(basename "$f")" | jq -r '
if .manifests then
.manifests[]
| select(.platform.os != "unknown")
| select(.annotations["vnd.docker.reference.type"] != "attestation-manifest")
| .digest
else empty end')"
[ -n "$targets" ] || targets="sha256:$(basename "$f")"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Distinguish a plain manifest from an index with no eligible children.

Line 54 includes entries with no declared platform because null != "unknown" is true in jq. Line 58 then scans an index when all its children were excluded. This bypasses the scanned == 0 failure path and can reintroduce the Trivy index-resolution failure.

Only use the original digest when the inspected document is a plain manifest. For an index, require a platform object and leave targets empty when no eligible child exists.

Proposed fix
-          targets="$(docker buildx imagetools inspect --raw "${IMAGE}`@sha256`:$(basename "$f")" | jq -r '
-            if .manifests then
+          raw_manifest="$(docker buildx imagetools inspect --raw "${IMAGE}`@sha256`:$(basename "$f")")"
+          if jq -e '.manifests | type == "array"' >/dev/null <<<"$raw_manifest"; then
+            targets="$(jq -r '
               .manifests[]
-              | select(.platform.os != "unknown")
+              | select((.platform? | type) == "object")
+              | select(.platform.os? != null and .platform.os != "unknown")
               | select(.annotations["vnd.docker.reference.type"] != "attestation-manifest")
-              | .digest
-            else empty end')"
-          [ -n "$targets" ] || targets="sha256:$(basename "$f")"
+              | .digest' <<<"$raw_manifest")"
+          else
+            targets="sha256:$(basename "$f")"
+          fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
targets="$(docker buildx imagetools inspect --raw "${IMAGE}@sha256:$(basename "$f")" | jq -r '
if .manifests then
.manifests[]
| select(.platform.os != "unknown")
| select(.annotations["vnd.docker.reference.type"] != "attestation-manifest")
| .digest
else empty end')"
[ -n "$targets" ] || targets="sha256:$(basename "$f")"
raw_manifest="$(docker buildx imagetools inspect --raw "${IMAGE}`@sha256`:$(basename "$f")")"
if jq -e '.manifests | type == "array"' >/dev/null <<<"$raw_manifest"; then
targets="$(jq -r '
.manifests[]
| select((.platform? | type) == "object")
| select(.platform.os? != null and .platform.os != "unknown")
| select(.annotations["vnd.docker.reference.type"] != "attestation-manifest")
| .digest' <<<"$raw_manifest")"
else
targets="sha256:$(basename "$f")"
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/image-cve-gate/action.yml around lines 51 - 58, Update the
jq selection in the targets assignment to require a non-null platform object
before excluding unknown operating systems, and distinguish plain manifests from
indexes when choosing the fallback. Use the original digest only when the
inspected document is a plain manifest; leave targets empty for indexes with no
eligible children so the existing scanned == 0 failure path runs.

…2266)

A push-by-digest build publishes one index per platform, each holding the
platform manifest plus an attestation child. The gate handed those index
digests to trivy, which resolves an index against the runner's own platform
and fatals on the arm64 one -- "no child with platform linux/amd64 in index".
The amd64 index scanned clean in the same job, so an attestation child is not
what trivy chokes on; the cross-architecture resolution is.

The gate now expands each index and scans the platform manifests by their own
digest, which needs no --platform and drops the attestation children on the
way. Digests that are already a plain manifest are scanned as they are, so a
build without provenance still works. A counter fails the step if the
expansion ever yields nothing, keeping the "a gate that scanned nothing is not
a pass" property the empty-directory check already gives.

Verified against the digests from the failing run: both now scan (debian
12.15, 91 packages each) and exit 0. A multi-arch index yields two scans with
the attestation children skipped, findings still exit 1, and a plain manifest
digest takes the fallback.

Signed-off-by: Gregory Gogin <grigoriy.gogin@constructor.tech>
No behaviour change.

Signed-off-by: Gregory Gogin <grigoriy.gogin@constructor.tech>
@Gregory91G
Gregory91G force-pushed the fix/2266-image-cve-gate-index branch from 2f7a6c3 to 4dfc325 Compare August 6, 2026 08:16
@Gregory91G
Gregory91G enabled auto-merge August 6, 2026 08:17
@Gregory91G
Gregory91G added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit a0d67a8 Aug 6, 2026
23 checks passed
@Gregory91G
Gregory91G deleted the fix/2266-image-cve-gate-index branch August 6, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

github_actions Pull requests that update GitHub Actions code security review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Image CVE gate fatals on attestation digests and blocks merge jobs — temporarily disabled, must be fixed and re-enabled

2 participants