




















@@ -0,0 +1,202 @@
1+#!/usr/bin/env node
2+3+import { execFileSync } from "node:child_process";
4+import process from "node:process";
5+6+const ATTESTATION_REFERENCE_TYPE = "attestation-manifest";
7+const REQUIRED_PREDICATES = ["https://spdx.dev/Document", "https://slsa.dev/provenance/v1"];
8+9+export function imageRefForDigest(imageRef, digest) {
10+const atIndex = imageRef.indexOf("@");
11+if (atIndex >= 0) {
12+return `${imageRef.slice(0, atIndex)}@${digest}`;
13+}
14+const lastSlash = imageRef.lastIndexOf("/");
15+const tagIndex = imageRef.indexOf(":", lastSlash + 1);
16+const base = tagIndex >= 0 ? imageRef.slice(0, tagIndex) : imageRef;
17+return `${base}@${digest}`;
18+}
19+20+export function parsePlatform(value) {
21+const [os, architecture, variant] = value.split("/");
22+if (!os || !architecture || value.split("/").length > 3) {
23+throw new Error(`Invalid platform ${JSON.stringify(value)}. Expected os/architecture.`);
24+}
25+return { architecture, os, variant };
26+}
27+28+function formatPlatform(platform) {
29+return platform.variant
30+ ? `${platform.os}/${platform.architecture}/${platform.variant}`
31+ : `${platform.os}/${platform.architecture}`;
32+}
33+34+function platformMatches(actual, expected) {
35+return (
36+actual?.os === expected.os &&
37+actual?.architecture === expected.architecture &&
38+(expected.variant ? actual?.variant === expected.variant : true)
39+);
40+}
41+42+function parseJson(raw, label) {
43+try {
44+return JSON.parse(raw);
45+} catch (error) {
46+const reason = error instanceof Error ? error.message : String(error);
47+throw new Error(`Failed to parse ${label}: ${reason}`, { cause: error });
48+}
49+}
50+51+export function collectDockerAttestationErrors(params) {
52+const {
53+ imageRef,
54+ index,
55+ inspectAttestation,
56+ requiredPlatforms,
57+ requiredPredicates = REQUIRED_PREDICATES,
58+} = params;
59+const errors = [];
60+const manifests = Array.isArray(index?.manifests) ? index.manifests : [];
61+if (manifests.length === 0) {
62+return [`${imageRef}: expected an image index with manifest descriptors`];
63+}
64+65+for (const platform of requiredPlatforms) {
66+const platformLabel = formatPlatform(platform);
67+const imageManifest = manifests.find((entry) => platformMatches(entry.platform, platform));
68+if (!imageManifest?.digest) {
69+errors.push(`${imageRef}: missing image manifest for ${platformLabel}`);
70+continue;
71+}
72+73+const attestationDescriptors = manifests.filter(
74+(entry) =>
75+entry?.annotations?.["vnd.docker.reference.type"] === ATTESTATION_REFERENCE_TYPE &&
76+entry?.annotations?.["vnd.docker.reference.digest"] === imageManifest.digest &&
77+typeof entry.digest === "string" &&
78+entry.digest.length > 0,
79+);
80+if (attestationDescriptors.length === 0) {
81+errors.push(`${imageRef}: missing attestation manifest for ${platformLabel}`);
82+continue;
83+}
84+85+const predicates = new Set();
86+for (const descriptor of attestationDescriptors) {
87+const attestation = inspectAttestation(descriptor.digest);
88+if (attestation?.artifactType !== "application/vnd.docker.attestation.manifest.v1+json") {
89+errors.push(
90+`${imageRef}: ${platformLabel} attestation ${descriptor.digest} has unexpected artifactType ${JSON.stringify(
91+ attestation?.artifactType,
92+ )}`,
93+);
94+}
95+for (const layer of attestation?.layers ?? []) {
96+const predicate = layer?.annotations?.["in-toto.io/predicate-type"];
97+if (typeof predicate === "string") {
98+predicates.add(predicate);
99+}
100+}
101+}
102+103+for (const predicate of requiredPredicates) {
104+if (!predicates.has(predicate)) {
105+errors.push(`${imageRef}: ${platformLabel} missing predicate ${predicate}`);
106+}
107+}
108+}
109+110+return errors;
111+}
112+113+function inspectRaw(imageRef) {
114+return execFileSync("docker", ["buildx", "imagetools", "inspect", "--raw", imageRef], {
115+encoding: "utf8",
116+maxBuffer: 20 * 1024 * 1024,
117+stdio: ["ignore", "pipe", "pipe"],
118+});
119+}
120+121+function parseArgs(argv) {
122+const imageRefs = [];
123+const requiredPlatforms = [];
124+for (let i = 0; i < argv.length; i += 1) {
125+const arg = argv[i];
126+if (arg === "--platform") {
127+const value = argv[i + 1];
128+if (!value) {
129+throw new Error("--platform requires a value");
130+}
131+requiredPlatforms.push(parsePlatform(value));
132+i += 1;
133+continue;
134+}
135+if (arg === "--help" || arg === "-h") {
136+return { help: true, imageRefs, requiredPlatforms };
137+}
138+if (arg?.startsWith("-")) {
139+throw new Error(`Unknown option: ${arg}`);
140+}
141+imageRefs.push(arg);
142+}
143+return { help: false, imageRefs, requiredPlatforms };
144+}
145+146+function printHelp() {
147+console.log(
148+`Usage: node scripts/verify-docker-attestations.mjs --platform linux/amd64 --platform linux/arm64 IMAGE...`,
149+);
150+}
151+152+async function main() {
153+const parsed = parseArgs(process.argv.slice(2));
154+if (parsed.help) {
155+printHelp();
156+return;
157+}
158+if (parsed.imageRefs.length === 0) {
159+throw new Error("At least one image reference is required.");
160+}
161+if (parsed.requiredPlatforms.length === 0) {
162+throw new Error("At least one --platform is required.");
163+}
164+165+const allErrors = [];
166+for (const imageRef of parsed.imageRefs) {
167+const index = parseJson(inspectRaw(imageRef), `${imageRef} index`);
168+const errors = collectDockerAttestationErrors({
169+ imageRef,
170+ index,
171+requiredPlatforms: parsed.requiredPlatforms,
172+inspectAttestation(digest) {
173+return parseJson(
174+inspectRaw(imageRefForDigest(imageRef, digest)),
175+`${imageRef} attestation ${digest}`,
176+);
177+},
178+});
179+if (errors.length === 0) {
180+console.log(
181+`Verified Docker attestations for ${imageRef}: ${parsed.requiredPlatforms
182+ .map(formatPlatform)
183+ .join(", ")}`,
184+);
185+}
186+allErrors.push(...errors);
187+}
188+189+if (allErrors.length > 0) {
190+for (const error of allErrors) {
191+console.error(`[docker-attestations] ${error}`);
192+}
193+process.exit(1);
194+}
195+}
196+197+if (import.meta.url === `file://${process.argv[1]}`) {
198+main().catch((error) => {
199+console.error(error instanceof Error ? error.message : String(error));
200+process.exit(1);
201+});
202+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。