mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
test(01-15): reconcile audit finding ledger
- deduplicate research, correctness, live, and performance findings - enforce required evidence fields and privacy-safe report content
This commit is contained in:
1 parent
92f3b03ac0
commit
4ba4ee38cd
2 files changed
+743
No files matched your search
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync, statSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const ledgerArg = process.argv[2];
|
||||
if (!ledgerArg) {
|
||||
console.error("usage: node scripts/v1341-audit-ledger-check.mjs <ledger.md>");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const ledgerPath = resolve(ledgerArg);
|
||||
const MAX_BYTES = 256 * 1024;
|
||||
const REQUIRED_FIELDS = [
|
||||
"Source",
|
||||
"Status",
|
||||
"Severity",
|
||||
"Category",
|
||||
"Evidence",
|
||||
"Affected path",
|
||||
"Attempted refutation",
|
||||
"Impact",
|
||||
"Requirement",
|
||||
"Planned verification",
|
||||
"Confidence",
|
||||
"Unresolved",
|
||||
];
|
||||
const ALLOWED_STATUS = new Set(["confirmed", "suspected", "blocked", "cleared", "refuted"]);
|
||||
const ALLOWED_SEVERITY = new Set(["critical", "high", "medium", "low", "info"]);
|
||||
const ALLOWED_CONFIDENCE = new Set(["high", "medium", "low"]);
|
||||
const REQUIRED_SOURCES = ["research:", "correctness:", "live:", "performance:"];
|
||||
const REQUIRED_CATEGORIES = [
|
||||
"day-boundary",
|
||||
"silent-success",
|
||||
"cache-freshness",
|
||||
"persistence",
|
||||
"outcome-honesty",
|
||||
];
|
||||
const PRIVACY_PATTERNS = [
|
||||
{
|
||||
name: "email address",
|
||||
pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,
|
||||
},
|
||||
{ name: "authorization credential", pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]+/i },
|
||||
{
|
||||
name: "secret assignment",
|
||||
pattern:
|
||||
/\b(?:token|secret|password|api[_-]?key|client[_-]?secret)\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{8,}/i,
|
||||
},
|
||||
{
|
||||
name: "URL or endpoint with authority",
|
||||
pattern: /\bhttps?:\/\/[^\s)>\]]+/i,
|
||||
},
|
||||
{
|
||||
name: "IPv4 address",
|
||||
pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/,
|
||||
},
|
||||
{
|
||||
name: "UUID-like identifier",
|
||||
pattern:
|
||||
/\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/i,
|
||||
},
|
||||
{
|
||||
name: "long hexadecimal identifier",
|
||||
pattern: /\b[0-9a-f]{32,}\b/i,
|
||||
},
|
||||
{
|
||||
name: "local user filesystem path",
|
||||
pattern: /(?:\/Users\/|\/home\/)[^\s`]+/,
|
||||
},
|
||||
{
|
||||
name: "explicit health value",
|
||||
pattern:
|
||||
/\b(?:glucose|blood pressure|heart rate|weight|bmi|hba1c)\s*(?:value\s*)?[:=]\s*-?\d+(?:\.\d+)?/i,
|
||||
},
|
||||
];
|
||||
|
||||
const errors = [];
|
||||
let source;
|
||||
try {
|
||||
if (statSync(ledgerPath).size > MAX_BYTES) {
|
||||
errors.push(`ledger exceeds ${MAX_BYTES} bytes`);
|
||||
}
|
||||
source = readFileSync(ledgerPath, "utf8");
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`v1341-audit-ledger-check: cannot read ledger (${error instanceof Error ? error.message : "unknown error"})`,
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
for (const check of PRIVACY_PATTERNS) {
|
||||
if (check.pattern.test(source)) errors.push(`privacy-dangerous ${check.name} detected`);
|
||||
}
|
||||
|
||||
const headingPattern = /^## (AUD-\d{3}) — (.+)$/gm;
|
||||
const headings = [...source.matchAll(headingPattern)];
|
||||
if (headings.length === 0) errors.push("no finding headings found");
|
||||
if (headings.length > 200) errors.push("finding count exceeds 200");
|
||||
|
||||
const ids = new Set();
|
||||
const sourceRecords = new Set();
|
||||
const records = [];
|
||||
|
||||
for (let index = 0; index < headings.length; index += 1) {
|
||||
const match = headings[index];
|
||||
const id = match[1];
|
||||
const title = match[2].trim();
|
||||
const bodyStart = match.index + match[0].length;
|
||||
const bodyEnd = headings[index + 1]?.index ?? source.length;
|
||||
const body = source.slice(bodyStart, bodyEnd);
|
||||
const fields = new Map();
|
||||
|
||||
if (ids.has(id)) errors.push(`${id}: duplicate finding ID`);
|
||||
ids.add(id);
|
||||
if (!title) errors.push(`${id}: missing title`);
|
||||
|
||||
for (const line of body.split("\n")) {
|
||||
const fieldMatch = line.match(/^- \*\*([^*]+):\*\* (.+)$/);
|
||||
if (!fieldMatch) continue;
|
||||
const [, name, value] = fieldMatch;
|
||||
if (fields.has(name)) errors.push(`${id}: duplicate field ${name}`);
|
||||
fields.set(name, value.trim());
|
||||
}
|
||||
|
||||
for (const field of REQUIRED_FIELDS) {
|
||||
if (!fields.get(field)) errors.push(`${id}: missing ${field}`);
|
||||
}
|
||||
|
||||
const status = fields.get("Status")?.toLowerCase();
|
||||
const severity = fields.get("Severity")?.toLowerCase();
|
||||
const confidence = fields.get("Confidence")?.toLowerCase();
|
||||
const unresolved = fields.get("Unresolved")?.toLowerCase();
|
||||
const sourceField = fields.get("Source");
|
||||
|
||||
if (status && !ALLOWED_STATUS.has(status)) {
|
||||
errors.push(`${id}: invalid Status ${status}`);
|
||||
}
|
||||
if (severity && !ALLOWED_SEVERITY.has(severity)) {
|
||||
errors.push(`${id}: invalid Severity ${severity}`);
|
||||
}
|
||||
if (confidence && !ALLOWED_CONFIDENCE.has(confidence)) {
|
||||
errors.push(`${id}: invalid Confidence ${confidence}`);
|
||||
}
|
||||
if (unresolved && !["yes", "no"].includes(unresolved)) {
|
||||
errors.push(`${id}: Unresolved must be yes or no`);
|
||||
}
|
||||
if (["cleared", "refuted"].includes(status) && unresolved !== "no") {
|
||||
errors.push(`${id}: ${status} findings must set Unresolved to no`);
|
||||
}
|
||||
if (sourceField) {
|
||||
if (sourceRecords.has(sourceField)) errors.push(`${id}: duplicate Source record`);
|
||||
sourceRecords.add(sourceField);
|
||||
}
|
||||
|
||||
records.push({ id, fields });
|
||||
}
|
||||
|
||||
for (let index = 0; index < records.length; index += 1) {
|
||||
const expected = `AUD-${String(index + 1).padStart(3, "0")}`;
|
||||
if (records[index].id !== expected) {
|
||||
errors.push(`${records[index].id}: expected sequential ID ${expected}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of REQUIRED_SOURCES) {
|
||||
if (!records.some((record) => record.fields.get("Source")?.includes(prefix))) {
|
||||
errors.push(`missing source family ${prefix}`);
|
||||
}
|
||||
}
|
||||
for (const category of REQUIRED_CATEGORIES) {
|
||||
if (!records.some((record) => record.fields.get("Category") === category)) {
|
||||
errors.push(`missing required category ${category}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(`v1341-audit-ledger-check: FAIL (${errors.length} issue(s))`);
|
||||
for (const error of errors) console.error(`- ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`v1341-audit-ledger-check: PASS (${records.length} unique findings; privacy scan clean)`,
|
||||
);
|
||||
Reference in new issue
Block a user