audit-labs/gh-attest
GitHub Audit Evidence Extractor
clone: git clone https://gitbay.org/audit-labs/gh-attest.git
v1.0.3: scripts/check-mappings.mjs · raw
1// Sync check: assert that the control-framework mappings in migrations/ exactly
2// match the reference table in docs/framework-mapping.md. The migrations are the
3// source of truth the engine reads; the doc is the human-readable copy an auditor
4// relies on. If they drift, the doc is lying — so this fails CI.
5//
6// It works by actually applying every migration to an in-memory SQLite database
7// (so migration 0003's delete-and-reinsert is handled exactly as production D1
8// would), reading back control_mappings, and diffing against the rows parsed out
9// of the doc's "Complete mapping reference" table.
10//
11// Run: npm run test:mappings (no dependencies — uses Node's built-in sqlite)
12
13import { DatabaseSync } from "node:sqlite";
14import { readFileSync, readdirSync } from "node:fs";
15import { fileURLToPath } from "node:url";
16import { dirname, join } from "node:path";
17
18const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
19const migrationsDir = join(repoRoot, "migrations");
20const docPath = join(repoRoot, "docs", "framework-mapping.md");
21
22// A "·" in the doc's Status column means the mapping's status is NULL (matches
23// any status); normalize both sides to this sentinel so they compare equal.
24const NULL_STATUS = "·";
25const key = (resource, status, framework, control, posture) =>
26 `${resource}|${status ?? NULL_STATUS}|${framework}|${control}|${posture}`;
27
28// --- 1. Source of truth: apply migrations, read control_mappings. ---
29function rowsFromMigrations() {
30 const db = new DatabaseSync(":memory:");
31 const files = readdirSync(migrationsDir)
32 .filter((f) => f.endsWith(".sql"))
33 .sort();
34 for (const file of files) {
35 db.exec(readFileSync(join(migrationsDir, file), "utf8"));
36 }
37 const rows = db
38 .prepare("SELECT resource, status, framework, control_id, posture FROM control_mappings")
39 .all();
40 db.close();
41 return new Set(rows.map((r) => key(r.resource, r.status, r.framework, r.control_id, r.posture)));
42}
43
44// --- 2. Human-readable copy: parse the doc's reference table. ---
45// Rows are parsed by splitting on "|" rather than a single big regex — that is
46// unambiguous and linear (no backtracking). A row counts only if it has the
47// reference table's shape: a backtick-wrapped resource, a known framework
48// label, and a bare posture word. That shape excludes the header/separator
49// rows, the per-section tables (framework-first, no backticked resource), and
50// the glossary (fewer columns) — so only reference-table data rows match.
51const FRAMEWORK_LABELS = { "SOC 2": "soc2", "ISO 27001": "iso27001" };
52const POSTURES = new Set(["positive", "negative", "informational"]);
53const BACKTICKED = /^`.+`$/;
54
55function rowsFromDoc() {
56 const set = new Set();
57 for (const line of readFileSync(docPath, "utf8").split("\n")) {
58 if (!line.startsWith("|")) continue;
59 // Leading "|" yields an empty cells[0]; data lives in cells[1..5].
60 const cells = line.split("|").map((c) => c.trim());
61 const [, resource, statusCell, frameworkLabel, control, posture] = cells;
62 const framework = FRAMEWORK_LABELS[frameworkLabel];
63 if (!framework || !POSTURES.has(posture) || !BACKTICKED.test(resource ?? "")) continue;
64 const status = statusCell.replaceAll("`", ""); // "·" for NULL
65 set.add(key(resource.replaceAll("`", ""), status, framework, control, posture));
66 }
67 return set;
68}
69
70// --- 3. Diff. ---
71const db = rowsFromMigrations();
72const doc = rowsFromDoc();
73
74const onlyInDb = [...db].filter((k) => !doc.has(k)).sort((a, b) => a.localeCompare(b));
75const onlyInDoc = [...doc].filter((k) => !db.has(k)).sort((a, b) => a.localeCompare(b));
76
77if (onlyInDb.length === 0 && onlyInDoc.length === 0) {
78 console.log(`✓ mappings in sync: ${db.size} rows match between migrations/ and docs/framework-mapping.md`);
79 process.exit(0);
80}
81
82console.error("✗ control-mapping drift between migrations/ and docs/framework-mapping.md\n");
83console.error(" columns: resource | status | framework | control | posture\n");
84if (onlyInDb.length) {
85 console.error(` In migrations but MISSING from the doc (${onlyInDb.length}):`);
86 for (const k of onlyInDb) console.error(` + ${k}`);
87}
88if (onlyInDoc.length) {
89 console.error(` In the doc but MISSING from migrations (${onlyInDoc.length}):`);
90 for (const k of onlyInDoc) console.error(` - ${k}`);
91}
92console.error("\n Fix: update whichever is wrong so migrations/ and the doc's reference table agree.");
93process.exit(1);