Hygiene gate: process-vocabulary patterns + JWT claim decoder (public-exposure audit)
Some checks are pending
build-gate / build (push) Waiting to run

Two widenings from the 2026-09-24 anonymous-clone audit:

1. LEAK_STRICT: role nouns (the Driver/Driver), private repo/org names,
   forge/platform names, operational-context vocabulary, CI workflow
   paths. Every one was found live in a public mirror before this rule
   existed. WARNINGS in the normal build (the source tree still
   carries them; failing build:unlocked overnight gets a gate
   disabled), FINDINGS under --strict -- which the publish/export flow
   must run: publishing is a gated action, not a habit.

2. A credential is a DOCUMENT, not a string: every JWT-shaped value's
   payload is decoded and name-like claims flagged. The pk token
   accepted by the 2026-09-21 ruling carries a u claim naming a real
   account -- the ruling covered the token's scope, not the identity
   inside it.

Both modes negative-tested: strict exits 1 against the current tree
(110 findings), normal exits 0 (118 warnings).
This commit is contained in:
doxa-maps-88655c70 2026-09-24 02:23:47 +00:00
parent dd2e7d3c2a
commit 75c16ac32d

View file

@ -63,6 +63,22 @@ const LEAK = [
[/\beco-dx\b/, 'internal folder name (eco-dx)'], [/qa-cycle/, 'internal folder name (qa-cycle)'], [/\b_archive\b/, 'internal folder name (_archive)'],
[/localhost:(5174|3001|3033)\b/, 'dev-server URL'],
]
// ★ ADDED 2026-09-24 (public-exposure audit, policies P3/P4/P7). A public tree carries
// ARTIFACTS, never PROCESS or operational context — each of these was found live in a public
// mirror by anonymous clone before this rule existed. They are WARNINGS in the normal build
// (the source tree's own public/ still carries them, and failing build:unlocked for every
// writer overnight would get this gate disabled) and FINDINGS under --strict, which the
// publish/export flow MUST run (policy P5: publishing is a gated action, not a habit).
const LEAK_STRICT = [
[/\bthe Driver\b/, 'internal role noun (the Driver) — process vocabulary'],
[/\bDriver\b/, 'internal role noun (Driver) — process vocabulary'],
[/doxa-life\/doxa-maps/, 'private repo name'],
[/\bdoxa-life\b/, 'private org name'],
[/\bforgejo\b/i, 'self-hosted forge name'],
[/\bnuxtbase\b/, 'internal platform name'],
[/\bpersecution\b/, 'operational-context vocabulary (allowed only inside data CSVs)'],
[/\.forgejo\/workflows/, 'CI workflow path — process, never ships in a public tree'],
]
// ★ WIDENED 2026-09-21. This held ONE pattern (the Mapbox JWT), on the reasoning that public/
// is a tree we GENERATE, so the only realistic leak is the token the build itself writes.
// THAT REASONING WAS WRONG: 855 files under public/ are TRACKED, 95 of them text — boundaries,
@ -106,6 +122,24 @@ for (const f of files) {
if (!isText || f.size > 40 * 1024 * 1024) continue
const t = readFileSync(f.abs, 'utf8')
for (const [re, why] of LEAK) if (re.test(t)) hit(findings, 'leaks', f.rel, why)
for (const [re, why] of LEAK_STRICT) if (re.test(t) && !/\.csv$/.test(f.rel)) hit(STRICT ? findings : warnings, 'leaks', f.rel, why)
// ★ P2 (2026-09-24): a credential is a DOCUMENT, not a string. Every JWT-shaped value gets
// its payload DECODED and its claims reviewed: 'it is a public token' is not sufficient —
// the pk. accepted by the 2026-09-21 ruling turned out to carry a `u` claim naming a real,
// searchable ACCOUNT in base64 anyone can decode. The ruling covered the token's scope,
// not the identity inside it. Flag any claim value that looks like a meaningful name.
for (const m of t.matchAll(/\b(?:pk|sk|tk)\.(eyJ[A-Za-z0-9_-]{8,})/g)) {
try {
const pad = m[1] + '='.repeat((4 - (m[1].length % 4)) % 4)
const claims = JSON.parse(Buffer.from(pad, 'base64').toString('utf8'))
for (const [k, v] of Object.entries(claims)) {
if (typeof v === 'string' && /^[a-z][a-z0-9_-]{3,}$/i.test(v) && !/^[0-9a-f]+$/i.test(v)) {
hit(STRICT ? findings : warnings, 'secrets', f.rel, `JWT claim "${k}" carries a name-like value (${v.length} chars) — decode + review before publishing`)
break
}
}
} catch { /* not decodable — not a JWT payload */ }
}
for (const [re, why] of SECRET) if (re.test(t)) hit(findings, 'secrets', f.rel, why)
if (/sourceMappingURL=/.test(t)) hit(warnings, 'debug', f.rel, 'sourceMappingURL comment (map file absent → harmless, but points at a missing map)')
if (ext === '.js' && /console\.(log|debug)\(/.test(t)) hit(warnings, 'debug', f.rel, 'console.log/debug in a shipped bundle')