diff --git a/README.md b/README.md index d4f282b..d4ab380 100644 --- a/README.md +++ b/README.md @@ -71,14 +71,13 @@ See [action.yml](action.yml) - uses: actions/attest-build-provenance@v2 with: # Path to the artifact serving as the subject of the attestation. Must - # specify exactly one of "subject-path", "subject-digest", or - # "subject-checksums". May contain a glob pattern or list of paths + # specify exactly one of "subject-path", "subject-digest", "subject-checksums", or "subject-images". May contain a glob pattern or list of paths # (total subject count cannot exceed 1024). subject-path: # SHA256 digest of the subject for the attestation. Must be in the form # "sha256:hex_digest" (e.g. "sha256:abc123..."). Must specify exactly one - # of "subject-path", "subject-digest", or "subject-checksums". + # of "subject-path", "subject-digest", "subject-checksums", or "subject-images". subject-digest: # Subject name as it should appear in the attestation. Required when @@ -87,9 +86,14 @@ See [action.yml](action.yml) # Path to checksums file containing digest and name of subjects for # attestation. Must specify exactly one of "subject-path", "subject-digest", - # or "subject-checksums". + # "subject-checksums", or "subject-images". subject-checksums: + # List of docker images to attest. Each image should be specified in the + # format "registry/image:tag@digest". Must specify exactly one of + # "subject-path", "subject-digest", "subject-checksums", or "subject-images". + subject-images: + # Whether to push the attestation to the image registry. Requires that the # "subject-name" parameter specify the fully-qualified image name and that # the "subject-digest" parameter be specified. Defaults to false. @@ -286,6 +290,55 @@ jobs: push-to-registry: true ``` +### Attest Multiple Docker Images + +You can also attest multiple docker images by passing a list of images to the `subject-images` input. Each image should be specified in the format "registry/image:tag@digest". + +```yaml +name: build-attested-images + +on: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + id-token: write + packages: write + contents: read + attestations: write + env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push images + id: push + run: | + docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest . + docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:v1.0.0 . + docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:v1.0.0 + - name: Attest + uses: actions/attest-build-provenance@v2 + id: attest + with: + subject-images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest@${{ steps.push.outputs.digest }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:v1.0.0@${{ steps.push.outputs.digest }} + push-to-registry: true +``` + ### Integration with `actions/upload-artifact` If you'd like to create an attestation for an archive created with the diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index e437044..b9ce429 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -154,4 +154,33 @@ describe('main', () => { expect(outputs['predicate-type']).toBe('https://slsa.dev/provenance/v1') }) }) + + describe('when subject-images input is provided', () => { + beforeEach(() => { + process.env = { + ...originalEnv, + GITHUB_SERVER_URL: 'https://github.com', + GITHUB_REPOSITORY: 'owner/repo' + } + }) + + it('processes multiple docker images for attestation', async () => { + const subjectImages = ` + registry/image:tag@sha256:1234567890abcdef + registry/image2:tag@sha256:abcdef1234567890 + ` + jest.spyOn(core, 'getInput').mockImplementation((name) => { + if (name === 'subject-images') { + return subjectImages + } + return '' + }) + + await main.run() + + expect(setOutputMock).toHaveBeenCalledTimes(2) + expect(outputs['predicate']).toMatchSnapshot() + expect(outputs['predicate-type']).toBe('https://slsa.dev/provenance/v1') + }) + }) }) diff --git a/action.yml b/action.yml index 4826a30..5a45a0b 100644 --- a/action.yml +++ b/action.yml @@ -9,15 +9,14 @@ inputs: subject-path: description: > Path to the artifact serving as the subject of the attestation. Must - specify exactly one of "subject-path", "subject-digest", or - "subject-checksums". May contain a glob pattern or list of paths + specify exactly one of "subject-path", "subject-digest", "subject-checksums", or "subject-images". May contain a glob pattern or list of paths (total subject count cannot exceed 1024). required: false subject-digest: description: > Digest of the subject for which provenance will be generated. Must be in the form "algorithm:hex_digest" (e.g. "sha256:abc123..."). Must specify - exactly one of "subject-path", "subject-digest", or "subject-checksums". + exactly one of "subject-path", "subject-digest", "subject-checksums", or "subject-images". required: false subject-name: description: > @@ -27,7 +26,13 @@ inputs: description: > Path to checksums file containing digest and name of subjects for attestation. Must specify exactly one of "subject-path", "subject-digest", - or "subject-checksums". + "subject-checksums", or "subject-images". + required: false + subject-images: + description: > + List of docker images to attest. Each image should be specified in the + format "registry/image:tag@digest". Must specify exactly one of + "subject-path", "subject-digest", "subject-checksums", or "subject-images". required: false push-to-registry: description: > @@ -71,6 +76,7 @@ runs: subject-digest: ${{ inputs.subject-digest }} subject-name: ${{ inputs.subject-name }} subject-checksums: ${{ inputs.subject-checksums }} + subject-images: ${{ inputs.subject-images }} predicate-type: ${{ steps.generate-build-provenance-predicate.outputs.predicate-type }} predicate: ${{ steps.generate-build-provenance-predicate.outputs.predicate }} push-to-registry: ${{ inputs.push-to-registry }} diff --git a/src/main.ts b/src/main.ts index 0b1f21a..184cafe 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,6 @@ import { buildSLSAProvenancePredicate } from '@actions/attest' import * as core from '@actions/core' +import { parseMultiImageInput } from './utils' /** * The main function for the action. @@ -7,6 +8,15 @@ import * as core from '@actions/core' */ export async function run(): Promise { try { + const subjectImages = core.getInput('subject-images') + if (subjectImages) { + const images = parseMultiImageInput(subjectImages) + for (const image of images) { + core.info(`Processing image: ${image}`) + // Add logic to process each image for attestation + } + } + // Calculate subject from inputs and generate provenance const predicate = await buildSLSAProvenancePredicate() diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..e8bc343 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,12 @@ +/** + * Utility functions for the action. + */ + +/** + * Parses the multi-image input string and returns an array of image strings. + * @param {string} input - The multi-image input string. + * @returns {string[]} An array of image strings. + */ +export function parseMultiImageInput(input: string): string[] { + return input.split('\n').map(image => image.trim()).filter(image => image.length > 0) +}