diff --git a/review-enrichment/src/analyzers/secret-scan.ts b/review-enrichment/src/analyzers/secret-scan.ts index 1f011c2a98..2e491ce07e 100644 --- a/review-enrichment/src/analyzers/secret-scan.ts +++ b/review-enrichment/src/analyzers/secret-scan.ts @@ -955,6 +955,47 @@ const RULES: Rule[] = [ re: /https:\/\/chat\.googleapis\.com\/v1\/spaces\/[A-Za-z0-9_-]+\/messages\?[^\s"']*key=/, confidence: "high", }, + { + // Managed-database connection strings that embed credentials, like the `cloudinary_url` rule above: + // `scheme://:@`. Each requires a NON-empty user:password pair (so an angle-bracket + // `:` docs placeholder does not match — `<`/`>` are excluded) AND a distinctive provider + // host, so an ordinary `postgres://user:pass@localhost` (no managed host) is never flagged. The host is + // followed by a negative lookahead so a look-alike suffix host (`…mongodb.net.evil.com`) can't match. + kind: "mongodb_atlas_uri", + re: /\bmongodb(?:\+srv)?:\/\/[^\s:/<>]+:[^\s@/<>]+@[a-z0-9.-]+\.mongodb\.net(?![a-z0-9.-])/i, + confidence: "high", + }, + { + // Neon serverless-Postgres connection string (`…@.neon.tech`). + kind: "neon_postgres_uri", + re: /\bpostgres(?:ql)?:\/\/[^\s:/<>]+:[^\s@/<>]+@[a-z0-9.-]+\.neon\.tech(?![a-z0-9.-])/i, + confidence: "high", + }, + { + // Supabase Postgres connection string (`…@db..supabase.co`). + kind: "supabase_postgres_uri", + re: /\bpostgres(?:ql)?:\/\/[^\s:/<>]+:[^\s@/<>]+@[a-z0-9.-]+\.supabase\.co(?![a-z0-9.-])/i, + confidence: "high", + }, + { + // Upstash Redis connection string (`rediss://default:@.upstash.io`). Upstash uses the + // default user, so the user segment may be empty. + kind: "upstash_redis_uri", + re: /\brediss?:\/\/[^\s:/<>]*:[^\s@/<>]+@[a-z0-9.-]+\.upstash\.io(?![a-z0-9.-])/i, + confidence: "high", + }, + { + // PlanetScale MySQL connection string (`…@.connect.psdb.cloud`). + kind: "planetscale_mysql_uri", + re: /\bmysql:\/\/[^\s:/<>]+:[^\s@/<>]+@[a-z0-9.-]+\.psdb\.cloud(?![a-z0-9.-])/i, + confidence: "high", + }, + { + // CockroachDB Cloud connection string (`…@.cockroachlabs.cloud`). + kind: "cockroachdb_uri", + re: /\bpostgres(?:ql)?:\/\/[^\s:/<>]+:[^\s@/<>]+@[a-z0-9.-]+\.cockroachlabs\.cloud(?![a-z0-9.-])/i, + confidence: "high", + }, { // Netlify build-hook URL — the trailing id triggers a production build, like the webhook rules above. kind: "netlify_build_hook_url", diff --git a/review-enrichment/test/secret-scan.test.ts b/review-enrichment/test/secret-scan.test.ts index 726de47e42..321e2df30d 100644 --- a/review-enrichment/test/secret-scan.test.ts +++ b/review-enrichment/test/secret-scan.test.ts @@ -1615,3 +1615,41 @@ test("scanPatch does not flag near-misses of the deployment-hook URL formats", ( assert.equal(findings.length, 0, `near-miss should not match: ${nm}`); } }); + +test("scanPatch flags managed-database connection strings that embed credentials", () => { + // `:` fragments joined into a URI at test time — never a contiguous real secret in source. + const u = "dbuser"; + const p = "s3cr3tP" + "w0rd1234"; + const cases = [ + ["mongodb_atlas_uri", "mongodb+srv://" + u + ":" + p + "@cluster0.ab12c.mongodb.net/mydb"], + ["neon_postgres_uri", "postgresql://" + u + ":" + p + "@ep-cool-name-123.us-east-2.aws.neon.tech/neondb"], + ["supabase_postgres_uri", "postgres://" + u + ":" + p + "@db.abcdefghij.supabase.co:5432/postgres"], + ["upstash_redis_uri", "rediss://default:" + p + "@apn1-cool-cat-12345.upstash.io:6379"], + ["planetscale_mysql_uri", "mysql://" + u + ":" + p + "@aws.connect.psdb.cloud/mydb"], + ["cockroachdb_uri", "postgresql://" + u + ":" + p + "@cool-cluster-123.abc.cockroachlabs.cloud:26257/defaultdb"], + ]; + for (const [kind, secret] of cases) { + const findings = scanPatch("src/config.ts", hunk([`const c = "${secret}";`])); + assert.equal(findings.length, 1, `${kind}: expected exactly one finding, got ${JSON.stringify(findings)}`); + assert.equal(findings[0].kind, kind, `${kind}: wrong kind`); + assert.equal(findings[0].confidence, "high", `${kind}: wrong confidence`); + } +}); + +test("scanPatch does not flag connection-string placeholders or non-managed hosts", () => { + const p = "s3cr3tP" + "w0rd1234"; + const nearMisses = [ + // Angle-bracket docs placeholders — ``/`` are excluded by the rule's charset. + "mongodb+srv://:@cluster0.ab12c.mongodb.net/mydb", + "postgresql://:@ep-x.neon.tech/db", + // A plain local/self-hosted host is not a managed provider, so nothing is flagged. + "postgres://dbuser:" + p + "@localhost:5432/postgres", + "redis://default:" + p + "@127.0.0.1:6379", + // A look-alike suffix host must not match via the provider host prefix. + "mongodb+srv://dbuser:" + p + "@cluster0.ab12c.mongodb.net.evil.com/mydb", + ]; + for (const nm of nearMisses) { + const findings = scanPatch("src/config.ts", hunk([`const c = "${nm}";`])); + assert.equal(findings.length, 0, `near-miss should not match: ${nm}`); + } +});