1
24import { describe, expect, it } from "vitest";
25import { existsSync, readFileSync } from "node:fs";
26import path from "node:path";
27import { fileURLToPath } from "node:url";
28
29const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
30const REPORT = path.join(REPO_ROOT, "docs/security/audit-report.md");
31const BASELINE = path.join(REPO_ROOT, "gitleaks-baseline.json");
32
33const md = readFileSync(REPORT, "utf8");
34
35
36function cells(line: string): string[] {
37 return line
38 .split("|")
39 .slice(1, -1)
40 .map((c) => c.trim());
41}
42const stripBackticks = (s: string) => s.replace(/`/g, "");
43/** `noUncheckedIndexedAccess`-safe indexed access (row shape is asserted). */
44const get = (row: string[], i: number): string => row[i] ?? "";
45
46// ---------------------------------------------------------------------------
47// 1. The report exists and names the standard
48// ---------------------------------------------------------------------------
49describe("audit-report / existence + standard", () => {
50 it("exists at docs/security/audit-report.md and names ASVS 4.0.3 Level 2", () => {
51 expect(existsSync(REPORT), "docs/security/audit-report.md must exist").toBe(true);
52 expect(md).toMatch(/ASVS[^.]{0,80}Level 2|Level 2[^.]{0,80}ASVS/i);
53 expect(md).toMatch(/4\.0\.3/);
54 });
55});
56
57// ---------------------------------------------------------------------------
58// 2. All 16 ASVS verification requirement areas are present
59// ---------------------------------------------------------------------------
60describe("audit-report / ASVS L2 checklist completeness", () => {
61 it("covers all 16 verification requirement areas (V1–V16)", () => {
62 for (let n = 1; n <= 16; n++) {
63 expect(md, `the ASVS area "V${n}" must have a section`).toMatch(new RegExp(`^### V${n} `, "m"));
64 }
65 });
66
67 it("every L2 control row carries a valid status", () => {
68 const rows = md
69 .split("\n")
70 .filter((l) => l.startsWith("| L2 ") && !get(cells(l), 1).toLowerCase().startsWith("control"));
71 expect(rows.length, "the checklist must have L2 control rows (one per verified control)").toBeGreaterThanOrEqual(40);
72 const validStatus = /^(pass|N-A|fixed|mitigated|accepted)\b/i;
73 for (const row of rows) {
74 const c = cells(row);
75 expect(c.length, `control row must have 4 cells (L2 | control | status | evidence), got "${row.slice(0, 60)}"`).toBe(4);
76 expect(validStatus.test(get(c, 2)), `control "${get(c, 1).slice(0, 60)}" has an invalid status "${get(c, 2)}"`).toBe(true);
77 }
78 });
79});
80
81// ---------------------------------------------------------------------------
82// 3 + 5. Findings table: complete, append-only, cross-referenced
83// ---------------------------------------------------------------------------
84describe("audit-report / findings table", () => {
85 const FINDING = md
86 .split("\n")
87 .filter((l) => /^\| F-\d{3} /.test(l))
88 .map((l) => cells(l));
89
90 it("has findings, each with id/asvs-ref/severity/status/fix-task/regression-test", () => {
91 expect(FINDING.length, "the findings table must not be empty").toBeGreaterThan(0);
92 const validStatus = /^(fixed|mitigated|accepted|N-A)\b/i;
93 for (const c of FINDING) {
94 expect(c.length, `finding row must have 7 cells, got "${get(c, 0)}"`).toBe(7);
95 expect(get(c, 0), "finding id must be F-\\d{3}").toMatch(/^F-\d{3}$/);
96 expect(get(c, 1), `${get(c, 0)}: an ASVS reference is required`).toMatch(/^V\d{1,2}\./);
97 expect(get(c, 2), `${get(c, 0)}: a severity is required`).toMatch(/^(Critical|High|Medium|Low|Info)$/);
98 expect(validStatus.test(get(c, 3)), `${get(c, 0)}: status must be fixed|mitigated|accepted|N-A, got "${get(c, 3)}"`).toBe(true);
99 expect(get(c, 5).length, `${get(c, 0)}: a fix task (or explicit n/a) is required`).toBeGreaterThan(0);
100 // the regression-test path must exist:
101 const tpath = stripBackticks(get(c, 6));
102 expect(tpath.endsWith(".test.ts"), `${get(c, 0)}: the regression test must be a .test.ts file, got "${tpath}"`).toBe(true);
103 expect(existsSync(path.join(REPO_ROOT, tpath)), `${get(c, 0)}: the regression test path must exist: ${tpath}`).toBe(true);
104 }
105 });
106
107 it("is append-only: ids are unique and in ascending order", () => {
108 const ids = FINDING.map((c) => get(c, 0));
109 expect(new Set(ids).size, "finding ids must be unique").toBe(ids.length);
110 for (let i = 1; i < ids.length; i++) {
111 const a = Number(get(ids, i - 1).slice(2));
112 const b = Number(get(ids, i).slice(2));
113 expect(b, `finding ids must be ascending (append-only), but ${get(ids, i)} follows ${get(ids, i - 1)}`).toBeGreaterThan(a);
114 }
115 });
116
117 it("every gitleaks-baseline remediation resolves to a finding row", () => {
118 const baseline = JSON.parse(readFileSync(BASELINE, "utf8")) as { remediation: { finding: string } }[];
119 const ids = new Set(FINDING.map((c) => get(c, 0)));
120 const refs = new Set(baseline.map((e) => e.remediation.finding));
121 for (const ref of refs) {
122 expect(ids.has(ref), `baseline remediation "${ref}" must resolve to a finding row in the report`).toBe(true);
123 }
124 });
125});
126
127// ---------------------------------------------------------------------------
128// 6. The 10-suite table references existing test files
129// ---------------------------------------------------------------------------
130describe("audit-report / suite table", () => {
131 it("references exactly 10 security suites, each an existing file", () => {
132 const rows = md
133 .split("\n")
134 .filter((l) => /^\| \d+ \|/.test(l))
135 .map((l) => cells(l));
136 expect(rows.length, "the suite table must list the 10 deliverable suites").toBe(10);
137 const paths = rows.map((c) => stripBackticks(get(c, 2)));
138 for (const p of paths) {
139 expect(p.endsWith(".test.ts"), `suite path must be a .test.ts file: ${p}`).toBe(true);
140 expect(existsSync(path.join(REPO_ROOT, p)), `the suite file must exist: ${p}`).toBe(true);
141 }
142 });
143});
144
145
146
147
148describe("audit-report / sign-off", () => {
149 it("is signed (author + date + verified against CI run)", () => {
150 expect(md, "the report must have a sign-off section").toMatch(/^## 7\. Sign-off/m);
151 expect(md, "the sign-off must name an author").toMatch(/\*\*Author:\*\*/);
152 expect(md, "the sign-off must carry a date").toMatch(/\*\*Date:\*\*\s*\d{4}-\d{2}-\d{2}/);
153 expect(md, "the sign-off must reference the CI run").toMatch(/\*\*Verified against CI run:\*\*/);
154 });
155});
156