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);