handle attests correctly with provenance and sbom inputs

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax 2024-03-26 15:31:37 +01:00
parent 090ca155fc
commit 5bb8c00e8f
No known key found for this signature in database
GPG Key ID: ADE44D8C9D44FBE4
3 changed files with 145 additions and 41 deletions

View File

@ -481,7 +481,7 @@ nproc=3`],
[ [
'build', 'build',
'--iidfile', path.join(tmpDir, 'iidfile'), '--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'), '--metadata-file', path.join(tmpDir, 'metadata-file'),
'.' '.'
] ]
@ -500,7 +500,7 @@ nproc=3`],
[ [
'build', 'build',
'--iidfile', path.join(tmpDir, 'iidfile'), '--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'), '--metadata-file', path.join(tmpDir, 'metadata-file'),
'.' '.'
] ]
@ -519,7 +519,7 @@ nproc=3`],
[ [
'build', 'build',
'--iidfile', path.join(tmpDir, 'iidfile'), '--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'), '--metadata-file', path.join(tmpDir, 'metadata-file'),
'.' '.'
] ]
@ -538,7 +538,7 @@ nproc=3`],
[ [
'build', 'build',
'--iidfile', path.join(tmpDir, 'iidfile'), '--iidfile', path.join(tmpDir, 'iidfile'),
"--provenance", 'false', '--attest', 'type=provenance,false',
'--metadata-file', path.join(tmpDir, 'metadata-file'), '--metadata-file', path.join(tmpDir, 'metadata-file'),
'.' '.'
] ]
@ -557,7 +557,7 @@ nproc=3`],
[ [
'build', 'build',
'--iidfile', path.join(tmpDir, 'iidfile'), '--iidfile', path.join(tmpDir, 'iidfile'),
"--provenance", 'builder-id=foo', '--attest', 'type=provenance,builder-id=foo',
'--metadata-file', path.join(tmpDir, 'metadata-file'), '--metadata-file', path.join(tmpDir, 'metadata-file'),
'.' '.'
] ]
@ -620,7 +620,7 @@ nproc=3`],
] ]
], ],
[ [
25, 26,
'0.10.0', '0.10.0',
new Map<string, string>([ new Map<string, string>([
['context', '.'], ['context', '.'],
@ -642,7 +642,7 @@ ANOTHER_SECRET=ANOTHER_SECRET_ENV`]
] ]
], ],
[ [
26, 27,
'0.10.0', '0.10.0',
new Map<string, string>([ new Map<string, string>([
['context', '.'], ['context', '.'],
@ -663,7 +663,7 @@ ANOTHER_SECRET=ANOTHER_SECRET_ENV`]
] ]
], ],
[ [
27, 28,
'0.11.0', '0.11.0',
new Map<string, string>([ new Map<string, string>([
['context', '.'], ['context', '.'],
@ -677,13 +677,13 @@ ANOTHER_SECRET=ANOTHER_SECRET_ENV`]
[ [
'build', 'build',
'--output', 'type=local,dest=./release-out', '--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'), '--metadata-file', path.join(tmpDir, 'metadata-file'),
'.' '.'
] ]
], ],
[ [
28, 29,
'0.12.0', '0.12.0',
new Map<string, string>([ new Map<string, string>([
['context', '.'], ['context', '.'],
@ -701,13 +701,13 @@ ANOTHER_SECRET=ANOTHER_SECRET_ENV`]
'--annotation', 'manifest:example3=yyy', '--annotation', 'manifest:example3=yyy',
'--annotation', 'manifest-descriptor[linux/amd64]:example4=zzz', '--annotation', 'manifest-descriptor[linux/amd64]:example4=zzz',
'--output', 'type=local,dest=./release-out', '--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'), '--metadata-file', path.join(tmpDir, 'metadata-file'),
'.' '.'
] ]
], ],
[ [
29, 30,
'0.12.0', '0.12.0',
new Map<string, string>([ new Map<string, string>([
['context', '.'], ['context', '.'],
@ -721,11 +721,71 @@ ANOTHER_SECRET=ANOTHER_SECRET_ENV`]
'build', 'build',
'--iidfile', path.join(tmpDir, 'iidfile'), '--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`, "--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'), '--metadata-file', path.join(tmpDir, 'metadata-file'),
'.' '.'
] ]
],
[
31,
'0.13.1',
new Map<string, string>([
['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<string, string>([
['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<string, string>([
['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', '[%d] given %p with %p as inputs, returns %p',
async (num: number, buildxVersion: string, inputs: Map<string, string>, expected: Array<string>) => { async (num: number, buildxVersion: string, inputs: Map<string, string>, expected: Array<string>) => {

View File

@ -28,6 +28,7 @@
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@docker/actions-toolkit": "0.19.0", "@docker/actions-toolkit": "0.19.0",
"csv-parse": "^5.5.5",
"handlebars": "^4.7.7" "handlebars": "^4.7.7"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,5 +1,6 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as handlebars from 'handlebars'; import * as handlebars from 'handlebars';
import {parse} from 'csv-parse/sync';
import {Context} from '@docker/actions-toolkit/lib/context'; import {Context} from '@docker/actions-toolkit/lib/context';
import {GitHub} from '@docker/actions-toolkit/lib/github'; import {GitHub} from '@docker/actions-toolkit/lib/github';
import {Inputs as BuildxInputs} from '@docker/actions-toolkit/lib/buildx/inputs'; 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) { if (inputs.allow.length > 0) {
args.push('--allow', inputs.allow.join(',')); 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')) { if (await toolkit.buildx.versionSatisfies('>=0.12.0')) {
await Util.asyncForEach(inputs.annotations, async annotation => { await Util.asyncForEach(inputs.annotations, async annotation => {
args.push('--annotation', annotation); args.push('--annotation', annotation);
@ -157,26 +151,9 @@ async function getBuildArgs(inputs: Inputs, context: string, toolkit: Toolkit):
args.push('--platform', inputs.platforms.join(',')); args.push('--platform', inputs.platforms.join(','));
} }
if (await toolkit.buildx.versionSatisfies('>=0.10.0')) { if (await toolkit.buildx.versionSatisfies('>=0.10.0')) {
if (inputs.provenance) { args.push(...(await getAttestArgs(inputs, toolkit)));
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 { } else {
// for a public repository, we set max provenance mode. core.warning("Attestations are only supported by buildx >= 0.10.0; the inputs 'attests', 'provenance' and 'sbom' are ignored.");
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.");
} }
await Util.asyncForEach(inputs.secrets, async secret => { await Util.asyncForEach(inputs.secrets, async secret => {
try { try {
@ -238,3 +215,69 @@ async function getCommonArgs(inputs: Inputs, toolkit: Toolkit): Promise<Array<st
} }
return args; return args;
} }
async function getAttestArgs(inputs: Inputs, toolkit: Toolkit): Promise<Array<string>> {
const args: Array<string> = [];
// 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;
}