name: 'Provenance / SBOM / Sign' description: 'Creates SBOM & provenance files and signs the image' inputs: image-name: description: "name of the image" required: true default: '' image-tag: description: "image tag" required: true default: "" runs: using: "composite" steps: - name: Install cosign # https://github.com/sigstore/cosign-installer/releases/tag/v4.0.0 uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 with: cosign-release: 'v3.0.2' - name: Install Syft # https://github.com/anchore/sbom-action/releases/tag/v0.22.2 uses: anchore/sbom-action/download-syft@28d71544de8eaf1b958d335707167c5f783590ad # v0.22.2 with: syft-version: v1.41.2 - name: Check Cosign install shell: bash run: cosign version - name: Login to ghcr.io uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - name: Setup Go uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version-file: go.mod - name: Set up crane shell: bash run: go install github.com/google/go-containerregistry/cmd/crane@v0.11.0 - name: Get docker image tag id: container_info shell: bash env: IMAGE_NAME: ${{ inputs.image-name }} IMAGE_TAG: ${{ inputs.image-tag }} run: | echo "::group::Crane digest lookup" echo "Looking up digest for ${IMAGE_NAME}:${IMAGE_TAG}" DIGEST=$(crane digest ${IMAGE_NAME}:${IMAGE_TAG}) echo "Found digest: ${DIGEST}" echo "digest=${DIGEST}" >> $GITHUB_OUTPUT echo "::endgroup::" - name: Sign image shell: bash env: IMAGE_NAME: ${{ inputs.image-name }} CONTAINER_DIGEST: ${{ steps.container_info.outputs.digest }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} run: | echo "::group::Cosign sign" echo "Signing ${IMAGE_NAME}@${CONTAINER_DIGEST}" cosign sign --yes --new-bundle-format=false --use-signing-config=false -a GITHUB_ACTOR=${GITHUB_TRIGGERING_ACTOR} "${IMAGE_NAME}@${CONTAINER_DIGEST}" echo "::endgroup::" - name: Attach SBOM to image shell: bash id: sbom env: IMAGE_NAME: ${{ inputs.image-name }} IMAGE_TAG: ${{ inputs.image-tag }} CONTAINER_DIGEST: ${{ steps.container_info.outputs.digest }} run: | echo "::group::Image SBOM generation" # Image SBOM (OS + application libs contained in the image) echo "Generating image SBOM for ${IMAGE_NAME}@${CONTAINER_DIGEST}" syft "${IMAGE_NAME}@${CONTAINER_DIGEST}" -o spdx-json=sbom.${IMAGE_TAG}.spdx.json ORIGINAL_IMAGE_SBOM_SIZE="$(wc -c < sbom.${IMAGE_TAG}.spdx.json)" echo "Original image SBOM size: ${ORIGINAL_IMAGE_SBOM_SIZE} bytes" MAX_SBOM_SIZE_BYTES=10000000 echo "Deduplicating image SPDX package nodes and relationships" bash ./hack/dedupe-spdx-gomod.sh \ --input sbom.${IMAGE_TAG}.spdx.json \ --output sbom.${IMAGE_TAG}.dedup.spdx.json DEDUP_IMAGE_SBOM_SIZE="$(wc -c < sbom.${IMAGE_TAG}.dedup.spdx.json)" echo "Deduplicated image SBOM size: ${DEDUP_IMAGE_SBOM_SIZE} bytes" if [[ "${DEDUP_IMAGE_SBOM_SIZE}" -gt "${MAX_SBOM_SIZE_BYTES}" ]]; then echo "Deduped image SBOM still above ${MAX_SBOM_SIZE_BYTES} bytes, dropping file ownership data" bash ./hack/dedupe-spdx-gomod.sh \ --input sbom.${IMAGE_TAG}.spdx.json \ --output sbom.${IMAGE_TAG}.dedup.spdx.json \ --drop-file-ownership DEDUP_IMAGE_SBOM_SIZE="$(wc -c < sbom.${IMAGE_TAG}.dedup.spdx.json)" echo "Ownership-pruned deduplicated image SBOM size: ${DEDUP_IMAGE_SBOM_SIZE} bytes" fi if [[ "${DEDUP_IMAGE_SBOM_SIZE}" -gt "${MAX_SBOM_SIZE_BYTES}" ]]; then echo "Image SBOM predicate is still too large (${DEDUP_IMAGE_SBOM_SIZE} bytes)." echo "Refusing attestation to avoid Rekor submission retries/failure." exit 1 fi echo "::endgroup::" echo "::group::Attest image SBOM" cosign attest --yes --new-bundle-format=false --use-signing-config=false --predicate sbom.${IMAGE_TAG}.dedup.spdx.json --type spdx "${IMAGE_NAME}@${CONTAINER_DIGEST}" echo "::endgroup::" echo "::group::Verify image SBOM attestation" echo "Using certificate-identity-regexp: https://github.com/$GITHUB_REPOSITORY/.*" cosign verify-attestation --type spdx ${IMAGE_NAME}@${CONTAINER_DIGEST} \ --certificate-identity-regexp "https://github.com/$GITHUB_REPOSITORY/.*" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com | jq '.payload |= @base64d | .payload | fromjson' echo "::endgroup::" echo "::group::Go modules SBOM generation" # Go modules SBOM (dependencies from the source tree) # Requires repository to be checked out before this composite action runs. syft dir:. -o spdx-json=sbom.gomod.${IMAGE_TAG}.spdx.json ORIGINAL_GOMOD_SBOM_SIZE="$(wc -c < sbom.gomod.${IMAGE_TAG}.spdx.json)" echo "Original Go modules SBOM size: ${ORIGINAL_GOMOD_SBOM_SIZE} bytes" echo "Deduplicating Go modules SPDX package nodes and relationships" bash ./hack/dedupe-spdx-gomod.sh \ --input sbom.gomod.${IMAGE_TAG}.spdx.json \ --output sbom.gomod.${IMAGE_TAG}.dedup.spdx.json DEDUP_GOMOD_SBOM_SIZE="$(wc -c < sbom.gomod.${IMAGE_TAG}.dedup.spdx.json)" echo "Deduplicated Go modules SBOM size: ${DEDUP_GOMOD_SBOM_SIZE} bytes" # Rekor requests can fail when predicates are too large. If the deduped # SBOM is still big, drop file ownership-heavy data and re-check size. if [[ "${DEDUP_GOMOD_SBOM_SIZE}" -gt "${MAX_SBOM_SIZE_BYTES}" ]]; then echo "Deduped SBOM still above ${MAX_SBOM_SIZE_BYTES} bytes, dropping file ownership data" bash ./hack/dedupe-spdx-gomod.sh \ --input sbom.gomod.${IMAGE_TAG}.spdx.json \ --output sbom.gomod.${IMAGE_TAG}.dedup.spdx.json \ --drop-file-ownership DEDUP_GOMOD_SBOM_SIZE="$(wc -c < sbom.gomod.${IMAGE_TAG}.dedup.spdx.json)" echo "Ownership-pruned deduplicated Go modules SBOM size: ${DEDUP_GOMOD_SBOM_SIZE} bytes" fi if [[ "${DEDUP_GOMOD_SBOM_SIZE}" -gt "${MAX_SBOM_SIZE_BYTES}" ]]; then echo "Go modules SBOM predicate is still too large (${DEDUP_GOMOD_SBOM_SIZE} bytes)." echo "Refusing attestation to avoid Rekor submission retries/failure." exit 1 fi echo "::endgroup::" echo "::group::Attest Go modules SBOM" cosign attest --yes --new-bundle-format=false --use-signing-config=false --predicate sbom.gomod.${IMAGE_TAG}.dedup.spdx.json --type spdx "${IMAGE_NAME}@${CONTAINER_DIGEST}" echo "::endgroup::" echo "::group::Verify Go modules SBOM attestation" cosign verify-attestation --type spdx ${IMAGE_NAME}@${CONTAINER_DIGEST} \ --certificate-identity-regexp "https://github.com/$GITHUB_REPOSITORY/.*" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com | jq ' .payload |= @base64d | .payload | fromjson | .subject' echo "::endgroup::" - name: Generate provenance shell: bash env: IMAGE_NAME: ${{ inputs.image-name }} IMAGE_TAG: ${{ inputs.image-tag }} CONTAINER_DIGEST: ${{ steps.container_info.outputs.digest }} run: | echo "::group::Generate provenance" ./hack/generate-provenance.sh \ --repository "${IMAGE_NAME}" \ --digest "${CONTAINER_DIGEST}" \ --tags "${IMAGE_TAG}" \ --output-path "provenance.${IMAGE_TAG}.intoto.jsonl" echo "::endgroup::" - name: Attach provenance shell: bash id: provenance env: IMAGE_NAME: ${{ inputs.image-name }} IMAGE_TAG: ${{ inputs.image-tag }} CONTAINER_DIGEST: ${{ steps.container_info.outputs.digest }} run: | echo "::group::Prepare provenance predicate" jq '.predicate' provenance.${IMAGE_TAG}.intoto.jsonl > provenance-predicate.att echo "::endgroup::" echo "::group::Attest provenance" cosign attest --yes --new-bundle-format=false --use-signing-config=false --predicate provenance-predicate.att --type slsaprovenance "${IMAGE_NAME}@${CONTAINER_DIGEST}" echo "::endgroup::" echo "::group::Verify provenance attestation" cosign verify-attestation --type slsaprovenance ${IMAGE_NAME}@${CONTAINER_DIGEST} \ --certificate-identity-regexp "https://github.com/$GITHUB_REPOSITORY/.*" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com echo "::endgroup::"