feat(07-03): the privacy-audit CLI + root npm script — orchestration, evidence report, 0/1/2 exit codes
- runs the five static gates in Pattern-4 order; --live adds the KV-expiry and asset-header probes; the no-flag skip prints '(pass --live)' and is never silently green - exit 0 CLEAN / 1 FAILED (violations counted across gates) / 2 NOT RUN (credentials or build or deployment drift — never mistaken for a pass) - header states the maintainer-time-tool law, the GPL fork-reuse contract, and D-02's zero-telemetry prove-the-absence posture - AU1-3 CLI vectors green: CLEAN exit 0 on this tree, FAILED exit 1 via the PRIVACY_AUDIT_ROOT fixture, NOT RUN exit 2 for --live without credentials
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"sheet": "npm run sheet --workspace theme",
|
||||
"gallery": "npm run gallery --workspace theme",
|
||||
"audit": "npm run audit --workspace theme",
|
||||
"privacy-audit": "node scripts/privacy-audit.mjs",
|
||||
"capture": "npm run capture --workspace theme",
|
||||
"bot:test": "npm test -w bot",
|
||||
"bot:typecheck": "npm run typecheck -w bot",
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* privacy-audit.mjs — the ONE consolidated privacy audit (07-03, D-01/D-02).
|
||||
* ---------------------------------------------------------------------------
|
||||
* A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src,
|
||||
* web/src, routing/src, or search/src imports it. GPL-3.0: a fork reruns
|
||||
* `npm run privacy-audit` over its own trees before its FIRST driver — the
|
||||
* audit doubles as the fork onboarding check.
|
||||
*
|
||||
* D-02: v1 carries ZERO telemetry — opt-in telemetry stays an IDEAS.md
|
||||
* aspiration post-validation; there is nothing to opt into yet. This audit
|
||||
* proves an ABSENCE, it does not audit a consent surface that does not exist:
|
||||
* no analytics, no beacon, no fetch to any host outside the positive
|
||||
* allowlist, no persistence outside the TTL-only KV families, no coordinate
|
||||
* or payload in any log line.
|
||||
*
|
||||
* What it runs, in order (07-RESEARCH Pattern 4):
|
||||
* runStorageGate() id: storage-api — the memory-only law, repo-wide
|
||||
* runKvTtlGate() id: kv-ttl — the PG1 same-line TTL law, repo-wide
|
||||
* runLogVocabularyGate() id: log-vocabulary — the D-14 category-only law
|
||||
* runRuntimeHostGate() id: runtime-hosts — the POSITIVE fetch-host allowlist
|
||||
* runBundleTelemetryGate() id: bundle-telemetry — the PRODUCTION web/dist scan
|
||||
* [--live] runKvExpiryProbe() id: kv-expiry — LIVE: expiry metadata + cursor
|
||||
* [--live] runAssetHeaderProbe() id: asset-headers — LIVE: cache headers VERBATIM
|
||||
*
|
||||
* Usage: node scripts/privacy-audit.mjs [--live] (the `privacy-audit` npm script)
|
||||
*
|
||||
* Exit codes (no silent passes — the theme-audit contract, T-1-07):
|
||||
* 0 clean — every required gate PASSed
|
||||
* 1 violations — each printed as a per-gate evidence line above
|
||||
* 2 NOT RUN — environment/credential drift (a --live probe could not run
|
||||
* because credentials are absent, or the build is absent); a skipped
|
||||
* probe is NEVER mistaken for a pass (T-07-16)
|
||||
*
|
||||
* PRIVACY_AUDIT_ROOT: repo root override (test suite / fork checkouts). The
|
||||
* gates are otherwise cwd-INDEPENDENT — a stray subdirectory run must not
|
||||
* scan the wrong tree and false-pass.
|
||||
*
|
||||
* Category-only output law: this script NEVER prints a coordinate, a chat
|
||||
* id, a user id, a token, or an initData payload — its evidence is
|
||||
* category-only, exactly like the bot's logs (D-14).
|
||||
*/
|
||||
import {
|
||||
runStorageGate,
|
||||
runKvTtlGate,
|
||||
runLogVocabularyGate,
|
||||
runRuntimeHostGate,
|
||||
} from "./lib/source-gates.mjs";
|
||||
import { runBundleTelemetryGate } from "./lib/bundle-gates.mjs";
|
||||
import { runKvExpiryProbe, runAssetHeaderProbe } from "./lib/cloudflare-probes.mjs";
|
||||
|
||||
const live = process.argv.includes("--live");
|
||||
|
||||
/** The five static gates — always required. */
|
||||
const results = [
|
||||
await runStorageGate(),
|
||||
await runKvTtlGate(),
|
||||
await runLogVocabularyGate(),
|
||||
await runRuntimeHostGate(),
|
||||
await runBundleTelemetryGate(),
|
||||
];
|
||||
|
||||
if (live) {
|
||||
results.push(await runKvExpiryProbe(), await runAssetHeaderProbe());
|
||||
} else {
|
||||
// Flag-skip placeholders — visible, honest, and NEVER green: the skip is
|
||||
// the operator's explicit choice (no --live), not an environment failure,
|
||||
// so it is excluded from the exit-2 class below.
|
||||
results.push(
|
||||
{ id: "kv-expiry", pass: false, notRun: true, flagSkip: true, evidence: "(pass --live)" },
|
||||
{ id: "asset-headers", pass: false, notRun: true, flagSkip: true, evidence: "(pass --live)" },
|
||||
);
|
||||
}
|
||||
|
||||
for (const result of results) {
|
||||
const status = result.pass ? "PASS" : result.notRun ? "NOT RUN" : "FAIL";
|
||||
console.log(`${result.id}: ${status} — ${result.evidence}`);
|
||||
}
|
||||
|
||||
const failed = results.filter((r) => !r.pass && !r.notRun);
|
||||
const notRun = results.filter((r) => !r.pass && r.notRun && !r.flagSkip);
|
||||
const passed = results.filter((r) => r.pass).length;
|
||||
const totalViolations = results.reduce((sum, r) => sum + (r.violations ?? 0), 0);
|
||||
|
||||
if (failed.length > 0) {
|
||||
console.log(`PRIVACY AUDIT FAILED — ${totalViolations} violation(s) across ${failed.length} gate(s)`);
|
||||
process.exit(1); // violations — each printed above
|
||||
}
|
||||
if (notRun.length > 0) {
|
||||
const reasons = notRun.map((r) => `${r.id}: ${r.evidence}`).join("; ");
|
||||
console.log(`PRIVACY AUDIT NOT RUN — ${reasons} (environment drift; never a pass)`);
|
||||
process.exit(2); // environment/credential drift — fix the environment and rerun
|
||||
}
|
||||
console.log(`PRIVACY AUDIT CLEAN — ${passed} gate(s) passed, 0 violations`);
|
||||
process.exit(0); // clean
|
||||
Reference in New Issue
Block a user