From 689ea6ea3e2bb1beeb87f53cfbf1b6747f16c17a Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Sat, 24 Oct 2026 06:47:24 -0700 Subject: [PATCH] release: digest-bound provenance and SBOM attestations for container images (#12024) * release: emit digest-bound provenance and SBOM attestations Per-platform builds now record SLSA provenance at mode=max and a BuildKit SBOM, so the builder identity and resolved dependencies are captured instead of the empty predicate the 4.48 build record carried. After the multi-arch index is assembled and signed, the merge job publishes GitHub artifact attestations bound to the merged digest and a syft SPDX SBOM of the image, giving digest-pinning consumers a verifiable record on the exact index they pull. * release: keep unfixed findings in the trivy SARIF upload ignore-unfixed dropped every finding without an available patch, so the uploaded report understated the image's exposure. The scan stays report-only; only the completeness of what is uploaded changes. * release: generate and attest SBOMs per platform digest An index-wide syft scan only describes its default platform, so the SBOM attested to the merged multi-arch digest misrepresented every other manifest. Scan and attest each platform digest instead, which also drops the sbom-action upload defaults that raced release-asset permissions and artifact names across the merge matrix. * release: resolve per-platform index to its image manifest before syft/cosign push-by-digest records point at OCI indexes (image + attestation manifests), so scanning or attesting the index digest picks the runner's platform and fails outright for the other arches under set -e. Resolve the non-attestation manifest inside each index and bind the SBOM attestation to the image digest a same-platform pull of the merged index actually returns. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../workflows/container_release_unified.yml | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/.github/workflows/container_release_unified.yml b/.github/workflows/container_release_unified.yml index 3080595eb4..320436cb2e 100644 --- a/.github/workflows/container_release_unified.yml +++ b/.github/workflows/container_release_unified.yml @@ -271,8 +271,11 @@ jobs: file: ${{ matrix.dockerfile }} platforms: ${{ matrix.platform }} labels: ${{ steps.docker_meta.outputs.labels }} - # Flat single-platform manifest so imagetools create assembles cleanly. - provenance: false + # mode=max keeps resolvedDependencies complete and records the + # builder identity; attestations ride inside each pushed index and + # survive the imagetools merge into the multi-arch manifest. + provenance: mode=max + sbom: true outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true cache-from: type=gha,scope=${{ matrix.variant }}-${{ matrix.arch }} # max only for rocksdb: its RocksDB compile is sha-independent and worth @@ -308,6 +311,8 @@ jobs: permissions: contents: read id-token: write + attestations: write + packages: write strategy: fail-fast: false matrix: @@ -415,6 +420,45 @@ jobs: ${{ env.IMAGE }}@${{ steps.manifest.outputs.digest }} chrislusf/seaweedfs@${{ steps.manifest.outputs.digest }} + # Attest the merged multi-arch digest itself, so digest-pinning consumers + # can verify provenance against the exact index they pull. Both steps + # reuse the GHCR login above and push as OCI referrers. + - name: Attest build provenance for ${{ matrix.variant }} + if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant + uses: actions/attest-build-provenance@v4 + with: + subject-name: ${{ env.IMAGE }} + subject-digest: ${{ steps.manifest.outputs.digest }} + push-to-registry: true + + # SBOMs are generated and attested per platform manifest: an index-wide + # scan only describes syft's default platform, while the manifests it + # merges differ (some ship the real binary, others placeholders), so the + # SBOM has to be bound to the platform digest a consumer actually pulls. + - name: Install syft + if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant + uses: anchore/sbom-action/download-syft@v0 + + - name: Attest per-platform SBOMs for ${{ matrix.variant }} + if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant + working-directory: /tmp/digests + run: | + set -euo pipefail + for f in *; do + # Each saved digest is an OCI index (image + attestation manifests); + # scan and attest the image manifest a same-platform pull resolves. + ref="${{ env.IMAGE }}@sha256:${f}" + image=$(crane manifest "$ref" | jq -r ' + [.manifests[]? | select((.annotations["vnd.docker.reference.type"] // "") != "attestation-manifest" + and (.platform.os // "unknown") != "unknown") | .digest][0] // empty') + ref="${{ env.IMAGE }}@${image:-sha256:${f}}" + syft "registry:${ref}" -o "spdx-json=/tmp/sbom-${f}.spdx.json" + cosign attest --yes \ + --predicate "/tmp/sbom-${f}.spdx.json" \ + --type spdxjson \ + "$ref" + done + # Report-only trivy scan: uploads fixable HIGH/CRITICAL findings to GitHub # Security for visibility, but never blocks the release. Releases (including # `latest`) ship regardless — vulnerabilities are tracked, not gated, since @@ -453,7 +497,9 @@ jobs: scanners: vuln vuln-type: os,library severity: HIGH,CRITICAL - ignore-unfixed: true + # Keep unfixed findings in the report so digest-pinning consumers see + # the complete picture, not just what a patch could resolve. + ignore-unfixed: false limit-severities-for-sarif: true format: sarif output: trivy-results.sarif