igneum/site/scrub.mjs
igneum-labs ff0d0dde61 Scrub: the founder list leaves the repository in every encoding; the never-push founder check decodes base64, hex and .b64 blobs too (7 October 2026, 21:4x UK)
tools/ci/founder-strings.b64 was the founder check's pattern list base64-encoded: a grep could not read it, any reader of the public host could (git.igneum.network was public from 21:26 UK; read off at 21:36). The list now lives only in a private file (~/.config/igneum/founder-strings on the Mac, /srv/discord-hooks/founder-strings on build-1 for the Discord guard; $IGNEUM_FOUNDER_STRINGS overrides) and site/forbidden-strings.txt carries no encoded copy. Readers: founder-strings-check.sh (skips with a line where the file is absent; the Mac's hook is the guard), site/scrub.mjs and launch-gates-check.mjs (the private file's patterns added where it exists), discord-hooks.mjs (three locations; the test writes a fixture list and loads the module after it), fresh-repo.sh (the private file; drops tools/ci/founder-strings.b64 from every commit; rewrites the base64 of every list regex out of every blob and scans for it).

The check's second pass (main's addition): every base64 literal of 24 characters or more, every hex literal of 24 or more and every *.b64 file is decoded and scanned, so no encoding hides a term again; the self-test plants each fixture sample in plain text, in a .b64 file, as a base64 literal and as a hex literal, each caught and named, and a tree without the list skips with its line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 20:40:36 +00:00

80 lines
4.6 KiB
JavaScript

// Scrub for the public /bench page (review round 4, R4.6.1). The same rules the public repository export applies
// (its sync.sh generic scrub and bench-log rules, and the maintainers' local rules), plus site-only ones, in plain Node
// so the build runs the same on this Mac, on Vercel and on the CI runner. The build fails if any pattern in
// site/forbidden-strings.txt survives, so a private name, host or address can never reach the page.
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const utc = (h, m, s) => `${String((Number(h) + 23) % 24).padStart(2, '0')}:${m}${s || ''} UTC`;
const RULES = [
// machines: model names only (6 October 2026, site audit: the two Windows machines are described, never numbered)
[/\bPC 1's\b/g, "the three-card Windows rig's"],
[/\bPC 1\b/g, 'the three-card Windows rig (RTX 5090, RTX 4070, RX 9070 XT)'],
[/\bPC 2's\b/g, "the RTX 5090 Windows rig's"],
[/\bPC 2\b/g, 'the RTX 5090 Windows rig'],
[/the PC node at 192\.168\.[0-9.]+/g, 'the RTX 5090 node on the LAN'],
[/\bthe PC node\b/g, 'the RTX 5090 node'],
[/\bWindows PC\b/g, 'an RTX 5090 on Windows'],
[/\bthe PC's\b/g, "the RTX 5090 machine's"],
[/\bthe PC\b/g, 'the RTX 5090 machine'],
[/\bPC (joins|start|period)\b/g, 'RTX 5090 $1'],
[/\(Mac side only;/g, '(Apple M5 Max side only;'],
[/\bthe Mac's\b/g, "the Apple M5 Max's"],
[/\bthe Mac\b/g, 'the Apple M5 Max'],
[/\bJosh's\b/g, "the maintainers'"],
[/\bJosh\b/g, 'the maintainers'],
[/ Not deployed to Vercel tonight\./g, ''],
// addresses, hosts, paths
[/192\.168\.[0-9]+\.[0-9]+/g, '<lan-ip>'],
[/\b100\.[0-9]+\.[0-9]+\.[0-9]+\b/g, '<overlay-ip>'],
[/DESKTOP-[A-Z0-9]{7}/g, '<pc-hostname>'],
[/igneum-seed-\d+/g, 'the seed node'],
[/\bHetzner\b/g, 'the cloud provider'],
[/\bhcloud\b/g, 'the cloud CLI'],
[/\/root\/[A-Za-z0-9_./-]*/g, '<server path>'],
[/\/opt\/igneum[A-Za-z0-9_./-]*/g, '<server path>'],
[/dl\.igneum\.network[^ )`]*/g, 'the downloads host'],
[/,? ?log intake id \d+/g, ''],
[/\bintake id \d+\b/g, 'intake'],
// the site audit's page-leak shapes (6 October 2026): process ids, listen addresses, config paths, the node's listen flag
[/\bpid \d+\b/g, 'pid <n>'],
[/0\.0\.0\.0:\d+/g, '<listen address>'],
[/--rpclisten=\S*/g, 'the RPC listen flag'],
[/~\/\.config\/[A-Za-z0-9_./-]*/g, '<config path>'],
[/~\/\.config/g, '<config path>'],
[/~\/Desktop\//g, '`'], [/``/g, '`'],
[/~\/\.cargo\/bin\/cargo/g, 'cargo'],
[/\/Users\/[A-Za-z0-9_.-]+/g, '~'],
[/C:\\Users\\[A-Za-z0-9_.-]+/g, '%USERPROFILE%'],
// unpublished documents
[/`docs\/fud-ledger\.md`/g, 'the break ledger (kept by the maintainers, not yet published)'],
[/\(CLAUDE\.md, /g, '(the design document, '],
[/\bCLAUDE\.md\b/g, 'the design document'],
// local times to UTC (the log is written in UTC+1)
[/2,193 blocks at 22:35;/g, '2,193 blocks at 21:35 UTC;'],
[/(\d{1,2}:\d{2}(?::\d{2})? UTC) = \d{1,2}:\d{2} B[S]T/g, '$1'],
[/(\d{1,2}):(\d{2})(:\d{2})? to (\d{1,2}):(\d{2})(:\d{2})? B[S]T/g, (_, h1, m1, s1, h2, m2, s2) => `${utc(h1, m1, s1).replace(' UTC', '')} to ${utc(h2, m2, s2)}`],
[/(\d{1,2}):(\d{2})(:\d{2})? B[S]T/g, (_, h, m, s) => utc(h, m, s)],
[/\(local time, UTC\+1\)/g, '(UTC)'],
[/\bB[S]T\b/g, 'UTC'],
];
// the private founder list (~/.config/igneum/founder-strings or $IGNEUM_FOUNDER_STRINGS; perl regex, a tab, a sample per row; # comments);
// absent here: no founder patterns from this reader (the Mac's pre-push hook carries the file and is the guard)
function founderPatternsFromFile() {
try {
const f = process.env.IGNEUM_FOUNDER_STRINGS || join(homedir(), '.config', 'igneum', 'founder-strings');
return readFileSync(f, 'utf8').split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#')).map((l) => new RegExp(l.split('\t')[0], 'i'));
} catch { return []; }
}
export function scrubBench(text) {
let out = text;
for (const [re, rep] of RULES) out = out.replace(re, rep);
const pats = readFileSync(join(here, 'forbidden-strings.txt'), 'utf8').split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))
.map(l => new RegExp(l)).concat(founderPatternsFromFile()); // plus the private founder list where it exists
const hits = [];
out.split('\n').forEach((line, i) => { for (const p of pats) if (p.test(line)) { hits.push(`${i + 1}: ${p.flags.includes('i') ? '(a founder pattern from the private list)' : p.source}`); break; } });
if (hits.length) throw new Error(`scrub: forbidden strings remain on the bench page:\n${hits.slice(0, 20).join('\n')}`);
return out;
}