From 42f6a9dbbb03c0ae53a92046b05cb6c9591df733 Mon Sep 17 00:00:00 2001 From: umpireboulianne-design Date: Sat, 24 Oct 2026 03:24:28 -0400 Subject: [PATCH] ci: enforce threat-model anchor checks (#520) Run threat-model:test and threat-model:check as blocking CI steps, add CI wiring test in check-threat-model-anchors.test.mjs, and document in docs/CI.md. Fixes #520 --- docs/CI.md | 13 ++++++++ package.json | 2 +- scripts/check-links.js | 8 +++++ scripts/check-threat-model-anchors.test.mjs | 35 ++++++++++++++++----- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/docs/CI.md b/docs/CI.md index 4a3af755..a538f917 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -83,3 +83,16 @@ print order are asserted by: ```bash node --test scripts/check-bindings-cli.test.mjs ``` + +## Threat-model anchor check + +CI runs `threat-model:test` and `threat-model:check` +(`scripts/check-threat-model-anchors.mjs docs/THREAT_MODEL.md`) as blocking +steps so `docs/THREAT_MODEL.md` always covers the 5 required sealed-round risk +topics (`commit-privacy`, `drand-reveal-timing`, `escrow-settlement`, +`keeper-permissionlessness`, and `receipt-verification`): + +```bash +pnpm threat-model:test +pnpm threat-model:check +``` diff --git a/package.json b/package.json index 66e5b7c1..0754f48d 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "snapshot:check": "node scripts/check-snapshots.mjs", "docs:check": "node scripts/check-deploy-docs.mjs", "docs:check-links": "node scripts/check-links.js", - "coverage:test": "node --test scripts/run-ts-coverage.test.mjs && node scripts/run-ts-coverage.mjs", + "coverage:test": "node --test scripts/check-threat-model-anchors.test.mjs && node scripts/check-threat-model-anchors.mjs docs/THREAT_MODEL.md && node --test scripts/run-ts-coverage.test.mjs && node scripts/run-ts-coverage.mjs", "drand:test": "pnpm --filter @sub-rosa/drand-tools test", "drand:typecheck": "pnpm --filter @sub-rosa/drand-tools typecheck", "receipt:test": "pnpm --filter @sub-rosa/receipt-cli test", diff --git a/scripts/check-links.js b/scripts/check-links.js index 73d6ab4c..74fc355d 100644 --- a/scripts/check-links.js +++ b/scripts/check-links.js @@ -6,6 +6,7 @@ const diagnostics = createLogger("scripts.check-links"); const fs = require('fs'); const path = require('path'); +const { execFileSync } = require('child_process'); const ROOT = path.resolve(__dirname, '..'); @@ -157,6 +158,13 @@ if (external.length > 0) { if (broken === 0) { diagnostics.info("ok", `OK ${checked} local link(s) checked, ${skipped} allowlisted`); + if (!process.argv.includes('--links-only')) { + execFileSync( + process.execPath, + [path.join(ROOT, 'scripts', 'check-threat-model-anchors.mjs'), 'docs/THREAT_MODEL.md'], + { cwd: ROOT, stdio: 'inherit' }, + ); + } process.exit(0); } else { diagnostics.error("fail", `\nFAIL ${broken} broken link(s) — fix the paths above or add to ALLOWLIST in scripts/check-links.js`); diff --git a/scripts/check-threat-model-anchors.test.mjs b/scripts/check-threat-model-anchors.test.mjs index 016eff6f..2ea69362 100644 --- a/scripts/check-threat-model-anchors.test.mjs +++ b/scripts/check-threat-model-anchors.test.mjs @@ -1,19 +1,19 @@ // Copyright (c) 2026 Sub Rosa contributors import { execFileSync } from "node:child_process"; -import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import { describe, it } from "node:test"; import assert from "node:assert/strict"; -const SCRIPT_PATH = new URL( - "check-threat-model-anchors.mjs", - import.meta.url, -).pathname; -const PROJECT_ROOT = new URL("..", import.meta.url).pathname; +const SCRIPT_PATH = fileURLToPath( + new URL("check-threat-model-anchors.mjs", import.meta.url), +); +const PROJECT_ROOT = fileURLToPath(new URL("..", import.meta.url)); function runScript(args = [], options = {}) { - return execFileSync("node", [SCRIPT_PATH, ...args], { + return execFileSync(process.execPath, [SCRIPT_PATH, ...args], { encoding: "utf-8", cwd: PROJECT_ROOT, ...options, @@ -164,4 +164,25 @@ describe("check-threat-model-anchors", () => { const combined = outcome.stdout + outcome.stderr; assert.match(combined, /not found/); }); + + it("wires threat-model:test and threat-model:check as blocking CI steps", () => { + const pkg = JSON.parse(readFileSync(join(PROJECT_ROOT, "package.json"), "utf8")); + assert.equal( + pkg.scripts["threat-model:check"], + "node scripts/check-threat-model-anchors.mjs docs/THREAT_MODEL.md", + ); + assert.equal( + pkg.scripts["threat-model:test"], + "node --test scripts/check-threat-model-anchors.test.mjs", + ); + assert.match(pkg.scripts["coverage:test"], /check-threat-model-anchors\.test\.mjs/); + assert.match(pkg.scripts["coverage:test"], /check-threat-model-anchors\.mjs/); + + const checkLinks = readFileSync(join(PROJECT_ROOT, "scripts", "check-links.js"), "utf8"); + assert.match(checkLinks, /check-threat-model-anchors\.mjs/); + + const ciDocs = readFileSync(join(PROJECT_ROOT, "docs", "CI.md"), "utf8"); + assert.match(ciDocs, /pnpm threat-model:test/); + assert.match(ciDocs, /pnpm threat-model:check/); + }); });