From 945a5542e0c390e810fb7cad394604d8096b28b5 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 26 Mar 2024 15:31:37 +0100 Subject: [PATCH] handle attests correctly with provenance and sbom inputs Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- __tests__/context.test.ts | 88 +++++++++++++++++++++++++++++------ src/context.ts | 97 ++++++++++++++++++++++++++++----------- 2 files changed, 144 insertions(+), 41 deletions(-) diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts index bcc8bba45..0f8f1d69e 100644 --- a/__tests__/context.test.ts +++ b/__tests__/context.test.ts @@ -481,7 +481,7 @@ nproc=3`], [ 'build', '--iidfile', path.join(tmpDir, 'iidfile'), - "--provenance", `mode=min,inline-only=true,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, + '--attest', `type=provenance,mode=min,inline-only=true,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, '--metadata-file', path.join(tmpDir, 'metadata-file'), '.' ] @@ -500,7 +500,7 @@ nproc=3`], [ 'build', '--iidfile', path.join(tmpDir, 'iidfile'), - "--provenance", `builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, + '--attest', `type=provenance,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, '--metadata-file', path.join(tmpDir, 'metadata-file'), '.' ] @@ -519,7 +519,7 @@ nproc=3`], [ 'build', '--iidfile', path.join(tmpDir, 'iidfile'), - "--provenance", `mode=max,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, + '--attest', `type=provenance,mode=max,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, '--metadata-file', path.join(tmpDir, 'metadata-file'), '.' ] @@ -538,7 +538,7 @@ nproc=3`], [ 'build', '--iidfile', path.join(tmpDir, 'iidfile'), - "--provenance", 'false', + '--attest', 'type=provenance,false', '--metadata-file', path.join(tmpDir, 'metadata-file'), '.' ] @@ -557,7 +557,7 @@ nproc=3`], [ 'build', '--iidfile', path.join(tmpDir, 'iidfile'), - "--provenance", 'builder-id=foo', + '--attest', 'type=provenance,builder-id=foo', '--metadata-file', path.join(tmpDir, 'metadata-file'), '.' ] @@ -620,7 +620,7 @@ nproc=3`], ] ], [ - 25, + 26, '0.10.0', new Map([ ['context', '.'], @@ -642,7 +642,7 @@ ANOTHER_SECRET=ANOTHER_SECRET_ENV`] ] ], [ - 26, + 27, '0.10.0', new Map([ ['context', '.'], @@ -663,7 +663,7 @@ ANOTHER_SECRET=ANOTHER_SECRET_ENV`] ] ], [ - 27, + 28, '0.11.0', new Map([ ['context', '.'], @@ -677,13 +677,13 @@ ANOTHER_SECRET=ANOTHER_SECRET_ENV`] [ 'build', '--output', 'type=local,dest=./release-out', - "--provenance", `mode=min,inline-only=true,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, + '--attest', `type=provenance,mode=min,inline-only=true,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, '--metadata-file', path.join(tmpDir, 'metadata-file'), '.' ] ], [ - 28, + 29, '0.12.0', new Map([ ['context', '.'], @@ -701,13 +701,13 @@ ANOTHER_SECRET=ANOTHER_SECRET_ENV`] '--annotation', 'manifest:example3=yyy', '--annotation', 'manifest-descriptor[linux/amd64]:example4=zzz', '--output', 'type=local,dest=./release-out', - "--provenance", `mode=min,inline-only=true,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, + '--attest', `type=provenance,mode=min,inline-only=true,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, '--metadata-file', path.join(tmpDir, 'metadata-file'), '.' ] ], [ - 29, + 30, '0.12.0', new Map([ ['context', '.'], @@ -721,11 +721,71 @@ ANOTHER_SECRET=ANOTHER_SECRET_ENV`] 'build', '--iidfile', path.join(tmpDir, 'iidfile'), "--output", `type=image,"name=localhost:5000/name/app:latest,localhost:5000/name/app:foo",push-by-digest=true,name-canonical=true,push=true`, - "--provenance", `mode=min,inline-only=true,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, + '--attest', `type=provenance,mode=min,inline-only=true,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, '--metadata-file', path.join(tmpDir, 'metadata-file'), '.' ] - ] + ], + [ + 31, + '0.13.1', + new Map([ + ['context', '.'], + ['load', 'false'], + ['no-cache', 'false'], + ['push', 'false'], + ['pull', 'false'], + ['provenance', 'mode=max'], + ['sbom', 'true'], + ]), + [ + 'build', + '--iidfile', path.join(tmpDir, 'iidfile'), + '--attest', `type=provenance,mode=max,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, + '--attest', `type=sbom,true`, + '--metadata-file', path.join(tmpDir, 'metadata-file'), + '.' + ] + ], + [ + 32, + '0.13.1', + new Map([ + ['context', '.'], + ['load', 'false'], + ['no-cache', 'false'], + ['push', 'false'], + ['pull', 'false'], + ['attests', 'type=provenance,mode=min'], + ['provenance', 'mode=max'], + ]), + [ + 'build', + '--iidfile', path.join(tmpDir, 'iidfile'), + '--attest', `type=provenance,mode=max,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, + '--metadata-file', path.join(tmpDir, 'metadata-file'), + '.' + ] + ], + [ + 33, + '0.13.1', + new Map([ + ['context', '.'], + ['load', 'false'], + ['no-cache', 'false'], + ['push', 'false'], + ['pull', 'false'], + ['attests', 'type=provenance,mode=min'], + ]), + [ + 'build', + '--iidfile', path.join(tmpDir, 'iidfile'), + '--attest', `type=provenance,mode=min,builder-id=https://github.com/docker/build-push-action/actions/runs/123456789`, + '--metadata-file', path.join(tmpDir, 'metadata-file'), + '.' + ] + ], ])( '[%d] given %p with %p as inputs, returns %p', async (num: number, buildxVersion: string, inputs: Map, expected: Array) => { diff --git a/src/context.ts b/src/context.ts index 28ec8710f..dfb99106c 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,5 +1,6 @@ import * as core from '@actions/core'; import * as handlebars from 'handlebars'; +import {parse} from 'csv-parse/sync'; import {Context} from '@docker/actions-toolkit/lib/context'; import {GitHub} from '@docker/actions-toolkit/lib/github'; import {Inputs as BuildxInputs} from '@docker/actions-toolkit/lib/buildx/inputs'; @@ -98,13 +99,6 @@ async function getBuildArgs(inputs: Inputs, context: string, toolkit: Toolkit): if (inputs.allow.length > 0) { args.push('--allow', inputs.allow.join(',')); } - if (await toolkit.buildx.versionSatisfies('>=0.10.0')) { - await Util.asyncForEach(inputs.attests, async attest => { - args.push('--attest', attest); - }); - } else if (inputs.attests.length > 0) { - core.warning("Attestations are only supported by buildx >= 0.10.0; the input 'attests' is ignored."); - } if (await toolkit.buildx.versionSatisfies('>=0.12.0')) { await Util.asyncForEach(inputs.annotations, async annotation => { args.push('--annotation', annotation); @@ -157,26 +151,9 @@ async function getBuildArgs(inputs: Inputs, context: string, toolkit: Toolkit): args.push('--platform', inputs.platforms.join(',')); } if (await toolkit.buildx.versionSatisfies('>=0.10.0')) { - if (inputs.provenance) { - args.push('--provenance', inputs.provenance); - } else if ((await toolkit.buildkit.versionSatisfies(inputs.builder, '>=0.11.0')) && !BuildxInputs.hasDockerExporter(inputs.outputs, inputs.load)) { - // if provenance not specified and BuildKit version compatible for - // attestation, set default provenance. Also needs to make sure user - // doesn't want to explicitly load the image to docker. - if (GitHub.context.payload.repository?.private ?? false) { - // if this is a private repository, we set the default provenance - // attributes being set in buildx: https://github.com/docker/buildx/blob/fb27e3f919dcbf614d7126b10c2bc2d0b1927eb6/build/build.go#L603 - args.push('--provenance', BuildxInputs.resolveProvenanceAttrs(`mode=min,inline-only=true`)); - } else { - // for a public repository, we set max provenance mode. - args.push('--provenance', BuildxInputs.resolveProvenanceAttrs(`mode=max`)); - } - } - if (inputs.sbom) { - args.push('--sbom', inputs.sbom); - } - } else if (inputs.provenance || inputs.sbom) { - core.warning("Attestations are only supported by buildx >= 0.10.0; the inputs 'provenance' and 'sbom' are ignored."); + args.push(...(await getAttestArgs(inputs, toolkit))); + } else { + core.warning("Attestations are only supported by buildx >= 0.10.0; the inputs 'attests', 'provenance' and 'sbom' are ignored."); } await Util.asyncForEach(inputs.secrets, async secret => { try { @@ -238,3 +215,69 @@ async function getCommonArgs(inputs: Inputs, toolkit: Toolkit): Promise> { + const args: Array = []; + + // check if provenance attestation is set in attests input + let hasAttestProvenance = false; + await Util.asyncForEach(inputs.attests, async (attest: string) => { + if (hasAttestationType('provenance', attest)) { + hasAttestProvenance = true; + } + }); + + let provenanceSet = false; + let sbomSet = false; + if (inputs.provenance) { + args.push('--attest', `type=provenance,${inputs.provenance}`); + provenanceSet = true; + } else if (!hasAttestProvenance && (await toolkit.buildkit.versionSatisfies(inputs.builder, '>=0.11.0')) && !BuildxInputs.hasDockerExporter(inputs.outputs, inputs.load)) { + // if provenance not specified in provenance or attests inputs and BuildKit + // version compatible for attestation, set default provenance. Also needs + // to make sure user doesn't want to explicitly load the image to docker. + if (GitHub.context.payload.repository?.private ?? false) { + // if this is a private repository, we set the default provenance + // attributes being set in buildx: https://github.com/docker/buildx/blob/fb27e3f919dcbf614d7126b10c2bc2d0b1927eb6/build/build.go#L603 + args.push('--attest', `type=provenance,${BuildxInputs.resolveProvenanceAttrs(`mode=min,inline-only=true`)}`); + } else { + // for a public repository, we set max provenance mode. + args.push('--attest', `type=provenance,${BuildxInputs.resolveProvenanceAttrs(`mode=max`)}`); + } + } + if (inputs.sbom) { + args.push('--attest', `type=sbom,${inputs.sbom}`); + sbomSet = true; + } + + // set attests but check if provenance or sbom types already set as + // provenance and sbom inputs take precedence over attests input. + await Util.asyncForEach(inputs.attests, async (attest: string) => { + if (!hasAttestationType('provenance', attest) && !hasAttestationType('sbom', attest)) { + args.push('--attest', attest); + } else if (!provenanceSet && hasAttestationType('provenance', attest)) { + args.push('--attest', BuildxInputs.resolveProvenanceAttrs(attest)); + } else if (!sbomSet && hasAttestationType('sbom', attest)) { + args.push('--attest', attest); + } + }); + + return args; +} + +function hasAttestationType(name: string, attrs: string): boolean { + const attributes = parse(attrs, { + delimiter: ',', + trim: true, + columns: false, + relaxColumnCount: true + }); + for (const attr of attributes) { + for (const [key, value] of attr.map((chunk: string) => chunk.split('=').map(item => item.trim()))) { + if (key == 'type' && value == name) { + return true; + } + } + } + return false; +}