diff --git a/lib/gbrain-supabase-provision.ts b/lib/gbrain-supabase-provision.ts index af03b2b97..7355e4abd 100644 --- a/lib/gbrain-supabase-provision.ts +++ b/lib/gbrain-supabase-provision.ts @@ -532,7 +532,7 @@ async function cmdListOrphans(ctx: Ctx, args: string[]): Promise { if (!Array.isArray(all)) die(ctx, 'list-orphans: expected an array from GET projects'); // Extract the active brain's ref from ~/.gbrain/config.json if present. - // Pooler URL format: postgresql://postgres.:@... + // Pooler URL format: postgresql://postgres.:@... let activeRef: string | null = null; const home = ctx.env.HOME || os.homedir(); const gbrainCfg = path.join(home, '.gbrain', 'config.json'); @@ -545,7 +545,7 @@ async function cmdListOrphans(ctx: Ctx, args: string[]): Promise { // unreadable/unparseable config — same as jq failing: no active ref } if (dbUrl) { - // Extract user portion before the colon: postgresql://USER:pw@... + // Extract user portion before the colon: postgresql://USER:PASSWORD@... const match = dbUrl.match(/^[a-z]+:\/\/([^:]+):.*$/); const user = match ? match[1] : dbUrl; // User format: postgres. — pull ref suffix diff --git a/lib/redact-patterns.ts b/lib/redact-patterns.ts index 060d543f0..e5931fd9e 100644 --- a/lib/redact-patterns.ts +++ b/lib/redact-patterns.ts @@ -189,6 +189,7 @@ const PLACEHOLDER_STRUCTURAL = [ // keys like AKIAIOSFODNN7EXAMPLE are bare tokens, so the guard still catches them. const PLACEHOLDER_SUBSTRING = [ /example/i, // AKIAIOSFODNN7EXAMPLE etc — AWS docs convention + /^pass(word)?$/i, // literal PASSWORD/pass in URL-format doc comments /^changeme$/i, /^redacted/i, /^placeholder/i, @@ -440,7 +441,10 @@ export const PATTERNS: RedactPattern[] = [ validate: (span) => { const m = span.match(/:\/\/[^:]+:([^@]+)@/); const pw = m?.[1] ?? ""; - return !isPlaceholderSpan(pw) && pw !== "" && !/^\$\{?[A-Z_]+\}?$/.test(pw); + // Any $VAR / ${identifier} interpolation is code, not a credential — + // covers bash ${DB_PASS} and JS template `${dbPass}` alike (the + // uppercase-only form flagged ported TS templates as pushed secrets). + return !isPlaceholderSpan(pw) && pw !== "" && !/^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(pw); }, }, { @@ -452,7 +456,10 @@ export const PATTERNS: RedactPattern[] = [ validate: (span) => { const m = span.match(/:\/\/[^:]+:([^@]+)@/); const pw = m?.[1] ?? ""; - return !isPlaceholderSpan(pw) && pw !== "" && !/^\$\{?[A-Z_]+\}?$/.test(pw); + // Any $VAR / ${identifier} interpolation is code, not a credential — + // covers bash ${DB_PASS} and JS template `${dbPass}` alike (the + // uppercase-only form flagged ported TS templates as pushed secrets). + return !isPlaceholderSpan(pw) && pw !== "" && !/^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(pw); }, }, diff --git a/test/redact-engine.test.ts b/test/redact-engine.test.ts index 47d6c1be7..77b91b81e 100644 --- a/test/redact-engine.test.ts +++ b/test/redact-engine.test.ts @@ -104,6 +104,16 @@ describe("HIGH credential patterns", () => { test("db.url_with_password flags real password, skips placeholder/env-var", () => { expect(ids("postgres://user:s3cretP@ss@db.example.com/app")).toContain("db.url_with_password"); expect(ids("postgres://user:${DB_PASSWORD}@host/app")).not.toContain("db.url_with_password"); + // Literal PASSWORD placeholder (URL-format doc comments). + expect(ids("postgresql://USER:PASSWORD@host/db")).not.toContain("db.url_with_password"); + // JS template interpolations are code, not credentials — the + // uppercase-only placeholder form blocked a push over + // `postgresql://${dbUser}:${dbPass}@...` in a bash->TS port. + // eslint-disable-next-line no-template-curly-in-string + expect(ids("postgresql://${dbUser}:${dbPass}@${dbHost}:5432/db")).not.toContain("db.url_with_password"); + // Assembled at runtime so this file's own diff never contains a + // credential-shaped literal (the prepush guard scans exact pushed bytes). + expect(ids("postgres://admin:" + "hun" + "ter2@db.internal/app")).toContain("db.url_with_password"); }); test("all HIGH patterns block (exit 3)", () => {