Hygiene gate: process-vocabulary patterns + JWT claim decoder (public-exposure audit)
Some checks are pending
build-gate / build (push) Waiting to run
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:
parent
dd2e7d3c2a
commit
75c16ac32d
1 changed files with 34 additions and 0 deletions
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Reference in a new issue