-
Notifications
You must be signed in to change notification settings - Fork 0
feat(acquire): attach acquisition-provenance referrer on mirror #144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
204 changes: 204 additions & 0 deletions
204
.github/actions/attach-acquisition-provenance/action.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| # Composite action: attach-acquisition-provenance | ||
| # | ||
| # Attach an acquisition-provenance OCI 1.1 referrer to a freshly-mirrored image. | ||
| # For every image acquired from an external registry into quarantine, this | ||
| # records where the image came from (source registry/repository/tag/digest), | ||
| # where it landed, when it was acquired, and by which workflow run. | ||
| # | ||
| # The referrer is an in-toto Statement (artifact type | ||
| # application/vnd.in-toto+json, predicate type | ||
| # https://toddysm.com/acquisition-provenance/v0.1). One statement is attached to | ||
| # the index/tag manifest and one to each per-platform child manifest; the | ||
| # per-platform statements additionally record the platform source digest. | ||
| # | ||
| # This action is intended to run only on the external -> quarantine acquisition | ||
| # (from _mirror-image.yml), not on quarantine -> golden promotion. | ||
| # | ||
| # Terminology and the action catalogue: see docs/reference/workflow-actions.md. | ||
| name: attach-acquisition-provenance | ||
| description: Attach an acquisition-provenance in-toto referrer to a mirrored image. | ||
|
|
||
| inputs: | ||
| source-image: | ||
| description: "Fully qualified source image without tag (e.g. docker.io/library/python)." | ||
| required: true | ||
| source-tag: | ||
| description: "Source image tag that was acquired (e.g. 3.14-slim)." | ||
| required: true | ||
| dest-image: | ||
| description: "Fully qualified destination image without tag (e.g. ghcr.io/owner/quarantine/python)." | ||
| required: true | ||
| dest-tag: | ||
| description: "Destination image tag." | ||
| required: true | ||
| acquired-digest: | ||
| description: "Digest of the acquired image at the destination (the mirror-image 'digest' output)." | ||
| required: true | ||
| copy-referrers: | ||
| description: "Whether the mirror copied OCI referrers (selects the copy method oras vs crane)." | ||
| required: false | ||
| default: "false" | ||
| source-authenticated: | ||
| description: "'true' when the mirror logged in to the source registry (e.g. dhi.io) instead of an anonymous pull." | ||
| required: false | ||
| default: "false" | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - name: Attach acquisition provenance to ${{ inputs.dest-image }}:${{ inputs.dest-tag }} | ||
| shell: bash | ||
| env: | ||
| SOURCE_IMAGE: "${{ inputs.source-image }}" | ||
| SOURCE_TAG: "${{ inputs.source-tag }}" | ||
| DEST_IMAGE: "${{ inputs.dest-image }}" | ||
| DEST_TAG: "${{ inputs.dest-tag }}" | ||
| ACQUIRED_DIGEST: "${{ inputs.acquired-digest }}" | ||
| COPY_REFERRERS: "${{ inputs.copy-referrers }}" | ||
| SOURCE_AUTHENTICATED: "${{ inputs.source-authenticated }}" | ||
| ACTOR: "${{ github.actor }}" | ||
| WORKFLOW: "${{ github.workflow }}" | ||
| RUN_ID: "${{ github.run_id }}" | ||
| RUN_ATTEMPT: "${{ github.run_attempt }}" | ||
| SERVER_URL: "${{ github.server_url }}" | ||
| REPOSITORY: "${{ github.repository }}" | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| PREDICATE_TYPE="https://toddysm.com/acquisition-provenance/v0.1" | ||
|
|
||
| source_ref="${SOURCE_IMAGE}:${SOURCE_TAG}" | ||
| dest_ref="${DEST_IMAGE}:${DEST_TAG}" | ||
| # Digests are preserved during the copy, so the acquired digest is also | ||
| # the source digest. | ||
| source_digest="${ACQUIRED_DIGEST}" | ||
|
|
||
| # Split the source image into registry + repository. The registry is the | ||
| # first path segment; everything after it is the repository. | ||
| source_registry="${SOURCE_IMAGE%%/*}" | ||
| source_repository="${SOURCE_IMAGE#*/}" | ||
|
|
||
| if [ "${COPY_REFERRERS}" = "true" ]; then | ||
| copy_method="oras" | ||
| else | ||
| copy_method="crane" | ||
| fi | ||
|
|
||
| acquired_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | ||
| run_url="${SERVER_URL}/${REPOSITORY}/actions/runs/${RUN_ID}" | ||
|
|
||
| work="$(mktemp -d)" | ||
|
|
||
| # Build one in-toto Statement. Arguments: | ||
| # $1 = subject digest (sha256:...) | ||
| # $2 = output file | ||
| # $3 = platform source digest ("" for the index-level statement) | ||
| build_statement() { | ||
| local subject_digest="$1" out="$2" platform_digest="$3" | ||
| # Digests are <algorithm>:<hex> (e.g. sha256:...). Parse the algorithm | ||
| # generically rather than assuming sha256, and fail fast on anything | ||
| # that does not look like a digest. | ||
| case "${subject_digest}" in | ||
| ?*:?*) : ;; | ||
| *) | ||
| echo "::error::Unexpected digest '${subject_digest}' (expected <algorithm>:<hex>)." | ||
| return 1 | ||
| ;; | ||
| esac | ||
| local subject_algo="${subject_digest%%:*}" | ||
| local subject_hex="${subject_digest#*:}" | ||
| jq -n \ | ||
| --arg predicate_type "${PREDICATE_TYPE}" \ | ||
| --arg dest_image "${DEST_IMAGE}" \ | ||
| --arg subject_algo "${subject_algo}" \ | ||
| --arg subject_hex "${subject_hex}" \ | ||
| --arg source_ref "${source_ref}" \ | ||
| --arg source_registry "${source_registry}" \ | ||
| --arg source_repository "${source_repository}" \ | ||
| --arg source_tag "${SOURCE_TAG}" \ | ||
| --arg source_digest "${source_digest}" \ | ||
| --arg dest_ref "${dest_ref}" \ | ||
| --arg dest_digest "${ACQUIRED_DIGEST}" \ | ||
| --arg acquired_at "${acquired_at}" \ | ||
| --arg run_url "${run_url}" \ | ||
| --arg actor "${ACTOR}" \ | ||
| --arg workflow "${WORKFLOW}" \ | ||
| --arg run_id "${RUN_ID}" \ | ||
| --arg run_attempt "${RUN_ATTEMPT}" \ | ||
| --arg copy_method "${copy_method}" \ | ||
| --argjson copy_referrers "${COPY_REFERRERS}" \ | ||
| --argjson source_authenticated "${SOURCE_AUTHENTICATED}" \ | ||
| --arg platform_digest "${platform_digest}" \ | ||
| '{ | ||
| _type: "https://in-toto.io/Statement/v1", | ||
| predicateType: $predicate_type, | ||
| subject: [ { name: $dest_image, digest: { ($subject_algo): $subject_hex } } ], | ||
| predicate: ({ | ||
| source: { | ||
| reference: $source_ref, | ||
| registry: $source_registry, | ||
| repository: $source_repository, | ||
| tag: $source_tag, | ||
| digest: $source_digest | ||
| }, | ||
| destination: { | ||
| reference: $dest_ref, | ||
| digest: $dest_digest | ||
| }, | ||
| acquiredAt: $acquired_at, | ||
| runUrl: $run_url, | ||
| actor: $actor, | ||
| workflow: { name: $workflow, runId: $run_id, runAttempt: $run_attempt }, | ||
| copyMethod: $copy_method, | ||
| copyReferrers: $copy_referrers, | ||
| sourceAuthenticated: $source_authenticated | ||
| } + (if $platform_digest == "" then {} else { platformSourceDigest: $platform_digest } end)) | ||
| }' > "${out}" | ||
| } | ||
|
|
||
| # Attach a statement file to a subject as an OCI 1.1 referrer. | ||
| # $1 = subject ref (image@digest) | ||
| # $2 = statement file | ||
| # $3 = title annotation value | ||
| attach_statement() { | ||
| local subject_ref="$1" file="$2" title="$3" | ||
| oras attach \ | ||
| --artifact-type application/vnd.in-toto+json \ | ||
| --annotation "in-toto.io/predicate-type=${PREDICATE_TYPE}" \ | ||
| --annotation "org.opencontainers.image.title=${title}" \ | ||
| --annotation "com.toddysm.acquisition.source=${source_ref}" \ | ||
| --annotation "com.toddysm.acquisition.source-digest=${source_digest}" \ | ||
| --annotation "com.toddysm.acquisition.timestamp=${acquired_at}" \ | ||
| --annotation "com.toddysm.acquisition.run-url=${run_url}" \ | ||
| --disable-path-validation \ | ||
| "${subject_ref}" \ | ||
| "${file}:application/vnd.in-toto+json" | ||
| } | ||
|
|
||
| # Index / tag level statement (whole acquired image). | ||
| index_file="${work}/acquisition-provenance.json" | ||
| build_statement "${ACQUIRED_DIGEST}" "${index_file}" "" | ||
| echo "Attaching index-level acquisition provenance to ${DEST_IMAGE}@${ACQUIRED_DIGEST}" | ||
| attach_statement "${DEST_IMAGE}@${ACQUIRED_DIGEST}" "${index_file}" "acquisition-provenance.json" | ||
|
|
||
| # Per-platform statements. Enumerate real platform child manifests (skip | ||
| # attestation/unknown entries), matching the scan-sbom platform filter. | ||
| child_digests="$(crane manifest "${DEST_IMAGE}@${ACQUIRED_DIGEST}" \ | ||
| | jq -r '.manifests[]? | ||
| | select((.platform.os // "") != "" and (.platform.os // "") != "unknown") | ||
| | [ .digest, | ||
| (.platform.os + "-" + .platform.architecture | ||
| + (if (.platform.variant // "") != "" then "-" + .platform.variant else "" end)) ] | ||
| | @tsv' || true)" | ||
|
|
||
| if [ -n "${child_digests}" ]; then | ||
| while IFS=$'\t' read -r digest platform; do | ||
| [ -n "${digest}" ] || continue | ||
| file="${work}/acquisition-provenance-${platform}.json" | ||
| build_statement "${digest}" "${file}" "${digest}" | ||
| echo "Attaching ${platform} acquisition provenance to ${DEST_IMAGE}@${digest}" | ||
| attach_statement "${DEST_IMAGE}@${digest}" "${file}" "acquisition-provenance-${platform}.json" | ||
| done <<< "${child_digests}" | ||
| else | ||
| echo "No per-platform child manifests found; index-level provenance only." | ||
| fi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.