- 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
96 lines
4.4 KiB
JavaScript
96 lines
4.4 KiB
JavaScript
#!/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
|