// secret-egress.test.mjs — Tests for the commons-backed secret pattern table. // // v8 Phase 5 step 4, fourth consumer swap: the 19 fixed credential shapes stop // being regex literals in hooks/scripts/pre-edit-secrets.mjs and are built // from the vendored commons artifact `signatures/secret-egress.json` instead. // // What this file covers is what the other two layers cannot see: // // - The golden gate walks `secret-egress:SECRET_PATTERNS` and pins every // pattern's source and flags, so byte-fidelity to the pre-swap literals is // ITS job (measured post-for-post before the swap, zero divergences) and // is not re-asserted here. // - tests/hooks/pre-edit-secrets.test.mjs drives all 19 through the real // hook, so BEHAVIOUR is its job. // - Left over, and asserted here: that the table came from commons at all, // that `order` is honoured rather than array position, and that a corrupt // or missing commons degrades instead of throwing inside a hook. // // The loud half matters for the same reason it did for the injection lexicon: // an empty table means the PreToolUse guard exits 0 for every Edit and Write — // a credential gate reporting success without running. import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { buildSecretPatterns, SECRET_PATTERNS } from '../../scanners/lib/secret-egress.mjs'; const MALFORMED_ROOT = new URL('../fixtures/commons-malformed-secret-egress/', import.meta.url).pathname; describe('secret-egress (commons credential shapes)', () => { describe('positive load through the real default commons root', () => { it('publishes the table at its declared size', () => { // A count that drifts means either commons changed the table or the // vendored copy is partial; both must be looked at, not adjusted away. assert.equal( SECRET_PATTERNS.length, 19, 'SECRET_PATTERNS lost entries — is scanners/commons vendored?' ); }); it('publishes compiled RegExp objects, not pattern strings', () => { // The hook calls `pattern.test(content)` directly. A string would throw // there, not here, and only once content reached that line. for (const entry of SECRET_PATTERNS) { assert.ok(entry.pattern instanceof RegExp, `${entry.name}: pattern is not a RegExp`); assert.equal(typeof entry.name, 'string'); } }); it('places the load-bearing last entry last', () => { // The artifact declares `ordering.last_entry_is_load_bearing`: first // match wins, so a JWT inside an Authorization header is reported as the // header only while the bare-JWT shape stays behind it. assert.equal(SECRET_PATTERNS.at(-1).name, 'JWT (three-part token)'); assert.equal(SECRET_PATTERNS[0].name, 'AWS Access Key ID'); assert.equal(SECRET_PATTERNS[15].name, 'Authorization header with token'); }); it('carries the flags the artifact declares, per pattern', () => { // Six entries are case-insensitive and thirteen carry no flags. A // builder that applied a blanket 'i' would pass a count check and // silently widen thirteen shapes. const insensitive = SECRET_PATTERNS.filter((p) => p.pattern.flags === 'i').map((p) => p.name); assert.deepEqual(insensitive, [ 'AWS Secret Access Key', 'Azure AD ClientSecret', 'Azure AI Services Key', 'JWT Secret', 'Generic credential assignment', 'Database connection string', ]); assert.equal(SECRET_PATTERNS.filter((p) => p.pattern.flags === '').length, 13); }); it('freezes the published table', () => { assert.ok(Object.isFrozen(SECRET_PATTERNS)); assert.throws(() => { SECRET_PATTERNS.push({ name: 'x', pattern: /x/ }); }, TypeError); }); }); describe('graceful degradation', () => { it('yields an empty table when commons is unresolvable, without throwing', () => { const table = buildSecretPatterns({ commonsRoot: '/nonexistent/commons-root' }); assert.deepEqual(table, []); }); it('drops malformed entries instead of publishing them', () => { // commons is vendored data, not code. An uncompilable pattern string // would throw inside `new RegExp` at module load — in a hook, that is a // broken tool call rather than a degraded scan. const table = buildSecretPatterns({ commonsRoot: MALFORMED_ROOT }); assert.deepEqual(table.map((e) => e.name), [ 'first by order, second in the array', 'second by order, first in the array', ]); }); it('orders by the declared `order`, not by array position', () => { // The artifact's match semantics are ascending `order`; the field exists // precisely so a JSON round-trip cannot reorder the table silently. const table = buildSecretPatterns({ commonsRoot: MALFORMED_ROOT }); assert.equal(table[0].pattern.source, 'fixture-shape-a'); assert.equal(table[0].pattern.flags, 'i'); assert.equal(table[1].pattern.source, 'fixture-shape-b'); }); }); });