From 9c55e9a8799cdeb066bba023a24cc653135c5764 Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Sun, 25 Oct 2026 04:39:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(07-03):=20the=20privacy-audit=20CLI=20+=20?= =?UTF-8?q?root=20npm=20script=20=E2=80=94=20orchestration,=20evidence=20r?= =?UTF-8?q?eport,=200/1/2=20exit=20codes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- package.json | 1 + scripts/privacy-audit.mjs | 95 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 scripts/privacy-audit.mjs diff --git a/package.json b/package.json index 89c6872..541e373 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/privacy-audit.mjs b/scripts/privacy-audit.mjs new file mode 100644 index 0000000..1345f07 --- /dev/null +++ b/scripts/privacy-audit.mjs @@ -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