@rebasepro/rls-check 0.17.3 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -111
- package/dist/checks/policy-anonymous-tautology.d.ts +50 -2
- package/dist/checks/policy-authenticated-tautology.d.ts +27 -0
- package/dist/checks/util.d.ts +28 -0
- package/dist/index.es.js +775 -293
- package/dist/index.es.js.map +1 -1
- package/dist/introspect.d.ts +33 -0
- package/dist/redact.d.ts +17 -1
- package/dist/types.d.ts +19 -4
- package/package.json +33 -21
package/dist/index.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.es.js","names":[],"sources":["../src/checks/sql.ts","../src/checks/util.ts","../src/checks/anonymous-write-allowed.ts","../src/checks/current-setting-throws.ts","../src/checks/grant-to-public.ts","../src/checks/junction-table-unprotected.ts","../src/checks/view-bypasses-rls.ts","../src/checks/matview-bypasses-rls.ts","../src/checks/policy-always-true.ts","../src/checks/policy-anonymous-tautology.ts","../src/checks/policy-role-unreachable.ts","../src/checks/rls-disabled.ts","../src/checks/rls-enabled-no-policies.ts","../src/checks/rls-enabled-not-forced.ts","../src/checks/security-definer-mutable-search-path.ts","../src/checks/unqualified-column-in-subquery.ts","../src/checks/index.ts","../src/introspect.ts","../src/redact.ts","../src/types.ts","../src/report.ts","../src/report-html.ts","../src/cli.ts"],"sourcesContent":["/**\n * A deliberately small SQL scanner for policy expressions.\n *\n * This is NOT a grammar. It knows exactly enough to answer three questions\n * without being fooled: where do the parens nest, what is inside a string\n * literal (so `'... EXISTS (SELECT ...'` is never mistaken for a subquery), and\n * which identifiers are written bare.\n *\n * Everything it reads comes from `pg_policies.qual` / `with_check`, which is\n * Postgres's own re-rendering of the parse tree rather than the SQL anyone\n * typed: parenthesised more heavily, casts made explicit, and — importantly for\n * the unqualified-column check — references usually re-qualified. So a hit here\n * is strong evidence and a miss proves nothing, which is what the checks say.\n */\n\nexport type TokenKind = \"ident\" | \"string\" | \"number\" | \"op\" | \"punct\";\n\nexport interface SqlToken {\n kind: TokenKind;\n /** Lower-cased for idents and operators; raw text for strings and numbers. */\n value: string;\n /** True when the identifier arrived double-quoted, so its case is literal. */\n quoted: boolean;\n /** Paren nesting the token sits at; `(` carries the depth it opens from. */\n depth: number;\n}\n\n// Postgres allows non-ASCII letters in unquoted identifiers; rather than\n// replicating its locale rules, anything above ASCII counts as an ident char.\nconst isIdentStart = (c: string): boolean => /[A-Za-z_]/.test(c) || c.charCodeAt(0) > 127;\nconst isIdentBody = (c: string): boolean => /[A-Za-z0-9_$]/.test(c) || c.charCodeAt(0) > 127;\nconst MULTI_CHAR_OPS = [\"->>\", \"#>>\", \"::\", \"<=\", \">=\", \"<>\", \"!=\", \"||\", \"->\", \"#>\", \"@>\", \"<@\", \"~~\", \"!~\"];\n\n/** Tokenize a policy expression. Never throws: unterminated quotes just run to EOF. */\nexport function tokenize(sql: string): SqlToken[] {\n const tokens: SqlToken[] = [];\n let depth = 0;\n let i = 0;\n\n while (i < sql.length) {\n const c = sql[i];\n\n if (/\\s/.test(c)) {\n i++;\n continue;\n }\n\n // -- line comment\n if (c === \"-\" && sql[i + 1] === \"-\") {\n const nl = sql.indexOf(\"\\n\", i);\n i = nl === -1 ? sql.length : nl + 1;\n continue;\n }\n\n // /* block comment */ — Postgres nests these.\n if (c === \"/\" && sql[i + 1] === \"*\") {\n let nest = 1;\n i += 2;\n while (i < sql.length && nest > 0) {\n if (sql[i] === \"/\" && sql[i + 1] === \"*\") {\n nest++;\n i += 2;\n } else if (sql[i] === \"*\" && sql[i + 1] === \"/\") {\n nest--;\n i += 2;\n } else i++;\n }\n continue;\n }\n\n // $tag$ dollar-quoted string $tag$. Must be recognised before identifiers,\n // because the body can contain anything at all — including `EXISTS (`.\n if (c === \"$\") {\n const tag = /^\\$[A-Za-z_][A-Za-z0-9_]*\\$|^\\$\\$/.exec(sql.slice(i));\n if (tag) {\n const end = sql.indexOf(tag[0], i + tag[0].length);\n const stop = end === -1 ? sql.length : end + tag[0].length;\n tokens.push({ kind: \"string\", value: sql.slice(i, stop), quoted: true, depth });\n i = stop;\n continue;\n }\n }\n\n // '...' with '' escaping, plus E'...' backslash escaping.\n if (c === \"'\" || ((c === \"e\" || c === \"E\") && sql[i + 1] === \"'\")) {\n const escapes = c !== \"'\";\n let j = escapes ? i + 2 : i + 1;\n while (j < sql.length) {\n if (escapes && sql[j] === \"\\\\\") {\n j += 2;\n continue;\n }\n if (sql[j] === \"'\") {\n if (sql[j + 1] === \"'\") {\n j += 2;\n continue;\n }\n j++;\n break;\n }\n j++;\n }\n tokens.push({ kind: \"string\", value: sql.slice(i, j), quoted: true, depth });\n i = j;\n continue;\n }\n\n // \"quoted identifier\"\n if (c === '\"') {\n let j = i + 1;\n let value = \"\";\n while (j < sql.length) {\n if (sql[j] === '\"') {\n if (sql[j + 1] === '\"') {\n value += '\"';\n j += 2;\n continue;\n }\n j++;\n break;\n }\n value += sql[j];\n j++;\n }\n tokens.push({ kind: \"ident\", value, quoted: true, depth });\n i = j;\n continue;\n }\n\n if (isIdentStart(c)) {\n let j = i + 1;\n while (j < sql.length && isIdentBody(sql[j])) j++;\n tokens.push({ kind: \"ident\", value: sql.slice(i, j).toLowerCase(), quoted: false, depth });\n i = j;\n continue;\n }\n\n if (/[0-9]/.test(c) || (c === \".\" && /[0-9]/.test(sql[i + 1] ?? \"\"))) {\n let j = i;\n while (j < sql.length && /[0-9.eE+-]/.test(sql[j])) {\n // Stop at a sign that is not part of an exponent.\n if ((sql[j] === \"+\" || sql[j] === \"-\") && !/[eE]/.test(sql[j - 1] ?? \"\")) break;\n j++;\n }\n tokens.push({ kind: \"number\", value: sql.slice(i, j), quoted: false, depth });\n i = j;\n continue;\n }\n\n if (c === \"(\") {\n tokens.push({ kind: \"punct\", value: \"(\", quoted: false, depth });\n depth++;\n i++;\n continue;\n }\n if (c === \")\") {\n depth = Math.max(0, depth - 1);\n tokens.push({ kind: \"punct\", value: \")\", quoted: false, depth });\n i++;\n continue;\n }\n if (c === \",\" || c === \".\" || c === \"[\" || c === \"]\" || c === \";\") {\n tokens.push({ kind: \"punct\", value: c, quoted: false, depth });\n i++;\n continue;\n }\n\n const multi = MULTI_CHAR_OPS.find((op) => sql.startsWith(op, i));\n if (multi) {\n tokens.push({ kind: \"op\", value: multi, quoted: false, depth });\n i += multi.length;\n continue;\n }\n\n tokens.push({ kind: \"op\", value: c, quoted: false, depth });\n i++;\n }\n\n return tokens;\n}\n\n/** Index of the `)` matching the `(` at `open`, or -1. */\nexport function matchParen(tokens: SqlToken[], open: number): number {\n const base = tokens[open]?.depth ?? 0;\n for (let i = open + 1; i < tokens.length; i++) {\n const t = tokens[i];\n if (t.kind === \"punct\" && t.value === \")\" && t.depth === base) return i;\n }\n return -1;\n}\n\n/**\n * Every parenthesised `SELECT` in the expression, as token-index ranges.\n *\n * Covers `EXISTS (SELECT …)`, `x IN (SELECT …)`, `= ANY (SELECT …)` and plain\n * scalar subqueries in one rule, because all of them are \"an open paren whose\n * first word is SELECT\" and none of the checks care which form it took.\n */\nexport function findSubqueries(tokens: SqlToken[]): { open: number; close: number }[] {\n const out: { open: number; close: number }[] = [];\n for (let i = 0; i < tokens.length; i++) {\n const t = tokens[i];\n if (t.kind !== \"punct\" || t.value !== \"(\") continue;\n const next = tokens[i + 1];\n if (!next || next.kind !== \"ident\" || next.quoted || next.value !== \"select\") continue;\n const close = matchParen(tokens, i);\n if (close !== -1) out.push({ open: i, close });\n }\n return out;\n}\n\n/** True when the ident at `i` is written bare: no `schema.` before, no `.col` after. */\nexport function isBareIdent(tokens: SqlToken[], i: number): boolean {\n const t = tokens[i];\n if (!t || t.kind !== \"ident\") return false;\n const prev = tokens[i - 1];\n const next = tokens[i + 1];\n if (prev && prev.kind === \"punct\" && prev.value === \".\") return false;\n if (next && next.kind === \"punct\" && next.value === \".\") return false;\n // `lower(x)` is a call, not a column.\n if (next && next.kind === \"punct\" && next.value === \"(\") return false;\n return true;\n}\n\n/** The dotted name ending at `i`, e.g. `[\"public\", \"orders\", \"id\"]`. */\nexport function qualifiedNameEndingAt(tokens: SqlToken[], i: number): string[] {\n const parts: string[] = [];\n let j = i;\n while (j >= 0 && tokens[j]?.kind === \"ident\") {\n parts.unshift(tokens[j].value);\n if (tokens[j - 1]?.kind === \"punct\" && tokens[j - 1]?.value === \".\") j -= 2;\n else break;\n }\n return parts;\n}\n\n/**\n * Does this expression normalize to an unconditional truth?\n *\n * Strictly structural: parens and boolean casts are dropped and the *whole*\n * remainder has to be the constant. A substring match on `true` would flag\n * `(is_public = true AND owner = auth.uid())`, which is exactly the kind of\n * false positive this tool cannot afford.\n */\nexport function isUnconditionalTrue(expr: string | null | undefined): boolean {\n if (expr == null) return false;\n const tokens = tokenize(expr).filter(\n (t) => !(t.kind === \"punct\" && (t.value === \"(\" || t.value === \")\"))\n );\n\n // Drop `::boolean` / `::bool` casts, which Postgres likes to render.\n const stripped: SqlToken[] = [];\n for (let i = 0; i < tokens.length; i++) {\n if (tokens[i].kind === \"op\" && tokens[i].value === \"::\") {\n i++; // skip the type name too\n continue;\n }\n stripped.push(tokens[i]);\n }\n\n const values = stripped.map((t) => t.value);\n if (values.length === 1 && values[0] === \"true\") return true;\n // `1 = 1`, `'x' = 'x'` — the same constant compared with itself.\n if (values.length === 3 && values[1] === \"=\" && values[0] === values[2]) {\n return stripped[0].kind === \"number\" || stripped[0].kind === \"string\";\n }\n return false;\n}\n\n/** Words that are never a column reference in a policy predicate. */\nexport const SQL_KEYWORDS = new Set([\n \"select\", \"from\", \"where\", \"and\", \"or\", \"not\", \"in\", \"exists\", \"is\", \"null\", \"true\", \"false\",\n \"as\", \"on\", \"join\", \"inner\", \"left\", \"right\", \"full\", \"outer\", \"cross\", \"natural\", \"lateral\",\n \"group\", \"by\", \"order\", \"having\", \"limit\", \"offset\", \"union\", \"intersect\", \"except\", \"all\",\n \"any\", \"some\", \"distinct\", \"case\", \"when\", \"then\", \"else\", \"end\", \"between\", \"like\", \"ilike\",\n \"similar\", \"escape\", \"asc\", \"desc\", \"nulls\", \"first\", \"last\", \"using\", \"with\", \"recursive\",\n \"current_user\", \"session_user\", \"current_role\", \"current_date\", \"current_time\",\n \"current_timestamp\", \"localtime\", \"localtimestamp\", \"cast\", \"array\", \"row\", \"values\",\n \"returning\", \"window\", \"fetch\", \"only\", \"filter\", \"collate\", \"at\", \"time\", \"zone\", \"interval\",\n \"int\", \"int2\", \"int4\", \"int8\", \"integer\", \"bigint\", \"smallint\", \"text\", \"varchar\", \"char\",\n \"boolean\", \"bool\", \"uuid\", \"json\", \"jsonb\", \"numeric\", \"decimal\", \"real\", \"float\", \"double\",\n \"precision\", \"date\", \"timestamp\", \"timestamptz\", \"bytea\", \"name\", \"regclass\", \"oid\"\n]);\n","/**\n * Shared vocabulary for the checks.\n *\n * Everything here is about answering one question honestly: *can an untrusted\n * caller actually reach this object?* A finding that cannot answer it becomes\n * noise, and noise is what gets a tool like this uninstalled — so the helpers\n * below err towards \"no\" whenever the catalog is ambiguous.\n */\nimport type { DbGrant, DbPolicy, DbRelation, DbSnapshot, Finding, Severity } from \"../types\";\n\nexport const SEVERITY_ORDER: Severity[] = [\"info\", \"low\", \"medium\", \"high\", \"critical\"];\n\nexport const docsFor = (id: string): string => `https://rebase.pro/docs/rls-check#${id}`;\n\nexport type Privilege = DbGrant[\"privileges\"][number];\n\n/** The privileges that actually move data. TRUNCATE/REFERENCES/TRIGGER are not exposure. */\nexport const DML: Privilege[] = [\"SELECT\", \"INSERT\", \"UPDATE\", \"DELETE\"];\nexport const WRITE_PRIVILEGES: Privilege[] = [\"INSERT\", \"UPDATE\", \"DELETE\"];\n\n/** Roles a caller reaches without ever authenticating. */\nexport const ANONYMOUS_ROLES = [\"public\", \"anon\", \"web_anon\"];\n\nexport const isPublicRole = (role: string): boolean => role.toLowerCase() === \"public\";\nexport const sameRole = (a: string, b: string): boolean => a.toLowerCase() === b.toLowerCase();\n\n// ---------------------------------------------------------------------------\n// Roles\n// ---------------------------------------------------------------------------\n\n/**\n * Every role whose grants `role` actually receives: itself, PUBLIC, and the\n * transitive closure of its memberships.\n *\n * Skipping the closure is the specific way this class of tool produces false\n * *negatives* on the databases that were configured most carefully — a shop\n * that grants to `app_reader` and makes `anon` a member of it looks clean to a\n * checker that only compares grantee names.\n */\nexport function rolesUsableBy(snapshot: DbSnapshot, role: string): Set<string> {\n const out = new Set<string>([\"public\", role.toLowerCase()]);\n const def = snapshot.roles.find((r) => sameRole(r.name, role));\n for (const m of def?.memberOf ?? []) out.add(m.toLowerCase());\n return out;\n}\n\n/** Privileges `role` effectively holds on a relation, memberships included. */\nexport function effectivePrivileges(\n snapshot: DbSnapshot,\n schema: string,\n table: string,\n role: string\n): Set<Privilege> {\n const via = rolesUsableBy(snapshot, role);\n const out = new Set<Privilege>();\n for (const g of snapshot.grants) {\n if (g.schema !== schema || g.table !== table) continue;\n if (!via.has(g.grantee.toLowerCase())) continue;\n for (const p of g.privileges) out.add(p);\n }\n return out;\n}\n\n/**\n * Which exposed roles hold at least one of `wanted` on this relation, and which.\n * Returns an empty array when nothing untrusted can touch it — the signal that\n * a finding would be noise.\n */\nexport function exposedGrantees(\n snapshot: DbSnapshot,\n schema: string,\n table: string,\n wanted: Privilege[] = DML\n): { role: string; privileges: Privilege[] }[] {\n const out: { role: string; privileges: Privilege[] }[] = [];\n for (const role of snapshot.exposedRoles) {\n const held = effectivePrivileges(snapshot, schema, table, role);\n const hit = wanted.filter((p) => held.has(p));\n if (hit.length > 0) out.push({ role, privileges: hit });\n }\n return out;\n}\n\n/** Does this policy's TO list name a role an untrusted caller arrives as? */\nexport function policyTargetsExposedRole(snapshot: DbSnapshot, policy: DbPolicy): string[] {\n const hits: string[] = [];\n for (const target of policy.roles) {\n if (isPublicRole(target)) {\n hits.push(\"PUBLIC\");\n continue;\n }\n // A policy TO `app_reader` applies to `anon` when anon inherits it.\n for (const exposed of snapshot.exposedRoles) {\n if (isPublicRole(exposed)) continue;\n if (rolesUsableBy(snapshot, exposed).has(target.toLowerCase())) {\n hits.push(exposed);\n break;\n }\n }\n }\n return [...new Set(hits)];\n}\n\n// ---------------------------------------------------------------------------\n// Relations\n// ---------------------------------------------------------------------------\n\nexport const TABLE_KINDS: DbRelation[\"kind\"][] = [\"table\", \"partitioned_table\"];\n\nexport const isTable = (r: DbRelation): boolean => TABLE_KINDS.includes(r.kind);\n\nexport function scannedTables(snapshot: DbSnapshot): DbRelation[] {\n return snapshot.relations.filter((r) => isTable(r) && snapshot.schemas.includes(r.schema));\n}\n\nexport function relationAt(snapshot: DbSnapshot, schema: string, name: string): DbRelation | undefined {\n return snapshot.relations.find((r) => r.schema === schema && r.name === name);\n}\n\nexport function policiesFor(snapshot: DbSnapshot, schema: string, table: string): DbPolicy[] {\n return snapshot.policies.filter((p) => p.schema === schema && p.table === table);\n}\n\nexport const hasColumn = (r: DbRelation, name: string): boolean =>\n r.columns.some((c) => c.name.toLowerCase() === name.toLowerCase());\n\n/** `≈ 12,000 rows` — only ever used to convey blast radius, never severity. */\nexport function rowsPhrase(rel: DbRelation | undefined): string {\n if (!rel || rel.estimatedRows < 0) return \"\";\n if (rel.estimatedRows === 0) return \" (the planner currently estimates 0 rows)\";\n return ` (≈${Math.round(rel.estimatedRows).toLocaleString(\"en-US\")} rows)`;\n}\n\n// ---------------------------------------------------------------------------\n// SQL rendering for the `fix` field\n// ---------------------------------------------------------------------------\n\nexport const qi = (ident: string): string => `\"${ident.replace(/\"/g, '\"\"')}\"`;\nexport const qrel = (schema: string, name: string): string => `${qi(schema)}.${qi(name)}`;\n/** PUBLIC is a keyword, not an identifier — quoting it changes what it means. */\nexport const qrole = (role: string): string => (isPublicRole(role) ? \"PUBLIC\" : qi(role));\n\n// ---------------------------------------------------------------------------\n// Findings\n// ---------------------------------------------------------------------------\n\nexport function finding(f: Omit<Finding, \"docs\"> & { docs?: string }): Finding {\n return { ...f, docs: f.docs ?? docsFor(f.id) };\n}\n\nexport const listAnd = (items: string[]): string =>\n items.length <= 1\n ? (items[0] ?? \"\")\n : `${items.slice(0, -1).join(\", \")} and ${items[items.length - 1]}`;\n\n/**\n * How to spell \"the id of the current caller\" in a fix suggestion, for the\n * platform this database actually is.\n *\n * Every remediation in this tool prints example SQL, and example SQL that calls\n * a function the reader's database does not have is worse than no example — it\n * looks authoritative and fails on paste. Rebase's helpers live in `rebase`\n * (they were in `auth` before 1.0, which is Supabase's schema and the reason\n * they moved); Supabase and PostgREST stacks have `auth.uid()`.\n *\n * `unknown` falls back to the Supabase spelling: this scanner is pointed at\n * plenty of databases Rebase did not create, and that is the wider convention.\n */\nexport function callerIdCall(snapshot: DbSnapshot): string {\n return snapshot.platform === \"rebase\" ? \"rebase.uid()\" : \"auth.uid()\";\n}\n","import type { Check, DbPolicy, DbSnapshot, Finding, PolicyCommand } from \"../types\";\n\nimport { isUnconditionalTrue } from \"./sql\";\nimport { callerIdCall,\n ANONYMOUS_ROLES,\n WRITE_PRIVILEGES,\n effectivePrivileges,\n finding,\n isPublicRole,\n listAnd,\n qi,\n qrel,\n type Privilege\n} from \"./util\";\n\nconst ID = \"anonymous-write-allowed\";\n\nconst COMMANDS_FOR: Record<PolicyCommand, Privilege[]> = {\n ALL: [\"INSERT\", \"UPDATE\", \"DELETE\"],\n INSERT: [\"INSERT\"],\n UPDATE: [\"UPDATE\"],\n DELETE: [\"DELETE\"],\n SELECT: []\n};\n\n/**\n * A write policy that lets an unauthenticated caller through, backed by a real grant.\n *\n * Two conditions beyond \"the policy targets anon\" carry this check, and both are\n * there to stop it firing on correct configurations:\n *\n * 1. The role must actually hold the matching write privilege. Postgres checks\n * the grant first; a policy that permits an INSERT nobody may attempt is inert.\n * 2. The policy's own expression must not scope the row. Supabase ships\n * `anon`/`authenticated` with broad table grants by default, so a policy\n * `FOR INSERT TO public WITH CHECK (user_id = auth.uid())` — a textbook\n * correct one — satisfies condition 1 on almost every project out there.\n * Flagging it would make this check fire on the whole ecosystem.\n *\n * So what is reported is the narrow, certain case: the check expression is\n * absent or constant-true, which in Postgres means \"accept any row\". Policies\n * whose expression is an anonymous *tautology* rather than a constant are the\n * business of `policy-anonymous-tautology`, which can weigh the platform.\n */\nexport const anonymousWriteAllowed: Check = {\n id: ID,\n title: \"Unauthenticated callers can write\",\n description:\n \"A permissive INSERT/UPDATE/DELETE policy reachable without authentication whose check \" +\n \"expression accepts any row, backed by a matching grant.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const uidCall = callerIdCall(snapshot);\n const findings: Finding[] = [];\n\n for (const policy of snapshot.policies) {\n if (!snapshot.schemas.includes(policy.schema)) continue;\n if (!policy.permissive) continue;\n\n const wanted = COMMANDS_FOR[policy.command] ?? [];\n if (wanted.length === 0) continue;\n\n const identities = anonymousIdentities(snapshot, policy.roles);\n if (identities.length === 0) continue;\n\n if (!acceptsAnyRow(policy)) continue;\n\n // Which write privileges those identities genuinely hold on the table.\n const granted = new Set<Privilege>();\n const grantedTo: string[] = [];\n for (const role of identities) {\n const held = effectivePrivileges(snapshot, policy.schema, policy.table, role);\n const hit = wanted.filter((p) => held.has(p));\n if (hit.length === 0) continue;\n hit.forEach((p) => granted.add(p));\n grantedTo.push(isPublicRole(role) ? \"PUBLIC\" : role);\n }\n if (granted.size === 0) continue;\n\n const commands = WRITE_PRIVILEGES.filter((p) => granted.has(p));\n const verbs = commands.map((c) => c.toLowerCase());\n\n findings.push(\n finding({\n id: ID,\n severity: \"high\",\n confidence: \"certain\",\n title:\n `${policy.schema}.${policy.table} accepts unauthenticated ` +\n `${listAnd(verbs)} via policy \"${policy.name}\"`,\n target: { schema: policy.schema, table: policy.table, policy: policy.name },\n detail:\n `Policy \"${policy.name}\" is a permissive ${policy.command} policy for ` +\n `${listAnd(grantedTo)}, and its check expression ` +\n `${policy.using == null && policy.withCheck == null\n ? \"is absent, which Postgres treats as accepting every row\"\n : \"is a constant truth, so every row satisfies it\"}. ` +\n `${listAnd(grantedTo)} also ${grantedTo.length > 1 ? \"hold\" : \"holds\"} ` +\n `${listAnd(commands)} on the table, so both the privilege check and the row ` +\n `check pass for a request that carries no credentials.`,\n impact:\n `An unauthenticated caller reaching this database over an API can ` +\n `${listAnd(verbs)} rows in ${policy.schema}.${policy.table} at will — inserting ` +\n `records attributed to other users, or ` +\n `${commands.includes(\"DELETE\") ? \"deleting the table's contents\" : \"modifying rows they do not own\"}.`,\n fix:\n `-- Scope the write to the caller, or take the privilege away entirely:\\n` +\n `ALTER POLICY ${qi(policy.name)} ON ${qrel(policy.schema, policy.table)}\\n` +\n ` WITH CHECK (user_id = ${uidCall});\\n` +\n `-- and if anonymous writes are never intended:\\n` +\n `REVOKE ${commands.join(\", \")} ON ${qrel(policy.schema, policy.table)} FROM ${grantedTo\n .map((r) => (r === \"PUBLIC\" ? \"PUBLIC\" : `\"${r}\"`))\n .join(\", \")};`\n })\n );\n }\n\n return findings;\n }\n};\n\n/**\n * Which unauthenticated identities a policy's TO list actually covers.\n *\n * `TO public` is not itself a role a request arrives as — it means \"every role\",\n * so the privileges that matter are the ones held by whichever anonymous roles\n * this database has (plus anything granted to PUBLIC). Resolving that is the\n * difference between catching `TO public` + `GRANT … TO anon` and missing it.\n */\nfunction anonymousIdentities(snapshot: DbSnapshot, policyRoles: string[]): string[] {\n const named = policyRoles.filter((r) => ANONYMOUS_ROLES.includes(r.toLowerCase()));\n if (named.length === 0) return [];\n if (!named.some(isPublicRole)) return named;\n\n const present = snapshot.roles\n .map((r) => r.name)\n .filter((name) => ANONYMOUS_ROLES.includes(name.toLowerCase()) && !isPublicRole(name));\n\n return [\"public\", ...present];\n}\n\n/**\n * Does this policy impose no row condition at all?\n *\n * For INSERT, Postgres falls back to USING when WITH CHECK is absent, and a\n * policy with neither clause admits every row. For UPDATE/DELETE the USING\n * clause selects the rows that may be touched.\n */\nfunction acceptsAnyRow(policy: DbPolicy): boolean {\n const clauses =\n policy.command === \"INSERT\"\n ? [policy.withCheck ?? policy.using]\n : [policy.using, policy.command === \"DELETE\" ? null : policy.withCheck];\n\n const present = clauses.filter((c): c is string => c != null);\n if (present.length === 0) return true;\n return present.every((c) => isUnconditionalTrue(c));\n}\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { matchParen, tokenize } from \"./sql\";\nimport { finding, qi, qrel } from \"./util\";\n\nconst ID = \"current-setting-throws\";\n\n/**\n * `current_setting('x')` in a policy, without the `missing_ok` second argument.\n *\n * With one argument the function *raises* when the setting has never been set in\n * the session — it does not return NULL. So a request that forgets to set the\n * GUC does not get denied, it gets an error, and the failure surfaces as a 500\n * rather than an empty result. Some middleware retries on 5xx, which turns a\n * missing header into a retry loop.\n *\n * Heuristic because the GUC may be guaranteed set by a connection-level `SET` or\n * a `set_config` in the same transaction, in which case one argument is fine —\n * the check cannot see the session that will run the query.\n */\nexport const currentSettingThrows: Check = {\n id: ID,\n title: \"Policy calls current_setting() without missing_ok\",\n description:\n \"A policy expression calling current_setting('x') with one argument, which raises rather \" +\n \"than returning NULL when the setting is unset.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const policy of snapshot.policies) {\n if (!snapshot.schemas.includes(policy.schema)) continue;\n\n const settings = new Set<string>();\n const clauses: string[] = [];\n for (const clause of [\"USING\", \"WITH CHECK\"] as const) {\n const expr = clause === \"USING\" ? policy.using : policy.withCheck;\n const hits = singleArgumentSettings(expr);\n if (hits.length === 0) continue;\n clauses.push(clause);\n hits.forEach((h) => settings.add(h));\n }\n if (settings.size === 0) continue;\n\n const names = [...settings];\n\n findings.push(\n finding({\n id: ID,\n severity: \"low\",\n confidence: \"heuristic\",\n title:\n `Policy \"${policy.name}\" on ${policy.schema}.${policy.table} calls ` +\n `current_setting(${names.map((n) => `'${n}'`).join(\", \")}) without missing_ok`,\n target: { schema: policy.schema, table: policy.table, policy: policy.name },\n detail:\n `The ${clauses.join(\" and \")} expression calls \\`current_setting\\` with a single ` +\n `argument. When ${names.length === 1 ? \"that setting has\" : \"those settings have\"} ` +\n `not been set in the session, the call raises \\`unrecognized configuration ` +\n `parameter\\` instead of returning NULL — so the policy cannot evaluate to false, ` +\n `it errors. Passing \\`true\\` as the second argument makes it return NULL, which ` +\n `the policy then treats as \"no match\" and denies the row, as intended.`,\n impact:\n `A request that reaches this table without ${names.join(\" / \")} set fails with a ` +\n `database error rather than being denied. The caller sees a 500 rather than an ` +\n `empty result, and any middleware that retries 5xx responses will retry a request ` +\n `that can never succeed.`,\n fix:\n `-- Add the missing_ok argument so an unset value denies instead of raising:\\n` +\n `ALTER POLICY ${qi(policy.name)} ON ${qrel(policy.schema, policy.table)}\\n` +\n ` USING (tenant_id = current_setting('${names[0]}', true)::uuid);`\n })\n );\n }\n\n return findings;\n }\n};\n\n/** Setting names passed to a one-argument `current_setting` call. */\nfunction singleArgumentSettings(expr: string | null | undefined): string[] {\n if (!expr) return [];\n const tokens = tokenize(expr);\n const out: string[] = [];\n\n for (let i = 0; i < tokens.length; i++) {\n const t = tokens[i];\n if (t.kind !== \"ident\" || t.quoted || t.value !== \"current_setting\") continue;\n\n const open = i + 1;\n if (tokens[open]?.kind !== \"punct\" || tokens[open].value !== \"(\") continue;\n const close = matchParen(tokens, open);\n if (close === -1) continue;\n\n // A comma at the call's own nesting level means missing_ok was supplied.\n const argDepth = tokens[open].depth + 1;\n const hasSecondArg = tokens\n .slice(open + 1, close)\n .some((a) => a.kind === \"punct\" && a.value === \",\" && a.depth === argDepth);\n if (hasSecondArg) continue;\n\n const first = tokens[open + 1];\n out.push(first?.kind === \"string\" ? first.value.replace(/^'|'$/g, \"\") : \"the setting\");\n }\n\n return [...new Set(out)];\n}\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { DML, finding, isPublicRole, listAnd, qrel, relationAt, rowsPhrase } from \"./util\";\n\nconst ID = \"grant-to-public\";\n\n/**\n * A DML privilege granted to PUBLIC.\n *\n * PUBLIC is every role in the cluster, present and future, including ones added\n * after the grant was written. Even with RLS enabled this is rarely intentional:\n * it widens the set of roles whose policies are evaluated, and any role that\n * later gets a permissive policy inherits table access it was never explicitly\n * given. When RLS is off it is simply open access — `rls-disabled` reports that\n * separately and with more force.\n */\nexport const grantToPublic: Check = {\n id: ID,\n title: \"Table privileges granted to PUBLIC\",\n description: \"A SELECT/INSERT/UPDATE/DELETE privilege granted to PUBLIC on a table.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const grant of snapshot.grants) {\n if (!isPublicRole(grant.grantee)) continue;\n if (!snapshot.schemas.includes(grant.schema)) continue;\n\n const rel = relationAt(snapshot, grant.schema, grant.table);\n if (!rel || (rel.kind !== \"table\" && rel.kind !== \"partitioned_table\")) continue;\n\n const privileges = DML.filter((p) => grant.privileges.includes(p));\n if (privileges.length === 0) continue;\n\n findings.push(\n finding({\n id: ID,\n severity: \"medium\",\n confidence: \"certain\",\n title: `${grant.schema}.${grant.table} grants ${listAnd(privileges)} to PUBLIC`,\n target: { schema: grant.schema, table: grant.table },\n detail:\n `PUBLIC is every role in this cluster, including roles created after this grant ` +\n `was made. ` +\n (rel.rlsEnabled\n ? `Row-level security is enabled on the table, so rows are still filtered by ` +\n `policy — but this grant decides *whose* policies get evaluated, and it ` +\n `answers \"everyone's\". A permissive policy added later for any role applies ` +\n `immediately, with no separate grant needed.`\n : `Row-level security is not enabled on this table, so nothing filters the rows ` +\n `this grant exposes.`),\n impact: rel.rlsEnabled\n ? `Every role in the database can attempt ${listAnd(privileges)} on this table` +\n `${rowsPhrase(rel)}; what they get back depends entirely on the policies, ` +\n `including any added in future.`\n : `Every role in the database, including any role an API connects as, can ` +\n `${listAnd(privileges.map((p) => p.toLowerCase()))} every row of this table` +\n `${rowsPhrase(rel)}.`,\n fix:\n `REVOKE ${privileges.join(\", \")} ON ${qrel(grant.schema, grant.table)} FROM PUBLIC;\\n` +\n `-- then grant explicitly to the roles that need it:\\n` +\n `-- GRANT ${privileges.join(\", \")} ON ${qrel(grant.schema, grant.table)} TO \"your_app_role\";`\n })\n );\n }\n\n return findings;\n }\n};\n","import type { Check, DbRelation, DbSnapshot, Finding } from \"../types\";\n\nimport { finding, qrel, relationAt, scannedTables } from \"./util\";\n\nconst ID = \"junction-table-unprotected\";\n\n/** Bookkeeping columns a join table is allowed to carry without ceasing to be one. */\nconst INCIDENTAL_COLUMNS = new Set([\n \"id\", \"uuid\", \"created_at\", \"updated_at\", \"inserted_at\", \"modified_at\", \"deleted_at\",\n \"created_on\", \"updated_on\", \"createdat\", \"updatedat\"\n]);\n\n/**\n * The edge table of a many-to-many relationship, left without RLS while both\n * endpoints have it.\n *\n * Join tables are the classic forgotten surface: the two tables people think of\n * as \"the data\" get policies, and the table that records *which user belongs to\n * which organisation* — often the most sensitive relation in the schema — is\n * treated as plumbing. It leaks the graph even when it leaks no attributes.\n *\n * The inference is deliberately narrow: exactly two foreign keys, to two\n * different tables that both have RLS on, and no columns beyond those keys and\n * ordinary bookkeeping. A table with its own payload columns is a domain table\n * that happens to have two foreign keys, and is not reported here.\n */\nexport const junctionTableUnprotected: Check = {\n id: ID,\n title: \"Many-to-many join table without RLS\",\n description:\n \"A table that is essentially just two foreign keys, both pointing at RLS-protected \" +\n \"tables, that has no row-level security of its own.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const rel of scannedTables(snapshot)) {\n if (rel.rlsEnabled) continue;\n\n const fks = snapshot.foreignKeys.filter(\n (fk) => fk.schema === rel.schema && fk.table === rel.name\n );\n if (fks.length !== 2) continue;\n\n const endpoints = fks.map((fk) => `${fk.refSchema}.${fk.refTable}`);\n if (endpoints[0] === endpoints[1]) continue;\n if (endpoints.some((e) => e === `${rel.schema}.${rel.name}`)) continue;\n\n const targets = fks.map((fk) => relationAt(snapshot, fk.refSchema, fk.refTable));\n if (!targets.every((t) => t?.rlsEnabled)) continue;\n\n if (!isMostlyKeys(rel, fks.flatMap((fk) => fk.columns))) continue;\n\n findings.push(\n finding({\n id: ID,\n severity: \"high\",\n confidence: \"heuristic\",\n title:\n `${rel.schema}.${rel.name} joins ${endpoints[0]} to ${endpoints[1]}, ` +\n `and is the only one of the three without RLS`,\n target: { schema: rel.schema, table: rel.name },\n detail:\n `This table looks like a many-to-many join table: its columns are the ` +\n `foreign keys to ${endpoints[0]} and ${endpoints[1]} plus bookkeeping. Both of ` +\n `those tables have row-level security enabled; this one does not, so no policy ` +\n `is consulted when it is read or written.`,\n impact:\n `If this table is reachable over an API, a caller reads the full membership ` +\n `graph between ${endpoints[0]} and ${endpoints[1]} — which rows relate to which — ` +\n `even though neither endpoint table can be read directly. Where the join table ` +\n `is also writable, a caller can grant themselves a relationship that the policies ` +\n `on the endpoint tables then honour.`,\n fix:\n `ALTER TABLE ${qrel(rel.schema, rel.name)} ENABLE ROW LEVEL SECURITY;\\n` +\n `-- A join table's policy normally follows its endpoints, e.g.:\\n` +\n `-- CREATE POLICY ${JSON.stringify(`${rel.name}_select`)} ON ${qrel(rel.schema, rel.name)}\\n` +\n `-- FOR SELECT USING (EXISTS (\\n` +\n `-- SELECT 1 FROM ${endpoints[0]} e\\n` +\n `-- WHERE e.id = ${qrel(rel.schema, rel.name)}.${fks[0].columns[0]}\\n` +\n `-- ));`\n })\n );\n }\n\n return findings;\n }\n};\n\nfunction isMostlyKeys(rel: DbRelation, keyColumns: string[]): boolean {\n const keys = new Set(keyColumns.map((c) => c.toLowerCase()));\n return rel.columns.every((c) => {\n const name = c.name.toLowerCase();\n if (keys.has(name)) return true;\n if (INCIDENTAL_COLUMNS.has(name)) return true;\n // A timestamp column under any name is bookkeeping, not payload.\n return /^timestamp/i.test(c.type);\n });\n}\n","import type { Check, DbSnapshot, DbView, Finding } from \"../types\";\n\nimport { exposedGrantees, finding, listAnd, qrel, qrole, relationAt } from \"./util\";\n\nconst ID = \"view-bypasses-rls\";\n\n/** Base relations of `view` that have RLS turned on. */\nexport function protectedBaseTables(snapshot: DbSnapshot, view: DbView): string[] {\n return view.dependsOn\n .map((d) => relationAt(snapshot, d.schema, d.table))\n .filter((r): r is NonNullable<typeof r> => Boolean(r?.rlsEnabled))\n .map((r) => `${r.schema}.${r.name}`);\n}\n\n/**\n * A view granted to an untrusted role that reads an RLS-protected table without\n * `security_invoker`.\n *\n * A view without that option executes its query as the view's *owner*, and the\n * owner is normally exempt from the base table's policies — so the view hands\n * back exactly the rows the policies were written to withhold. This is the most\n * common way a carefully locked-down schema leaks: the tables are audited, the\n * views over them are not.\n *\n * Before PG 15 the option does not exist, which means every view behaves this\n * way. That is worth saying, but it is not evidence that *this* view is a\n * mistake, so those findings are marked heuristic.\n */\nexport const viewBypassesRls: Check = {\n id: ID,\n title: \"View reads past its base table's RLS\",\n description:\n \"A view granted to an untrusted role that selects from an RLS-protected table and runs \" +\n \"with its owner's privileges instead of the caller's.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const view of snapshot.views) {\n if (!snapshot.schemas.includes(view.schema)) continue;\n\n // `views` carries materialized views too, for their dependencies;\n // they cannot take security_invoker and have their own check.\n const rel = relationAt(snapshot, view.schema, view.name);\n if (rel && rel.kind !== \"view\") continue;\n\n if (view.securityInvoker === true) continue;\n\n const bases = protectedBaseTables(snapshot, view);\n if (bases.length === 0) continue;\n\n const exposed = exposedGrantees(snapshot, view.schema, view.name, [\"SELECT\"]);\n if (exposed.length === 0) continue;\n\n const roles = exposed.map((e) => e.role);\n const legacy = view.securityInvoker === null;\n\n findings.push(\n finding({\n id: ID,\n severity: \"critical\",\n confidence: legacy ? \"heuristic\" : \"certain\",\n title:\n `View ${view.schema}.${view.name} reads ${listAnd(bases)} without ` +\n `security_invoker and is readable by ${listAnd(roles)}`,\n target: { schema: view.schema, view: view.name, table: view.name },\n detail: legacy\n ? `This server is PostgreSQL ${snapshot.serverVersion}, where the ` +\n `\\`security_invoker\\` view option does not exist — every view runs with its ` +\n `owner's privileges. This view is owned by ${view.owner} and selects from ` +\n `${listAnd(bases)}, which ${bases.length > 1 ? \"have\" : \"has\"} row-level ` +\n `security enabled. Unless ${view.owner} is itself constrained by those ` +\n `policies (it is not, unless FORCE ROW LEVEL SECURITY is set), the view ` +\n `returns rows the policies would have filtered out.`\n : `The view is owned by ${view.owner} and \\`security_invoker\\` is not set, so its ` +\n `query executes with ${view.owner}'s privileges rather than the caller's. ` +\n `Row-level security on ${listAnd(bases)} is evaluated for ${view.owner}, not ` +\n `for the role that selected from the view.`,\n impact:\n `If this view is reachable over an API as ${listAnd(roles)}, a caller reads rows ` +\n `from ${listAnd(bases)} that the policies on ${bases.length > 1 ? \"those tables\" : \"that table\"} ` +\n `were written to withhold — the view is an unfiltered path around them.`,\n fix: legacy\n ? `-- \\`security_invoker\\` requires PostgreSQL 15 or newer. On this server, either:\\n` +\n `-- 1. revoke access and let callers query the base table directly:\\n` +\n `REVOKE SELECT ON ${qrel(view.schema, view.name)} FROM ${qrole(roles[0])};\\n` +\n `-- 2. or set FORCE ROW LEVEL SECURITY on the base tables and add policies\\n` +\n `-- that apply to ${view.owner}, so the view's own execution is filtered.`\n : `ALTER VIEW ${qrel(view.schema, view.name)} SET (security_invoker = true);\\n` +\n `-- Callers then need their own SELECT privilege on ${listAnd(bases)}, and the\\n` +\n `-- policies there apply to them.`\n })\n );\n }\n\n return findings;\n }\n};\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { exposedGrantees, finding, listAnd, qrel, qrole, relationAt } from \"./util\";\nimport { protectedBaseTables } from \"./view-bypasses-rls\";\n\nconst ID = \"matview-bypasses-rls\";\n\n/**\n * A materialized view over an RLS-protected table, readable by an untrusted role.\n *\n * Materialized views cannot have row-level security at all — `ALTER MATERIALIZED\n * VIEW ... ENABLE ROW LEVEL SECURITY` is not valid syntax, and `security_invoker`\n * does not apply either. The contents are a snapshot taken by the owner at\n * refresh time, so there is no policy anywhere that can filter a read of it. The\n * only controls are the grant and what the defining query selects.\n */\nexport const matviewBypassesRls: Check = {\n id: ID,\n title: \"Materialized view exposes RLS-protected data\",\n description:\n \"A materialized view granted to an untrusted role whose defining query reads a table \" +\n \"with row-level security enabled.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const view of snapshot.views) {\n if (!snapshot.schemas.includes(view.schema)) continue;\n\n const rel = relationAt(snapshot, view.schema, view.name);\n if (rel?.kind !== \"materialized_view\") continue;\n\n const bases = protectedBaseTables(snapshot, view);\n if (bases.length === 0) continue;\n\n const exposed = exposedGrantees(snapshot, view.schema, view.name, [\"SELECT\"]);\n if (exposed.length === 0) continue;\n\n const roles = exposed.map((e) => e.role);\n\n findings.push(\n finding({\n id: ID,\n severity: \"high\",\n confidence: \"certain\",\n title:\n `Materialized view ${view.schema}.${view.name} snapshots ${listAnd(bases)} ` +\n `and is readable by ${listAnd(roles)}`,\n target: { schema: view.schema, view: view.name, table: view.name },\n detail:\n `This materialized view reads ${listAnd(bases)}, which ` +\n `${bases.length > 1 ? \"have\" : \"has\"} row-level security enabled. Materialized ` +\n `views cannot have policies of their own, and \\`security_invoker\\` does not apply ` +\n `to them — the rows were computed once, by ${view.owner}, and are stored ` +\n `unfiltered. Every caller with SELECT reads the same stored rows.`,\n impact:\n `If this materialized view is reachable over an API as ${listAnd(roles)}, a caller ` +\n `reads a full copy of the protected data in ${listAnd(bases)} as of the last ` +\n `REFRESH. No policy can restrict this; only the grant can.`,\n fix:\n `-- There is no RLS for materialized views. Restrict the grant:\\n` +\n `REVOKE SELECT ON ${qrel(view.schema, view.name)} FROM ${qrole(roles[0])};\\n` +\n `-- and, if callers need this data, expose it through a view that applies the\\n` +\n `-- caller's own privileges:\\n` +\n `-- CREATE VIEW ${qrel(view.schema, `${view.name}_scoped`)} WITH (security_invoker = true)\\n` +\n `-- AS SELECT * FROM ${bases[0].split(\".\").map((p) => `\"${p}\"`).join(\".\")} WHERE ...;`\n })\n );\n }\n\n return findings;\n }\n};\n","import type { Check, DbPolicy, DbSnapshot, Finding, Severity } from \"../types\";\n\nimport { isUnconditionalTrue } from \"./sql\";\nimport { callerIdCall, finding, listAnd, policyTargetsExposedRole, qi, qrel, relationAt, rowsPhrase } from \"./util\";\n\nconst ID = \"policy-always-true\";\n\n/**\n * A PERMISSIVE policy whose expression is a constant truth, targeted at a role\n * an untrusted caller reaches.\n *\n * Matching is structural rather than textual — see {@link isUnconditionalTrue}.\n * `USING (true)` is flagged; `USING (is_public = true)` is not, and a\n * substring-matching version of this check would flag both.\n *\n * The one thing that legitimately rescues `USING (true)` is a RESTRICTIVE\n * policy on the same command, because RESTRICTIVE clauses are ANDed after the\n * PERMISSIVE ones are ORed. \"Permissive default, restrictive gate\" is a real\n * pattern, so when one is present the finding degrades to a question instead of\n * an accusation rather than disappearing (the restrictive policy may well not\n * cover the same rows).\n */\nexport const policyAlwaysTrue: Check = {\n id: ID,\n title: \"Policy grants unconditional access\",\n description: \"A permissive policy whose USING or WITH CHECK expression is always true.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const uidCall = callerIdCall(snapshot);\n const findings: Finding[] = [];\n\n for (const policy of snapshot.policies) {\n if (!snapshot.schemas.includes(policy.schema)) continue;\n if (!policy.permissive) continue;\n\n const exposed = policyTargetsExposedRole(snapshot, policy);\n if (exposed.length === 0) continue;\n\n const clauses: string[] = [];\n if (isUnconditionalTrue(policy.using)) clauses.push(\"USING\");\n if (isUnconditionalTrue(policy.withCheck)) clauses.push(\"WITH CHECK\");\n if (clauses.length === 0) continue;\n\n const gate = restrictiveGate(snapshot, policy);\n const rel = relationAt(snapshot, policy.schema, policy.table);\n const verb = policy.command === \"SELECT\" ? \"read\" : \"act on\";\n const severity: Severity = gate ? \"medium\" : \"critical\";\n\n findings.push(\n finding({\n id: ID,\n severity,\n confidence: gate ? \"heuristic\" : \"certain\",\n title:\n `Policy \"${policy.name}\" on ${policy.schema}.${policy.table} is ` +\n `${listAnd(clauses)} (true) for ${listAnd(exposed)}`,\n target: { schema: policy.schema, table: policy.table, policy: policy.name },\n detail:\n `This permissive ${policy.command} policy's ${listAnd(clauses)} expression is a ` +\n `constant truth, so it matches every row for ${listAnd(exposed)}. Permissive ` +\n `policies are ORed together, so this one alone satisfies the table's row filter ` +\n `no matter how strict the others are.` +\n (gate\n ? ` A RESTRICTIVE policy (\"${gate}\") also applies to this command and is ANDed ` +\n `after it, so access may still be gated — verify that restrictive policy ` +\n `covers the rows and roles you expect, because nothing else here does.`\n : \"\"),\n impact: gate\n ? `Row filtering on this table rests entirely on the RESTRICTIVE policy \"${gate}\". ` +\n `If it does not cover a case, ${listAnd(exposed)} can ${verb} every row${rowsPhrase(rel)}.`\n : `If this table is reachable over an API as ${listAnd(exposed)}, a caller can ` +\n `${verb} every row${rowsPhrase(rel)} — the policy applies no scoping whatsoever.`,\n fix:\n `-- Replace the constant with the scoping you intended, e.g.:\\n` +\n `ALTER POLICY ${qi(policy.name)} ON ${qrel(policy.schema, policy.table)}\\n` +\n ` ${clauses.includes(\"USING\") ? `USING (user_id = ${uidCall})` : `WITH CHECK (user_id = ${uidCall})`};\\n` +\n `-- or, if unconditional access really is intended, drop the policy and say so\\n` +\n `-- with an explicit grant instead:\\n` +\n `-- DROP POLICY ${qi(policy.name)} ON ${qrel(policy.schema, policy.table)};`\n })\n );\n }\n\n return findings;\n }\n};\n\n/** Name of a RESTRICTIVE policy that is ANDed with this one, if any. */\nfunction restrictiveGate(snapshot: DbSnapshot, policy: DbPolicy): string | null {\n const overlapping = snapshot.policies.find(\n (p) =>\n !p.permissive &&\n p.schema === policy.schema &&\n p.table === policy.table &&\n (p.command === \"ALL\" || policy.command === \"ALL\" || p.command === policy.command)\n );\n return overlapping?.name ?? null;\n}\n","import type { Check, DbSnapshot, Finding, Severity } from \"../types\";\n\nimport { callerIdCall, finding, listAnd, policyTargetsExposedRole, qi, qrel } from \"./util\";\n\nconst ID = \"policy-anonymous-tautology\";\n\n/**\n * `auth.uid() IS NOT NULL` and its relatives.\n *\n * What this expression *means* depends entirely on the stack in front of the\n * database, and getting that wrong would fire a critical finding on essentially\n * every Supabase project in existence:\n *\n * - Supabase: `auth.uid()` reads a JWT claim and returns NULL for an anonymous\n * caller, so the expression is a legitimate \"signed in\" test. Its only real\n * failing is that it does not scope rows to their owner — worth `low`, worded\n * as a design observation rather than a vulnerability.\n * - Rebase / PostgREST-style stacks that coerce a missing id to a sentinel\n * (`'anonymous'`, `''`): the expression is true for signed-out callers, so it\n * is a straight authentication bypass. This exact policy shipped in this\n * project and granted anonymous access for weeks.\n * - Anything else: it comes down to whether the stack coerces, which the\n * database cannot tell us. `medium`, and say so out loud.\n *\n * A `<> 'anonymous'` guard alongside the null test is the corrected form and\n * clears the policy entirely.\n */\nexport const policyAnonymousTautology: Check = {\n id: ID,\n title: \"Policy only checks that a caller id exists\",\n description:\n \"A policy whose expression is `auth.uid() IS NOT NULL`-shaped: it separates signed-in \" +\n \"from signed-out callers but scopes no rows.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const uidCall = callerIdCall(snapshot);\n const findings: Finding[] = [];\n const { severity, meaning, impactSuffix } = platformReading(snapshot.platform);\n\n for (const policy of snapshot.policies) {\n if (!snapshot.schemas.includes(policy.schema)) continue;\n if (!policy.permissive) continue;\n\n const exposed = policyTargetsExposedRole(snapshot, policy);\n if (exposed.length === 0) continue;\n\n const clauses: string[] = [];\n const usingMatch = matchTautology(policy.using);\n const checkMatch = matchTautology(policy.withCheck);\n if (usingMatch) clauses.push(\"USING\");\n if (checkMatch) clauses.push(\"WITH CHECK\");\n if (clauses.length === 0) continue;\n\n const shape = usingMatch ?? checkMatch ?? \"the caller id\";\n\n findings.push(\n finding({\n id: ID,\n severity,\n confidence: \"heuristic\",\n title:\n `Policy \"${policy.name}\" on ${policy.schema}.${policy.table} only checks ` +\n `that ${shape} is not null`,\n target: { schema: policy.schema, table: policy.table, policy: policy.name },\n detail:\n `The ${listAnd(clauses)} expression of this ${policy.command} policy tests only ` +\n `that ${shape} is non-null. It does not compare anything to a column, so every ` +\n `row of the table satisfies it equally — the policy distinguishes signed-in from ` +\n `signed-out callers and nothing else. ${meaning}`,\n impact:\n `Any caller for whom ${shape} is non-null can reach every row this policy covers, ` +\n `including rows belonging to other users or tenants. ${impactSuffix}`,\n fix:\n `-- Scope the policy to the row's owner rather than to the existence of an id:\\n` +\n `ALTER POLICY ${qi(policy.name)} ON ${qrel(policy.schema, policy.table)}\\n` +\n ` USING (user_id = ${uidCall});\\n` +\n `-- If the intent really is \"any signed-in user\", make that explicit and make it\\n` +\n `-- reject the anonymous sentinel your stack may use:\\n` +\n `-- USING (${uidCall} IS NOT NULL AND ${uidCall} <> 'anonymous');`\n })\n );\n }\n\n return findings;\n }\n};\n\nfunction platformReading(platform: DbSnapshot[\"platform\"]): {\n severity: Severity;\n meaning: string;\n impactSuffix: string;\n} {\n switch (platform) {\n case \"supabase\":\n return {\n severity: \"low\",\n meaning:\n `On Supabase, \\`auth.uid()\\` returns NULL for an anonymous request, so this is a ` +\n `working authenticated-only check rather than a bypass. It is listed because it ` +\n `only distinguishes signed-in from signed-out; it does not scope rows to their owner.`,\n impactSuffix:\n `Anonymous callers are correctly excluded on Supabase, so this is a data-scoping ` +\n `gap between signed-in users, not an anonymous-access hole.`\n };\n case \"rebase\":\n case \"postgrest\":\n return {\n severity: \"critical\",\n meaning:\n `On this stack a request without a session is given a sentinel id (an 'anonymous' ` +\n `string rather than NULL), so this expression is true for signed-out callers too — ` +\n `it authorises everyone.`,\n impactSuffix:\n `Because signed-out requests arrive with a sentinel id rather than NULL, this ` +\n `includes unauthenticated callers.`\n };\n default:\n return {\n severity: \"medium\",\n meaning:\n `Whether this excludes anonymous callers depends on the layer in front of the ` +\n `database: if it leaves the setting unset for signed-out requests the expression is ` +\n `false and this is merely loose; if it coerces them to a sentinel id (an empty ` +\n `string or 'anonymous') the expression is true and this authorises everyone.`,\n impactSuffix:\n `Whether unauthenticated callers are included depends on whether your stack coerces ` +\n `a missing caller id to a sentinel value — check that before judging the severity.`\n };\n }\n}\n\n/**\n * Both schema spellings, deliberately.\n *\n * This runs over policy bodies read back from a live database, and those outlive\n * the release that wrote them: Rebase moved its helpers from `auth` to `rebase`\n * at 1.0, so a database mid-migration holds both, and a Supabase database holds\n * `auth.*` for good. A security check that stops recognising a dangerous clause\n * because a schema was renamed is a check that silently turned itself off.\n */\nconst CALLER_ID_CALLS: { re: RegExp; label: (m: RegExpExecArray) => string }[] = [\n { re: /(?:rebase|auth)\\.uid\\s*\\(\\s*\\)/g, label: (m) => m[0].replace(/\\s+/g, \"\") },\n { re: /auth\\.role\\s*\\(\\s*\\)/g, label: () => \"auth.role()\" },\n { re: /(?:rebase|auth)\\.roles\\s*\\(\\s*\\)/g, label: (m) => m[0].replace(/\\s+/g, \"\") },\n { re: /(?:rebase|auth)\\.jwt\\s*\\(\\s*\\)/g, label: (m) => m[0].replace(/\\s+/g, \"\") },\n { re: /current_setting\\s*\\(\\s*('[^']*')\\s*(?:,\\s*[a-z]+\\s*)?\\)/g, label: (m) => `current_setting(${m[1]})` }\n];\n\n/**\n * Recognise `<caller-id expression> IS NOT NULL` and name what was tested — but\n * only when that is the *entire* expression.\n *\n * The \"entire\" part is what keeps this check honest. `auth.uid() IS NOT NULL AND\n * user_id = auth.uid()` contains the shape and is a perfectly scoped policy; a\n * substring match would flag it, and flagging correct Supabase policies is the\n * fastest way to get this tool deleted. So the caller-id call is substituted for\n * a placeholder, casts, parens, `SELECT` wrappers and whitespace are stripped,\n * and what remains has to be exactly `<placeholder> is not null`.\n */\nfunction matchTautology(clause: string | null | undefined): string | null {\n if (!clause) return null;\n const flat = clause.toLowerCase().replace(/\\s+/g, \" \");\n\n // The corrected form guards against the anonymous sentinel; not this finding.\n if (/(<>|!=)\\s*'anonymous'/.test(flat) || /(<>|!=)\\s*''/.test(flat)) return null;\n\n for (const { re, label } of CALLER_ID_CALLS) {\n const first = new RegExp(re.source).exec(flat);\n if (!first) continue;\n\n const normalized = flat\n .replace(new RegExp(re.source, \"g\"), \"callerid\")\n .replace(/::[a-z0-9_[\\]]+/g, \"\")\n .replace(/\\bselect\\b/g, \"\")\n // Postgres renders a wrapped call as `( SELECT rebase.uid() AS uid)`.\n .replace(/\\bas [a-z0-9_]+/g, \"\")\n .replace(/[()\\s]/g, \"\");\n\n if (normalized === \"calleridisnotnull\") return label(first);\n }\n\n return null;\n}\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { finding, isPublicRole, policiesFor, qi, qrel, sameRole, scannedTables } from \"./util\";\n\nconst ID = \"policy-role-unreachable\";\n\n/**\n * Every policy on a table names roles that nothing can arrive as.\n *\n * The symptom is a table that reads as empty while its policies look perfectly\n * correct. It happens most often when policies are copied from Supabase\n * documentation — `TO authenticated` — onto a database where requests arrive as\n * some other role entirely. Postgres accepts the policy, it simply never\n * applies, and RLS with no *applicable* policy denies everything.\n *\n * This project's own demo database sat in exactly this state for weeks.\n *\n * Reachability is judged from `pg_auth_members`, not from what a superuser could\n * theoretically SET ROLE to. A superuser can become any role without membership,\n * so counting that would make this check unable to fire at all — and it would be\n * answering the wrong question anyway, which is \"do the requests my application\n * sends land on these policies?\"\n */\nexport const policyRoleUnreachable: Check = {\n id: ID,\n title: \"Policies target roles nothing connects as\",\n description:\n \"Every policy on a table names roles that do not exist, cannot log in, and that no \" +\n \"login role inherits — so no policy ever applies and the table reads as empty.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const rel of scannedTables(snapshot)) {\n if (!rel.rlsEnabled) continue;\n\n const policies = policiesFor(snapshot, rel.schema, rel.name);\n if (policies.length === 0) continue; // rls-enabled-no-policies owns this\n\n const named = [...new Set(policies.flatMap((p) => p.roles))];\n if (named.some((role) => isReachable(snapshot, role))) continue;\n\n const missing = named.filter((role) => !snapshot.roles.some((r) => sameRole(r.name, role)));\n\n findings.push(\n finding({\n id: ID,\n severity: \"medium\",\n confidence: \"certain\",\n title:\n `All ${policies.length} ${policies.length === 1 ? \"policy\" : \"policies\"} on ` +\n `${rel.schema}.${rel.name} ${policies.length === 1 ? \"targets\" : \"target\"} unreachable ${named.length === 1 ? \"role\" : \"roles\"} ` +\n `(${named.join(\", \")})`,\n target: { schema: rel.schema, table: rel.name },\n detail:\n `Row-level security is enabled on this table and every policy names only ` +\n `${named.join(\", \")}. ` +\n (missing.length > 0\n ? `${missing.join(\", \")} ${missing.length === 1 ? \"does\" : \"do\"} not exist in ` +\n `pg_roles at all. `\n : \"\") +\n `None of these roles can log in, and no login role is a member of ` +\n `${named.length === 1 ? \"it\" : \"any of them\"}, so no session ever has these ` +\n `policies applied. RLS with no applicable policy denies every row.`,\n impact:\n `Reads of this table return zero rows for every application role, and writes are ` +\n `rejected. Nothing is exposed — the data is invisible instead, and it looks ` +\n `identical to an empty table, which is why this usually goes unnoticed for a long ` +\n `time. The owner (${rel.owner}) still sees everything` +\n `${rel.rlsForced ? \" unless FORCE ROW LEVEL SECURITY changes that\" : \", since FORCE ROW LEVEL SECURITY is not set\"}.`,\n fix:\n `-- Point the policies at the role your requests actually arrive as. Confirm it with:\\n` +\n `-- SELECT current_user; -- run this from your application's connection\\n` +\n `ALTER POLICY ${qi(policies[0].name)} ON ${qrel(rel.schema, rel.name)} TO <that role>;\\n` +\n `-- Alternatively, if ${named[0]} is meant to be reachable, grant membership:\\n` +\n `-- GRANT ${qi(named[0])} TO <your login role>;`\n })\n );\n }\n\n return findings;\n }\n};\n\nfunction isReachable(snapshot: DbSnapshot, role: string): boolean {\n if (isPublicRole(role)) return true;\n\n const def = snapshot.roles.find((r) => sameRole(r.name, role));\n if (!def) return false;\n if (def.canLogin) return true;\n\n return snapshot.roles.some(\n (r) => r.canLogin && r.memberOf.some((m) => sameRole(m, role))\n );\n}\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { callerIdCall, DML, exposedGrantees, finding, listAnd, qrel, qrole, rowsPhrase, scannedTables } from \"./util\";\n\nconst ID = \"rls-disabled\";\n\n/**\n * A table with RLS off *and* a DML grant to a role an untrusted caller arrives as.\n *\n * The grant half is not a formality. Most databases contain plenty of tables\n * with RLS off that nothing outside the owner can address — migration bookkeeping,\n * lookup tables reachable only by the service role. Flagging those is how a\n * scanner produces forty findings on a healthy database and gets ignored, so a\n * table nobody exposed produces no finding at all.\n */\nexport const rlsDisabled: Check = {\n id: ID,\n title: \"Table exposed without row-level security\",\n description:\n \"A table with RLS disabled that also grants SELECT/INSERT/UPDATE/DELETE to a role \" +\n \"an unauthenticated or untrusted caller can reach.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const uidCall = callerIdCall(snapshot);\n const findings: Finding[] = [];\n\n for (const rel of scannedTables(snapshot)) {\n if (rel.rlsEnabled) continue;\n\n const exposed = exposedGrantees(snapshot, rel.schema, rel.name, DML);\n if (exposed.length === 0) continue;\n\n const roles = exposed.map((e) => e.role);\n const privileges = [...new Set(exposed.flatMap((e) => e.privileges))].sort();\n const canRead = privileges.includes(\"SELECT\");\n const writes = privileges.filter((p) => p !== \"SELECT\");\n\n const reach: string[] = [];\n if (canRead) reach.push(`read every row${rowsPhrase(rel)}`);\n if (writes.length > 0) reach.push(`${listAnd(writes.map((w) => w.toLowerCase()))} any row`);\n\n findings.push(\n finding({\n id: ID,\n severity: \"critical\",\n confidence: \"certain\",\n title: `${rel.schema}.${rel.name} has row-level security disabled and is granted to ${listAnd(roles)}`,\n target: { schema: rel.schema, table: rel.name },\n detail:\n `Row-level security is not enabled on this table, so Postgres applies no ` +\n `per-row filter at all — policies, if any exist, are never consulted. ` +\n `${listAnd(roles)} ${roles.length > 1 ? \"hold\" : \"holds\"} ` +\n `${listAnd(privileges)} on it.`,\n impact:\n `If this table is reachable over an API that connects as ${listAnd(roles)}, ` +\n `a caller can ${listAnd(reach)}, with no tenant or owner scoping.`,\n fix:\n `ALTER TABLE ${qrel(rel.schema, rel.name)} ENABLE ROW LEVEL SECURITY;\\n` +\n `-- Enabling RLS with no policies denies every row to everyone but the owner,\\n` +\n `-- so add the policy you intend in the same migration, for example:\\n` +\n `-- CREATE POLICY ${JSON.stringify(`${rel.name}_owner_select`)} ON ${qrel(rel.schema, rel.name)}\\n` +\n `-- FOR SELECT TO ${qrole(roles[0])} USING (user_id = ${uidCall});`\n })\n );\n }\n\n return findings;\n }\n};\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { callerIdCall, finding, policiesFor, qrel, scannedTables } from \"./util\";\n\nconst ID = \"rls-enabled-no-policies\";\n\n/**\n * RLS on, zero policies — deny-all for everyone except the table's owner.\n *\n * This is not a hole, it is a *correctness* bug, and an invisible one: the API\n * returns `[]` and an empty table is indistinguishable from a filtered one. It\n * is worth a finding precisely because nothing else reports it — a database in\n * this state passes every \"is RLS enabled?\" audit while serving nothing.\n */\nexport const rlsEnabledNoPolicies: Check = {\n id: ID,\n title: \"RLS enabled with no policies (denies everything)\",\n description: \"A table with row-level security enabled but not a single policy defined.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const uidCall = callerIdCall(snapshot);\n const findings: Finding[] = [];\n\n for (const rel of scannedTables(snapshot)) {\n if (!rel.rlsEnabled) continue;\n if (policiesFor(snapshot, rel.schema, rel.name).length > 0) continue;\n\n const ownerNote = rel.rlsForced\n ? \"FORCE ROW LEVEL SECURITY is also set, so even the owner sees nothing.\"\n : `The owner (${rel.owner}) still reads and writes the table normally, because ` +\n `FORCE ROW LEVEL SECURITY is not set — which is why this often looks fine from ` +\n `psql and returns nothing through the application.`;\n\n findings.push(\n finding({\n id: ID,\n severity: \"medium\",\n confidence: \"certain\",\n title: `${rel.schema}.${rel.name} has row-level security enabled but no policies`,\n target: { schema: rel.schema, table: rel.name },\n detail:\n `With RLS enabled and no policy defined, Postgres denies every row to every ` +\n `role. ${ownerNote}`,\n impact:\n `Reads of this table return zero rows and writes are rejected for every ` +\n `non-owner role. No data is exposed; data is silently missing instead, and ` +\n `an empty result is indistinguishable from a correctly filtered one.`,\n fix:\n `-- Either define the policy you intended:\\n` +\n `CREATE POLICY ${JSON.stringify(`${rel.name}_select`)} ON ${qrel(rel.schema, rel.name)}\\n` +\n ` FOR SELECT TO authenticated USING (user_id = ${uidCall});\\n` +\n `-- or, if this table is genuinely meant to be unreadable, drop the grants\\n` +\n `-- instead of relying on an empty policy set:\\n` +\n `-- REVOKE ALL ON ${qrel(rel.schema, rel.name)} FROM PUBLIC;`\n })\n );\n }\n\n return findings;\n }\n};\n","import type { Check, DbSnapshot, Finding, Severity } from \"../types\";\n\nimport { finding, qrel, sameRole, scannedTables } from \"./util\";\n\nconst ID = \"rls-enabled-not-forced\";\n\n/**\n * RLS on, FORCE off — the table owner is exempt from its own policies.\n *\n * How much that matters is entirely a question of *who the owner is*, and the\n * severity has to follow that or the check becomes noise:\n *\n * - Owner cannot log in: a provisioning role nothing connects as. Informational.\n * - Owner can log in and is otherwise ordinary: anything using that connection\n * string reads the whole table. This is the case worth waking up for.\n * - Owner is a superuser or has BYPASSRLS: FORCE would not help either way,\n * because those attributes skip RLS before ownership is even considered.\n * Reporting `high` here would be misleading — the fix is \"do not connect as\n * this role\", not \"set FORCE\".\n */\nexport const rlsEnabledNotForced: Check = {\n id: ID,\n title: \"RLS enabled but not forced for the table owner\",\n description: \"A table with RLS enabled where the owning role is exempt from its own policies.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const rel of scannedTables(snapshot)) {\n if (!rel.rlsEnabled || rel.rlsForced) continue;\n\n const owner = snapshot.roles.find((r) => sameRole(r.name, rel.owner));\n const bypasses = Boolean(owner?.superuser || owner?.bypassRls);\n const canLogin = Boolean(owner?.canLogin);\n\n let severity: Severity = \"medium\";\n let detail: string;\n let impact: string;\n let fix: string;\n\n if (bypasses) {\n severity = \"medium\";\n detail =\n `Policies on this table do not apply to its owner, ${rel.owner}, because ` +\n `FORCE ROW LEVEL SECURITY is not set. That role is additionally ` +\n `${owner?.superuser ? \"a superuser\" : \"marked BYPASSRLS\"}, so it skips row-level ` +\n `security on every table regardless of this setting — FORCE would not constrain it.`;\n impact =\n `Any connection made as ${rel.owner} reads and writes every row of this table, ` +\n `ignoring all policies. This is expected for an administrative role and dangerous ` +\n `only if an application connects with it.`;\n fix =\n `-- FORCE cannot constrain this role. Connect your application as a role that is\\n` +\n `-- neither the owner nor BYPASSRLS, and keep ${rel.owner} for migrations only.\\n` +\n `ALTER TABLE ${qrel(rel.schema, rel.name)} FORCE ROW LEVEL SECURITY; -- still worth setting`;\n } else if (canLogin) {\n severity = \"high\";\n detail =\n `FORCE ROW LEVEL SECURITY is not set, so the owning role ${rel.owner} is exempt ` +\n `from every policy on this table. ${rel.owner} can log in directly, which means a ` +\n `connection string for it bypasses all row filtering.`;\n impact =\n `Anything connecting as ${rel.owner} — including an application that was handed ` +\n `the owner's connection string, which is the default in most quick-start setups — ` +\n `reads and writes every row, ignoring the policies on this table.`;\n fix = `ALTER TABLE ${qrel(rel.schema, rel.name)} FORCE ROW LEVEL SECURITY;`;\n } else {\n severity = \"medium\";\n detail =\n `FORCE ROW LEVEL SECURITY is not set, so the owning role ${rel.owner} is exempt ` +\n `from every policy on this table. That role cannot log in` +\n `${owner ? \"\" : \" (it was not found in pg_roles, so it may have been dropped)\"}, ` +\n `so nothing connects as it directly today.`;\n impact =\n `No caller bypasses policies through ownership right now. If ${rel.owner} is ever ` +\n `granted LOGIN, or a role that can SET ROLE to it is used by an application, every ` +\n `policy on this table stops applying to that session.`;\n fix = `ALTER TABLE ${qrel(rel.schema, rel.name)} FORCE ROW LEVEL SECURITY;`;\n }\n\n findings.push(\n finding({\n id: ID,\n severity,\n confidence: \"certain\",\n title: `${rel.schema}.${rel.name}: policies do not apply to its owner ${rel.owner}`,\n target: { schema: rel.schema, table: rel.name },\n detail,\n impact,\n fix\n })\n );\n }\n\n return findings;\n }\n};\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { finding } from \"./util\";\n\nconst ID = \"security-definer-mutable-search-path\";\n\n/**\n * A SECURITY DEFINER routine whose `search_path` is not pinned.\n *\n * The routine executes as its owner — frequently a superuser or the schema owner\n * — while the *caller* decides what `public.some_table` or `now()` resolves to.\n * Anyone who can create objects in a schema on the caller's search_path can put\n * a shadowing function or table in front of one the routine uses, and it runs\n * with the owner's privileges. That includes reading and writing tables whose\n * RLS policies would otherwise have applied.\n *\n * This one is `certain`: it is a property of the catalog, not an inference.\n * Whether it is exploitable depends on who can create objects, which is why the\n * impact line says so rather than claiming a working escalation.\n */\nexport const securityDefinerMutableSearchPath: Check = {\n id: ID,\n title: \"SECURITY DEFINER routine with a mutable search_path\",\n description:\n \"A SECURITY DEFINER function or procedure that does not pin search_path, so the caller \" +\n \"controls how its identifiers resolve.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const routine of snapshot.routines) {\n if (!snapshot.schemas.includes(routine.schema)) continue;\n if (!routine.securityDefiner || !routine.mutableSearchPath) continue;\n\n findings.push(\n finding({\n id: ID,\n severity: \"medium\",\n confidence: \"certain\",\n title:\n `${routine.schema}.${routine.name} is SECURITY DEFINER and does not pin ` +\n `search_path`,\n target: { schema: routine.schema, routine: routine.name },\n detail:\n `This routine runs with the privileges of its owner (${routine.owner}) but ` +\n `resolves unqualified names using the *caller's* search_path, because no ` +\n `\\`SET search_path\\` is attached to it. A caller who can create objects in any ` +\n `schema on that path can shadow a table or function the routine uses, and their ` +\n `object then runs as ${routine.owner}.`,\n impact:\n `Any role that can both execute this routine and create objects in a schema on ` +\n `its search_path can have arbitrary SQL run as ${routine.owner} — which bypasses ` +\n `row-level security on every table ${routine.owner} can reach. Without CREATE on ` +\n `some schema this is not directly exploitable, so check who holds CREATE ` +\n `(commonly PUBLIC on \\`public\\` in older databases).`,\n fix:\n `-- Pin the search_path on every overload of this routine:\\n` +\n `DO $$\\n` +\n `DECLARE r record;\\n` +\n `BEGIN\\n` +\n ` FOR r IN\\n` +\n ` SELECT p.oid::regprocedure AS sig\\n` +\n ` FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace\\n` +\n ` WHERE n.nspname = ${literal(routine.schema)} AND p.proname = ${literal(routine.name)}\\n` +\n ` LOOP\\n` +\n ` EXECUTE format('ALTER ROUTINE %s SET search_path = pg_catalog, pg_temp', r.sig);\\n` +\n ` END LOOP;\\n` +\n `END $$;\\n` +\n `-- Then schema-qualify every identifier in the body, since nothing but\\n` +\n `-- pg_catalog is on the path any more.`\n })\n );\n }\n\n return findings;\n }\n};\n\nconst literal = (value: string): string => `'${value.replace(/'/g, \"''\")}'`;\n","import type { Check, DbPolicy, DbRelation, DbSnapshot, Finding } from \"../types\";\n\nimport { SQL_KEYWORDS, findSubqueries, isBareIdent, matchParen, tokenize, type SqlToken } from \"./sql\";\nimport { finding, hasColumn, qi, qrel, relationAt } from \"./util\";\n\nconst ID = \"unqualified-column-in-subquery\";\n\n/**\n * A bare column reference inside a policy subquery that resolves to the *inner*\n * relation when the outer row was meant.\n *\n * This is the bug this whole tool exists for. Postgres resolves an unqualified\n * name against the innermost scope that has it, silently, with no warning — so\n *\n * USING (EXISTS (SELECT 1 FROM org_members\n * WHERE user_id = auth.uid() AND organization_id = id))\n *\n * where the author meant `organizations.id` compares `org_members.organization_id`\n * to `org_members.id`. The correlation to the outer row disappears and the\n * predicate becomes something completely different — in the case this codebase\n * actually shipped, one that denied every row to everyone. Fail-open variants of\n * the same mistake are equally possible.\n *\n * Two deliberate restrictions keep the false-positive rate near zero:\n *\n * - Only the subquery's *predicate* is examined. A bare name in the select\n * list (`IN (SELECT org_id FROM members)`) is correct by construction.\n * - The bare name has to be compared against something that could be a column.\n * `WHERE user_id = auth.uid()` binds to the inner table on purpose, and\n * flagging it merely because the outer table also has a `user_id` would fire\n * on a large fraction of correct policies.\n *\n * Confidence is always heuristic, and the detail says why an absence proves\n * nothing: `pg_policies.qual` is Postgres's re-rendering of the parse tree, and\n * it normally re-qualifies references. A finding here therefore means the\n * ambiguity survived that rewrite, which is strong evidence — but a clean scan\n * is not proof that the original SQL was unambiguous.\n */\nexport const unqualifiedColumnInSubquery: Check = {\n id: ID,\n title: \"Unqualified column inside a policy subquery\",\n description:\n \"A bare column name in an EXISTS/IN subquery that exists on both the inner relation and \" +\n \"the policy's own table, so Postgres binds it to the inner one.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const policy of snapshot.policies) {\n if (!snapshot.schemas.includes(policy.schema)) continue;\n\n const outer = relationAt(snapshot, policy.schema, policy.table);\n if (!outer) continue;\n\n for (const clause of [\"USING\", \"WITH CHECK\"] as const) {\n const expr = clause === \"USING\" ? policy.using : policy.withCheck;\n if (!expr) continue;\n\n for (const hit of scanExpression(snapshot, outer, expr)) {\n findings.push(buildFinding(policy, outer, clause, hit));\n }\n }\n }\n\n return findings;\n }\n};\n\ninterface Ambiguity {\n /** The bare name as written. */\n column: string;\n /** The relation Postgres binds it to. */\n inner: string;\n /** What it is compared against, for the report. */\n comparedTo: string;\n}\n\ninterface FromItem {\n schema?: string;\n name: string;\n alias?: string;\n}\n\nconst FROM_TERMINATORS = new Set([\n \"where\", \"group\", \"having\", \"order\", \"limit\", \"offset\", \"union\", \"intersect\",\n \"except\", \"window\", \"fetch\", \"returning\"\n]);\nconst JOIN_WORDS = new Set([\"join\", \"inner\", \"left\", \"right\", \"full\", \"outer\", \"cross\", \"natural\", \"lateral\"]);\nconst COMPARISONS = new Set([\"=\", \"<>\", \"!=\", \"<\", \">\", \"<=\", \">=\", \"~~\", \"!~\", \"is\", \"like\", \"ilike\", \"in\"]);\n\nfunction scanExpression(snapshot: DbSnapshot, outer: DbRelation, expr: string): Ambiguity[] {\n const tokens = tokenize(expr);\n const subqueries = findSubqueries(tokens);\n const seen = new Set<string>();\n const out: Ambiguity[] = [];\n\n for (const sub of subqueries) {\n // A subquery's own tokens exclude anything belonging to a subquery nested\n // inside it — those are analysed on their own pass, against their own FROM.\n const nested = subqueries.filter((s) => s !== sub && s.open > sub.open && s.close < sub.close);\n const own: number[] = [];\n for (let i = sub.open + 1; i < sub.close; i++) {\n if (nested.some((n) => i >= n.open && i <= n.close)) continue;\n own.push(i);\n }\n\n const from = parseFrom(tokens, own);\n if (from.length === 0) continue;\n\n const relations = from\n .map((item) => resolveRelation(snapshot, outer.schema, item))\n .filter((r): r is DbRelation => Boolean(r))\n // A self-reference has no \"outer vs inner\" distinction worth warning\n // about: the author selected from this very table, so binding to the\n // inner copy is the ordinary reading.\n .filter((r) => !(r.schema === outer.schema && r.name === outer.name));\n if (relations.length === 0) continue;\n\n const aliases = new Set(from.map((f) => f.alias).filter((a): a is string => Boolean(a)));\n\n for (const idx of predicateIndices(tokens, own)) {\n const token = tokens[idx];\n if (token.kind !== \"ident\" || token.quoted) continue;\n if (SQL_KEYWORDS.has(token.value) || aliases.has(token.value)) continue;\n if (!isBareIdent(tokens, idx)) continue;\n if (!hasColumn(outer, token.value)) continue;\n\n const inner = relations.find((r) => hasColumn(r, token.value));\n if (!inner) continue;\n\n const partner = comparisonPartner(tokens, own, idx);\n if (!partner) continue;\n\n const key = `${token.value}|${inner.schema}.${inner.name}`;\n if (seen.has(key)) continue;\n seen.add(key);\n\n out.push({ column: token.value, inner: `${inner.schema}.${inner.name}`, comparedTo: partner });\n }\n }\n\n return out;\n}\n\n/** Relation references in the subquery's FROM list, with their aliases. */\nfunction parseFrom(tokens: SqlToken[], own: number[]): FromItem[] {\n const start = own.findIndex((i) => tokens[i].kind === \"ident\" && tokens[i].value === \"from\");\n if (start === -1) return [];\n\n const items: FromItem[] = [];\n let expectRelation = true;\n\n for (let k = start + 1; k < own.length; k++) {\n const t = tokens[own[k]];\n\n if (t.kind === \"ident\" && FROM_TERMINATORS.has(t.value)) break;\n if (t.kind === \"punct\" && t.value === \",\") {\n expectRelation = true;\n continue;\n }\n if (t.kind === \"ident\" && JOIN_WORDS.has(t.value)) {\n expectRelation = true;\n continue;\n }\n // `ON <predicate>` / `USING (...)` — skip to the next join or comma.\n if (t.kind === \"ident\" && (t.value === \"on\" || t.value === \"using\")) {\n expectRelation = false;\n continue;\n }\n if (!expectRelation || t.kind !== \"ident\") continue;\n\n // schema.name[.…] — take the last two parts.\n const parts: string[] = [t.value];\n let k2 = k;\n while (\n tokens[own[k2 + 1]]?.kind === \"punct\" &&\n tokens[own[k2 + 1]]?.value === \".\" &&\n tokens[own[k2 + 2]]?.kind === \"ident\"\n ) {\n parts.push(tokens[own[k2 + 2]].value);\n k2 += 2;\n }\n\n const item: FromItem = parts.length > 1\n ? { schema: parts[parts.length - 2], name: parts[parts.length - 1] }\n : { name: parts[0] };\n\n // Optional alias, with or without AS.\n let after = tokens[own[k2 + 1]];\n if (after?.kind === \"ident\" && after.value === \"as\") {\n k2 += 1;\n after = tokens[own[k2 + 1]];\n }\n if (after?.kind === \"ident\" && !SQL_KEYWORDS.has(after.value) && !JOIN_WORDS.has(after.value)) {\n item.alias = after.value;\n k2 += 1;\n }\n\n items.push(item);\n k = k2;\n expectRelation = false;\n }\n\n return items;\n}\n\n/** Token indices that sit in a WHERE or JOIN … ON predicate of this subquery. */\nfunction predicateIndices(tokens: SqlToken[], own: number[]): number[] {\n const out: number[] = [];\n let inPredicate = false;\n\n for (let k = 0; k < own.length; k++) {\n const t = tokens[own[k]];\n if (t.kind === \"ident\") {\n if (t.value === \"where\" || t.value === \"on\") {\n inPredicate = true;\n continue;\n }\n if (FROM_TERMINATORS.has(t.value) && t.value !== \"where\") {\n inPredicate = false;\n continue;\n }\n if (JOIN_WORDS.has(t.value)) {\n inPredicate = false;\n continue;\n }\n }\n if (inPredicate) out.push(own[k]);\n }\n\n return out;\n}\n\n/**\n * What the bare name is compared with, or null when it is not compared with\n * something that could be a column.\n *\n * Literals, function calls and NULL tests are excluded on purpose: those\n * predicates mean what they say when bound to the inner relation, and the outer\n * table happening to share the column name is a coincidence, not a bug.\n */\nfunction comparisonPartner(tokens: SqlToken[], own: number[], idx: number): string | null {\n const pos = own.indexOf(idx);\n if (pos === -1) return null;\n\n const at = (offset: number): SqlToken | undefined => tokens[own[pos + offset]];\n\n let partnerStart: number;\n if (at(1)?.kind === \"op\" && COMPARISONS.has(at(1)!.value)) partnerStart = pos + 2;\n else if (at(1)?.kind === \"ident\" && COMPARISONS.has(at(1)!.value)) partnerStart = pos + 2;\n else if (at(-1)?.kind === \"op\" && COMPARISONS.has(at(-1)!.value)) partnerStart = pos - 2;\n else if (at(-1)?.kind === \"ident\" && COMPARISONS.has(at(-1)!.value)) partnerStart = pos - 2;\n else return null;\n\n const step = partnerStart > pos ? 1 : -1;\n const head = tokens[own[partnerStart]];\n if (!head || head.kind !== \"ident\") return null;\n if (head.value === \"null\" || head.value === \"true\" || head.value === \"false\") return null;\n\n // Walk the dotted name in whichever direction the partner lies.\n const parts: string[] = [head.value];\n let cursor = partnerStart;\n while (\n tokens[own[cursor + step]]?.kind === \"punct\" &&\n tokens[own[cursor + step]]?.value === \".\" &&\n tokens[own[cursor + 2 * step]]?.kind === \"ident\"\n ) {\n const next = tokens[own[cursor + 2 * step]].value;\n if (step > 0) parts.push(next);\n else parts.unshift(next);\n cursor += 2 * step;\n }\n\n // A trailing `(` makes it a function call, not a column.\n const tail = step > 0 ? tokens[own[cursor + 1]] : tokens[own[partnerStart + 1]];\n if (tail?.kind === \"punct\" && tail.value === \"(\") return null;\n\n return parts.join(\".\");\n}\n\nfunction resolveRelation(\n snapshot: DbSnapshot,\n policySchema: string,\n item: FromItem\n): DbRelation | undefined {\n if (item.schema) return relationAt(snapshot, item.schema, item.name);\n\n const local = relationAt(snapshot, policySchema, item.name);\n if (local) return local;\n\n // Unqualified and not in the policy's schema: only accept an unambiguous hit.\n const candidates = snapshot.relations.filter(\n (r) => r.name === item.name && snapshot.schemas.includes(r.schema)\n );\n return candidates.length === 1 ? candidates[0] : undefined;\n}\n\nfunction buildFinding(\n policy: DbPolicy,\n outer: DbRelation,\n clause: \"USING\" | \"WITH CHECK\",\n hit: Ambiguity\n): Finding {\n return finding({\n id: ID,\n severity: \"high\",\n confidence: \"heuristic\",\n title:\n `Policy \"${policy.name}\" on ${policy.schema}.${policy.table}: does \\`${hit.column}\\` in ` +\n `the subquery mean ${outer.name}.${hit.column} or ${hit.inner}.${hit.column}?`,\n target: {\n schema: policy.schema,\n table: policy.table,\n policy: policy.name,\n column: hit.column\n },\n detail:\n `In the ${clause} expression, \\`${hit.column}\\` is written unqualified inside a subquery ` +\n `over ${hit.inner}, compared against \\`${hit.comparedTo}\\`. Both ${hit.inner} and ` +\n `${policy.schema}.${policy.table} have a column named \\`${hit.column}\\`, and Postgres ` +\n `resolves the bare name against the innermost scope that has it — so it binds to ` +\n `${hit.inner}.${hit.column}, not to the outer row. If the intent was to correlate the ` +\n `subquery with the row being checked, that correlation is not happening.\\n\\n` +\n `Note that \\`pg_policies\\` shows Postgres's own re-rendering of the policy, which usually ` +\n `re-qualifies column references. A match here means the ambiguity survived that rewrite, ` +\n `so it is strong evidence — but the absence of a match on other policies is not proof ` +\n `that they are unambiguous.`,\n impact:\n `The predicate does not mean what it reads like. Depending on the data it either matches ` +\n `far more rows than intended — exposing other users' or tenants' rows to anyone the policy ` +\n `applies to — or, if the inner comparison is never satisfiable, matches none, and the table ` +\n `silently returns empty results.`,\n fix:\n `-- Qualify every reference so the binding is explicit:\\n` +\n `ALTER POLICY ${qi(policy.name)} ON ${qrel(policy.schema, policy.table)}\\n` +\n ` ${clause === \"USING\" ? \"USING\" : \"WITH CHECK\"} (EXISTS (\\n` +\n ` SELECT 1 FROM ${hit.inner}\\n` +\n ` WHERE ${hit.inner}.${hit.comparedTo.includes(\".\") ? hit.comparedTo.split(\".\").pop() : hit.comparedTo}\\n` +\n ` = ${policy.table}.${hit.column}\\n` +\n ` ));\\n` +\n `-- Verify the intended direction first — this rewrite assumes the outer row was meant.`\n });\n}\n","/**\n * The check registry.\n *\n * Check ids are a public API: they appear in `--skip`, in CI baselines and in\n * people's documentation, so renaming one is a breaking change. Adding a check\n * is not, which is why the list is ordered by severity rather than alphabetically\n * — it is read by humans deciding what to look at first.\n */\nimport type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { anonymousWriteAllowed } from \"./anonymous-write-allowed\";\nimport { currentSettingThrows } from \"./current-setting-throws\";\nimport { grantToPublic } from \"./grant-to-public\";\nimport { junctionTableUnprotected } from \"./junction-table-unprotected\";\nimport { matviewBypassesRls } from \"./matview-bypasses-rls\";\nimport { policyAlwaysTrue } from \"./policy-always-true\";\nimport { policyAnonymousTautology } from \"./policy-anonymous-tautology\";\nimport { policyRoleUnreachable } from \"./policy-role-unreachable\";\nimport { rlsDisabled } from \"./rls-disabled\";\nimport { rlsEnabledNoPolicies } from \"./rls-enabled-no-policies\";\nimport { rlsEnabledNotForced } from \"./rls-enabled-not-forced\";\nimport { securityDefinerMutableSearchPath } from \"./security-definer-mutable-search-path\";\nimport { unqualifiedColumnInSubquery } from \"./unqualified-column-in-subquery\";\nimport { viewBypassesRls } from \"./view-bypasses-rls\";\nimport { SEVERITY_ORDER, relationAt } from \"./util\";\n\nexport const CHECKS: Check[] = [\n rlsDisabled,\n policyAlwaysTrue,\n policyAnonymousTautology,\n viewBypassesRls,\n matviewBypassesRls,\n anonymousWriteAllowed,\n unqualifiedColumnInSubquery,\n junctionTableUnprotected,\n rlsEnabledNotForced,\n rlsEnabledNoPolicies,\n policyRoleUnreachable,\n grantToPublic,\n securityDefinerMutableSearchPath,\n currentSettingThrows\n];\n\nexport interface RunOptions {\n /** Run only these check ids. */\n only?: string[];\n /** Skip these check ids. */\n skip?: string[];\n}\n\n/**\n * Run every selected check and return the findings, worst first.\n *\n * Never throws. A check that blows up on some shape of catalog nobody\n * anticipated degrades to a check that found nothing — reporting thirteen\n * findings is strictly better than reporting a crash, and this is a tool people\n * point at production and expect to be inert.\n */\nexport function runChecks(snapshot: DbSnapshot, opts: RunOptions = {}): Finding[] {\n const only = opts.only && opts.only.length > 0 ? new Set(opts.only) : null;\n const skip = new Set(opts.skip ?? []);\n\n const findings: Finding[] = [];\n for (const check of CHECKS) {\n if (only && !only.has(check.id)) continue;\n if (skip.has(check.id)) continue;\n\n try {\n findings.push(...check.run(snapshot));\n } catch {\n // Deliberately silent: `report.ts` owns what the user sees, and a\n // check that failed produced no findings, which is what the caller\n // is told by its absence.\n }\n }\n\n return sortFindings(snapshot, findings);\n}\n\n/**\n * Worst first, then biggest blast radius first.\n *\n * `estimatedRows` orders equally-severe findings so the table with a million\n * rows is read before the one with four. It never influences severity — a\n * never-analyzed table reports `reltuples = -1` and would otherwise be\n * systematically buried.\n */\nexport function sortFindings(snapshot: DbSnapshot, findings: Finding[]): Finding[] {\n const rows = (f: Finding): number => {\n if (!f.target.table) return -1;\n return relationAt(snapshot, f.target.schema, f.target.table)?.estimatedRows ?? -1;\n };\n\n return [...findings].sort((a, b) => {\n const bySeverity = SEVERITY_ORDER.indexOf(b.severity) - SEVERITY_ORDER.indexOf(a.severity);\n if (bySeverity !== 0) return bySeverity;\n\n const byRows = rows(b) - rows(a);\n if (byRows !== 0) return byRows;\n\n return (\n a.target.schema.localeCompare(b.target.schema) ||\n (a.target.table ?? \"\").localeCompare(b.target.table ?? \"\") ||\n a.id.localeCompare(b.id) ||\n a.title.localeCompare(b.title)\n );\n });\n}\n","/**\n * Read the catalogs into a {@link DbSnapshot}.\n *\n * This is the only part of the tool that talks to a database, and it is written\n * on the assumption that it will be pointed at production by someone who has\n * never heard of us. So:\n *\n * - The session is opened `default_transaction_read_only` and the work runs\n * inside `BEGIN READ ONLY`. Not as a convention — as a guarantee, so that a\n * bug here cannot write.\n * - `statement_timeout` is set before anything else runs, so a scan of a large\n * catalog cannot pin a connection.\n * - Privileges are read from `pg_class.relacl` via `aclexplode`, not from\n * `information_schema.role_table_grants`. The information_schema views only\n * show grants involving roles the caller is a member of, so a non-superuser\n * scanning a Supabase database would not see the grants to `anon` at all —\n * it would report a clean bill of health on a wide-open table.\n * - Every optional query is guarded: a server that lacks a catalog degrades\n * that one fact, not the whole scan.\n */\nimport { Client } from \"pg\";\n\nimport type {\n DbColumn,\n DbForeignKey,\n DbGrant,\n DbPolicy,\n DbRelation,\n DbRole,\n DbRoutine,\n DbSnapshot,\n DbView,\n PolicyCommand,\n PgRelationKind\n} from \"./types\";\n\nexport interface ConnectOptions {\n connectionString: string;\n /** Explicit schema allowlist. Default: every non-system schema. */\n schemas?: string[];\n /** Default 15000. Applied as statement_timeout. */\n statementTimeoutMs?: number;\n /**\n * Roles an untrusted caller arrives as, named by whoever runs the scan.\n *\n * Added to {@link CANDIDATE_EXPOSED_ROLES} rather than replacing it: a\n * union can only widen what the checks consider, and for a security scanner\n * the safe direction of a wrong guess is more findings, not fewer.\n */\n roles?: string[];\n}\n\n/**\n * Facts about *the scan* rather than about the database.\n *\n * `Finding`/`DbSnapshot` have nowhere to put these, and the report needs both:\n * a TLS downgrade must be disclosed, and \"we skipped 11 schemas\" is the\n * difference between a clean result and a misleading one.\n */\nexport interface IntrospectDiagnostics {\n /**\n * True when the server demanded TLS, presented a certificate this machine\n * could not verify, and the scan retried with verification off. Common with\n * managed Postgres behind a pooler — and never done silently.\n */\n tlsVerificationDisabled: boolean;\n /** Schemas that exist but were not scanned, with why. */\n excludedSchemas: { schema: string; reason: \"system\" | \"platform\" | \"not-requested\" }[];\n /** Catalog queries that failed and the fact they cost us. */\n degraded: { what: string; error: string }[];\n /**\n * Roles holding DML on scanned tables that the scan does not treat as\n * exposed, and cannot explain as trusted.\n *\n * The exposed-role set is a list of names this tool happens to know\n * ({@link CANDIDATE_EXPOSED_ROLES}). A database served by a role under any\n * other name — `app_user`, `api`, `hasura` — matches nothing, every check\n * gates on a grant to an exposed role, and the run prints \"No findings\" on\n * a wide-open database. That is a false negative, which is the one failure\n * mode a security scanner must not have quietly.\n *\n * So: anything that holds DML and is neither recognised nor demonstrably\n * privileged is reported, with the `--role` flag as the fix. The scan still\n * cannot know whether such a role is reachable — but it can refuse to\n * imply that it looked.\n */\n unrecognizedGrantees: string[];\n}\n\nexport interface IntrospectResult {\n snapshot: DbSnapshot;\n diagnostics: IntrospectDiagnostics;\n}\n\nconst SYSTEM_SCHEMAS = [\"pg_catalog\", \"information_schema\", \"pg_toast\"];\n\n/**\n * Schemas owned by the platform rather than by the person running the scan.\n *\n * Excluding these is the single biggest signal decision in the tool. Supabase's\n * `auth` and `storage` schemas contain dozens of tables with grants and no RLS\n * by design; a scan that reports forty findings inside them buries the one\n * finding in `public` that actually matters, and the reader concludes the tool\n * is wrong. They are still scannable — `--schema auth` opts back in.\n *\n * `rebase` is deliberately NOT on this list, though it is as much \"ours\" as\n * `auth` is Supabase's. It held refresh-token hashes, TOTP secrets and API-key\n * rows with RLS off and full DML granted to `rebase_user` — the exact shape\n * `rls-disabled` exists to catch — and this exclusion is why no scan ever said\n * so. Those grants are revoked at creation now (`revokeInternalTableSql`), so\n * scanning the schema is quiet; if a new internal table ships without its\n * revoke, `pnpm rls:check` fails instead of a reviewer having to notice. The\n * checks all gate on a grant to a reachable role, so an unexposed table still\n * produces nothing.\n */\nconst PLATFORM_SCHEMAS = [\n // Supabase\n \"auth\", \"storage\", \"realtime\", \"_realtime\", \"vault\", \"extensions\",\n \"graphql\", \"graphql_public\", \"supabase_functions\", \"supabase_migrations\",\n \"net\", \"cron\", \"pgsodium\", \"pgsodium_masks\",\n // Rebase\n \"drizzle\"\n];\n\nconst RELATION_KINDS: Record<string, PgRelationKind> = {\n r: \"table\",\n p: \"partitioned_table\",\n v: \"view\",\n m: \"materialized_view\",\n f: \"foreign_table\"\n};\n\nconst SCANNED_RELKINDS = [\"r\", \"p\", \"v\", \"m\", \"f\"];\n\n/**\n * Roles an untrusted request plausibly arrives as. `service_role` is the\n * trusted bypass.\n *\n * These are recognised *by name*, which is the one place this tool is not\n * framework-agnostic: it knows Supabase's pair, PostgREST's `web_anon` and\n * Rebase's `rebase_user`, and nothing else. Every check gates on a grant to an\n * exposed role, so a database served by `app_user` used to scan clean no\n * matter how open it was.\n *\n * Two things close that hole, and neither is a guess about your schema:\n * `--role` names the role explicitly, and `unrecognizedGranteesFor` reports\n * write-holding roles that matched nothing here, so an unrecognised setup\n * produces a caveat instead of a green checkmark.\n */\nconst CANDIDATE_EXPOSED_ROLES = [\"anon\", \"authenticated\", \"web_anon\", \"rebase_user\"];\n\nexport async function introspect(opts: ConnectOptions): Promise<DbSnapshot> {\n return (await introspectWithDiagnostics(opts)).snapshot;\n}\n\nexport async function introspectWithDiagnostics(opts: ConnectOptions): Promise<IntrospectResult> {\n const diagnostics: IntrospectDiagnostics = {\n tlsVerificationDisabled: false,\n excludedSchemas: [],\n degraded: [],\n unrecognizedGrantees: []\n };\n\n const { client, tlsVerificationDisabled } = await connect(opts.connectionString);\n diagnostics.tlsVerificationDisabled = tlsVerificationDisabled;\n\n try {\n const timeout = Math.max(1000, Math.floor(opts.statementTimeoutMs ?? 15000));\n await client.query(`SET statement_timeout = ${timeout}`);\n await client.query(\"SET default_transaction_read_only = on\");\n await client.query(\"BEGIN READ ONLY\");\n\n try {\n const snapshot = await readSnapshot(client, opts, diagnostics);\n await client.query(\"COMMIT\");\n return { snapshot, diagnostics };\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n throw error;\n }\n } finally {\n await client.end().catch(() => undefined);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Connecting\n// ---------------------------------------------------------------------------\n\ntype SslOption = false | { rejectUnauthorized: boolean };\n\n/**\n * Connect, negotiating TLS the way libpq would.\n *\n * `pg` lets the connection string override an explicit `ssl` option, so the\n * `sslmode` parameter is read and removed before the attempts are built —\n * otherwise the retry would silently reuse the setting that just failed.\n */\nasync function connect(connectionString: string): Promise<{ client: Client; tlsVerificationDisabled: boolean }> {\n const sslmode = readParam(connectionString, \"sslmode\");\n const cleaned = stripParam(connectionString, \"sslmode\");\n\n // Each entry is (ssl option, did we give up verification to get here?).\n let attempts: { ssl: SslOption; downgraded: boolean }[];\n switch (sslmode) {\n case \"disable\":\n attempts = [{ ssl: false, downgraded: false }];\n break;\n case \"require\":\n case \"no-verify\":\n // libpq's `require` encrypts without verifying. Honouring that is not\n // a downgrade — it is what the connection string asked for.\n attempts = [{ ssl: { rejectUnauthorized: false }, downgraded: false }];\n break;\n case \"verify-ca\":\n case \"verify-full\":\n // An explicit request to verify is never quietly relaxed.\n attempts = [{ ssl: { rejectUnauthorized: true }, downgraded: false }];\n break;\n default:\n attempts = [\n { ssl: false, downgraded: false },\n { ssl: { rejectUnauthorized: true }, downgraded: false },\n { ssl: { rejectUnauthorized: false }, downgraded: true }\n ];\n }\n\n let lastError: unknown;\n for (const attempt of attempts) {\n const client = new Client({ connectionString: cleaned, ssl: attempt.ssl });\n try {\n await client.connect();\n return { client, tlsVerificationDisabled: attempt.downgraded };\n } catch (error) {\n lastError = error;\n await client.end().catch(() => undefined);\n if (!isTlsNegotiationError(error)) throw error;\n }\n }\n\n throw lastError;\n}\n\n/** Errors that mean \"try a different TLS posture\", as opposed to a real failure. */\nfunction isTlsNegotiationError(error: unknown): boolean {\n const err = error as { code?: string; message?: string };\n const code = String(err?.code ?? \"\");\n const message = String(err?.message ?? \"\");\n\n if (\n [\n \"DEPTH_ZERO_SELF_SIGNED_CERT\",\n \"SELF_SIGNED_CERT_IN_CHAIN\",\n \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\",\n \"UNABLE_TO_GET_ISSUER_CERT_LOCALLY\",\n \"CERT_HAS_EXPIRED\",\n \"ERR_TLS_CERT_ALTNAME_INVALID\"\n ].includes(code)\n ) {\n return true;\n }\n\n return /SSL|TLS|certificate|self.signed|pg_hba\\.conf entry .*SSL|sslmode/i.test(message);\n}\n\nfunction readParam(connectionString: string, name: string): string | undefined {\n const match = new RegExp(`[?&]${name}=([^&]*)`, \"i\").exec(connectionString);\n return match ? decodeURIComponent(match[1]).toLowerCase() : undefined;\n}\n\nfunction stripParam(connectionString: string, name: string): string {\n return connectionString\n .replace(new RegExp(`([?&])${name}=[^&]*&?`, \"gi\"), \"$1\")\n .replace(/[?&]$/, \"\");\n}\n\n// ---------------------------------------------------------------------------\n// Reading\n// ---------------------------------------------------------------------------\n\ninterface Reader {\n query<R extends Record<string, unknown>>(\n what: string,\n text: string,\n values?: unknown[]\n ): Promise<R[]>;\n}\n\nfunction reader(client: Client, diagnostics: IntrospectDiagnostics): Reader {\n return {\n async query(what, text, values) {\n try {\n const result = await client.query(text, values);\n return result.rows;\n } catch (error) {\n // One catalog we could not read is a degraded fact, not a failed\n // scan — a server missing `pg_policies` should still tell us\n // which tables have RLS off.\n diagnostics.degraded.push({\n what,\n error: error instanceof Error ? error.message : String(error)\n });\n await client.query(\"ROLLBACK\").catch(() => undefined);\n await client.query(\"BEGIN READ ONLY\").catch(() => undefined);\n return [];\n }\n }\n };\n}\n\nasync function readSnapshot(\n client: Client,\n opts: ConnectOptions,\n diagnostics: IntrospectDiagnostics\n): Promise<DbSnapshot> {\n const db = reader(client, diagnostics);\n\n const [server] = await db.query<{\n version_num: string | number;\n version: string;\n role_name: string;\n is_super: boolean | null;\n bypass_rls: boolean | null;\n }>(\n \"server version and current role\",\n `SELECT current_setting('server_version_num') AS version_num,\n current_setting('server_version') AS version,\n current_user AS role_name,\n (SELECT rolsuper FROM pg_roles WHERE rolname = current_user) AS is_super,\n (SELECT rolbypassrls FROM pg_roles WHERE rolname = current_user) AS bypass_rls`\n );\n\n const serverVersionNum = Number(server?.version_num ?? 0);\n const serverVersion = String(server?.version ?? \"unknown\");\n const currentRole = String(server?.role_name ?? \"unknown\");\n\n const allSchemas = (\n await db.query<{ nspname: string }>(\n \"schema list\",\n \"SELECT nspname FROM pg_namespace ORDER BY nspname\"\n )\n ).map((r) => r.nspname);\n\n const schemas = selectSchemas(allSchemas, opts.schemas, diagnostics);\n\n const roles = await readRoles(db);\n const relations = await readRelations(db, schemas);\n const policies = await readPolicies(db, schemas);\n const grants = await readGrants(db, schemas);\n const views = await readViews(db, schemas, serverVersionNum, relations);\n const foreignKeys = await readForeignKeys(db, schemas);\n const routines = await readRoutines(db, schemas);\n\n const exposedRoles = exposedRolesFor(roles, opts.roles);\n diagnostics.unrecognizedGrantees = unrecognizedGranteesFor(\n grants,\n roles,\n relations,\n exposedRoles,\n currentRole\n );\n\n return {\n serverVersionNum,\n serverVersion,\n currentRole,\n scannerIsPrivileged: isPrivileged(currentRole, server, roles, relations),\n schemas,\n exposedRoles,\n platform: detectPlatform(allSchemas, roles),\n relations,\n policies,\n roles,\n grants,\n views,\n foreignKeys,\n routines\n };\n}\n\n/**\n * Which of the database's schemas this scan covers.\n *\n * Exported for its own test: which schemas are in scope is a security decision,\n * not an implementation detail — a schema quietly added to\n * {@link PLATFORM_SCHEMAS} silently stops being checked.\n */\nexport function selectSchemas(\n all: string[],\n requested: string[] | undefined,\n diagnostics: IntrospectDiagnostics\n): string[] {\n const isSystem = (s: string) =>\n SYSTEM_SCHEMAS.includes(s) || /^pg_temp(_\\d+)?$/.test(s) || /^pg_toast_temp(_\\d+)?$/.test(s);\n\n if (requested && requested.length > 0) {\n const wanted = new Set(requested);\n const kept: string[] = [];\n for (const schema of all) {\n if (wanted.has(schema)) kept.push(schema);\n else diagnostics.excludedSchemas.push({ schema, reason: \"not-requested\" });\n }\n return kept;\n }\n\n const kept: string[] = [];\n for (const schema of all) {\n if (isSystem(schema)) {\n diagnostics.excludedSchemas.push({ schema, reason: \"system\" });\n continue;\n }\n // `public` is never platform-internal, whatever it is named alongside.\n if (schema !== \"public\" && PLATFORM_SCHEMAS.includes(schema)) {\n diagnostics.excludedSchemas.push({ schema, reason: \"platform\" });\n continue;\n }\n kept.push(schema);\n }\n return kept;\n}\n\nasync function readRoles(db: Reader): Promise<DbRole[]> {\n const rows = await db.query<{\n rolname: string;\n rolcanlogin: boolean;\n rolsuper: boolean;\n rolbypassrls: boolean | null;\n }>(\n \"roles\",\n \"SELECT rolname, rolcanlogin, rolsuper, rolbypassrls FROM pg_roles ORDER BY rolname\"\n );\n\n const edges = await db.query<{ role: string; member: string }>(\n \"role memberships\",\n `SELECT r.rolname AS role, m.rolname AS member\n FROM pg_auth_members am\n JOIN pg_roles r ON r.oid = am.roleid\n JOIN pg_roles m ON m.oid = am.member`\n );\n\n // Direct memberships, then a fixpoint. A grant to a role that `anon` inherits\n // is a grant to `anon`; a checker that compares grantee names literally\n // reports \"clean\" on exactly the databases whose owners took the most care.\n const direct = new Map<string, Set<string>>();\n for (const edge of edges) {\n if (!direct.has(edge.member)) direct.set(edge.member, new Set());\n direct.get(edge.member)!.add(edge.role);\n }\n\n const closure = new Map<string, Set<string>>();\n for (const role of rows) closure.set(role.rolname, new Set(direct.get(role.rolname) ?? []));\n\n let changed = true;\n while (changed) {\n changed = false;\n for (const [member, parents] of closure) {\n for (const parent of [...parents]) {\n for (const grandparent of closure.get(parent) ?? []) {\n if (grandparent === member || parents.has(grandparent)) continue;\n parents.add(grandparent);\n changed = true;\n }\n }\n }\n }\n\n return rows.map((r) => ({\n name: r.rolname,\n canLogin: Boolean(r.rolcanlogin),\n superuser: Boolean(r.rolsuper),\n bypassRls: Boolean(r.rolbypassrls),\n memberOf: [...(closure.get(r.rolname) ?? [])].sort()\n }));\n}\n\nasync function readRelations(db: Reader, schemas: string[]): Promise<DbRelation[]> {\n const rows = await db.query<{\n schema: string;\n name: string;\n kind: string;\n owner: string;\n rls_enabled: boolean;\n rls_forced: boolean;\n estimated_rows: string | number;\n }>(\n \"relations\",\n `SELECT n.nspname AS schema, c.relname AS name, c.relkind::text AS kind,\n pg_get_userbyid(c.relowner) AS owner,\n c.relrowsecurity AS rls_enabled,\n c.relforcerowsecurity AS rls_forced,\n c.reltuples::float8 AS estimated_rows\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE c.relkind::text = ANY($2) AND n.nspname = ANY($1)\n ORDER BY n.nspname, c.relname`,\n [schemas, SCANNED_RELKINDS]\n );\n\n const columns = await readColumns(db, schemas);\n\n return rows.map((r) => ({\n schema: r.schema,\n name: r.name,\n kind: RELATION_KINDS[r.kind] ?? \"table\",\n owner: r.owner,\n // Postgres reports these false for views; do not infer anything from it.\n rlsEnabled: Boolean(r.rls_enabled),\n rlsForced: Boolean(r.rls_forced),\n columns: columns.get(`${r.schema}.${r.name}`) ?? [],\n estimatedRows: Number(r.estimated_rows ?? -1)\n }));\n}\n\nasync function readColumns(db: Reader, schemas: string[]): Promise<Map<string, DbColumn[]>> {\n const rows = await db.query<{\n schema: string;\n table_name: string;\n name: string;\n type: string;\n not_null: boolean;\n is_pk: boolean | null;\n }>(\n \"columns\",\n `SELECT n.nspname AS schema, c.relname AS table_name, a.attname AS name,\n format_type(a.atttypid, a.atttypmod) AS type,\n a.attnotnull AS not_null,\n EXISTS (\n SELECT 1 FROM pg_index i\n WHERE i.indrelid = c.oid AND i.indisprimary AND a.attnum = ANY(i.indkey)\n ) AS is_pk\n FROM pg_attribute a\n JOIN pg_class c ON c.oid = a.attrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE a.attnum > 0 AND NOT a.attisdropped\n AND c.relkind::text = ANY($2) AND n.nspname = ANY($1)\n ORDER BY n.nspname, c.relname, a.attnum`,\n [schemas, SCANNED_RELKINDS]\n );\n\n const map = new Map<string, DbColumn[]>();\n for (const row of rows) {\n const key = `${row.schema}.${row.table_name}`;\n if (!map.has(key)) map.set(key, []);\n map.get(key)!.push({\n name: row.name,\n type: row.type,\n notNull: Boolean(row.not_null),\n isPrimaryKey: Boolean(row.is_pk)\n });\n }\n return map;\n}\n\nasync function readPolicies(db: Reader, schemas: string[]): Promise<DbPolicy[]> {\n const rows = await db.query<{\n schemaname: string;\n tablename: string;\n policyname: string;\n permissive: string;\n roles: string[] | string;\n cmd: string;\n qual: string | null;\n with_check: string | null;\n }>(\n \"policies\",\n `SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check\n FROM pg_policies\n WHERE schemaname = ANY($1)\n ORDER BY schemaname, tablename, policyname`,\n [schemas]\n );\n\n return rows.map((r) => ({\n schema: r.schemaname,\n table: r.tablename,\n name: r.policyname,\n permissive: String(r.permissive ?? \"PERMISSIVE\").toUpperCase() !== \"RESTRICTIVE\",\n // node-postgres yields text[] as an array; some drivers hand back \"{a,b}\".\n roles: Array.isArray(r.roles)\n ? r.roles\n : String(r.roles ?? \"\")\n .replace(/^\\{|\\}$/g, \"\")\n .split(\",\")\n .map((s) => s.trim().replace(/^\"|\"$/g, \"\"))\n .filter(Boolean),\n command: (String(r.cmd ?? \"ALL\").toUpperCase() as PolicyCommand) ?? \"ALL\",\n using: r.qual,\n withCheck: r.with_check\n }));\n}\n\nasync function readGrants(db: Reader, schemas: string[]): Promise<DbGrant[]> {\n const rows = await db.query<{\n schema: string;\n table_name: string;\n grantee: string;\n privilege_type: string;\n }>(\n \"table privileges\",\n `SELECT n.nspname AS schema, c.relname AS table_name,\n CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE pg_get_userbyid(a.grantee) END AS grantee,\n a.privilege_type\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n CROSS JOIN LATERAL aclexplode(COALESCE(c.relacl, acldefault('r'::\"char\", c.relowner))) a\n WHERE c.relkind::text = ANY($2) AND n.nspname = ANY($1)`,\n [schemas, SCANNED_RELKINDS]\n );\n\n const known = new Set([\"SELECT\", \"INSERT\", \"UPDATE\", \"DELETE\", \"TRUNCATE\", \"REFERENCES\", \"TRIGGER\"]);\n const map = new Map<string, DbGrant>();\n for (const row of rows) {\n // PG17 added MAINTAIN; anything unknown is not exposure and is dropped.\n if (!known.has(row.privilege_type)) continue;\n const key = `${row.schema}.${row.table_name}.${row.grantee}`;\n if (!map.has(key)) {\n map.set(key, {\n schema: row.schema,\n table: row.table_name,\n grantee: row.grantee,\n privileges: []\n });\n }\n map.get(key)!.privileges.push(row.privilege_type as DbGrant[\"privileges\"][number]);\n }\n return [...map.values()];\n}\n\nasync function readViews(\n db: Reader,\n schemas: string[],\n serverVersionNum: number,\n relations: DbRelation[]\n): Promise<DbView[]> {\n const rows = await db.query<{\n schema: string;\n name: string;\n owner: string;\n kind: string;\n reloptions: string[] | null;\n }>(\n \"views\",\n `SELECT n.nspname AS schema, c.relname AS name, pg_get_userbyid(c.relowner) AS owner,\n c.relkind::text AS kind, c.reloptions\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE c.relkind = ANY(ARRAY['v','m']) AND n.nspname = ANY($1)\n ORDER BY n.nspname, c.relname`,\n [schemas]\n );\n\n const dependencies = await readViewDependencies(db, schemas, relations);\n\n return rows.map((r) => ({\n schema: r.schema,\n name: r.name,\n owner: r.owner,\n // The option does not exist before PG 15, and never applies to a\n // materialized view. `null` means \"the question does not apply here\",\n // which is what the dependent check keys its wording off.\n securityInvoker:\n serverVersionNum < 150000 || r.kind === \"m\"\n ? null\n : (r.reloptions ?? []).some((o) => /^security_invoker=(true|on)$/i.test(o)),\n dependsOn: dependencies.get(`${r.schema}.${r.name}`) ?? []\n }));\n}\n\n/**\n * Base relations of each view, resolved through `pg_depend` → `pg_rewrite`.\n *\n * Deliberately transitive: a view over a view over a protected table is still a\n * path to that table, and the intermediate view's own options do not save it\n * (the outer view still executes as its own owner). Base tables are not filtered\n * to the scanned schemas — a view in `public` reading a hidden schema is exactly\n * the case worth catching.\n */\nasync function readViewDependencies(\n db: Reader,\n schemas: string[],\n relations: DbRelation[]\n): Promise<Map<string, { schema: string; table: string }[]>> {\n const rows = await db.query<{\n view_schema: string;\n view_name: string;\n ref_schema: string;\n ref_name: string;\n }>(\n \"view dependencies\",\n `SELECT DISTINCT dn.nspname AS view_schema, dc.relname AS view_name,\n rn.nspname AS ref_schema, rc.relname AS ref_name\n FROM pg_depend d\n JOIN pg_rewrite rw ON rw.oid = d.objid\n JOIN pg_class dc ON dc.oid = rw.ev_class\n JOIN pg_namespace dn ON dn.oid = dc.relnamespace\n JOIN pg_class rc ON rc.oid = d.refobjid\n JOIN pg_namespace rn ON rn.oid = rc.relnamespace\n WHERE d.classid = 'pg_rewrite'::regclass\n AND d.refclassid = 'pg_class'::regclass\n AND dc.oid <> rc.oid\n AND dc.relkind = ANY(ARRAY['v','m'])\n AND rc.relkind::text = ANY($2)\n AND dn.nspname = ANY($1)`,\n [schemas, SCANNED_RELKINDS]\n );\n\n const direct = new Map<string, Set<string>>();\n for (const row of rows) {\n const key = `${row.view_schema}.${row.view_name}`;\n if (!direct.has(key)) direct.set(key, new Set());\n direct.get(key)!.add(`${row.ref_schema}.${row.ref_name}`);\n }\n\n const isView = new Set(\n relations\n .filter((r) => r.kind === \"view\" || r.kind === \"materialized_view\")\n .map((r) => `${r.schema}.${r.name}`)\n );\n\n const out = new Map<string, { schema: string; table: string }[]>();\n for (const key of direct.keys()) {\n const seen = new Set<string>();\n const queue = [...(direct.get(key) ?? [])];\n while (queue.length > 0) {\n const next = queue.shift()!;\n if (next === key || seen.has(next)) continue;\n seen.add(next);\n if (isView.has(next)) queue.push(...(direct.get(next) ?? []));\n }\n out.set(\n key,\n [...seen].map((ref) => {\n const dot = ref.indexOf(\".\");\n return { schema: ref.slice(0, dot), table: ref.slice(dot + 1) };\n })\n );\n }\n return out;\n}\n\nasync function readForeignKeys(db: Reader, schemas: string[]): Promise<DbForeignKey[]> {\n const rows = await db.query<{\n schema: string;\n table_name: string;\n ref_schema: string;\n ref_table: string;\n columns: string[] | null;\n ref_columns: string[] | null;\n }>(\n \"foreign keys\",\n `SELECT n.nspname AS schema, c.relname AS table_name,\n rn.nspname AS ref_schema, rc.relname AS ref_table,\n -- The ::text cast is load-bearing: attname is of type name, so\n -- array_agg yields name[] (OID 1003), which node-pg has no parser\n -- for and hands back as the raw literal \"{post_id}\". The declared\n -- string[] then lies, every column comparison silently fails to\n -- match, and junction-table-unprotected could never fire.\n (SELECT array_agg(att.attname::text ORDER BY k.ord)\n FROM unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord)\n JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = k.attnum) AS columns,\n -- The ::text cast is load-bearing: attname is of type name, so\n -- array_agg yields name[] (OID 1003), which node-pg has no parser\n -- for and hands back as the raw literal \"{post_id}\". The declared\n -- string[] then lies, every column comparison silently fails to\n -- match, and junction-table-unprotected could never fire.\n (SELECT array_agg(att.attname::text ORDER BY k.ord)\n FROM unnest(con.confkey) WITH ORDINALITY AS k(attnum, ord)\n JOIN pg_attribute att ON att.attrelid = con.confrelid AND att.attnum = k.attnum) AS ref_columns\n FROM pg_constraint con\n JOIN pg_class c ON c.oid = con.conrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n JOIN pg_class rc ON rc.oid = con.confrelid\n JOIN pg_namespace rn ON rn.oid = rc.relnamespace\n WHERE con.contype = 'f' AND n.nspname = ANY($1)\n ORDER BY n.nspname, c.relname, con.conname`,\n [schemas]\n );\n\n return rows.map((r) => ({\n schema: r.schema,\n table: r.table_name,\n columns: r.columns ?? [],\n refSchema: r.ref_schema,\n refTable: r.ref_table,\n refColumns: r.ref_columns ?? []\n }));\n}\n\nasync function readRoutines(db: Reader, schemas: string[]): Promise<DbRoutine[]> {\n const rows = await db.query<{\n schema: string;\n name: string;\n owner: string;\n security_definer: boolean;\n proconfig: string[] | null;\n }>(\n \"routines\",\n `SELECT n.nspname AS schema, p.proname AS name, pg_get_userbyid(p.proowner) AS owner,\n p.prosecdef AS security_definer, p.proconfig\n FROM pg_proc p\n JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = ANY($1) AND p.prokind = ANY(ARRAY['f','p'])\n ORDER BY n.nspname, p.proname`,\n [schemas]\n );\n\n return rows.map((r) => ({\n schema: r.schema,\n name: r.name,\n owner: r.owner,\n securityDefiner: Boolean(r.security_definer),\n mutableSearchPath: !(r.proconfig ?? []).some((c) => /^search_path=/i.test(c))\n }));\n}\n\n// ---------------------------------------------------------------------------\n// Derived facts\n// ---------------------------------------------------------------------------\n\n/**\n * Can the scanning role see past RLS?\n *\n * Reported rather than corrected. A privileged scanner reads the true catalog,\n * which is what makes the findings accurate — but every finding then describes\n * what some *other* role gets, and the report has to say so or a reader will\n * assume the tool tested its own access.\n */\nfunction isPrivileged(\n currentRole: string,\n server: { is_super?: boolean | null; bypass_rls?: boolean | null } | undefined,\n roles: DbRole[],\n relations: DbRelation[]\n): boolean {\n if (server?.is_super || server?.bypass_rls) return true;\n\n const me = roles.find((r) => r.name === currentRole);\n if (me?.superuser || me?.bypassRls) return true;\n\n // Membership in a privileged role is the same power, one SET ROLE away.\n const inherited = new Set(me?.memberOf ?? []);\n if (roles.some((r) => inherited.has(r.name) && (r.superuser || r.bypassRls))) return true;\n\n // Ownership exempts from non-FORCE RLS on the owned tables.\n return relations.some((r) => r.owner === currentRole || inherited.has(r.owner));\n}\n\n/**\n * Which platform is in front of this database.\n *\n * Cheap and total: it reads the schema and role lists that were already\n * fetched, so a database missing any of these catalogs falls through to\n * `unknown` rather than erroring. The answer only ever changes the *wording* and\n * severity of `policy-anonymous-tautology`, so guessing wrong degrades a message\n * rather than inventing or hiding a finding.\n */\nfunction detectPlatform(allSchemas: string[], roles: DbRole[]): DbSnapshot[\"platform\"] {\n const schema = new Set(allSchemas);\n const role = new Set(roles.map((r) => r.name));\n\n if (\n schema.has(\"auth\") &&\n schema.has(\"storage\") &&\n (role.has(\"anon\") || role.has(\"authenticated\") || role.has(\"service_role\"))\n ) {\n return \"supabase\";\n }\n\n if (role.has(\"rebase_user\") || schema.has(\"rebase\")) return \"rebase\";\n if (role.has(\"web_anon\")) return \"postgrest\";\n if (role.has(\"neon_superuser\") || schema.has(\"neon\")) return \"neon\";\n\n return \"unknown\";\n}\n\n/**\n * Roles an untrusted caller plausibly arrives as.\n *\n * `service_role` is excluded on purpose. It exists, and it is in `roles`, but it\n * is the *trusted* bypass identity: treating it as exposed would flag every\n * table in every Supabase project as critical, which is both useless and wrong.\n */\nfunction exposedRolesFor(roles: DbRole[], explicit?: string[]): string[] {\n const present = new Set(roles.map((r) => r.name));\n const named = [...CANDIDATE_EXPOSED_ROLES, ...(explicit ?? [])];\n // A role named with `--role` that does not exist is dropped here rather\n // than carried: `effectivePrivileges` would find nothing for it anyway, and\n // a phantom entry in `exposedRoles` reads like the scan covered something\n // it did not. `unrecognizedGranteesFor` reports the mismatch instead.\n return [\"PUBLIC\", ...new Set(named.filter((r) => present.has(r)))];\n}\n\n/**\n * Roles that hold DML on a scanned table and are neither exposed nor trusted.\n *\n * \"Trusted\" is deliberately generous — every role this can explain is one the\n * user does not have to think about. A grantee is explained when it is a\n * superuser, holds BYPASSRLS (RLS is a no-op for it, so no policy this tool\n * checks would constrain it anyway), owns the relation, is the role the scan\n * connected as, or is platform bookkeeping (`pg_*`, `cloudsql*`, Supabase's\n * `service_role`, which is the documented trusted bypass).\n *\n * What is left is the interesting set: a named role, holding write privileges,\n * that this tool has no opinion about. It may be a service account nothing can\n * authenticate as — `rebase_user` is NOLOGIN by design — or it may be exactly\n * the role the API connects as. The scan cannot tell from the catalog, so it\n * names them and lets the reader decide.\n */\nfunction unrecognizedGranteesFor(\n grants: DbGrant[],\n roles: DbRole[],\n relations: DbRelation[],\n exposed: string[],\n currentRole: string\n): string[] {\n const exposedSet = new Set(exposed.map((r) => r.toLowerCase()));\n const byName = new Map(roles.map((r) => [r.name.toLowerCase(), r]));\n const owners = new Set(relations.map((r) => r.owner.toLowerCase()));\n const writeable = new Set([\"INSERT\", \"UPDATE\", \"DELETE\"]);\n\n const out = new Set<string>();\n for (const grant of grants) {\n const name = grant.grantee;\n const key = name.toLowerCase();\n\n if (exposedSet.has(key) || isPublicName(name)) continue;\n if (key === currentRole.toLowerCase()) continue;\n if (owners.has(key)) continue;\n if (key.startsWith(\"pg_\") || key.startsWith(\"cloudsql\") || key === \"service_role\") continue;\n\n const role = byName.get(key);\n if (role?.superuser || role?.bypassRls) continue;\n\n // SELECT alone on a reference table is a normal, deliberate grant. It is\n // the write privileges that make an unrecognised role worth a sentence.\n if (!grant.privileges.some((p) => writeable.has(p))) continue;\n\n out.add(name);\n }\n return [...out].sort();\n}\n\nfunction isPublicName(name: string): boolean {\n return name.toUpperCase() === \"PUBLIC\";\n}\n","/**\n * Connection-string redaction.\n *\n * This tool is pointed at production databases by people who have never run it\n * before, and its output ends up in CI logs, GitHub issues and screenshots. The\n * credential must therefore never survive into *any* output — not the report,\n * not an error, not a debug line. Everything printed goes through here.\n *\n * The parser is deliberately hand-rolled rather than `new URL()`: real-world\n * Postgres URLs carry passwords with unencoded `@`, `:` and `/` in them, which\n * `URL` either rejects or silently mis-splits. Getting that wrong once means\n * printing a password, so the splitting rules are explicit:\n *\n * - the authority ends at the first `/`, `?` or `#` after `://`\n * - the userinfo ends at the LAST `@` in the authority\n * - the user ends at the FIRST `:` in the userinfo\n *\n * A libpq keyword string (`host=... password=...`) is understood too, because\n * `psql \"$PGSTRING\"` users paste those.\n */\n\n/** What replaces every credential. */\nexport const REDACTED = \"***\";\n\nexport interface ConnectionTarget {\n /** `postgres` / `postgresql`, or `postgresql` for keyword strings. */\n scheme: string;\n /** Hostname only, no port. `localhost` when the string omits it. */\n host: string;\n port: number | null;\n /** Database name, empty string when the string omits it. */\n database: string;\n user: string | null;\n password: string | null;\n}\n\n/** Query parameters worth keeping in the redacted form: informative, never secret. */\nconst SAFE_PARAMS = new Set([\"sslmode\", \"application_name\", \"connect_timeout\", \"target_session_attrs\"]);\n\nfunction decode(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\nfunction splitHostPort(hostport: string): { host: string; port: number | null } {\n // IPv6 literals are bracketed: [::1]:5432\n if (hostport.startsWith(\"[\")) {\n const close = hostport.indexOf(\"]\");\n if (close !== -1) {\n const host = hostport.slice(0, close + 1);\n const rest = hostport.slice(close + 1);\n const port = rest.startsWith(\":\") ? Number.parseInt(rest.slice(1), 10) : NaN;\n\n return { host, port: Number.isFinite(port) ? port : null };\n }\n }\n\n const colon = hostport.lastIndexOf(\":\");\n if (colon === -1) return { host: hostport, port: null };\n\n const port = Number.parseInt(hostport.slice(colon + 1), 10);\n if (!Number.isFinite(port)) return { host: hostport, port: null };\n\n return { host: hostport.slice(0, colon), port };\n}\n\nfunction parseKeywordString(raw: string): ConnectionTarget | null {\n // host=localhost port=5432 dbname=app user=me password='s3 cret'\n const pairs = raw.match(/(\\w+)\\s*=\\s*('(?:[^']|'')*'|[^\\s]+)/g);\n if (!pairs || pairs.length === 0) return null;\n\n const map = new Map<string, string>();\n for (const pair of pairs) {\n const eq = pair.indexOf(\"=\");\n const key = pair.slice(0, eq).trim().toLowerCase();\n let value = pair.slice(eq + 1).trim();\n if (value.startsWith(\"'\") && value.endsWith(\"'\") && value.length >= 2) {\n value = value.slice(1, -1).replace(/''/g, \"'\");\n }\n map.set(key, value);\n }\n\n if (!map.has(\"host\") && !map.has(\"dbname\") && !map.has(\"user\")) return null;\n\n const port = map.has(\"port\") ? Number.parseInt(map.get(\"port\")!, 10) : NaN;\n\n return {\n scheme: \"postgresql\",\n host: map.get(\"host\") ?? \"localhost\",\n port: Number.isFinite(port) ? port : null,\n database: map.get(\"dbname\") ?? \"\",\n user: map.get(\"user\") ?? null,\n password: map.get(\"password\") ?? null\n };\n}\n\n/**\n * A hostname, an IPv4 literal, a bracketed IPv6 literal, or a Unix socket path.\n * Anything else means the split went wrong.\n */\nconst PLAUSIBLE_HOST = /^(\\[[0-9A-Fa-f:.]+\\]|[A-Za-z0-9._-]+|\\/[^\\s?#@]*)$/;\n\n/** A database name in a URL: no delimiters, because they were not encoded. */\nconst PLAUSIBLE_DATABASE = /^[^\\s:@?#/]*$/;\n\n/**\n * Split a connection string into its parts. Returns `null` when the input is\n * not recognisably a connection string — callers must treat that as \"unknown\",\n * never as \"safe to print\".\n */\nexport function parseConnectionString(raw: string): ConnectionTarget | null {\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n\n const schemeMatch = /^([a-zA-Z][a-zA-Z0-9+.-]*):\\/\\//.exec(trimmed);\n if (!schemeMatch) return parseKeywordString(trimmed);\n\n const scheme = schemeMatch[1];\n const afterScheme = trimmed.slice(schemeMatch[0].length);\n\n const pathStart = afterScheme.search(/[/?#]/);\n const authority = pathStart === -1 ? afterScheme : afterScheme.slice(0, pathStart);\n const remainder = pathStart === -1 ? \"\" : afterScheme.slice(pathStart);\n\n let user: string | null = null;\n let password: string | null = null;\n let hostport = authority;\n\n const at = authority.lastIndexOf(\"@\");\n if (at !== -1) {\n const userinfo = authority.slice(0, at);\n hostport = authority.slice(at + 1);\n const colon = userinfo.indexOf(\":\");\n if (colon === -1) {\n user = decode(userinfo);\n } else {\n user = decode(userinfo.slice(0, colon));\n password = decode(userinfo.slice(colon + 1));\n }\n }\n\n const { host, port } = splitHostPort(hostport);\n\n let database = \"\";\n if (remainder.startsWith(\"/\")) {\n const end = remainder.search(/[?#]/);\n database = decode(end === -1 ? remainder.slice(1) : remainder.slice(1, end));\n }\n\n // A password can also arrive as a query parameter, which is why the\n // redacted form only ever re-emits an allowlist of parameters.\n const queryStart = remainder.search(/[?#]/);\n if (queryStart !== -1 && password === null) {\n const params = new URLSearchParams(remainder.slice(queryStart + 1));\n const fromQuery = params.get(\"password\");\n if (fromQuery) password = fromQuery;\n if (user === null && params.get(\"user\")) user = params.get(\"user\");\n }\n\n // A password containing an unencoded `/`, `?` or `@` makes the authority\n // boundary ambiguous, and the split lands somewhere inside the credential.\n // The tell is a host or database that does not look like one — at which\n // point the only safe answer is \"unparseable\", because printing the pieces\n // would print fragments of the password. libpq is no happier with these\n // than we are; the fix on the user's side is percent-encoding.\n const normalisedHost = host.length > 0 ? host : \"localhost\";\n if (!PLAUSIBLE_HOST.test(normalisedHost) || !PLAUSIBLE_DATABASE.test(database)) return null;\n\n return {\n scheme,\n host: normalisedHost,\n port,\n database,\n user,\n password\n };\n}\n\n/** `host:port` if a port is known, otherwise just the host. Never a credential. */\nexport function formatEndpoint(target: ConnectionTarget | null): string {\n if (!target) return \"unknown host\";\n\n return target.port === null ? target.host : `${target.host}:${target.port}`;\n}\n\n/**\n * The display form of a connection string: scheme, host, port and database\n * kept; user and password replaced. Safe to print anywhere.\n */\nexport function redactConnectionString(raw: string): string {\n const target = parseConnectionString(raw);\n if (!target) return \"<connection string>\";\n\n const credential = target.user !== null || target.password !== null ? `${REDACTED}:${REDACTED}@` : \"\";\n const endpoint = formatEndpoint(target);\n const database = target.database.length > 0 ? `/${target.database}` : \"\";\n\n let query = \"\";\n const queryStart = raw.indexOf(\"?\");\n if (queryStart !== -1) {\n const kept: string[] = [];\n for (const [key, value] of new URLSearchParams(raw.slice(queryStart + 1))) {\n if (SAFE_PARAMS.has(key.toLowerCase())) kept.push(`${key}=${value}`);\n }\n if (kept.length > 0) query = `?${kept.join(\"&\")}`;\n }\n\n return `${target.scheme}://${credential}${endpoint}${database}${query}`;\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Scrub credentials out of arbitrary text — driver error messages, stack\n * traces, anything we did not author. Two passes:\n *\n * 1. exact: the connection string we were given, and its password, wherever\n * they appear, however they were quoted;\n * 2. generic: any `scheme://user:pass@host` shaped substring, which catches\n * strings we were never told about (a `pg` error quoting `PGPASSWORD`, a\n * nested cause carrying another URL).\n *\n * The generic pass is what makes this safe by default: forgetting to pass\n * `connectionString` degrades the redaction, it does not disable it.\n */\nexport function redactSecrets(text: string, connectionString?: string): string {\n let out = text;\n\n if (connectionString && connectionString.trim().length > 0) {\n const raw = connectionString.trim();\n out = out.split(raw).join(redactConnectionString(raw));\n\n const target = parseConnectionString(raw);\n\n // `password authentication failed for user \"postgres\"` — the driver\n // quotes the role name back at us. Only the quoted form is replaced,\n // because a bare global replace of a name like `app` would shred the\n // sentence around it.\n if (target?.user && target.user.length > 0) {\n out = out.replace(new RegExp(`\"${escapeRegExp(target.user)}\"`, \"g\"), `\"${REDACTED}\"`);\n }\n\n // Short passwords are left alone: a global replace of \"a\" would shred\n // the message and tell an attacker more than it hides.\n if (target?.password && target.password.length >= 3) {\n out = out.replace(new RegExp(escapeRegExp(target.password), \"g\"), REDACTED);\n const encoded = encodeURIComponent(target.password);\n if (encoded !== target.password) {\n out = out.replace(new RegExp(escapeRegExp(encoded), \"g\"), REDACTED);\n }\n }\n }\n\n // scheme://userinfo@host — the userinfo is always a credential.\n out = out.replace(\n /\\b([a-zA-Z][a-zA-Z0-9+.-]*):\\/\\/([^\\s\"'<>]*?)@/g,\n (_match, scheme: string) => `${scheme}://${REDACTED}:${REDACTED}@`\n );\n\n // libpq keyword form, in whatever quoting the message used.\n out = out.replace(/\\bpassword\\s*=\\s*('(?:[^']|'')*'|\"[^\"]*\"|\\S+)/gi, `password=${REDACTED}`);\n\n return out;\n}\n\n/**\n * Is this endpoint a local proxy or tunnel rather than the database itself?\n *\n * Matters for diagnosis: cloud-sql-proxy, an SSH -L forward and a local pooler\n * all accept the TCP connection and then hang up when *their* upstream auth\n * fails, which reaches the client as a bare ECONNRESET. Advice about TLS is\n * wrong there — the proxy terminates TLS itself, and the real cause is only in\n * its log.\n *\n * Takes the display form produced by `formatEndpoint`, so `host`, `host:port`\n * and `[::1]:5432` all work.\n */\nexport function isLoopbackEndpoint(endpoint: string): boolean {\n const { host } = splitHostPort(endpoint.trim());\n const bare = host.replace(/^\\[|]$/g, \"\").toLowerCase();\n\n if (bare === \"localhost\" || bare.endsWith(\".localhost\")) return true;\n // The whole 127.0.0.0/8 block is loopback, not just 127.0.0.1.\n if (/^127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(bare)) return true;\n\n return bare === \"::1\" || bare === \"0:0:0:0:0:0:0:1\";\n}\n","/**\n * The contract between the three layers of `rls-check`:\n *\n * introspect.ts — reads the catalogs into a {@link DbSnapshot}. Talks to Postgres.\n * checks/*.ts — pure functions, snapshot in, {@link Finding}s out. No I/O.\n * report.ts — Findings to text or JSON. No knowledge of Postgres.\n *\n * Checks being pure is the point: every one of them is unit-testable against a\n * hand-written snapshot, so the test suite does not need a live database to\n * cover the interesting cases (it has a Docker-backed suite too, for the\n * introspection layer, which is the only part that can lie).\n */\n\n/** Ordered least → most severe; the CLI's `--fail-on` compares by index. */\nexport const SEVERITIES = [\"info\", \"low\", \"medium\", \"high\", \"critical\"] as const;\n\nexport type Severity = (typeof SEVERITIES)[number];\n\n/** What a finding points at. `table` is absent for database-wide findings. */\nexport interface FindingTarget {\n schema: string;\n table?: string;\n /** Policy name, for policy-level findings. */\n policy?: string;\n view?: string;\n routine?: string;\n column?: string;\n}\n\nexport interface Finding {\n /**\n * Stable, kebab-case check id — `rls-disabled`, `permissive-tautology`.\n * Stable because people put it in `--skip` lists and CI baselines; treat a\n * rename as a breaking change.\n */\n id: string;\n severity: Severity;\n /** One line, specific: names the object and what is wrong with it. */\n title: string;\n target: FindingTarget;\n /**\n * Why this is wrong, in concrete terms. Not \"RLS is recommended\" — say what\n * the database will actually do when a request arrives.\n */\n detail: string;\n /**\n * What an unauthenticated or wrong-tenant caller gets out of it. This is the\n * line people screenshot, so it must be true and not inflated: if reachability\n * depends on the API layer, say \"if this table is exposed over an API\".\n */\n impact: string;\n /** Copy-pasteable SQL, or a precise instruction when SQL cannot express it. */\n fix?: string;\n /** Anchor on https://rebase.pro/docs/rls-check#<id>, filled in by the check. */\n docs?: string;\n /**\n * How sure the check is. Heuristic checks (junction inference, unqualified\n * column detection) MUST set `\"heuristic\"` — the report separates them, and\n * a false positive in the confident bucket is what makes a tool like this\n * get uninstalled.\n */\n confidence: \"certain\" | \"heuristic\";\n}\n\n// ---------------------------------------------------------------------------\n// Snapshot — the read-only picture of the database the checks reason about.\n// ---------------------------------------------------------------------------\n\n/**\n * `pg_class.relkind`, spelled out.\n *\n * Named `RelationKind` until that collided with `RelationKind` in\n * `@rebasepro/types`, which is the cardinality of a *collection* relation\n * (`belongsTo` | `hasOne` | `hasMany` | `manyToMany` | `via`). Unrelated\n * meanings, and the shared word is \"relation\" in two different senses —\n * Postgres's (any table-like object) and the data model's (a link between\n * collections).\n */\nexport type PgRelationKind = \"table\" | \"partitioned_table\" | \"view\" | \"materialized_view\" | \"foreign_table\";\n\nexport interface DbRelation {\n schema: string;\n name: string;\n kind: PgRelationKind;\n owner: string;\n /** `pg_class.relrowsecurity`. Always false for views. */\n rlsEnabled: boolean;\n /** `pg_class.relforcerowsecurity` — without it the owner bypasses RLS. */\n rlsForced: boolean;\n columns: DbColumn[];\n /** Estimated live rows (`pg_class.reltuples`), -1 when never analyzed. */\n estimatedRows: number;\n}\n\nexport interface DbColumn {\n name: string;\n type: string;\n notNull: boolean;\n isPrimaryKey: boolean;\n}\n\nexport type PolicyCommand = \"ALL\" | \"SELECT\" | \"INSERT\" | \"UPDATE\" | \"DELETE\";\n\nexport interface DbPolicy {\n schema: string;\n table: string;\n name: string;\n /** PERMISSIVE policies OR together; RESTRICTIVE ones AND. */\n permissive: boolean;\n /** Roles in the TO clause. `[\"public\"]` means every role. */\n roles: string[];\n command: PolicyCommand;\n /** `pg_policies.qual` — the USING expression, as Postgres rewrote it. */\n using: string | null;\n /** `pg_policies.with_check`. */\n withCheck: string | null;\n}\n\nexport interface DbRole {\n name: string;\n canLogin: boolean;\n superuser: boolean;\n /** BYPASSRLS — every policy is a no-op for this role. */\n bypassRls: boolean;\n /** Roles this one is a member of, transitively resolved. */\n memberOf: string[];\n}\n\n/** One privilege grant on a relation, from `information_schema.role_table_grants`. */\nexport interface DbGrant {\n schema: string;\n table: string;\n grantee: string;\n privileges: (\"SELECT\" | \"INSERT\" | \"UPDATE\" | \"DELETE\" | \"TRUNCATE\" | \"REFERENCES\" | \"TRIGGER\")[];\n}\n\nexport interface DbView {\n schema: string;\n name: string;\n owner: string;\n /**\n * PG15+ `security_invoker=true`. When false, the view runs with the *owner's*\n * privileges, so it reads straight past the RLS on its base tables.\n * `null` on servers older than 15, where the option does not exist at all.\n */\n securityInvoker: boolean | null;\n /** Base relations the view reads, resolved through `pg_depend`. */\n dependsOn: { schema: string; table: string }[];\n}\n\nexport interface DbForeignKey {\n schema: string;\n table: string;\n columns: string[];\n refSchema: string;\n refTable: string;\n refColumns: string[];\n}\n\nexport interface DbRoutine {\n schema: string;\n name: string;\n owner: string;\n /** SECURITY DEFINER runs as the owner and can bypass RLS. */\n securityDefiner: boolean;\n /** True when `search_path` is NOT pinned in the routine's config. */\n mutableSearchPath: boolean;\n}\n\nexport interface DbSnapshot {\n /** `server_version_num`, e.g. 160004. */\n serverVersionNum: number;\n serverVersion: string;\n /** The role the scan itself connected as. */\n currentRole: string;\n /** True when the scanning role cannot be constrained by RLS — see report caveat. */\n scannerIsPrivileged: boolean;\n /** Schemas actually scanned, after `--schema` and the system-schema filter. */\n schemas: string[];\n /**\n * Roles that are plausibly reachable by an untrusted caller: Supabase's\n * `anon` / `authenticated`, PostgREST's `web_anon`, Rebase's `rebase_user`,\n * plus `PUBLIC` — and anything named with `--role`. Checks use this to\n * decide whether an exposure is real, so a stack whose app role is not on\n * that list must name it or the checks have nothing to gate on.\n */\n exposedRoles: string[];\n /** Detected platform, which changes the wording of several findings. */\n platform: \"supabase\" | \"neon\" | \"rebase\" | \"postgrest\" | \"unknown\";\n relations: DbRelation[];\n policies: DbPolicy[];\n roles: DbRole[];\n grants: DbGrant[];\n views: DbView[];\n foreignKeys: DbForeignKey[];\n routines: DbRoutine[];\n}\n\n// ---------------------------------------------------------------------------\n// Checks\n// ---------------------------------------------------------------------------\n\nexport interface Check {\n id: string;\n /** Shown in `--list-checks`. */\n title: string;\n /** One sentence on what the check looks for. */\n description: string;\n run(snapshot: DbSnapshot): Finding[];\n}\n\nexport interface ScanResult {\n /** ISO timestamp, stamped by the caller — checks stay pure. */\n scannedAt: string;\n /** Host and database only. NEVER the user, password or full URL. */\n database: { host: string; name: string };\n serverVersion: string;\n platform: DbSnapshot[\"platform\"];\n scannerIsPrivileged: boolean;\n stats: {\n schemas: number;\n tables: number;\n policies: number;\n tablesWithoutRls: number;\n checksRun: number;\n };\n findings: Finding[];\n /**\n * What the scan could not read.\n *\n * `introspect` records every catalogue query that failed, and `introspect()`\n * threw the record away before `scan` saw it — `ScanResult` had no field for\n * it, and neither the report nor the exit code mentioned it. A single failed\n * grants read silently disables `rls-disabled`, both view checks and\n * `anonymous-write-allowed`, and the run then prints \"✓ No unexpected RLS\n * findings\" and exits 0.\n *\n * A scanner that cannot say \"I could not look\" is worse than no scanner: it\n * answers the question it was asked with the wrong word.\n */\n diagnostics: {\n /** Catalogue reads that failed, and why. Non-empty means degraded. */\n degraded: { what: string; error: string }[];\n /** TLS certificate verification was turned off to connect. */\n tlsVerificationDisabled: boolean;\n /** Schemas left out of the scan, and why. */\n excludedSchemas: { schema: string; reason: \"system\" | \"platform\" | \"not-requested\" }[];\n /**\n * Roles holding write privileges that the scan neither recognises as\n * exposed nor can explain as trusted. Non-empty means the exposed-role\n * set may be incomplete, and every check gates on that set — so this is\n * the difference between \"clean\" and \"clean as far as I could tell\".\n */\n unrecognizedGrantees: string[];\n };\n}\n","/**\n * Rendering: {@link ScanResult} in, text or JSON out. No Postgres, no I/O.\n *\n * Two hard rules shape everything here.\n *\n * 1. Colour is decoration, never information. The plain-text form is what ends\n * up pasted into a CI log or a GitHub issue, so every severity is spelled\n * out as a word and every section has a textual heading. Turning colour off\n * must not remove a single fact.\n *\n * 2. Heuristic findings are quarantined. They render in their own section,\n * after the confident ones, behind a sentence that says they need human\n * judgement. A tool that mixes \"your table is public\" with \"this might be a\n * junction table\" trains people to distrust both.\n */\n\nimport type { Check, Finding, ScanResult, Severity } from \"./types\";\nimport { SEVERITIES } from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Severity ordering\n// ---------------------------------------------------------------------------\n\n/** Higher is worse. `info` = 0 … `critical` = 4. */\nexport function severityRank(severity: Severity): number {\n return SEVERITIES.indexOf(severity);\n}\n\n/** The worst severity present, or `null` for an empty list. */\nexport function maxSeverity(findings: readonly Finding[]): Severity | null {\n let worst: Severity | null = null;\n for (const finding of findings) {\n if (worst === null || severityRank(finding.severity) > severityRank(worst)) {\n worst = finding.severity;\n }\n }\n\n return worst;\n}\n\n/**\n * Does this result trip `--fail-on`? Heuristic findings count: a caller who\n * does not want them can `--skip` the check, but silently discounting them\n * would make the exit code disagree with the report.\n */\nexport function exceedsThreshold(findings: readonly Finding[], failOn: Severity | \"none\"): boolean {\n if (failOn === \"none\") return false;\n const threshold = severityRank(failOn);\n\n return findings.some((finding) => severityRank(finding.severity) >= threshold);\n}\n\n// ---------------------------------------------------------------------------\n// Colour\n// ---------------------------------------------------------------------------\n\ntype Paint = (value: string) => string;\n\nconst identity: Paint = (value) => value;\n\nconst code = (open: number, close: number): Paint => (value) => `\\u001B[${open}m${value}\\u001B[${close}m`;\n\ninterface Style {\n bold: Paint;\n dim: Paint;\n red: Paint;\n boldRed: Paint;\n yellow: Paint;\n cyan: Paint;\n green: Paint;\n}\n\nconst PLAIN: Style = {\n bold: identity,\n dim: identity,\n red: identity,\n boldRed: identity,\n yellow: identity,\n cyan: identity,\n green: identity\n};\n\nconst COLOUR: Style = {\n bold: code(1, 22),\n dim: code(2, 22),\n red: code(31, 39),\n boldRed: (value) => `\\u001B[1m\\u001B[31m${value}\\u001B[39m\\u001B[22m`,\n yellow: code(33, 39),\n cyan: code(36, 39),\n green: code(32, 39)\n};\n\nfunction severityPaint(style: Style, severity: Severity): Paint {\n switch (severity) {\n case \"critical\":\n return style.boldRed;\n case \"high\":\n return style.red;\n case \"medium\":\n return style.yellow;\n case \"low\":\n return style.cyan;\n case \"info\":\n return style.dim;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Text helpers\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_WIDTH = 88;\nconst MIN_WIDTH = 48;\nconst MAX_WIDTH = 100;\n\nexport function clampWidth(columns: number | undefined): number {\n if (!columns || !Number.isFinite(columns)) return DEFAULT_WIDTH;\n\n return Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.floor(columns)));\n}\n\n/** Word-wrap, preserving deliberate newlines in the source text. */\nfunction wrap(text: string, width: number): string[] {\n const lines: string[] = [];\n for (const paragraph of text.split(\"\\n\")) {\n const words = paragraph.trim().split(/\\s+/).filter((word) => word.length > 0);\n if (words.length === 0) {\n lines.push(\"\");\n continue;\n }\n let current = \"\";\n for (const word of words) {\n if (current.length === 0) {\n current = word;\n } else if (current.length + 1 + word.length <= width) {\n current += ` ${word}`;\n } else {\n lines.push(current);\n current = word;\n }\n }\n if (current.length > 0) lines.push(current);\n }\n\n return lines;\n}\n\nfunction indent(lines: readonly string[], pad: string): string[] {\n return lines.map((line) => (line.length === 0 ? \"\" : pad + line));\n}\n\n/**\n * Human-readable object path for a finding. Kept boring on purpose — people\n * grep these out of CI logs.\n */\nexport function formatTarget(target: Finding[\"target\"]): string {\n const parts: string[] = [];\n\n if (target.table) {\n parts.push(target.column ? `${target.schema}.${target.table}.${target.column}` : `${target.schema}.${target.table}`);\n } else if (target.view) {\n parts.push(`${target.schema}.${target.view}`);\n } else if (target.routine) {\n parts.push(`${target.schema}.${target.routine}()`);\n } else {\n parts.push(`schema ${target.schema}`);\n }\n\n if (target.policy) parts.push(`policy \"${target.policy}\"`);\n\n return parts.join(\" · \");\n}\n\n/** Sort key: worst first, then a stable schema/table/id ordering. */\nfunction compareFindings(a: Finding, b: Finding): number {\n const bySeverity = severityRank(b.severity) - severityRank(a.severity);\n if (bySeverity !== 0) return bySeverity;\n\n const bySchema = a.target.schema.localeCompare(b.target.schema);\n if (bySchema !== 0) return bySchema;\n\n const aName = a.target.table ?? a.target.view ?? a.target.routine ?? \"\";\n const bName = b.target.table ?? b.target.view ?? b.target.routine ?? \"\";\n const byName = aName.localeCompare(bName);\n if (byName !== 0) return byName;\n\n const byId = a.id.localeCompare(b.id);\n if (byId !== 0) return byId;\n\n return (a.target.policy ?? \"\").localeCompare(b.target.policy ?? \"\");\n}\n\nconst PLATFORM_LABEL: Record<ScanResult[\"platform\"], string> = {\n supabase: \"Supabase\",\n neon: \"Neon\",\n rebase: \"Rebase\",\n postgrest: \"PostgREST\",\n unknown: \"PostgreSQL (no platform markers)\"\n};\n\n// ---------------------------------------------------------------------------\n// Report\n// ---------------------------------------------------------------------------\n\nexport interface RenderOptions {\n color: boolean;\n /** Findings only: no banner, no summary. The privilege caveat still prints. */\n quiet?: boolean;\n /** Echoed in the summary so the exit code is never a mystery. */\n failOn: Severity | \"none\";\n /** Terminal width; clamped into a readable range. */\n width?: number;\n /**\n * `host:port` for the header. {@link ScanResult} deliberately carries no\n * port (it is a redaction surface), so the CLI passes the display form in.\n */\n endpoint?: string;\n /** Shown in the banner. */\n version?: string;\n}\n\nconst RULE = \"─\";\n\n/**\n * What the scan could not read, and what that costs.\n *\n * Named per failed read rather than summarised, because which catalogue failed\n * decides which checks went quiet — and the reader is the only one who can\n * judge whether the missing ones mattered.\n */\nfunction renderDegradedCaveat(\n degraded: { what: string; error: string }[],\n style: Style,\n width: number\n): string[] {\n const out: string[] = [];\n out.push(style.yellow(`⚠ This scan was incomplete: ${degraded.length} catalogue read(s) failed.`));\n out.push(...wrap(\n \"Checks that depend on what could not be read report nothing, which looks exactly \" +\n \"like finding nothing. Treat this run as inconclusive rather than clean.\",\n width\n ));\n for (const item of degraded) {\n out.push(` • ${item.what}: ${item.error}`);\n }\n return out;\n}\n\nexport function renderReport(result: ScanResult, options: RenderOptions): string {\n const style = options.color ? COLOUR : PLAIN;\n const width = clampWidth(options.width);\n const out: string[] = [];\n\n const certain = result.findings.filter((finding) => finding.confidence !== \"heuristic\").sort(compareFindings);\n const heuristic = result.findings.filter((finding) => finding.confidence === \"heuristic\").sort(compareFindings);\n\n if (!options.quiet) {\n out.push(...renderHeader(result, style, width, options));\n }\n\n // The privilege caveat survives --quiet. Suppressing it would let someone\n // read a clean report as \"we are fine\" from a scan that could not possibly\n // have observed what a low-privilege caller sees.\n if (result.scannerIsPrivileged) {\n out.push(...renderPrivilegeCaveat(style, width));\n out.push(\"\");\n }\n\n // Same reasoning as the privilege caveat above, and it survives --quiet for\n // the same reason: a check whose catalogue read failed returns no findings,\n // which is indistinguishable in the output from a check that found nothing.\n // One failed grants read silently disables `rls-disabled`, both view checks\n // and `anonymous-write-allowed`.\n const degraded = result.diagnostics?.degraded ?? [];\n if (degraded.length > 0) {\n out.push(...renderDegradedCaveat(degraded, style, width));\n out.push(\"\");\n }\n\n // Also survives --quiet, and for the sharpest version of the same reason:\n // every check gates on a grant to an exposed role, and the exposed set is\n // recognised by name. A database served by a role this tool has never heard\n // of produces no findings at all — a green report on an open database.\n const unrecognized = result.diagnostics?.unrecognizedGrantees ?? [];\n if (unrecognized.length > 0) {\n out.push(...renderUnrecognizedRolesCaveat(unrecognized, style, width));\n out.push(\"\");\n }\n\n if (certain.length === 0 && heuristic.length === 0) {\n out.push(degraded.length > 0\n // Not \"no findings\": the scan did not finish looking.\n ? style.yellow(\"No findings from the checks that ran — but the scan was incomplete. See above.\")\n : style.green(\"No findings. Every table, view and policy in scope passed all checks.\"));\n out.push(\"\");\n }\n\n if (certain.length > 0) {\n out.push(...renderFindingSections(certain, style, width));\n }\n\n if (heuristic.length > 0) {\n out.push(style.bold(\"WORTH CHECKING\"));\n out.push(\n ...indent(\n wrap(\n \"These are heuristics, not proofs. They match a shape that is usually a mistake, \" +\n \"but each one may be deliberate in your schema — read them and decide. They are \" +\n \"listed separately so nothing above needs a second opinion.\",\n width - 2\n ),\n \" \"\n ).map(style.dim)\n );\n out.push(\"\");\n out.push(...renderFindingSections(heuristic, style, width, { headings: false }));\n }\n\n if (!options.quiet) {\n out.push(...renderSummary(result, certain, heuristic, style, width, options));\n }\n\n return `${out.join(\"\\n\").replace(/\\n{3,}/g, \"\\n\\n\").trimEnd()}\\n`;\n}\n\nfunction renderHeader(result: ScanResult, style: Style, width: number, options: RenderOptions): string[] {\n const out: string[] = [];\n const title = options.version ? `rls-check ${options.version}` : \"rls-check\";\n\n out.push(`${style.bold(title)}${style.dim(\" · read-only Row-Level Security audit\")}`);\n out.push(style.dim(RULE.repeat(width)));\n out.push(\"\");\n\n const endpoint = options.endpoint ?? result.database.host;\n\n // Some servers report a bare `18.4`, others a full `PostgreSQL 15.6 on …`.\n // A header row reading \"Server 18.4\" tells the reader nothing.\n const serverVersion = /^[\\d.]/.test(result.serverVersion.trim())\n ? `PostgreSQL ${result.serverVersion}`\n : result.serverVersion;\n\n const rows: [string, string][] = [\n [\"Database\", `${endpoint}/${result.database.name}`],\n [\"Server\", serverVersion],\n [\"Platform\", PLATFORM_LABEL[result.platform]],\n [\n \"Scanned\",\n [\n `${result.stats.schemas} ${plural(result.stats.schemas, \"schema\", \"schemas\")}`,\n `${result.stats.tables} ${plural(result.stats.tables, \"table\", \"tables\")}`,\n `${result.stats.policies} ${plural(result.stats.policies, \"policy\", \"policies\")}`,\n `${result.stats.checksRun} ${plural(result.stats.checksRun, \"check\", \"checks\")}`\n ].join(\" · \")\n ]\n ];\n\n for (const [label, value] of rows) {\n out.push(`${style.dim(label.padEnd(10))}${value}`);\n }\n out.push(\"\");\n\n return out;\n}\n\nfunction renderPrivilegeCaveat(style: Style, width: number): string[] {\n const body = wrap(\n \"This scan connected as a role that row-level security cannot constrain — a superuser, \" +\n \"a table owner, or a role with BYPASSRLS. That is why it can read the true catalog, \" +\n \"and it is also why nothing below describes what this connection experiences. \" +\n \"The findings are about what OTHER roles get.\",\n width - 8\n );\n\n const out: string[] = [];\n out.push(`${style.yellow(\"Note\")} ${body[0] ?? \"\"}`);\n for (const line of body.slice(1)) out.push(` ${line}`);\n\n return out;\n}\n\n/**\n * The caveat that keeps a false negative from reading as a pass.\n *\n * Deliberately not a finding: the scan has no evidence that any of these roles\n * is reachable, and inventing a critical for every service account would be the\n * mirror of the bug it exists to prevent. It is a caveat with an exact remedy —\n * one flag, named roles — so the reader can convert it into real coverage.\n */\nfunction renderUnrecognizedRolesCaveat(roles: string[], style: Style, width: number): string[] {\n const shown = roles.slice(0, 6);\n const rest = roles.length - shown.length;\n const list = shown.map((r) => `\"${r}\"`).join(\", \") + (rest > 0 ? `, and ${rest} more` : \"\");\n\n const body = wrap(\n `${list} ${plural(roles.length, \"holds\", \"hold\")} write privileges here, and ` +\n \"this scan does not know whether requests arrive as \" +\n `${plural(roles.length, \"it\", \"them\")}. The checks only report a table as exposed when an ` +\n \"exposed role can reach it, so anything served through \" +\n `${plural(roles.length, \"this role\", \"these roles\")} was NOT assessed. If your ` +\n \"application connects as one of them, re-run naming it — \" +\n `for example: --role ${shown[0] ?? \"app_user\"}`,\n width - 8\n );\n\n const out: string[] = [];\n out.push(`${style.yellow(\"Note\")} ${body[0] ?? \"\"}`);\n for (const line of body.slice(1)) out.push(` ${line}`);\n\n return out;\n}\n\nfunction renderFindingSections(\n findings: readonly Finding[],\n style: Style,\n width: number,\n opts: { headings?: boolean } = {}\n): string[] {\n const out: string[] = [];\n const showHeadings = opts.headings !== false;\n\n for (const severity of [...SEVERITIES].reverse()) {\n const group = findings.filter((finding) => finding.severity === severity);\n if (group.length === 0) continue;\n\n if (showHeadings) {\n const paint = severityPaint(style, severity);\n out.push(\n `${paint(severity.toUpperCase())}${style.dim(\n ` · ${group.length} ${plural(group.length, \"finding\", \"findings\")}`\n )}`\n );\n out.push(style.dim(RULE.repeat(width)));\n out.push(\"\");\n }\n\n for (const finding of group) {\n out.push(...renderFinding(finding, style, width));\n out.push(\"\");\n }\n }\n\n return out;\n}\n\nfunction renderFinding(finding: Finding, style: Style, width: number): string[] {\n const out: string[] = [];\n const paint = severityPaint(style, finding.severity);\n const body = width - 6;\n\n out.push(` ${paint(`[${finding.severity}]`)} ${style.bold(finding.id)} ${style.cyan(formatTarget(finding.target))}`);\n out.push(...indent(wrap(finding.title, body), \" \"));\n\n if (finding.detail.trim().length > 0) {\n // A blank line, because without colour the title and the detail are the\n // same shape, and this is the form that ends up in a bug report.\n out.push(\"\");\n out.push(...indent(wrap(finding.detail, body), \" \").map(style.dim));\n }\n\n if (finding.impact.trim().length > 0) {\n const impact = wrap(finding.impact, body - 8);\n out.push(` ${style.bold(\"Impact\")} ${impact[0] ?? \"\"}`);\n for (const line of impact.slice(1)) out.push(` ${line}`);\n }\n\n if (finding.fix && finding.fix.trim().length > 0) {\n out.push(` ${style.bold(\"Fix\")}`);\n // Never wrapped: the point of this block is that it survives a copy.\n for (const line of finding.fix.replace(/\\s+$/, \"\").split(\"\\n\")) {\n out.push(line.length === 0 ? \"\" : ` ${style.green(line)}`);\n }\n }\n\n if (finding.docs) {\n out.push(` ${style.dim(\"Docs\")} ${style.dim(finding.docs)}`);\n }\n\n return out;\n}\n\nfunction renderSummary(\n result: ScanResult,\n certain: readonly Finding[],\n heuristic: readonly Finding[],\n style: Style,\n width: number,\n options: RenderOptions\n): string[] {\n const out: string[] = [];\n\n out.push(style.dim(RULE.repeat(width)));\n out.push(style.bold(\"Summary\"));\n out.push(\"\");\n\n const counts = [...SEVERITIES]\n .reverse()\n .map((severity) => {\n const count = result.findings.filter((finding) => finding.severity === severity).length;\n const text = `${severity} ${count}`;\n\n return count === 0 ? style.dim(text) : severityPaint(style, severity)(text);\n })\n .join(style.dim(\" \"));\n out.push(` ${counts}`);\n\n out.push(\n ` ${style.dim(\n `${certain.length} confirmed · ${heuristic.length} worth checking · ` +\n `${result.stats.checksRun} ${plural(result.stats.checksRun, \"check\", \"checks\")} run against ` +\n `${result.stats.tables} ${plural(result.stats.tables, \"table\", \"tables\")} in ` +\n `${result.stats.schemas} ${plural(result.stats.schemas, \"schema\", \"schemas\")}`\n )}`\n );\n\n if (result.stats.tablesWithoutRls > 0) {\n out.push(\n ` ${style.dim(\n `${result.stats.tablesWithoutRls} of ${result.stats.tables} ${plural(\n result.stats.tables,\n \"table has\",\n \"tables have\"\n )} row-level security disabled`\n )}`\n );\n }\n out.push(\"\");\n\n const failing = exceedsThreshold(result.findings, options.failOn);\n if (options.failOn === \"none\") {\n out.push(` ${style.dim(\"Exit code 0 — --fail-on none, so findings never fail the run.\")}`);\n } else if (failing) {\n out.push(\n ` ${style.bold(\"Exit code 1\")} ${style.dim(\n `— at least one finding is \"${options.failOn}\" or worse (--fail-on ${options.failOn}).`\n )}`\n );\n } else {\n out.push(\n ` ${style.bold(\"Exit code 0\")} ${style.dim(\n `— nothing at or above \"${options.failOn}\" (--fail-on ${options.failOn}).`\n )}`\n );\n }\n out.push(` ${style.dim(`Scanned ${result.scannedAt} · read-only, and nothing left this machine.`)}`);\n out.push(\"\");\n out.push(style.dim(\"rls-check is free and maintained by the team behind Rebase — https://rebase.pro\"));\n\n return out;\n}\n\nfunction plural(count: number, one: string, many: string): string {\n return count === 1 ? one : many;\n}\n\n// ---------------------------------------------------------------------------\n// JSON\n// ---------------------------------------------------------------------------\n\n/**\n * Exactly the {@link ScanResult} shape, pretty-printed, nothing else. This is\n * the contract other people build on: stable keys, no ANSI, no wrapper object,\n * no timing noise that would make two identical scans diff.\n */\nexport function renderJson(result: ScanResult): string {\n const sorted: ScanResult = {\n ...result,\n findings: [...result.findings].sort(compareFindings)\n };\n\n return JSON.stringify(sorted, null, 2);\n}\n\n// ---------------------------------------------------------------------------\n// Catalog\n// ---------------------------------------------------------------------------\n\n/**\n * `--list-checks`. Ordered by the severity the check *can* emit is tempting,\n * but a check has no fixed severity — so this is plain alphabetical by id,\n * which is also the order people write `--skip` lists in.\n */\nexport function renderCheckCatalog(checks: readonly Check[], options: { color: boolean; width?: number }): string {\n const style = options.color ? COLOUR : PLAIN;\n const width = clampWidth(options.width);\n const sorted = [...checks].sort((a, b) => a.id.localeCompare(b.id));\n const idWidth = sorted.reduce((max, check) => Math.max(max, check.id.length), 0);\n\n const out: string[] = [];\n out.push(style.bold(`${sorted.length} ${plural(sorted.length, \"check\", \"checks\")}`));\n out.push(style.dim(RULE.repeat(width)));\n out.push(\"\");\n\n for (const check of sorted) {\n out.push(` ${style.bold(check.id.padEnd(idWidth))} ${check.title}`);\n const description = wrap(check.description, Math.max(MIN_WIDTH, width - idWidth - 6));\n for (const line of description) {\n out.push(` ${\" \".repeat(idWidth)} ${style.dim(line)}`);\n }\n out.push(\"\");\n }\n\n out.push(style.dim(\"Use --only <id> or --skip <id> (repeatable, or comma-separated) to select checks.\"));\n\n return `${out.join(\"\\n\")}\\n`;\n}\n","/**\n * Rendering: {@link ScanResult} in, one self-contained HTML file out. No\n * Postgres, no I/O — the same contract `report.ts` holds to.\n *\n * This exists because the text report is written for a terminal and the person\n * who needs to act on it is usually not the person who ran it. A `--fail-on`\n * exit code stops a pipeline; it does not survive being forwarded to whoever\n * owns the database. So the HTML form is the artifact: one file, attachable to\n * a ticket, readable by someone who will never install this tool.\n *\n * Four rules shape everything here, and three of them are inherited from\n * `report.ts` on purpose — a second renderer that quietly relaxed them would\n * make the two disagree about the same scan.\n *\n * 1. **Colour is decoration, never information.** Every severity is spelled out\n * as a word next to its swatch, every section has a textual heading. The\n * report has to survive being printed in greyscale, which is exactly what\n * happens when it reaches a compliance review.\n *\n * 2. **Heuristic findings are quarantined.** Their own section, after the\n * confident ones, behind a sentence saying they need human judgement.\n *\n * 3. **The caveats outrank the findings.** A scan by a privileged role, or one\n * whose catalogue reads failed, renders its warning above everything — for\n * the same reason the text report does it: a check that could not run\n * returns no findings, which looks identical to a check that found nothing.\n *\n * 4. **Nothing leaves the machine, including at read time.** No stylesheet\n * link, no font host, no script, no image URL — a security report that makes\n * a network request when opened is not one. It also has to render from\n * `file://` on a laptop with no connection, which is where it will be read.\n *\n * The security property that matters most here is escaping. Every string in a\n * {@link ScanResult} that is not our own prose — table names, policy names,\n * roles, the `fix` SQL, the server version banner — comes from the scanned\n * database, and this file turns them into markup. A table called\n * `<img src=x onerror=…>` is a legal Postgres identifier. {@link escapeHtml} is\n * applied at every interpolation without exception, including inside `<pre>`,\n * and `renderHtml` never accepts pre-built markup from a caller.\n */\n\nimport type { Finding, ScanResult, Severity } from \"./types\";\nimport { SEVERITIES } from \"./types\";\nimport { formatTarget, severityRank } from \"./report\";\n\n/**\n * The one primitive this whole file rests on.\n *\n * Escapes the five characters that can change the meaning of markup, in both\n * element and attribute position, so a single function covers every\n * interpolation site and there is no \"safe here, unsafe there\" judgement to get\n * wrong. Ampersand goes first or it double-escapes the replacements.\n */\nexport function escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\nexport interface HtmlRenderOptions {\n /** Echoed in the summary so the exit code is never a mystery. */\n failOn: Severity | \"none\";\n /**\n * `host:port` for the header. {@link ScanResult} deliberately carries no\n * port (it is a redaction surface), so the CLI passes the display form in.\n */\n endpoint?: string;\n /** Shown in the header. */\n version?: string;\n}\n\nconst PLATFORM_LABEL: Record<ScanResult[\"platform\"], string> = {\n supabase: \"Supabase\",\n neon: \"Neon\",\n rebase: \"Rebase\",\n postgrest: \"PostgREST\",\n unknown: \"PostgreSQL (no platform markers)\"\n};\n\n/** Sort key: worst first, then a stable schema/table/id ordering. */\nfunction compareFindings(a: Finding, b: Finding): number {\n const bySeverity = severityRank(b.severity) - severityRank(a.severity);\n if (bySeverity !== 0) return bySeverity;\n\n const bySchema = a.target.schema.localeCompare(b.target.schema);\n if (bySchema !== 0) return bySchema;\n\n const aName = a.target.table ?? a.target.view ?? a.target.routine ?? \"\";\n const bName = b.target.table ?? b.target.view ?? b.target.routine ?? \"\";\n const byName = aName.localeCompare(bName);\n if (byName !== 0) return byName;\n\n const byId = a.id.localeCompare(b.id);\n if (byId !== 0) return byId;\n\n return (a.target.policy ?? \"\").localeCompare(b.target.policy ?? \"\");\n}\n\nfunction plural(count: number, one: string, many: string): string {\n return count === 1 ? one : many;\n}\n\n/**\n * Only `https://` and `http://` links are emitted, and only as the whole href.\n *\n * `Finding.docs` is set by our own checks today, so this is defence against a\n * future check that builds the URL from something the database said — a\n * `javascript:` href would be a scripting vector that escaping alone does not\n * close, because the escaped text is perfectly valid inside an attribute.\n */\nfunction safeHref(url: string): string | null {\n return /^https?:\\/\\//i.test(url) ? url : null;\n}\n\n// ---------------------------------------------------------------------------\n// Styles\n// ---------------------------------------------------------------------------\n\n/**\n * Inline, because rule 4 forbids a stylesheet link, and a system font stack\n * because a font host is a network request too.\n *\n * The dark palette is defined only in a `prefers-color-scheme` block over a\n * complete light `:root`, so a viewer whose browser reports no preference gets\n * the light set rather than half of each. Print rules flatten the surfaces to\n * white and keep the severity words, which are what carry the meaning once the\n * colour is gone.\n */\nconst STYLES = `\n:root{\n --ground:#F7F8FA; --surface:#FFFFFF; --surface-2:#EEF1F5;\n --ink:#12161C; --ink-2:#454E5C; --ink-3:#6C7583;\n --line:#DEE3EA; --line-strong:#C3CAD5;\n --brand:#0070F4;\n --critical:#B3261E; --high:#C2410C; --medium:#9A6207; --low:#1F6FA8; --info:#6C7583;\n --critical-bg:rgba(179,38,30,.09); --high-bg:rgba(194,65,12,.09);\n --medium-bg:rgba(154,98,7,.10); --low-bg:rgba(31,111,168,.09); --info-bg:rgba(108,117,131,.09);\n --ok:#0E7A55; --ok-bg:rgba(14,122,85,.10);\n --warn:#9A6207; --warn-bg:rgba(154,98,7,.10);\n}\n@media (prefers-color-scheme:dark){\n :root{\n --ground:#0D1014; --surface:#151A21; --surface-2:#1C232C;\n --ink:#EAEEF3; --ink-2:#AAB4C1; --ink-3:#7B8592;\n --line:#232B35; --line-strong:#333D49;\n --brand:#5AA6FF;\n --critical:#FF7B72; --high:#FFA657; --medium:#E3B341; --low:#79C0FF; --info:#8B949E;\n --critical-bg:rgba(255,123,114,.12); --high-bg:rgba(255,166,87,.12);\n --medium-bg:rgba(227,179,65,.12); --low-bg:rgba(121,192,255,.12); --info-bg:rgba(139,148,158,.12);\n --ok:#56D4A0; --ok-bg:rgba(86,212,160,.13);\n --warn:#E3B341; --warn-bg:rgba(227,179,65,.13);\n }\n}\n*{box-sizing:border-box}\nbody{\n margin:0; background:var(--ground); color:var(--ink);\n font:16px/1.6 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif;\n -webkit-font-smoothing:antialiased;\n}\ncode,pre,.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,\"Liberation Mono\",monospace}\n.wrap{max-width:940px;margin:0 auto;padding:40px 24px 80px}\na{color:var(--brand)}\n\nheader.top{border-bottom:1px solid var(--line-strong);padding-bottom:24px}\n.tool{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);font-family:ui-monospace,monospace}\nh1{font-size:30px;line-height:1.15;letter-spacing:-.02em;margin:12px 0 0;font-weight:700}\n.facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:14px 28px;margin-top:22px}\n.fact .k{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3);font-family:ui-monospace,monospace}\n.fact .v{margin-top:3px;font-size:14px;color:var(--ink-2);word-break:break-word}\n\n.verdict{margin:28px 0 0;padding:16px 20px;border-radius:6px;border:1px solid var(--line);background:var(--surface)}\n.verdict.clean{background:var(--ok-bg);border-color:var(--ok)}\n.verdict.warn{background:var(--warn-bg);border-color:var(--warn)}\n.verdict.bad{background:var(--critical-bg);border-color:var(--critical)}\n.verdict h2{margin:0 0 4px;font-size:17px;letter-spacing:-.01em}\n.verdict p{margin:0;font-size:14.5px;color:var(--ink-2)}\n\n.caveat{margin-top:18px;padding:16px 20px;border-radius:6px;background:var(--warn-bg);border:1px solid var(--warn)}\n.caveat h3{margin:0 0 6px;font-size:14px;letter-spacing:.02em}\n.caveat p{margin:0 0 8px;font-size:14px;color:var(--ink-2)}\n.caveat ul{margin:8px 0 0;padding-left:20px;font-size:13.5px;color:var(--ink-2)}\n.caveat li{margin-bottom:4px}\n.caveat li code{font-size:12.5px}\n\n.counts{display:flex;flex-wrap:wrap;gap:8px;margin-top:26px}\n.count{display:flex;align-items:baseline;gap:7px;padding:7px 12px;border-radius:5px;border:1px solid var(--line);background:var(--surface);font-size:13px}\n.count b{font-family:ui-monospace,monospace;font-size:15px;font-variant-numeric:tabular-nums}\n.count.zero{opacity:.5}\n\nh2.section{font-size:13px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);\n font-family:ui-monospace,monospace;margin:44px 0 4px;padding-bottom:8px;border-bottom:1px solid var(--line-strong)}\n.lede{font-size:14px;color:var(--ink-2);margin:12px 0 0}\n\n.finding{margin-top:16px;background:var(--surface);border:1px solid var(--line);border-radius:6px;overflow:hidden}\n.finding > .head{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid var(--line)}\n.sev{display:inline-flex;align-items:center;gap:6px;font-family:ui-monospace,monospace;font-size:11px;\n font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:3px 8px;border-radius:4px}\n.sev::before{content:\"\";width:7px;height:7px;border-radius:50%;background:currentColor;flex:none}\n.sev.critical{color:var(--critical);background:var(--critical-bg)}\n.sev.high{color:var(--high);background:var(--high-bg)}\n.sev.medium{color:var(--medium);background:var(--medium-bg)}\n.sev.low{color:var(--low);background:var(--low-bg)}\n.sev.info{color:var(--info);background:var(--info-bg)}\n.cid{font-family:ui-monospace,monospace;font-size:12.5px;font-weight:600}\n.target{font-family:ui-monospace,monospace;font-size:12.5px;color:var(--ink-3);word-break:break-all}\n.finding > .body{padding:16px 18px}\n.finding h3{margin:0 0 10px;font-size:16px;line-height:1.35;letter-spacing:-.01em}\n.finding p{margin:0 0 12px;font-size:14.5px;color:var(--ink-2)}\n.row{display:grid;grid-template-columns:74px 1fr;gap:12px;margin-top:12px;align-items:start}\n.row .k{font-family:ui-monospace,monospace;font-size:10.5px;letter-spacing:.11em;text-transform:uppercase;\n color:var(--ink-3);padding-top:3px}\n.row .v{font-size:14.5px;color:var(--ink-2)}\npre{margin:0;padding:12px 14px;background:var(--surface-2);border:1px solid var(--line);border-radius:5px;\n overflow-x:auto;font-size:13px;line-height:1.5;color:var(--ink);white-space:pre}\n\nfooter{margin-top:56px;padding-top:20px;border-top:1px solid var(--line-strong);font-size:13px;color:var(--ink-3)}\nfooter p{margin:0 0 6px}\n\n@media print{\n :root{--ground:#FFF;--surface:#FFF;--surface-2:#F4F4F4;--ink:#000;--ink-2:#222;--ink-3:#555;\n --line:#CCC;--line-strong:#999}\n body{font-size:11pt}\n .wrap{max-width:none;padding:0}\n .finding{break-inside:avoid;page-break-inside:avoid}\n .verdict,.caveat{break-inside:avoid}\n}\n`.trim();\n\n// ---------------------------------------------------------------------------\n// Fragments\n// ---------------------------------------------------------------------------\n\nfunction renderFacts(result: ScanResult, options: HtmlRenderOptions): string {\n const endpoint = options.endpoint ?? result.database.host;\n\n // Some servers report a bare `18.4`, others a full `PostgreSQL 15.6 on …`.\n // A row reading \"Server 18.4\" tells the reader nothing.\n const serverVersion = /^[\\d.]/.test(result.serverVersion.trim())\n ? `PostgreSQL ${result.serverVersion}`\n : result.serverVersion;\n\n const scanned = [\n `${result.stats.schemas} ${plural(result.stats.schemas, \"schema\", \"schemas\")}`,\n `${result.stats.tables} ${plural(result.stats.tables, \"table\", \"tables\")}`,\n `${result.stats.policies} ${plural(result.stats.policies, \"policy\", \"policies\")}`,\n `${result.stats.checksRun} ${plural(result.stats.checksRun, \"check\", \"checks\")}`\n ].join(\" · \");\n\n const facts: [string, string][] = [\n [\"Database\", `${endpoint}/${result.database.name}`],\n [\"Server\", serverVersion],\n [\"Platform\", PLATFORM_LABEL[result.platform]],\n [\"Scanned\", scanned]\n ];\n\n return `<div class=\"facts\">${facts\n .map(\n ([key, value]) =>\n `<div class=\"fact\"><div class=\"k\">${escapeHtml(key)}</div><div class=\"v\">${escapeHtml(value)}</div></div>`\n )\n .join(\"\")}</div>`;\n}\n\n/**\n * The one-sentence answer, above everything.\n *\n * \"Inconclusive\" is its own state and outranks \"clean\": a scan whose catalogue\n * reads failed has not earned the word clean, and a reader who skims one line\n * of this report must not be told otherwise.\n */\nfunction renderVerdict(result: ScanResult, certain: readonly Finding[], heuristic: readonly Finding[]): string {\n const degraded = result.diagnostics?.degraded ?? [];\n\n if (degraded.length > 0) {\n return `<div class=\"verdict warn\"><h2>Inconclusive</h2><p>${escapeHtml(\n `${degraded.length} catalogue ${plural(degraded.length, \"read\", \"reads\")} failed, so some checks could not run. ` +\n \"Findings below are real, but their absence proves nothing.\"\n )}</p></div>`;\n }\n\n if (certain.length === 0 && heuristic.length === 0) {\n return `<div class=\"verdict clean\"><h2>No findings</h2><p>${escapeHtml(\n \"Every table, view and policy in scope passed all checks.\"\n )}</p></div>`;\n }\n\n const worst = certain[0]?.severity;\n const tone = worst === \"critical\" || worst === \"high\" ? \"bad\" : \"warn\";\n const parts: string[] = [];\n if (certain.length > 0) parts.push(`${certain.length} confirmed ${plural(certain.length, \"finding\", \"findings\")}`);\n if (heuristic.length > 0) parts.push(`${heuristic.length} worth checking`);\n\n return `<div class=\"verdict ${tone}\"><h2>${escapeHtml(parts.join(\" · \"))}</h2><p>${escapeHtml(\n certain.length > 0\n ? \"Confirmed findings are listed first. Each one names the object, what the database will actually do, and how to fix it.\"\n : \"Nothing confirmed. The heuristics below match a shape that is usually a mistake — read them and decide.\"\n )}</p></div>`;\n}\n\nfunction renderPrivilegeCaveat(): string {\n return `<div class=\"caveat\"><h3>This scan ran as a role RLS cannot constrain</h3><p>${escapeHtml(\n \"The connection was a superuser, a table owner, or a role with BYPASSRLS. That is why it could read the true \" +\n \"catalog, and it is also why nothing below describes what this connection experiences. The findings are \" +\n \"about what OTHER roles get.\"\n )}</p></div>`;\n}\n\nfunction renderDegradedCaveat(degraded: readonly { what: string; error: string }[]): string {\n const items = degraded\n .map((item) => `<li><code>${escapeHtml(item.what)}</code> — ${escapeHtml(item.error)}</li>`)\n .join(\"\");\n\n return `<div class=\"caveat\"><h3>${escapeHtml(\n `This scan was incomplete: ${degraded.length} catalogue ${plural(degraded.length, \"read\", \"reads\")} failed`\n )}</h3><p>${escapeHtml(\n \"Checks that depend on what could not be read report nothing, which looks exactly like finding nothing. \" +\n \"Treat this run as inconclusive rather than clean.\"\n )}</p><ul>${items}</ul></div>`;\n}\n\nfunction renderCounts(result: ScanResult): string {\n const counts = [...SEVERITIES]\n .reverse()\n .map((severity) => {\n const count = result.findings.filter((finding) => finding.severity === severity).length;\n\n return `<div class=\"count${count === 0 ? \" zero\" : \"\"}\"><span class=\"sev ${severity}\">${escapeHtml(\n severity\n )}</span><b>${count}</b></div>`;\n })\n .join(\"\");\n\n return `<div class=\"counts\">${counts}</div>`;\n}\n\nfunction renderFinding(finding: Finding): string {\n const out: string[] = [];\n\n out.push('<article class=\"finding\">');\n out.push(\n `<div class=\"head\"><span class=\"sev ${finding.severity}\">${escapeHtml(\n finding.severity\n )}</span><span class=\"cid\">${escapeHtml(finding.id)}</span><span class=\"target\">${escapeHtml(\n formatTarget(finding.target)\n )}</span></div>`\n );\n out.push('<div class=\"body\">');\n out.push(`<h3>${escapeHtml(finding.title)}</h3>`);\n\n if (finding.detail.trim().length > 0) {\n out.push(`<p>${escapeHtml(finding.detail)}</p>`);\n }\n\n if (finding.impact.trim().length > 0) {\n out.push(`<div class=\"row\"><div class=\"k\">Impact</div><div class=\"v\">${escapeHtml(finding.impact)}</div></div>`);\n }\n\n if (finding.fix && finding.fix.trim().length > 0) {\n // `<pre>` and never wrapped: the point of this block is that it survives\n // a copy into a psql session exactly as written.\n out.push(\n `<div class=\"row\"><div class=\"k\">Fix</div><div class=\"v\"><pre>${escapeHtml(\n finding.fix.replace(/\\s+$/, \"\")\n )}</pre></div></div>`\n );\n }\n\n const href = finding.docs ? safeHref(finding.docs) : null;\n if (href) {\n out.push(\n `<div class=\"row\"><div class=\"k\">Docs</div><div class=\"v\"><a href=\"${escapeHtml(\n href\n )}\">${escapeHtml(href)}</a></div></div>`\n );\n }\n\n out.push(\"</div></article>\");\n\n return out.join(\"\");\n}\n\n/** Findings grouped under a textual severity heading, worst group first. */\nfunction renderFindingGroups(findings: readonly Finding[], opts: { headings?: boolean } = {}): string {\n const out: string[] = [];\n const showHeadings = opts.headings !== false;\n\n for (const severity of [...SEVERITIES].reverse()) {\n const group = findings.filter((finding) => finding.severity === severity);\n if (group.length === 0) continue;\n\n if (showHeadings) {\n out.push(\n `<h2 class=\"section\">${escapeHtml(severity)} · ${group.length} ${escapeHtml(\n plural(group.length, \"finding\", \"findings\")\n )}</h2>`\n );\n }\n for (const finding of group) out.push(renderFinding(finding));\n }\n\n return out.join(\"\");\n}\n\nfunction renderFooter(result: ScanResult, options: HtmlRenderOptions): string {\n const failing = options.failOn !== \"none\" &&\n result.findings.some((finding) => severityRank(finding.severity) >= severityRank(options.failOn as Severity));\n\n const exitLine =\n options.failOn === \"none\"\n ? \"Exit code 0 — --fail-on none, so findings never fail the run.\"\n : failing\n ? `Exit code 1 — at least one finding is \"${options.failOn}\" or worse (--fail-on ${options.failOn}).`\n : `Exit code 0 — nothing at or above \"${options.failOn}\" (--fail-on ${options.failOn}).`;\n\n return `<footer><p>${escapeHtml(exitLine)}</p><p>${escapeHtml(\n `Scanned ${result.scannedAt} · read-only, and nothing left this machine. This file makes no network requests.`\n )}</p><p>rls-check is free and maintained by the team behind Rebase — <a href=\"https://rebase.pro\">rebase.pro</a></p></footer>`;\n}\n\n// ---------------------------------------------------------------------------\n// Document\n// ---------------------------------------------------------------------------\n\n/**\n * The whole report as one HTML document, ready to write to a file.\n *\n * Deterministic: two scans of an unchanged database differ only by\n * `scannedAt`, which keeps the output diffable when someone commits it.\n */\nexport function renderHtml(result: ScanResult, options: HtmlRenderOptions): string {\n const certain = result.findings.filter((finding) => finding.confidence !== \"heuristic\").sort(compareFindings);\n const heuristic = result.findings.filter((finding) => finding.confidence === \"heuristic\").sort(compareFindings);\n const degraded = result.diagnostics?.degraded ?? [];\n\n const title = options.version ? `rls-check ${options.version}` : \"rls-check\";\n const endpoint = options.endpoint ?? result.database.host;\n\n const body: string[] = [];\n\n body.push('<div class=\"wrap\">');\n body.push('<header class=\"top\">');\n body.push(`<div class=\"tool\">${escapeHtml(title)} · read-only Row-Level Security audit</div>`);\n body.push(`<h1>${escapeHtml(`${endpoint}/${result.database.name}`)}</h1>`);\n body.push(renderFacts(result, options));\n body.push(\"</header>\");\n\n body.push(renderVerdict(result, certain, heuristic));\n\n // Both caveats outrank the findings, and both survive every other option,\n // for the same reason: a check that could not run is indistinguishable in\n // the output from a check that found nothing.\n if (result.scannerIsPrivileged) body.push(renderPrivilegeCaveat());\n if (degraded.length > 0) body.push(renderDegradedCaveat(degraded));\n\n body.push(renderCounts(result));\n\n if (certain.length > 0) {\n body.push(renderFindingGroups(certain));\n }\n\n if (heuristic.length > 0) {\n body.push('<h2 class=\"section\">Worth checking</h2>');\n body.push(\n `<p class=\"lede\">${escapeHtml(\n \"These are heuristics, not proofs. They match a shape that is usually a mistake, but each one may be \" +\n \"deliberate in your schema — read them and decide. They are listed separately so nothing above \" +\n \"needs a second opinion.\"\n )}</p>`\n );\n body.push(renderFindingGroups(heuristic, { headings: false }));\n }\n\n body.push(renderFooter(result, options));\n body.push(\"</div>\");\n\n return [\n \"<!doctype html>\",\n '<html lang=\"en\">',\n \"<head>\",\n '<meta charset=\"utf-8\">',\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">',\n // Belt and braces against rule 4: even if a future edit introduced an\n // external reference, the document is not permitted to fetch it.\n `<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; style-src 'unsafe-inline'\">`,\n `<title>${escapeHtml(`RLS audit · ${endpoint}/${result.database.name}`)}</title>`,\n `<style>${STYLES}</style>`,\n \"</head>\",\n \"<body>\",\n body.join(\"\\n\"),\n \"</body>\",\n \"</html>\",\n \"\"\n ].join(\"\\n\");\n}\n","/**\n * The command line: argument parsing, connection resolution, error translation\n * and exit codes. Everything a stranger's first thirty seconds with this tool\n * touches.\n *\n * Three invariants, in order of importance:\n *\n * 1. {@link runCli} never throws and never calls `process.exit`. `bin/rls-check.js`\n * assigns its return value to `process.exitCode`, so an escaping exception\n * would surface as a stack trace and exit code 1 — which CI would read as\n * \"findings\", not \"broken\".\n *\n * 2. Exit 1 and exit 2 are never conflated. 1 means the database has a problem;\n * 2 means the scan did not happen. A pipeline that treats a DNS failure as a\n * clean bill of health is worse than no tool at all.\n *\n * 3. The connection string never reaches an output stream. Every string that\n * could have come from `pg`, Node or the user goes through `redactSecrets`\n * on its way out.\n */\n\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport { CHECKS, runChecks } from \"./checks\";\nimport { introspectWithDiagnostics } from \"./introspect\";\nimport { formatEndpoint, isLoopbackEndpoint, parseConnectionString, redactSecrets } from \"./redact\";\nimport { exceedsThreshold, renderCheckCatalog, renderJson, renderReport } from \"./report\";\nimport { renderHtml } from \"./report-html\";\nimport type { DbSnapshot, Finding, ScanResult, Severity } from \"./types\";\nimport { SEVERITIES } from \"./types\";\n\n/** Clean, or nothing at or above `--fail-on`. */\nexport const EXIT_OK = 0;\n/** Findings at or above `--fail-on`. */\nexport const EXIT_FINDINGS = 1;\n/** The scan did not happen: bad arguments, bad connection, timeout. */\nexport const EXIT_ERROR = 2;\n\nconst DEFAULT_TIMEOUT_MS = 15_000;\nconst DEFAULT_FAIL_ON: Severity = \"high\";\n\nconst TABLE_KINDS = new Set([\"table\", \"partitioned_table\"]);\n\n// ---------------------------------------------------------------------------\n// Programmatic API\n// ---------------------------------------------------------------------------\n\nexport interface ScanOptions {\n connectionString: string;\n /** Restrict to these schemas. Empty or omitted means \"every user schema\". */\n schemas?: string[];\n /**\n * Additional roles an untrusted caller can arrive as, on top of the names\n * this tool recognises. Needed by any stack whose app role is not called\n * `anon`, `authenticated`, `web_anon` or `rebase_user`.\n */\n roles?: string[];\n /** Run only these check ids. */\n only?: string[];\n /** Skip these check ids. */\n skip?: string[];\n statementTimeoutMs?: number;\n /** Injectable so callers (and tests) control the `scannedAt` stamp. */\n now?: Date;\n}\n\n/**\n * Connect, introspect, run the checks, and assemble a {@link ScanResult}.\n *\n * The one impure step in the pipeline. Everything downstream of `introspect`\n * is a pure function of the snapshot, which is why the checks have unit tests\n * and this has an integration test.\n */\nexport async function scan(options: ScanOptions): Promise<ScanResult> {\n // `introspectWithDiagnostics`, not `introspect`: the latter drops the record\n // of what could not be read, and a check whose inputs went missing returns\n // no findings — which is indistinguishable from a clean table.\n const { snapshot, diagnostics } = await introspectWithDiagnostics({\n connectionString: options.connectionString,\n schemas: options.schemas && options.schemas.length > 0 ? options.schemas : undefined,\n roles: options.roles && options.roles.length > 0 ? options.roles : undefined,\n statementTimeoutMs: options.statementTimeoutMs ?? DEFAULT_TIMEOUT_MS\n });\n\n const findings = runChecks(snapshot, { only: options.only, skip: options.skip });\n\n return buildScanResult(snapshot, findings, {\n connectionString: options.connectionString,\n checksRun: selectCheckIds(options).length,\n scannedAt: (options.now ?? new Date()).toISOString(),\n diagnostics\n });\n}\n\n/**\n * The check ids `runChecks` will actually execute for these options.\n *\n * Recomputed here rather than reported back by `runChecks`, because\n * {@link ScanResult.stats} needs the count and the contract has no channel for\n * it. Same filter, same order, so the number in the report is the number that\n * ran — as long as `runChecks` keeps `only` as a whitelist and `skip` as a\n * blacklist applied after it.\n */\nexport function selectCheckIds(options: { only?: string[]; skip?: string[] }): string[] {\n const skip = new Set(options.skip ?? []);\n const only = options.only && options.only.length > 0 ? new Set(options.only) : null;\n\n return CHECKS.filter((check) => (only === null || only.has(check.id)) && !skip.has(check.id)).map(\n (check) => check.id\n );\n}\n\n/**\n * The verdict, as an exit code.\n *\n * Pulled out of `runCli` so it can be tested: `runCli` needs a database, and\n * the one line that decides whether CI goes red had no coverage at all —\n * deleting it broke no test.\n *\n * A degraded scan exits 2, the same code a crash uses, rather than 0. Checks\n * whose catalogue reads failed return no findings, which is indistinguishable\n * from finding none, so exiting 0 would have the scanner answer \"no problems\"\n * to a question it never managed to ask. Both codes mean the same thing here:\n * no verdict.\n */\nexport function exitCodeFor(result: ScanResult, failOn: Severity | \"none\"): number {\n if (result.diagnostics.degraded.length > 0) return EXIT_ERROR;\n return exceedsThreshold(result.findings, failOn) ? EXIT_FINDINGS : EXIT_OK;\n}\n\nfunction buildScanResult(\n snapshot: DbSnapshot,\n findings: Finding[],\n meta: {\n connectionString: string;\n checksRun: number;\n scannedAt: string;\n diagnostics: ScanResult[\"diagnostics\"];\n }\n): ScanResult {\n const target = parseConnectionString(meta.connectionString);\n const tables = snapshot.relations.filter((relation) => TABLE_KINDS.has(relation.kind));\n\n return {\n scannedAt: meta.scannedAt,\n // Host and database only — never the user, password or full URL.\n database: {\n host: target?.host ?? \"unknown\",\n name: target?.database && target.database.length > 0 ? target.database : \"unknown\"\n },\n serverVersion: snapshot.serverVersion,\n platform: snapshot.platform,\n scannerIsPrivileged: snapshot.scannerIsPrivileged,\n stats: {\n schemas: snapshot.schemas.length,\n tables: tables.length,\n policies: snapshot.policies.length,\n tablesWithoutRls: tables.filter((relation) => !relation.rlsEnabled).length,\n checksRun: meta.checksRun\n },\n findings,\n diagnostics: meta.diagnostics\n };\n}\n\n// ---------------------------------------------------------------------------\n// Argument parsing\n// ---------------------------------------------------------------------------\n\nexport interface CliOptions {\n connectionString: string | null;\n json: boolean;\n /**\n * `--html <path>`: also write a self-contained HTML report there.\n *\n * \"Also\", not \"instead\": the terminal output is what a CI log keeps, and\n * the file is what gets forwarded to whoever owns the database. Making the\n * flag replace stdout would trade one audience for the other.\n */\n html: string | null;\n schemas: string[];\n /** Extra roles to treat as reachable by an untrusted caller. */\n roles: string[];\n failOn: Severity | \"none\";\n skip: string[];\n only: string[];\n listChecks: boolean;\n /** `null` = decide from NO_COLOR / TTY. */\n color: boolean | null;\n quiet: boolean;\n timeoutMs: number;\n help: boolean;\n version: boolean;\n}\n\nexport type ParseResult = { ok: true; options: CliOptions } | { ok: false; message: string };\n\nconst FAIL_ON_VALUES = [...SEVERITIES, \"none\"] as const;\n\nfunction emptyOptions(): CliOptions {\n return {\n connectionString: null,\n json: false,\n html: null,\n schemas: [],\n roles: [],\n failOn: DEFAULT_FAIL_ON,\n skip: [],\n only: [],\n listChecks: false,\n color: null,\n quiet: false,\n timeoutMs: DEFAULT_TIMEOUT_MS,\n help: false,\n version: false\n };\n}\n\n/** `--schema a,b --schema c` and `--schema a --schema b` mean the same thing. */\nfunction splitList(value: string): string[] {\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter((item) => item.length > 0);\n}\n\nexport function parseArgs(argv: readonly string[]): ParseResult {\n const options = emptyOptions();\n let positionals = 0;\n\n for (let index = 0; index < argv.length; index += 1) {\n const arg = argv[index];\n\n // Everything after `--` is positional, so a connection string that\n // somehow starts with a dash still works.\n if (arg === \"--\") {\n for (const rest of argv.slice(index + 1)) {\n positionals += 1;\n // Never echo the value: the extra argument is usually a second\n // connection string, and this message goes to a terminal.\n if (positionals > 1) {\n return { ok: false, message: \"Unexpected extra argument. Only one connection string is accepted.\" };\n }\n options.connectionString = rest;\n }\n break;\n }\n\n if (!arg.startsWith(\"-\")) {\n positionals += 1;\n if (positionals > 1) {\n return {\n ok: false,\n message: `Unexpected extra argument. Only one connection string is accepted; quote it if it contains a \"?\" or \"&\".`\n };\n }\n options.connectionString = arg;\n continue;\n }\n\n // --flag=value and --flag value are both accepted.\n const eq = arg.indexOf(\"=\");\n const flag = eq === -1 ? arg : arg.slice(0, eq);\n const inlineValue = eq === -1 ? null : arg.slice(eq + 1);\n\n const takeValue = (): string | null => {\n if (inlineValue !== null) return inlineValue;\n const next = argv[index + 1];\n if (next === undefined || (next.startsWith(\"-\") && next.length > 1)) return null;\n index += 1;\n\n return next;\n };\n\n switch (flag) {\n case \"-h\":\n case \"--help\":\n options.help = true;\n break;\n case \"-v\":\n case \"--version\":\n options.version = true;\n break;\n case \"--json\":\n options.json = true;\n break;\n case \"--quiet\":\n case \"-q\":\n options.quiet = true;\n break;\n case \"--list-checks\":\n options.listChecks = true;\n break;\n case \"--no-color\":\n case \"--no-colour\":\n options.color = false;\n break;\n case \"--color\":\n case \"--colour\":\n options.color = true;\n break;\n case \"--html\": {\n const value = takeValue();\n if (value === null) {\n return { ok: false, message: \"--html needs a file path, e.g. --html rls-report.html.\" };\n }\n options.html = value;\n break;\n }\n case \"--schema\": {\n const value = takeValue();\n if (value === null) return { ok: false, message: \"--schema needs a schema name.\" };\n options.schemas.push(...splitList(value));\n break;\n }\n case \"--role\": {\n const value = takeValue();\n if (value === null) {\n return { ok: false, message: \"--role needs a role name, e.g. --role app_user.\" };\n }\n options.roles.push(...splitList(value));\n break;\n }\n case \"--skip\": {\n const value = takeValue();\n if (value === null) return { ok: false, message: \"--skip needs a check id. See --list-checks.\" };\n options.skip.push(...splitList(value));\n break;\n }\n case \"--only\": {\n const value = takeValue();\n if (value === null) return { ok: false, message: \"--only needs a check id. See --list-checks.\" };\n options.only.push(...splitList(value));\n break;\n }\n case \"--fail-on\": {\n const value = takeValue();\n if (value === null) {\n return { ok: false, message: `--fail-on needs one of: ${FAIL_ON_VALUES.join(\", \")}.` };\n }\n const normalised = value.toLowerCase();\n if (!(FAIL_ON_VALUES as readonly string[]).includes(normalised)) {\n return {\n ok: false,\n message: `--fail-on ${value} is not a severity. Use one of: ${FAIL_ON_VALUES.join(\", \")}.`\n };\n }\n options.failOn = normalised as Severity | \"none\";\n break;\n }\n case \"--timeout\": {\n const value = takeValue();\n if (value === null) return { ok: false, message: \"--timeout needs a number of milliseconds.\" };\n const ms = Number(value);\n if (!Number.isFinite(ms) || ms <= 0) {\n return { ok: false, message: `--timeout ${value} is not a positive number of milliseconds.` };\n }\n options.timeoutMs = Math.floor(ms);\n break;\n }\n default:\n return { ok: false, message: `Unknown option: ${flag}. Run --help for the full list.` };\n }\n }\n\n return { ok: true, options };\n}\n\n// ---------------------------------------------------------------------------\n// Connection resolution\n// ---------------------------------------------------------------------------\n\nexport interface ResolvedConnection {\n connectionString: string;\n /** Where it came from, for the \"using DATABASE_URL from .env\" line. */\n source: string;\n}\n\n/**\n * A three-line .env reader. Deliberately not `dotenv`: the whole promise of\n * this package is that `npx` installs `pg` and nothing else, and a transitive\n * dependency is a supply-chain surface on a tool people point at production.\n *\n * Handles what a Postgres URL actually needs: `export` prefixes, `#` comments,\n * and single or double quotes. It does not do variable interpolation.\n */\nexport function readDotEnv(directory: string): Map<string, string> {\n const values = new Map<string, string>();\n\n let raw: string;\n try {\n raw = readFileSync(join(directory, \".env\"), \"utf8\");\n } catch {\n return values;\n }\n\n for (const line of raw.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (trimmed.length === 0 || trimmed.startsWith(\"#\")) continue;\n\n const body = trimmed.startsWith(\"export \") ? trimmed.slice(\"export \".length).trim() : trimmed;\n const eq = body.indexOf(\"=\");\n if (eq <= 0) continue;\n\n const key = body.slice(0, eq).trim();\n let value = body.slice(eq + 1).trim();\n\n const quoted =\n (value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"));\n if (quoted && value.length >= 2) {\n value = value.slice(1, -1);\n } else {\n // An unquoted trailing comment. `#` inside a password is why this\n // only strips a `#` that follows whitespace.\n const comment = value.search(/\\s#/);\n if (comment !== -1) value = value.slice(0, comment).trim();\n }\n\n if (key.length > 0) values.set(key, value);\n }\n\n return values;\n}\n\nexport function resolveConnection(\n positional: string | null,\n env: NodeJS.ProcessEnv,\n cwd: string\n): ResolvedConnection | null {\n if (positional && positional.trim().length > 0) {\n return { connectionString: positional.trim(), source: \"the command line\" };\n }\n\n if (env.DATABASE_URL && env.DATABASE_URL.trim().length > 0) {\n return { connectionString: env.DATABASE_URL.trim(), source: \"$DATABASE_URL\" };\n }\n\n if (env.POSTGRES_URL && env.POSTGRES_URL.trim().length > 0) {\n return { connectionString: env.POSTGRES_URL.trim(), source: \"$POSTGRES_URL\" };\n }\n\n const dotenv = readDotEnv(cwd);\n for (const key of [\"DATABASE_URL\", \"POSTGRES_URL\"]) {\n const value = dotenv.get(key);\n if (value && value.trim().length > 0) {\n return { connectionString: value.trim(), source: `${key} in .env` };\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Error translation\n// ---------------------------------------------------------------------------\n\nexport interface FriendlyError {\n headline: string;\n hint?: string;\n /** The driver's own words, redacted. Kept short — this is not a stack trace. */\n detail?: string;\n}\n\n/** `pg` sets `code` to either an errno string or a five-character SQLSTATE. */\nfunction errorCode(error: unknown): string | undefined {\n let current: unknown = error;\n for (let depth = 0; depth < 5 && current !== null && current !== undefined; depth += 1) {\n const code = (current as { code?: unknown }).code;\n if (typeof code === \"string\" && code.length > 0) return code;\n current = (current as { cause?: unknown }).cause;\n }\n\n return undefined;\n}\n\nfunction errorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n\n return String(error);\n}\n\n/**\n * Turn a driver failure into one sentence a human can act on.\n *\n * Deliberately covers the failures that account for nearly every first run:\n * wrong host, wrong password, wrong database, TLS, and a firewall that drops\n * rather than refuses. Anything unmapped still prints a redacted one-liner\n * rather than a stack trace.\n */\nexport function explainError(\n error: unknown,\n context: { endpoint: string; timeoutMs: number; connectionString?: string }\n): FriendlyError {\n const code = errorCode(error);\n const raw = errorMessage(error);\n const message = redactSecrets(raw, context.connectionString).replace(/\\s+/g, \" \").trim();\n const detail = code ? `${code}: ${message}` : message;\n const at = context.endpoint;\n\n const friendly = (headline: string, hint?: string): FriendlyError => ({ headline, hint, detail });\n\n switch (code) {\n case \"ECONNREFUSED\":\n return friendly(\n `Nothing is accepting connections at ${at}.`,\n \"Check the host and port, that the server is running, and that you are on the network it allows — a VPN, an SSH tunnel or an IP allowlist is the usual cause.\"\n );\n case \"ENOTFOUND\":\n case \"EAI_AGAIN\":\n return friendly(\n `The host in the connection string does not resolve (${at}).`,\n \"Check it for a typo. A Supabase host looks like db.<project-ref>.supabase.co; a Neon host ends in .neon.tech.\"\n );\n case \"ETIMEDOUT\":\n case \"ENETUNREACH\":\n case \"EHOSTUNREACH\":\n return friendly(\n `Timed out reaching ${at}.`,\n \"Packets are being dropped rather than refused, which almost always means a firewall or an IP allowlist. Add this machine's address to the database's allowed list.\"\n );\n case \"ECONNRESET\":\n // A loopback endpoint is a proxy or tunnel, never the database: it\n // accepted the TCP connection and then hung up because its own\n // upstream auth failed. sslmode cannot help — the proxy terminates\n // TLS itself — and the reason is only in the proxy's log.\n return isLoopbackEndpoint(at)\n ? friendly(\n `A local proxy or tunnel on ${at} closed the connection before the handshake finished.`,\n \"Something is listening on that port and hung up — cloud-sql-proxy, an SSH -L forward or a local pooler. The real cause is in its log, not in your TLS mode.\"\n )\n : friendly(\n `${at} closed the connection before the handshake finished.`,\n \"Most often TLS. Try appending ?sslmode=require to the connection string; managed providers reject plaintext.\"\n );\n case \"EPROTO\":\n case \"DEPTH_ZERO_SELF_SIGNED_CERT\":\n case \"SELF_SIGNED_CERT_IN_CHAIN\":\n case \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\":\n case \"ERR_TLS_CERT_ALTNAME_INVALID\":\n return friendly(\n `The TLS certificate presented by ${at} could not be verified.`,\n \"Append ?sslmode=no-verify to connect without verifying it, or point sslrootcert at your provider's CA bundle if you would rather keep verification on.\"\n );\n case \"28P01\":\n return friendly(\n `Password authentication failed on ${at}.`,\n \"Check the password. If it contains @ : / ? or #, it has to be percent-encoded inside a URL — that is the single most common cause of this error.\"\n );\n case \"28000\":\n return friendly(\n `${at} refused the connection for this role.`,\n \"Either the role does not exist, or pg_hba.conf has no rule matching this host, user and database combination.\"\n );\n case \"3D000\":\n return friendly(\n \"That database does not exist on the server.\",\n \"The database name is the last path segment of the connection string. On Supabase it is usually `postgres`.\"\n );\n case \"42501\":\n return friendly(\n \"The connected role is not allowed to read the catalogs this scan needs.\",\n \"Run it as the database owner or another role that can read pg_policies and pg_class. The scan itself only issues SELECTs.\"\n );\n case \"53300\":\n return friendly(\n `${at} has no connection slots left.`,\n \"Retry when the pool frees up, or point the scan at a direct connection rather than a saturated pooler.\"\n );\n case \"57014\":\n return friendly(\n `A catalog query exceeded the ${context.timeoutMs}ms statement timeout.`,\n \"Databases with tens of thousands of relations need longer: re-run with --timeout 60000.\"\n );\n case \"08006\":\n case \"08001\":\n case \"08004\":\n // Same trap as ECONNRESET above: on loopback there is no TLS mode\n // to get wrong, because the thing that refused you is a proxy.\n return isLoopbackEndpoint(at)\n ? friendly(\n `Could not establish a connection through the local proxy or tunnel on ${at}.`,\n \"The port answered but the session never came up. That is the proxy's upstream failing, not your TLS mode — its log has the reason.\"\n )\n : friendly(\n `Could not establish a connection to ${at}.`,\n \"Check the host, port and TLS mode. Managed providers usually need ?sslmode=require.\"\n );\n default:\n break;\n }\n\n if (/does not support SSL/i.test(raw)) {\n return friendly(\n `${at} is not configured for TLS.`,\n \"Append ?sslmode=disable if this is a local database you trust the network path to.\"\n );\n }\n if (/self.signed certificate|certificate/i.test(raw) && /SSL|TLS|certificate/i.test(raw)) {\n return friendly(\n `The TLS certificate presented by ${at} could not be verified.`,\n \"Append ?sslmode=no-verify, or point sslrootcert at your provider's CA bundle.\"\n );\n }\n if (/timeout/i.test(raw)) {\n return friendly(\n `Timed out talking to ${at}.`,\n `The scan uses a ${context.timeoutMs}ms statement timeout; raise it with --timeout <ms>, or check for a firewall dropping the connection.`\n );\n }\n if (/Connection terminated/i.test(raw)) {\n return friendly(\n `The connection to ${at} was closed unexpectedly.`,\n \"A pooler or proxy in front of the database is the usual cause. Try a direct connection.\"\n );\n }\n\n return friendly(`The scan against ${at} failed.`);\n}\n\n// ---------------------------------------------------------------------------\n// Presentation of operational failures\n// ---------------------------------------------------------------------------\n\nfunction formatFriendlyError(error: FriendlyError, color: boolean): string {\n const bold = (value: string): string => (color ? `\\u001B[1m${value}\\u001B[22m` : value);\n const dim = (value: string): string => (color ? `\\u001B[2m${value}\\u001B[22m` : value);\n const red = (value: string): string => (color ? `\\u001B[31m${value}\\u001B[39m` : value);\n\n const lines = [`${red(\"Error\")} ${bold(error.headline)}`];\n if (error.hint) lines.push(` ${error.hint}`);\n if (error.detail) lines.push(` ${dim(truncate(error.detail, 240))}`);\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction truncate(value: string, max: number): string {\n return value.length <= max ? value : `${value.slice(0, max - 1)}…`;\n}\n\n// ---------------------------------------------------------------------------\n// Static text\n// ---------------------------------------------------------------------------\n\nfunction helpText(version: string): string {\n return `rls-check ${version} — audit Row-Level Security on any PostgreSQL database.\n\nUsage\n npx @rebasepro/rls-check [connection-string] [options]\n\nThe connection string is taken from, in order: the argument, $DATABASE_URL,\n$POSTGRES_URL, then DATABASE_URL in a .env file in the current directory.\n\nOptions\n --json Machine-readable ScanResult on stdout, and nothing else.\n --html <path> Also write a self-contained HTML report to <path>. One file,\n no network requests, safe to attach to a ticket.\n --schema <name> Restrict the scan to a schema. Repeatable or comma-separated.\n --role <name> Treat this role as one an untrusted caller arrives as, in\n addition to anon, authenticated, web_anon and rebase_user.\n Name the role your application connects as — the checks\n only report a table as exposed when an exposed role can\n reach it. Repeatable or comma-separated.\n --fail-on <severity> Exit 1 at or above this severity: info, low, medium, high,\n critical, or none to never fail. Default: high.\n --only <id> Run only these checks. Repeatable or comma-separated.\n --skip <id> Skip these checks. Repeatable or comma-separated.\n --list-checks Print the check catalog and exit.\n --timeout <ms> Statement timeout for each catalog query. Default: 15000.\n --quiet Findings only: no banner, no summary.\n --no-color Disable ANSI colour. NO_COLOR and a non-TTY stdout are\n honoured automatically; --color forces it back on.\n -h, --help Show this text.\n -v, --version Print the version.\n\nExit codes\n 0 Clean, or nothing at or above --fail-on.\n 1 Findings at or above --fail-on.\n 2 The scan did not run: bad arguments, connection refused, auth failed, timeout.\n\nExamples\n npx @rebasepro/rls-check \"postgresql://user:pass@db.abcdef.supabase.co:5432/postgres\"\n npx @rebasepro/rls-check --schema public --schema billing --fail-on medium\n npx @rebasepro/rls-check --json > rls-report.json\n npx @rebasepro/rls-check --html rls-report.html\n\nIt is read-only: it issues SELECTs against the system catalogs, writes nothing,\nand sends nothing anywhere.\n`;\n}\n\nfunction usageText(): string {\n return `rls-check needs a PostgreSQL connection string.\n\n npx @rebasepro/rls-check \"postgresql://user:password@host:5432/database\"\n\nOr set DATABASE_URL (or POSTGRES_URL) in the environment, or put DATABASE_URL in\na .env file in this directory.\n\nIt is read-only — it reads the system catalogs and writes nothing. Run with\n--help for all options.\n`;\n}\n\n/**\n * Read the version out of the package manifest at runtime rather than inlining\n * it at build time, so a published tarball and a `pnpm build` never disagree.\n * `../package.json` is correct from both `src/cli.ts` and `dist/index.es.js`.\n */\nexport function resolveVersion(): string {\n for (const candidate of [\"../package.json\", \"../../package.json\"]) {\n try {\n const raw = readFileSync(new URL(candidate, import.meta.url), \"utf8\");\n const parsed = JSON.parse(raw) as { name?: string; version?: string };\n if (parsed.name === \"@rebasepro/rls-check\" && typeof parsed.version === \"string\") {\n return parsed.version;\n }\n } catch {\n // Fall through: a missing manifest must never break a scan.\n }\n }\n\n return \"unknown\";\n}\n\n// ---------------------------------------------------------------------------\n// runCli\n// ---------------------------------------------------------------------------\n\nexport interface CliIo {\n stdout(text: string): void;\n stderr(text: string): void;\n /**\n * Write a file, for `--html`. Injected rather than imported so the CLI\n * tests can exercise the whole path without touching a disk.\n */\n writeFile?(path: string, contents: string): void;\n env: NodeJS.ProcessEnv;\n cwd: string;\n /** Whether stdout is a terminal; drives colour when nothing else decides. */\n isTty: boolean;\n columns?: number;\n}\n\nfunction defaultIo(): CliIo {\n return {\n stdout: (text) => process.stdout.write(text),\n stderr: (text) => process.stderr.write(text),\n writeFile: (path, contents) => writeFileSync(path, contents, \"utf8\"),\n env: process.env,\n cwd: process.cwd(),\n isTty: Boolean(process.stdout.isTTY),\n columns: process.stdout.columns\n };\n}\n\n/**\n * Colour is on only when every signal agrees: not JSON, not NO_COLOR, not a\n * dumb terminal, and either a TTY or an explicit --color.\n */\nexport function resolveColor(explicit: boolean | null, json: boolean, env: NodeJS.ProcessEnv, isTty: boolean): boolean {\n if (json) return false;\n if (explicit !== null) return explicit;\n if (typeof env.NO_COLOR === \"string\" && env.NO_COLOR !== \"\") return false;\n if (env.TERM === \"dumb\") return false;\n if (typeof env.FORCE_COLOR === \"string\" && env.FORCE_COLOR !== \"\" && env.FORCE_COLOR !== \"0\") return true;\n\n return isTty;\n}\n\n/**\n * The entry point `bin/rls-check.js` calls. Returns the process exit code and\n * never throws — an unexpected failure becomes exit 2 with a redacted message.\n */\nexport async function runCli(argv: readonly string[], io: CliIo = defaultIo()): Promise<number> {\n let connectionString: string | undefined;\n\n try {\n const parsed = parseArgs(argv);\n if (!parsed.ok) {\n io.stderr(formatFriendlyError({ headline: parsed.message }, resolveColor(null, false, io.env, io.isTty)));\n\n return EXIT_ERROR;\n }\n\n const options = parsed.options;\n const color = resolveColor(options.color, options.json, io.env, io.isTty);\n const version = resolveVersion();\n\n if (options.help) {\n io.stdout(helpText(version));\n\n return EXIT_OK;\n }\n\n if (options.version) {\n io.stdout(`${version}\\n`);\n\n return EXIT_OK;\n }\n\n if (options.listChecks) {\n if (options.json) {\n io.stdout(\n `${JSON.stringify(\n CHECKS.map((check) => ({ id: check.id, title: check.title, description: check.description })),\n null,\n 2\n )}\\n`\n );\n } else {\n io.stdout(renderCheckCatalog(CHECKS, { color, width: io.columns }));\n }\n\n return EXIT_OK;\n }\n\n const unknownIds = [...options.only, ...options.skip].filter(\n (id) => !CHECKS.some((check) => check.id === id)\n );\n if (unknownIds.length > 0) {\n io.stderr(\n formatFriendlyError(\n {\n headline: `Unknown check ${unknownIds.length === 1 ? \"id\" : \"ids\"}: ${unknownIds.join(\", \")}.`,\n hint: \"Run --list-checks for the catalog. Ids are stable, so a typo here silently weakens the scan rather than failing it — which is why this is an error.\"\n },\n color\n )\n );\n\n return EXIT_ERROR;\n }\n\n const resolved = resolveConnection(options.connectionString, io.env, io.cwd);\n if (!resolved) {\n io.stderr(usageText());\n\n return EXIT_ERROR;\n }\n connectionString = resolved.connectionString;\n\n const target = parseConnectionString(connectionString);\n const endpoint = formatEndpoint(target);\n\n if (!target) {\n io.stderr(\n formatFriendlyError(\n {\n headline: \"That does not look like a PostgreSQL connection string.\",\n hint: \"Expected postgresql://user:password@host:5432/database, or a libpq keyword string such as host=… dbname=…. If the password contains @ : / ? or #, percent-encode it — otherwise the URL is ambiguous and neither this tool nor libpq can tell where the credential ends.\"\n },\n color\n )\n );\n\n return EXIT_ERROR;\n }\n\n // There is no way to abort an in-flight libpq query from Node, so the\n // handler exits directly rather than unwinding. 130 is the shell's\n // convention for SIGINT and keeps it distinct from 1 and 2.\n const onSigint = (): void => {\n io.stderr(\"\\nInterrupted. Nothing was written — the scan is read-only.\\n\");\n process.exit(130);\n };\n process.on(\"SIGINT\", onSigint);\n\n let result: ScanResult;\n try {\n result = await scan({\n connectionString,\n schemas: options.schemas,\n roles: options.roles,\n only: options.only,\n skip: options.skip,\n statementTimeoutMs: options.timeoutMs\n });\n } catch (error) {\n io.stderr(\n formatFriendlyError(explainError(error, { endpoint, timeoutMs: options.timeoutMs, connectionString }), color)\n );\n\n return EXIT_ERROR;\n } finally {\n process.removeListener(\"SIGINT\", onSigint);\n }\n\n if (options.json) {\n io.stdout(`${renderJson(result)}\\n`);\n } else {\n io.stdout(\n renderReport(result, {\n color,\n quiet: options.quiet,\n failOn: options.failOn,\n width: io.columns,\n endpoint,\n version\n })\n );\n }\n\n // The artifact is the whole point of asking for it, so failing to write\n // it is a failed run even though the scan itself succeeded — exit 2,\n // the \"no verdict\" code, rather than letting a missing report look like\n // a clean one. The findings are already on stdout either way.\n if (options.html !== null) {\n const write = io.writeFile;\n if (!write) {\n io.stderr(\n formatFriendlyError(\n { headline: \"--html is not available: this environment cannot write files.\" },\n color\n )\n );\n\n return EXIT_ERROR;\n }\n try {\n write(options.html, renderHtml(result, { failOn: options.failOn, endpoint, version }));\n io.stderr(`Wrote ${options.html}\\n`);\n } catch (error) {\n io.stderr(\n formatFriendlyError(\n {\n headline: `Could not write ${options.html}.`,\n hint: \"Check the directory exists and is writable.\",\n detail: redactSecrets(errorMessage(error), connectionString)\n },\n color\n )\n );\n\n return EXIT_ERROR;\n }\n }\n\n // A degraded scan is not a clean scan. Checks whose catalogue reads\n // failed return nothing, which is indistinguishable from finding\n // nothing — so reporting EXIT_OK here would be the scanner answering\n // \"no problems\" to a question it never managed to ask. Exit 2, the same\n // code an outright crash uses, because both mean \"no verdict\".\n return exitCodeFor(result, options.failOn);\n } catch (error) {\n // Anything that escapes the expected paths. Exit 2, never 1: a crash is\n // not a clean bill of health and it is not a finding either.\n io.stderr(\n formatFriendlyError(\n {\n headline: \"rls-check failed before it could report.\",\n hint: \"This is a bug — please open an issue at https://github.com/rebasepro/rebase/issues.\",\n detail: redactSecrets(errorMessage(error), connectionString)\n },\n false\n )\n );\n\n return EXIT_ERROR;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.es.js","names":[],"sources":["../src/checks/sql.ts","../src/checks/util.ts","../src/checks/anonymous-write-allowed.ts","../src/checks/current-setting-throws.ts","../src/checks/grant-to-public.ts","../src/checks/junction-table-unprotected.ts","../src/checks/view-bypasses-rls.ts","../src/checks/matview-bypasses-rls.ts","../src/checks/policy-always-true.ts","../src/types.ts","../src/checks/policy-anonymous-tautology.ts","../src/checks/policy-authenticated-tautology.ts","../src/checks/policy-role-unreachable.ts","../src/checks/rls-disabled.ts","../src/checks/rls-enabled-no-policies.ts","../src/checks/rls-enabled-not-forced.ts","../src/checks/security-definer-mutable-search-path.ts","../src/checks/unqualified-column-in-subquery.ts","../src/checks/index.ts","../src/redact.ts","../src/introspect.ts","../src/report.ts","../src/report-html.ts","../src/cli.ts"],"sourcesContent":["/**\n * A deliberately small SQL scanner for policy expressions.\n *\n * This is NOT a grammar. It knows exactly enough to answer three questions\n * without being fooled: where do the parens nest, what is inside a string\n * literal (so `'... EXISTS (SELECT ...'` is never mistaken for a subquery), and\n * which identifiers are written bare.\n *\n * Everything it reads comes from `pg_policies.qual` / `with_check`, which is\n * Postgres's own re-rendering of the parse tree rather than the SQL anyone\n * typed: parenthesised more heavily, casts made explicit, and — importantly for\n * the unqualified-column check — references usually re-qualified. So a hit here\n * is strong evidence and a miss proves nothing, which is what the checks say.\n */\n\nexport type TokenKind = \"ident\" | \"string\" | \"number\" | \"op\" | \"punct\";\n\nexport interface SqlToken {\n kind: TokenKind;\n /** Lower-cased for idents and operators; raw text for strings and numbers. */\n value: string;\n /** True when the identifier arrived double-quoted, so its case is literal. */\n quoted: boolean;\n /** Paren nesting the token sits at; `(` carries the depth it opens from. */\n depth: number;\n}\n\n// Postgres allows non-ASCII letters in unquoted identifiers; rather than\n// replicating its locale rules, anything above ASCII counts as an ident char.\nconst isIdentStart = (c: string): boolean => /[A-Za-z_]/.test(c) || c.charCodeAt(0) > 127;\nconst isIdentBody = (c: string): boolean => /[A-Za-z0-9_$]/.test(c) || c.charCodeAt(0) > 127;\nconst MULTI_CHAR_OPS = [\"->>\", \"#>>\", \"::\", \"<=\", \">=\", \"<>\", \"!=\", \"||\", \"->\", \"#>\", \"@>\", \"<@\", \"~~\", \"!~\"];\n\n/** Tokenize a policy expression. Never throws: unterminated quotes just run to EOF. */\nexport function tokenize(sql: string): SqlToken[] {\n const tokens: SqlToken[] = [];\n let depth = 0;\n let i = 0;\n\n while (i < sql.length) {\n const c = sql[i];\n\n if (/\\s/.test(c)) {\n i++;\n continue;\n }\n\n // -- line comment\n if (c === \"-\" && sql[i + 1] === \"-\") {\n const nl = sql.indexOf(\"\\n\", i);\n i = nl === -1 ? sql.length : nl + 1;\n continue;\n }\n\n // /* block comment */ — Postgres nests these.\n if (c === \"/\" && sql[i + 1] === \"*\") {\n let nest = 1;\n i += 2;\n while (i < sql.length && nest > 0) {\n if (sql[i] === \"/\" && sql[i + 1] === \"*\") {\n nest++;\n i += 2;\n } else if (sql[i] === \"*\" && sql[i + 1] === \"/\") {\n nest--;\n i += 2;\n } else i++;\n }\n continue;\n }\n\n // $tag$ dollar-quoted string $tag$. Must be recognised before identifiers,\n // because the body can contain anything at all — including `EXISTS (`.\n if (c === \"$\") {\n const tag = /^\\$[A-Za-z_][A-Za-z0-9_]*\\$|^\\$\\$/.exec(sql.slice(i));\n if (tag) {\n const end = sql.indexOf(tag[0], i + tag[0].length);\n const stop = end === -1 ? sql.length : end + tag[0].length;\n tokens.push({ kind: \"string\", value: sql.slice(i, stop), quoted: true, depth });\n i = stop;\n continue;\n }\n }\n\n // '...' with '' escaping, plus E'...' backslash escaping.\n if (c === \"'\" || ((c === \"e\" || c === \"E\") && sql[i + 1] === \"'\")) {\n const escapes = c !== \"'\";\n let j = escapes ? i + 2 : i + 1;\n while (j < sql.length) {\n if (escapes && sql[j] === \"\\\\\") {\n j += 2;\n continue;\n }\n if (sql[j] === \"'\") {\n if (sql[j + 1] === \"'\") {\n j += 2;\n continue;\n }\n j++;\n break;\n }\n j++;\n }\n tokens.push({ kind: \"string\", value: sql.slice(i, j), quoted: true, depth });\n i = j;\n continue;\n }\n\n // \"quoted identifier\"\n if (c === '\"') {\n let j = i + 1;\n let value = \"\";\n while (j < sql.length) {\n if (sql[j] === '\"') {\n if (sql[j + 1] === '\"') {\n value += '\"';\n j += 2;\n continue;\n }\n j++;\n break;\n }\n value += sql[j];\n j++;\n }\n tokens.push({ kind: \"ident\", value, quoted: true, depth });\n i = j;\n continue;\n }\n\n if (isIdentStart(c)) {\n let j = i + 1;\n while (j < sql.length && isIdentBody(sql[j])) j++;\n tokens.push({ kind: \"ident\", value: sql.slice(i, j).toLowerCase(), quoted: false, depth });\n i = j;\n continue;\n }\n\n if (/[0-9]/.test(c) || (c === \".\" && /[0-9]/.test(sql[i + 1] ?? \"\"))) {\n let j = i;\n while (j < sql.length && /[0-9.eE+-]/.test(sql[j])) {\n // Stop at a sign that is not part of an exponent.\n if ((sql[j] === \"+\" || sql[j] === \"-\") && !/[eE]/.test(sql[j - 1] ?? \"\")) break;\n j++;\n }\n tokens.push({ kind: \"number\", value: sql.slice(i, j), quoted: false, depth });\n i = j;\n continue;\n }\n\n if (c === \"(\") {\n tokens.push({ kind: \"punct\", value: \"(\", quoted: false, depth });\n depth++;\n i++;\n continue;\n }\n if (c === \")\") {\n depth = Math.max(0, depth - 1);\n tokens.push({ kind: \"punct\", value: \")\", quoted: false, depth });\n i++;\n continue;\n }\n if (c === \",\" || c === \".\" || c === \"[\" || c === \"]\" || c === \";\") {\n tokens.push({ kind: \"punct\", value: c, quoted: false, depth });\n i++;\n continue;\n }\n\n const multi = MULTI_CHAR_OPS.find((op) => sql.startsWith(op, i));\n if (multi) {\n tokens.push({ kind: \"op\", value: multi, quoted: false, depth });\n i += multi.length;\n continue;\n }\n\n tokens.push({ kind: \"op\", value: c, quoted: false, depth });\n i++;\n }\n\n return tokens;\n}\n\n/** Index of the `)` matching the `(` at `open`, or -1. */\nexport function matchParen(tokens: SqlToken[], open: number): number {\n const base = tokens[open]?.depth ?? 0;\n for (let i = open + 1; i < tokens.length; i++) {\n const t = tokens[i];\n if (t.kind === \"punct\" && t.value === \")\" && t.depth === base) return i;\n }\n return -1;\n}\n\n/**\n * Every parenthesised `SELECT` in the expression, as token-index ranges.\n *\n * Covers `EXISTS (SELECT …)`, `x IN (SELECT …)`, `= ANY (SELECT …)` and plain\n * scalar subqueries in one rule, because all of them are \"an open paren whose\n * first word is SELECT\" and none of the checks care which form it took.\n */\nexport function findSubqueries(tokens: SqlToken[]): { open: number; close: number }[] {\n const out: { open: number; close: number }[] = [];\n for (let i = 0; i < tokens.length; i++) {\n const t = tokens[i];\n if (t.kind !== \"punct\" || t.value !== \"(\") continue;\n const next = tokens[i + 1];\n if (!next || next.kind !== \"ident\" || next.quoted || next.value !== \"select\") continue;\n const close = matchParen(tokens, i);\n if (close !== -1) out.push({ open: i, close });\n }\n return out;\n}\n\n/** True when the ident at `i` is written bare: no `schema.` before, no `.col` after. */\nexport function isBareIdent(tokens: SqlToken[], i: number): boolean {\n const t = tokens[i];\n if (!t || t.kind !== \"ident\") return false;\n const prev = tokens[i - 1];\n const next = tokens[i + 1];\n if (prev && prev.kind === \"punct\" && prev.value === \".\") return false;\n if (next && next.kind === \"punct\" && next.value === \".\") return false;\n // `lower(x)` is a call, not a column.\n if (next && next.kind === \"punct\" && next.value === \"(\") return false;\n return true;\n}\n\n/** The dotted name ending at `i`, e.g. `[\"public\", \"orders\", \"id\"]`. */\nexport function qualifiedNameEndingAt(tokens: SqlToken[], i: number): string[] {\n const parts: string[] = [];\n let j = i;\n while (j >= 0 && tokens[j]?.kind === \"ident\") {\n parts.unshift(tokens[j].value);\n if (tokens[j - 1]?.kind === \"punct\" && tokens[j - 1]?.value === \".\") j -= 2;\n else break;\n }\n return parts;\n}\n\n/**\n * Does this expression normalize to an unconditional truth?\n *\n * Strictly structural: parens and boolean casts are dropped and the *whole*\n * remainder has to be the constant. A substring match on `true` would flag\n * `(is_public = true AND owner = auth.uid())`, which is exactly the kind of\n * false positive this tool cannot afford.\n */\nexport function isUnconditionalTrue(expr: string | null | undefined): boolean {\n if (expr == null) return false;\n const tokens = tokenize(expr).filter(\n (t) => !(t.kind === \"punct\" && (t.value === \"(\" || t.value === \")\"))\n );\n\n // Drop `::boolean` / `::bool` casts, which Postgres likes to render.\n const stripped: SqlToken[] = [];\n for (let i = 0; i < tokens.length; i++) {\n if (tokens[i].kind === \"op\" && tokens[i].value === \"::\") {\n i++; // skip the type name too\n continue;\n }\n stripped.push(tokens[i]);\n }\n\n const values = stripped.map((t) => t.value);\n if (values.length === 1 && values[0] === \"true\") return true;\n // `1 = 1`, `'x' = 'x'` — the same constant compared with itself.\n if (values.length === 3 && values[1] === \"=\" && values[0] === values[2]) {\n return stripped[0].kind === \"number\" || stripped[0].kind === \"string\";\n }\n return false;\n}\n\n/** Words that are never a column reference in a policy predicate. */\nexport const SQL_KEYWORDS = new Set([\n \"select\", \"from\", \"where\", \"and\", \"or\", \"not\", \"in\", \"exists\", \"is\", \"null\", \"true\", \"false\",\n \"as\", \"on\", \"join\", \"inner\", \"left\", \"right\", \"full\", \"outer\", \"cross\", \"natural\", \"lateral\",\n \"group\", \"by\", \"order\", \"having\", \"limit\", \"offset\", \"union\", \"intersect\", \"except\", \"all\",\n \"any\", \"some\", \"distinct\", \"case\", \"when\", \"then\", \"else\", \"end\", \"between\", \"like\", \"ilike\",\n \"similar\", \"escape\", \"asc\", \"desc\", \"nulls\", \"first\", \"last\", \"using\", \"with\", \"recursive\",\n \"current_user\", \"session_user\", \"current_role\", \"current_date\", \"current_time\",\n \"current_timestamp\", \"localtime\", \"localtimestamp\", \"cast\", \"array\", \"row\", \"values\",\n \"returning\", \"window\", \"fetch\", \"only\", \"filter\", \"collate\", \"at\", \"time\", \"zone\", \"interval\",\n \"int\", \"int2\", \"int4\", \"int8\", \"integer\", \"bigint\", \"smallint\", \"text\", \"varchar\", \"char\",\n \"boolean\", \"bool\", \"uuid\", \"json\", \"jsonb\", \"numeric\", \"decimal\", \"real\", \"float\", \"double\",\n \"precision\", \"date\", \"timestamp\", \"timestamptz\", \"bytea\", \"name\", \"regclass\", \"oid\"\n]);\n","/**\n * Shared vocabulary for the checks.\n *\n * Everything here is about answering one question honestly: *can an untrusted\n * caller actually reach this object?* A finding that cannot answer it becomes\n * noise, and noise is what gets a tool like this uninstalled — so the helpers\n * below err towards \"no\" whenever the catalog is ambiguous.\n */\nimport type { DbGrant, DbPolicy, DbRelation, DbSnapshot, Finding, Severity } from \"../types\";\n\nexport const SEVERITY_ORDER: Severity[] = [\"info\", \"low\", \"medium\", \"high\", \"critical\"];\n\nexport const docsFor = (id: string): string => `https://rebase.pro/docs/rls-check#${id}`;\n\nexport type Privilege = DbGrant[\"privileges\"][number];\n\n/** The privileges that actually move data. TRUNCATE/REFERENCES/TRIGGER are not exposure. */\nexport const DML: Privilege[] = [\"SELECT\", \"INSERT\", \"UPDATE\", \"DELETE\"];\nexport const WRITE_PRIVILEGES: Privilege[] = [\"INSERT\", \"UPDATE\", \"DELETE\"];\n\n/** Roles a caller reaches without ever authenticating. */\nexport const ANONYMOUS_ROLES = [\"public\", \"anon\", \"web_anon\"];\n\nexport const isPublicRole = (role: string): boolean => role.toLowerCase() === \"public\";\nexport const sameRole = (a: string, b: string): boolean => a.toLowerCase() === b.toLowerCase();\n\n// ---------------------------------------------------------------------------\n// Roles\n// ---------------------------------------------------------------------------\n\n/**\n * Every role whose grants `role` actually receives: itself, PUBLIC, and the\n * transitive closure of its memberships.\n *\n * Skipping the closure is the specific way this class of tool produces false\n * *negatives* on the databases that were configured most carefully — a shop\n * that grants to `app_reader` and makes `anon` a member of it looks clean to a\n * checker that only compares grantee names.\n */\nexport function rolesUsableBy(snapshot: DbSnapshot, role: string): Set<string> {\n const out = new Set<string>([\"public\", role.toLowerCase()]);\n const def = snapshot.roles.find((r) => sameRole(r.name, role));\n for (const m of def?.memberOf ?? []) out.add(m.toLowerCase());\n return out;\n}\n\n/** Privileges `role` effectively holds on a relation, memberships included. */\nexport function effectivePrivileges(\n snapshot: DbSnapshot,\n schema: string,\n table: string,\n role: string\n): Set<Privilege> {\n const via = rolesUsableBy(snapshot, role);\n const out = new Set<Privilege>();\n for (const g of snapshot.grants) {\n if (g.schema !== schema || g.table !== table) continue;\n if (!via.has(g.grantee.toLowerCase())) continue;\n for (const p of g.privileges) out.add(p);\n }\n return out;\n}\n\n/**\n * Which exposed roles hold at least one of `wanted` on this relation, and which.\n * Returns an empty array when nothing untrusted can touch it — the signal that\n * a finding would be noise.\n */\nexport function exposedGrantees(\n snapshot: DbSnapshot,\n schema: string,\n table: string,\n wanted: Privilege[] = DML\n): { role: string; privileges: Privilege[] }[] {\n const out: { role: string; privileges: Privilege[] }[] = [];\n for (const role of snapshot.exposedRoles) {\n const held = effectivePrivileges(snapshot, schema, table, role);\n const hit = wanted.filter((p) => held.has(p));\n if (hit.length > 0) out.push({ role, privileges: hit });\n }\n return out;\n}\n\n/** Does this policy's TO list name a role an untrusted caller arrives as? */\nexport function policyTargetsExposedRole(snapshot: DbSnapshot, policy: DbPolicy): string[] {\n const hits: string[] = [];\n for (const target of policy.roles) {\n if (isPublicRole(target)) {\n hits.push(\"PUBLIC\");\n continue;\n }\n // A policy TO `app_reader` applies to `anon` when anon inherits it.\n for (const exposed of snapshot.exposedRoles) {\n if (isPublicRole(exposed)) continue;\n if (rolesUsableBy(snapshot, exposed).has(target.toLowerCase())) {\n hits.push(exposed);\n break;\n }\n }\n }\n return [...new Set(hits)];\n}\n\n// ---------------------------------------------------------------------------\n// Relations\n// ---------------------------------------------------------------------------\n\nexport const TABLE_KINDS: DbRelation[\"kind\"][] = [\"table\", \"partitioned_table\"];\n\nexport const isTable = (r: DbRelation): boolean => TABLE_KINDS.includes(r.kind);\n\nexport function scannedTables(snapshot: DbSnapshot): DbRelation[] {\n return snapshot.relations.filter((r) => isTable(r) && snapshot.schemas.includes(r.schema));\n}\n\nexport function relationAt(snapshot: DbSnapshot, schema: string, name: string): DbRelation | undefined {\n return snapshot.relations.find((r) => r.schema === schema && r.name === name);\n}\n\nexport function policiesFor(snapshot: DbSnapshot, schema: string, table: string): DbPolicy[] {\n return snapshot.policies.filter((p) => p.schema === schema && p.table === table);\n}\n\nexport const hasColumn = (r: DbRelation, name: string): boolean =>\n r.columns.some((c) => c.name.toLowerCase() === name.toLowerCase());\n\n/** `≈ 12,000 rows` — only ever used to convey blast radius, never severity. */\nexport function rowsPhrase(rel: DbRelation | undefined): string {\n if (!rel || rel.estimatedRows < 0) return \"\";\n if (rel.estimatedRows === 0) return \" (the planner currently estimates 0 rows)\";\n return ` (≈${Math.round(rel.estimatedRows).toLocaleString(\"en-US\")} rows)`;\n}\n\n// ---------------------------------------------------------------------------\n// SQL rendering for the `fix` field\n// ---------------------------------------------------------------------------\n\nexport const qi = (ident: string): string => `\"${ident.replace(/\"/g, '\"\"')}\"`;\nexport const qrel = (schema: string, name: string): string => `${qi(schema)}.${qi(name)}`;\n/** PUBLIC is a keyword, not an identifier — quoting it changes what it means. */\nexport const qrole = (role: string): string => (isPublicRole(role) ? \"PUBLIC\" : qi(role));\n\n// ---------------------------------------------------------------------------\n// Findings\n// ---------------------------------------------------------------------------\n\nexport function finding(f: Omit<Finding, \"docs\"> & { docs?: string }): Finding {\n return { ...f, docs: f.docs ?? docsFor(f.id) };\n}\n\nexport const listAnd = (items: string[]): string =>\n items.length <= 1\n ? (items[0] ?? \"\")\n : `${items.slice(0, -1).join(\", \")} and ${items[items.length - 1]}`;\n\n/**\n * How to spell \"the id of the current caller\" in a fix suggestion, for the\n * platform this database actually is.\n *\n * Every remediation in this tool prints example SQL, and example SQL that calls\n * a function the reader's database does not have is worse than no example — it\n * looks authoritative and fails on paste. Rebase's helpers live in `rebase`\n * (they were in `auth` before 1.0, which is Supabase's schema and the reason\n * they moved); Supabase and PostgREST stacks have `auth.uid()`.\n *\n * `unknown` falls back to the Supabase spelling: this scanner is pointed at\n * plenty of databases Rebase did not create, and that is the wider convention.\n */\nexport function callerIdCall(snapshot: DbSnapshot): string {\n return snapshot.platform === \"rebase\" ? \"rebase.uid()\" : \"auth.uid()\";\n}\n\n// ---------------------------------------------------------------------------\n// Rebase-managed policies\n// ---------------------------------------------------------------------------\n\n/**\n * The helper functions a Rebase deployment creates in its `rebase` schema, as\n * they appear inside a policy expression. A call to one of these is the\n * strongest single signal that the policy was compiled from a collection's\n * `securityRules` rather than written by hand.\n */\nconst REBASE_HELPER_CALL = /\\brebase\\.(uid|roles|jwt|is_anonymous)\\s*\\(/i;\n\n/**\n * `<table>_<operation>_<7 hex>` — the name Rebase derives for a rule that does\n * not carry one of its own. The hash is of the rule's *semantics*, which is why\n * editing a rule abandons the old policy instead of updating it.\n *\n * Kept in step with `isGeneratedPolicyName` in\n * `packages/server-postgres/src/security/policy-drift.ts`, which is the\n * predicate the product itself uses to decide what it owns. This package cannot\n * import it — it ships with `pg` and nothing else so that `npx` is fast — so the\n * shape is restated here and anchored to the table for the same reason it is\n * there: a name that merely looks similar is not ours to reason about.\n */\nfunction hasGeneratedPolicyName(policy: DbPolicy): boolean {\n const table = policy.table.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`^${table}_(select|insert|update|delete|all)_[0-9a-f]{7}(_\\\\d+)?$`).test(policy.name);\n}\n\n/**\n * Is this policy one Rebase derives from a collection's `securityRules` and\n * re-applies at every boot?\n *\n * It matters because the ordinary remediation — edit the policy in the database\n * — is *silently undone* on such a policy: boot drops and recreates every\n * generated policy from the collection config, so a fix applied with SQL\n * survives exactly until the next restart. Prescribing it is worse than\n * prescribing nothing, because the operator watches the finding disappear and\n * files it as done.\n *\n * Gated on the platform as well as the shape: this scanner is pointed at plenty\n * of databases Rebase did not create, and a hash-suffixed policy name on one of\n * those is a coincidence, not a contract.\n */\nexport function isRebaseManagedPolicy(snapshot: DbSnapshot, policy: DbPolicy): boolean {\n if (snapshot.platform !== \"rebase\") return false;\n if (REBASE_HELPER_CALL.test(policy.using ?? \"\")) return true;\n if (REBASE_HELPER_CALL.test(policy.withCheck ?? \"\")) return true;\n return hasGeneratedPolicyName(policy);\n}\n\n/** Where the rule that produces a managed policy is documented. */\nexport const SECURITY_RULES_DOCS = \"https://rebase.pro/docs/collections/security-rules\";\n\n/**\n * The remediation for a Rebase-managed policy: change the rule it is compiled\n * from. `intent` is one clause saying what to change the rule to, in the\n * vocabulary of the check that found it.\n *\n * Deliberately contains no SQL. Every other fix in this tool is copy-pasteable\n * because pasting it works; here it would not, and an example that is reverted\n * on the next deploy teaches the wrong model of where access control lives.\n */\nexport function managedPolicyFix(policy: DbPolicy, intent: string): string {\n return (\n `This policy is not hand-written: Rebase compiles it from the collection's\\n` +\n `\\`securityRules\\` and re-applies it (drop, then create) on every boot, so a\\n` +\n `change made to the policy in the database is undone the next time the runtime\\n` +\n `starts. Change the rule instead:\\n` +\n ` 1. find the collection whose table is ${qrel(policy.schema, policy.table)} — in a scaffold,\\n` +\n ` under config/collections/;\\n` +\n ` 2. ${intent};\\n` +\n ` 3. a collection that declares no \\`securityRules\\` of its own inherits\\n` +\n ` \\`defaultSecurityRules\\` from config/collections/index.ts — a stock scaffold's\\n` +\n ` unfiltered read rule lives there, not on the collection;\\n` +\n ` 4. redeploy (boot re-applies the policies), or run \\`rebase db push\\`.\\n` +\n `${SECURITY_RULES_DOCS}`\n );\n}\n","import type { Check, DbPolicy, DbSnapshot, Finding, PolicyCommand } from \"../types\";\n\nimport { isUnconditionalTrue } from \"./sql\";\nimport { callerIdCall,\n ANONYMOUS_ROLES,\n WRITE_PRIVILEGES,\n effectivePrivileges,\n finding,\n isPublicRole,\n isRebaseManagedPolicy,\n listAnd,\n managedPolicyFix,\n qi,\n qrel,\n type Privilege\n} from \"./util\";\n\nconst ID = \"anonymous-write-allowed\";\n\nconst COMMANDS_FOR: Record<PolicyCommand, Privilege[]> = {\n ALL: [\"INSERT\", \"UPDATE\", \"DELETE\"],\n INSERT: [\"INSERT\"],\n UPDATE: [\"UPDATE\"],\n DELETE: [\"DELETE\"],\n SELECT: []\n};\n\n/**\n * A write policy that lets an unauthenticated caller through, backed by a real grant.\n *\n * Two conditions beyond \"the policy targets anon\" carry this check, and both are\n * there to stop it firing on correct configurations:\n *\n * 1. The role must actually hold the matching write privilege. Postgres checks\n * the grant first; a policy that permits an INSERT nobody may attempt is inert.\n * 2. The policy's own expression must not scope the row. Supabase ships\n * `anon`/`authenticated` with broad table grants by default, so a policy\n * `FOR INSERT TO public WITH CHECK (user_id = auth.uid())` — a textbook\n * correct one — satisfies condition 1 on almost every project out there.\n * Flagging it would make this check fire on the whole ecosystem.\n *\n * So what is reported is the narrow, certain case: the check expression is\n * absent or constant-true, which in Postgres means \"accept any row\". Policies\n * whose expression is an anonymous *tautology* rather than a constant are the\n * business of `policy-anonymous-tautology`, which can weigh the platform.\n */\nexport const anonymousWriteAllowed: Check = {\n id: ID,\n title: \"Unauthenticated callers can write\",\n description:\n \"A permissive INSERT/UPDATE/DELETE policy reachable without authentication whose check \" +\n \"expression accepts any row, backed by a matching grant.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const uidCall = callerIdCall(snapshot);\n const findings: Finding[] = [];\n\n for (const policy of snapshot.policies) {\n if (!snapshot.schemas.includes(policy.schema)) continue;\n if (!policy.permissive) continue;\n\n const wanted = COMMANDS_FOR[policy.command] ?? [];\n if (wanted.length === 0) continue;\n\n const identities = anonymousIdentities(snapshot, policy.roles);\n if (identities.length === 0) continue;\n\n if (!acceptsAnyRow(policy)) continue;\n\n // Which write privileges those identities genuinely hold on the table.\n const granted = new Set<Privilege>();\n const grantedTo: string[] = [];\n for (const role of identities) {\n const held = effectivePrivileges(snapshot, policy.schema, policy.table, role);\n const hit = wanted.filter((p) => held.has(p));\n if (hit.length === 0) continue;\n hit.forEach((p) => granted.add(p));\n grantedTo.push(isPublicRole(role) ? \"PUBLIC\" : role);\n }\n if (granted.size === 0) continue;\n\n const commands = WRITE_PRIVILEGES.filter((p) => granted.has(p));\n const verbs = commands.map((c) => c.toLowerCase());\n\n findings.push(\n finding({\n id: ID,\n severity: \"high\",\n confidence: \"certain\",\n title:\n `${policy.schema}.${policy.table} accepts unauthenticated ` +\n `${listAnd(verbs)} via policy \"${policy.name}\"`,\n target: { schema: policy.schema, table: policy.table, policy: policy.name },\n detail:\n `Policy \"${policy.name}\" is a permissive ${policy.command} policy for ` +\n `${listAnd(grantedTo)}, and its check expression ` +\n `${policy.using == null && policy.withCheck == null\n ? \"is absent, which Postgres treats as accepting every row\"\n : \"is a constant truth, so every row satisfies it\"}. ` +\n `${listAnd(grantedTo)} also ${grantedTo.length > 1 ? \"hold\" : \"holds\"} ` +\n `${listAnd(commands)} on the table, so both the privilege check and the row ` +\n `check pass for a request that carries no credentials.`,\n impact:\n `An unauthenticated caller reaching this database over an API can ` +\n `${listAnd(verbs)} rows in ${policy.schema}.${policy.table} at will — inserting ` +\n `records attributed to other users, or ` +\n `${commands.includes(\"DELETE\") ? \"deleting the table's contents\" : \"modifying rows they do not own\"}.`,\n fix: isRebaseManagedPolicy(snapshot, policy)\n ? managedPolicyFix(\n policy,\n `scope the write rule to the caller (an \\`ownerField\\`) or restrict it to \\`roles\\` — ` +\n `and if anonymous writes are never intended, say so there rather than by ` +\n `revoking the grant, which boot re-makes`\n )\n : `-- Scope the write to the caller, or take the privilege away entirely:\\n` +\n `ALTER POLICY ${qi(policy.name)} ON ${qrel(policy.schema, policy.table)}\\n` +\n ` WITH CHECK (user_id = ${uidCall});\\n` +\n `-- and if anonymous writes are never intended:\\n` +\n `REVOKE ${commands.join(\", \")} ON ${qrel(policy.schema, policy.table)} FROM ${grantedTo\n .map((r) => (r === \"PUBLIC\" ? \"PUBLIC\" : `\"${r}\"`))\n .join(\", \")};`\n })\n );\n }\n\n return findings;\n }\n};\n\n/**\n * Which unauthenticated identities a policy's TO list actually covers.\n *\n * `TO public` is not itself a role a request arrives as — it means \"every role\",\n * so the privileges that matter are the ones held by whichever anonymous roles\n * this database has (plus anything granted to PUBLIC). Resolving that is the\n * difference between catching `TO public` + `GRANT … TO anon` and missing it.\n */\nfunction anonymousIdentities(snapshot: DbSnapshot, policyRoles: string[]): string[] {\n const named = policyRoles.filter((r) => ANONYMOUS_ROLES.includes(r.toLowerCase()));\n if (named.length === 0) return [];\n if (!named.some(isPublicRole)) return named;\n\n const present = snapshot.roles\n .map((r) => r.name)\n .filter((name) => ANONYMOUS_ROLES.includes(name.toLowerCase()) && !isPublicRole(name));\n\n return [\"public\", ...present];\n}\n\n/**\n * Does this policy impose no row condition at all?\n *\n * For INSERT, Postgres falls back to USING when WITH CHECK is absent, and a\n * policy with neither clause admits every row. For UPDATE/DELETE the USING\n * clause selects the rows that may be touched.\n */\nfunction acceptsAnyRow(policy: DbPolicy): boolean {\n const clauses =\n policy.command === \"INSERT\"\n ? [policy.withCheck ?? policy.using]\n : [policy.using, policy.command === \"DELETE\" ? null : policy.withCheck];\n\n const present = clauses.filter((c): c is string => c != null);\n if (present.length === 0) return true;\n return present.every((c) => isUnconditionalTrue(c));\n}\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { matchParen, tokenize } from \"./sql\";\nimport { finding, isRebaseManagedPolicy, managedPolicyFix, qi, qrel } from \"./util\";\n\nconst ID = \"current-setting-throws\";\n\n/**\n * `current_setting('x')` in a policy, without the `missing_ok` second argument.\n *\n * With one argument the function *raises* when the setting has never been set in\n * the session — it does not return NULL. So a request that forgets to set the\n * GUC does not get denied, it gets an error, and the failure surfaces as a 500\n * rather than an empty result. Some middleware retries on 5xx, which turns a\n * missing header into a retry loop.\n *\n * Heuristic because the GUC may be guaranteed set by a connection-level `SET` or\n * a `set_config` in the same transaction, in which case one argument is fine —\n * the check cannot see the session that will run the query.\n */\nexport const currentSettingThrows: Check = {\n id: ID,\n title: \"Policy calls current_setting() without missing_ok\",\n description:\n \"A policy expression calling current_setting('x') with one argument, which raises rather \" +\n \"than returning NULL when the setting is unset.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const policy of snapshot.policies) {\n if (!snapshot.schemas.includes(policy.schema)) continue;\n\n const settings = new Set<string>();\n const clauses: string[] = [];\n for (const clause of [\"USING\", \"WITH CHECK\"] as const) {\n const expr = clause === \"USING\" ? policy.using : policy.withCheck;\n const hits = singleArgumentSettings(expr);\n if (hits.length === 0) continue;\n clauses.push(clause);\n hits.forEach((h) => settings.add(h));\n }\n if (settings.size === 0) continue;\n\n const names = [...settings];\n\n findings.push(\n finding({\n id: ID,\n severity: \"low\",\n confidence: \"heuristic\",\n title:\n `Policy \"${policy.name}\" on ${policy.schema}.${policy.table} calls ` +\n `current_setting(${names.map((n) => `'${n}'`).join(\", \")}) without missing_ok`,\n target: { schema: policy.schema, table: policy.table, policy: policy.name },\n detail:\n `The ${clauses.join(\" and \")} expression calls \\`current_setting\\` with a single ` +\n `argument. When ${names.length === 1 ? \"that setting has\" : \"those settings have\"} ` +\n `not been set in the session, the call raises \\`unrecognized configuration ` +\n `parameter\\` instead of returning NULL — so the policy cannot evaluate to false, ` +\n `it errors. Passing \\`true\\` as the second argument makes it return NULL, which ` +\n `the policy then treats as \"no match\" and denies the row, as intended.`,\n impact:\n `A request that reaches this table without ${names.join(\" / \")} set fails with a ` +\n `database error rather than being denied. The caller sees a 500 rather than an ` +\n `empty result, and any middleware that retries 5xx responses will retry a request ` +\n `that can never succeed.`,\n fix: isRebaseManagedPolicy(snapshot, policy)\n ? managedPolicyFix(\n policy,\n `pass the missing_ok argument in the rule's raw SQL — \\`current_setting('${names[0]}', true)\\` — ` +\n `so an unset value denies instead of raising`\n )\n : `-- Add the missing_ok argument so an unset value denies instead of raising:\\n` +\n `ALTER POLICY ${qi(policy.name)} ON ${qrel(policy.schema, policy.table)}\\n` +\n ` USING (tenant_id = current_setting('${names[0]}', true)::uuid);`\n })\n );\n }\n\n return findings;\n }\n};\n\n/** Setting names passed to a one-argument `current_setting` call. */\nfunction singleArgumentSettings(expr: string | null | undefined): string[] {\n if (!expr) return [];\n const tokens = tokenize(expr);\n const out: string[] = [];\n\n for (let i = 0; i < tokens.length; i++) {\n const t = tokens[i];\n if (t.kind !== \"ident\" || t.quoted || t.value !== \"current_setting\") continue;\n\n const open = i + 1;\n if (tokens[open]?.kind !== \"punct\" || tokens[open].value !== \"(\") continue;\n const close = matchParen(tokens, open);\n if (close === -1) continue;\n\n // A comma at the call's own nesting level means missing_ok was supplied.\n const argDepth = tokens[open].depth + 1;\n const hasSecondArg = tokens\n .slice(open + 1, close)\n .some((a) => a.kind === \"punct\" && a.value === \",\" && a.depth === argDepth);\n if (hasSecondArg) continue;\n\n const first = tokens[open + 1];\n out.push(first?.kind === \"string\" ? first.value.replace(/^'|'$/g, \"\") : \"the setting\");\n }\n\n return [...new Set(out)];\n}\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { DML, finding, isPublicRole, listAnd, qrel, relationAt, rowsPhrase } from \"./util\";\n\nconst ID = \"grant-to-public\";\n\n/**\n * A DML privilege granted to PUBLIC.\n *\n * PUBLIC is every role in the cluster, present and future, including ones added\n * after the grant was written. Even with RLS enabled this is rarely intentional:\n * it widens the set of roles whose policies are evaluated, and any role that\n * later gets a permissive policy inherits table access it was never explicitly\n * given. When RLS is off it is simply open access — `rls-disabled` reports that\n * separately and with more force.\n */\nexport const grantToPublic: Check = {\n id: ID,\n title: \"Table privileges granted to PUBLIC\",\n description: \"A SELECT/INSERT/UPDATE/DELETE privilege granted to PUBLIC on a table.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const grant of snapshot.grants) {\n if (!isPublicRole(grant.grantee)) continue;\n if (!snapshot.schemas.includes(grant.schema)) continue;\n\n const rel = relationAt(snapshot, grant.schema, grant.table);\n if (!rel || (rel.kind !== \"table\" && rel.kind !== \"partitioned_table\")) continue;\n\n const privileges = DML.filter((p) => grant.privileges.includes(p));\n if (privileges.length === 0) continue;\n\n findings.push(\n finding({\n id: ID,\n severity: \"medium\",\n confidence: \"certain\",\n title: `${grant.schema}.${grant.table} grants ${listAnd(privileges)} to PUBLIC`,\n target: { schema: grant.schema, table: grant.table },\n detail:\n `PUBLIC is every role in this cluster, including roles created after this grant ` +\n `was made. ` +\n (rel.rlsEnabled\n ? `Row-level security is enabled on the table, so rows are still filtered by ` +\n `policy — but this grant decides *whose* policies get evaluated, and it ` +\n `answers \"everyone's\". A permissive policy added later for any role applies ` +\n `immediately, with no separate grant needed.`\n : `Row-level security is not enabled on this table, so nothing filters the rows ` +\n `this grant exposes.`),\n impact: rel.rlsEnabled\n ? `Every role in the database can attempt ${listAnd(privileges)} on this table` +\n `${rowsPhrase(rel)}; what they get back depends entirely on the policies, ` +\n `including any added in future.`\n : `Every role in the database, including any role an API connects as, can ` +\n `${listAnd(privileges.map((p) => p.toLowerCase()))} every row of this table` +\n `${rowsPhrase(rel)}.`,\n fix:\n `REVOKE ${privileges.join(\", \")} ON ${qrel(grant.schema, grant.table)} FROM PUBLIC;\\n` +\n `-- then grant explicitly to the roles that need it:\\n` +\n `-- GRANT ${privileges.join(\", \")} ON ${qrel(grant.schema, grant.table)} TO \"your_app_role\";`\n })\n );\n }\n\n return findings;\n }\n};\n","import type { Check, DbRelation, DbSnapshot, Finding } from \"../types\";\n\nimport { finding, qrel, relationAt, scannedTables } from \"./util\";\n\nconst ID = \"junction-table-unprotected\";\n\n/** Bookkeeping columns a join table is allowed to carry without ceasing to be one. */\nconst INCIDENTAL_COLUMNS = new Set([\n \"id\", \"uuid\", \"created_at\", \"updated_at\", \"inserted_at\", \"modified_at\", \"deleted_at\",\n \"created_on\", \"updated_on\", \"createdat\", \"updatedat\"\n]);\n\n/**\n * The edge table of a many-to-many relationship, left without RLS while both\n * endpoints have it.\n *\n * Join tables are the classic forgotten surface: the two tables people think of\n * as \"the data\" get policies, and the table that records *which user belongs to\n * which organisation* — often the most sensitive relation in the schema — is\n * treated as plumbing. It leaks the graph even when it leaks no attributes.\n *\n * The inference is deliberately narrow: exactly two foreign keys, to two\n * different tables that both have RLS on, and no columns beyond those keys and\n * ordinary bookkeeping. A table with its own payload columns is a domain table\n * that happens to have two foreign keys, and is not reported here.\n */\nexport const junctionTableUnprotected: Check = {\n id: ID,\n title: \"Many-to-many join table without RLS\",\n description:\n \"A table that is essentially just two foreign keys, both pointing at RLS-protected \" +\n \"tables, that has no row-level security of its own.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const rel of scannedTables(snapshot)) {\n if (rel.rlsEnabled) continue;\n\n const fks = snapshot.foreignKeys.filter(\n (fk) => fk.schema === rel.schema && fk.table === rel.name\n );\n if (fks.length !== 2) continue;\n\n const endpoints = fks.map((fk) => `${fk.refSchema}.${fk.refTable}`);\n if (endpoints[0] === endpoints[1]) continue;\n if (endpoints.some((e) => e === `${rel.schema}.${rel.name}`)) continue;\n\n const targets = fks.map((fk) => relationAt(snapshot, fk.refSchema, fk.refTable));\n if (!targets.every((t) => t?.rlsEnabled)) continue;\n\n if (!isMostlyKeys(rel, fks.flatMap((fk) => fk.columns))) continue;\n\n findings.push(\n finding({\n id: ID,\n severity: \"high\",\n confidence: \"heuristic\",\n title:\n `${rel.schema}.${rel.name} joins ${endpoints[0]} to ${endpoints[1]}, ` +\n `and is the only one of the three without RLS`,\n target: { schema: rel.schema, table: rel.name },\n detail:\n `This table looks like a many-to-many join table: its columns are the ` +\n `foreign keys to ${endpoints[0]} and ${endpoints[1]} plus bookkeeping. Both of ` +\n `those tables have row-level security enabled; this one does not, so no policy ` +\n `is consulted when it is read or written.`,\n impact:\n `If this table is reachable over an API, a caller reads the full membership ` +\n `graph between ${endpoints[0]} and ${endpoints[1]} — which rows relate to which — ` +\n `even though neither endpoint table can be read directly. Where the join table ` +\n `is also writable, a caller can grant themselves a relationship that the policies ` +\n `on the endpoint tables then honour.`,\n fix:\n `ALTER TABLE ${qrel(rel.schema, rel.name)} ENABLE ROW LEVEL SECURITY;\\n` +\n `-- A join table's policy normally follows its endpoints, e.g.:\\n` +\n `-- CREATE POLICY ${JSON.stringify(`${rel.name}_select`)} ON ${qrel(rel.schema, rel.name)}\\n` +\n `-- FOR SELECT USING (EXISTS (\\n` +\n `-- SELECT 1 FROM ${endpoints[0]} e\\n` +\n `-- WHERE e.id = ${qrel(rel.schema, rel.name)}.${fks[0].columns[0]}\\n` +\n `-- ));`\n })\n );\n }\n\n return findings;\n }\n};\n\nfunction isMostlyKeys(rel: DbRelation, keyColumns: string[]): boolean {\n const keys = new Set(keyColumns.map((c) => c.toLowerCase()));\n return rel.columns.every((c) => {\n const name = c.name.toLowerCase();\n if (keys.has(name)) return true;\n if (INCIDENTAL_COLUMNS.has(name)) return true;\n // A timestamp column under any name is bookkeeping, not payload.\n return /^timestamp/i.test(c.type);\n });\n}\n","import type { Check, DbSnapshot, DbView, Finding } from \"../types\";\n\nimport { exposedGrantees, finding, listAnd, qrel, qrole, relationAt } from \"./util\";\n\nconst ID = \"view-bypasses-rls\";\n\n/** Base relations of `view` that have RLS turned on. */\nexport function protectedBaseTables(snapshot: DbSnapshot, view: DbView): string[] {\n return view.dependsOn\n .map((d) => relationAt(snapshot, d.schema, d.table))\n .filter((r): r is NonNullable<typeof r> => Boolean(r?.rlsEnabled))\n .map((r) => `${r.schema}.${r.name}`);\n}\n\n/**\n * A view granted to an untrusted role that reads an RLS-protected table without\n * `security_invoker`.\n *\n * A view without that option executes its query as the view's *owner*, and the\n * owner is normally exempt from the base table's policies — so the view hands\n * back exactly the rows the policies were written to withhold. This is the most\n * common way a carefully locked-down schema leaks: the tables are audited, the\n * views over them are not.\n *\n * Before PG 15 the option does not exist, which means every view behaves this\n * way. That is worth saying, but it is not evidence that *this* view is a\n * mistake, so those findings are marked heuristic.\n */\nexport const viewBypassesRls: Check = {\n id: ID,\n title: \"View reads past its base table's RLS\",\n description:\n \"A view granted to an untrusted role that selects from an RLS-protected table and runs \" +\n \"with its owner's privileges instead of the caller's.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const view of snapshot.views) {\n if (!snapshot.schemas.includes(view.schema)) continue;\n\n // `views` carries materialized views too, for their dependencies;\n // they cannot take security_invoker and have their own check.\n const rel = relationAt(snapshot, view.schema, view.name);\n if (rel && rel.kind !== \"view\") continue;\n\n if (view.securityInvoker === true) continue;\n\n const bases = protectedBaseTables(snapshot, view);\n if (bases.length === 0) continue;\n\n const exposed = exposedGrantees(snapshot, view.schema, view.name, [\"SELECT\"]);\n if (exposed.length === 0) continue;\n\n const roles = exposed.map((e) => e.role);\n const legacy = view.securityInvoker === null;\n\n findings.push(\n finding({\n id: ID,\n severity: \"critical\",\n confidence: legacy ? \"heuristic\" : \"certain\",\n title:\n `View ${view.schema}.${view.name} reads ${listAnd(bases)} without ` +\n `security_invoker and is readable by ${listAnd(roles)}`,\n target: { schema: view.schema, view: view.name, table: view.name },\n detail: legacy\n ? `This server is PostgreSQL ${snapshot.serverVersion}, where the ` +\n `\\`security_invoker\\` view option does not exist — every view runs with its ` +\n `owner's privileges. This view is owned by ${view.owner} and selects from ` +\n `${listAnd(bases)}, which ${bases.length > 1 ? \"have\" : \"has\"} row-level ` +\n `security enabled. Unless ${view.owner} is itself constrained by those ` +\n `policies (it is not, unless FORCE ROW LEVEL SECURITY is set), the view ` +\n `returns rows the policies would have filtered out.`\n : `The view is owned by ${view.owner} and \\`security_invoker\\` is not set, so its ` +\n `query executes with ${view.owner}'s privileges rather than the caller's. ` +\n `Row-level security on ${listAnd(bases)} is evaluated for ${view.owner}, not ` +\n `for the role that selected from the view.`,\n impact:\n `If this view is reachable over an API as ${listAnd(roles)}, a caller reads rows ` +\n `from ${listAnd(bases)} that the policies on ${bases.length > 1 ? \"those tables\" : \"that table\"} ` +\n `were written to withhold — the view is an unfiltered path around them.`,\n fix: legacy\n ? `-- \\`security_invoker\\` requires PostgreSQL 15 or newer. On this server, either:\\n` +\n `-- 1. revoke access and let callers query the base table directly:\\n` +\n `REVOKE SELECT ON ${qrel(view.schema, view.name)} FROM ${qrole(roles[0])};\\n` +\n `-- 2. or set FORCE ROW LEVEL SECURITY on the base tables and add policies\\n` +\n `-- that apply to ${view.owner}, so the view's own execution is filtered.`\n : `ALTER VIEW ${qrel(view.schema, view.name)} SET (security_invoker = true);\\n` +\n `-- Callers then need their own SELECT privilege on ${listAnd(bases)}, and the\\n` +\n `-- policies there apply to them.`\n })\n );\n }\n\n return findings;\n }\n};\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { exposedGrantees, finding, listAnd, qrel, qrole, relationAt } from \"./util\";\nimport { protectedBaseTables } from \"./view-bypasses-rls\";\n\nconst ID = \"matview-bypasses-rls\";\n\n/**\n * A materialized view over an RLS-protected table, readable by an untrusted role.\n *\n * Materialized views cannot have row-level security at all — `ALTER MATERIALIZED\n * VIEW ... ENABLE ROW LEVEL SECURITY` is not valid syntax, and `security_invoker`\n * does not apply either. The contents are a snapshot taken by the owner at\n * refresh time, so there is no policy anywhere that can filter a read of it. The\n * only controls are the grant and what the defining query selects.\n */\nexport const matviewBypassesRls: Check = {\n id: ID,\n title: \"Materialized view exposes RLS-protected data\",\n description:\n \"A materialized view granted to an untrusted role whose defining query reads a table \" +\n \"with row-level security enabled.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const view of snapshot.views) {\n if (!snapshot.schemas.includes(view.schema)) continue;\n\n const rel = relationAt(snapshot, view.schema, view.name);\n if (rel?.kind !== \"materialized_view\") continue;\n\n const bases = protectedBaseTables(snapshot, view);\n if (bases.length === 0) continue;\n\n const exposed = exposedGrantees(snapshot, view.schema, view.name, [\"SELECT\"]);\n if (exposed.length === 0) continue;\n\n const roles = exposed.map((e) => e.role);\n\n findings.push(\n finding({\n id: ID,\n severity: \"high\",\n confidence: \"certain\",\n title:\n `Materialized view ${view.schema}.${view.name} snapshots ${listAnd(bases)} ` +\n `and is readable by ${listAnd(roles)}`,\n target: { schema: view.schema, view: view.name, table: view.name },\n detail:\n `This materialized view reads ${listAnd(bases)}, which ` +\n `${bases.length > 1 ? \"have\" : \"has\"} row-level security enabled. Materialized ` +\n `views cannot have policies of their own, and \\`security_invoker\\` does not apply ` +\n `to them — the rows were computed once, by ${view.owner}, and are stored ` +\n `unfiltered. Every caller with SELECT reads the same stored rows.`,\n impact:\n `If this materialized view is reachable over an API as ${listAnd(roles)}, a caller ` +\n `reads a full copy of the protected data in ${listAnd(bases)} as of the last ` +\n `REFRESH. No policy can restrict this; only the grant can.`,\n fix:\n `-- There is no RLS for materialized views. Restrict the grant:\\n` +\n `REVOKE SELECT ON ${qrel(view.schema, view.name)} FROM ${qrole(roles[0])};\\n` +\n `-- and, if callers need this data, expose it through a view that applies the\\n` +\n `-- caller's own privileges:\\n` +\n `-- CREATE VIEW ${qrel(view.schema, `${view.name}_scoped`)} WITH (security_invoker = true)\\n` +\n `-- AS SELECT * FROM ${bases[0].split(\".\").map((p) => `\"${p}\"`).join(\".\")} WHERE ...;`\n })\n );\n }\n\n return findings;\n }\n};\n","import type { Check, DbPolicy, DbSnapshot, Finding, Severity } from \"../types\";\n\nimport { isUnconditionalTrue } from \"./sql\";\nimport {\n callerIdCall,\n finding,\n isRebaseManagedPolicy,\n listAnd,\n managedPolicyFix,\n policyTargetsExposedRole,\n qi,\n qrel,\n relationAt,\n rowsPhrase\n} from \"./util\";\n\nconst ID = \"policy-always-true\";\n\n/**\n * A PERMISSIVE policy whose expression is a constant truth, targeted at a role\n * an untrusted caller reaches.\n *\n * Matching is structural rather than textual — see {@link isUnconditionalTrue}.\n * `USING (true)` is flagged; `USING (is_public = true)` is not, and a\n * substring-matching version of this check would flag both.\n *\n * The one thing that legitimately rescues `USING (true)` is a RESTRICTIVE\n * policy on the same command, because RESTRICTIVE clauses are ANDed after the\n * PERMISSIVE ones are ORed. \"Permissive default, restrictive gate\" is a real\n * pattern, so when one is present the finding degrades to a question instead of\n * an accusation rather than disappearing (the restrictive policy may well not\n * cover the same rows).\n */\nexport const policyAlwaysTrue: Check = {\n id: ID,\n title: \"Policy grants unconditional access\",\n description: \"A permissive policy whose USING or WITH CHECK expression is always true.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const uidCall = callerIdCall(snapshot);\n const findings: Finding[] = [];\n\n for (const policy of snapshot.policies) {\n if (!snapshot.schemas.includes(policy.schema)) continue;\n if (!policy.permissive) continue;\n\n const exposed = policyTargetsExposedRole(snapshot, policy);\n if (exposed.length === 0) continue;\n\n const clauses: string[] = [];\n if (isUnconditionalTrue(policy.using)) clauses.push(\"USING\");\n if (isUnconditionalTrue(policy.withCheck)) clauses.push(\"WITH CHECK\");\n if (clauses.length === 0) continue;\n\n const gate = restrictiveGate(snapshot, policy);\n const rel = relationAt(snapshot, policy.schema, policy.table);\n const verb = policy.command === \"SELECT\" ? \"read\" : \"act on\";\n const severity: Severity = gate ? \"medium\" : \"critical\";\n\n findings.push(\n finding({\n id: ID,\n severity,\n confidence: gate ? \"heuristic\" : \"certain\",\n title:\n `Policy \"${policy.name}\" on ${policy.schema}.${policy.table} is ` +\n `${listAnd(clauses)} (true) for ${listAnd(exposed)}`,\n target: { schema: policy.schema, table: policy.table, policy: policy.name },\n detail:\n `This permissive ${policy.command} policy's ${listAnd(clauses)} expression is a ` +\n `constant truth, so it matches every row for ${listAnd(exposed)}. Permissive ` +\n `policies are ORed together, so this one alone satisfies the table's row filter ` +\n `no matter how strict the others are.` +\n (gate\n ? ` A RESTRICTIVE policy (\"${gate}\") also applies to this command and is ANDed ` +\n `after it, so access may still be gated — verify that restrictive policy ` +\n `covers the rows and roles you expect, because nothing else here does.`\n : \"\"),\n impact: gate\n ? `Row filtering on this table rests entirely on the RESTRICTIVE policy \"${gate}\". ` +\n `If it does not cover a case, ${listAnd(exposed)} can ${verb} every row${rowsPhrase(rel)}.`\n : `If this table is reachable over an API as ${listAnd(exposed)}, a caller can ` +\n `${verb} every row${rowsPhrase(rel)} — the policy applies no scoping whatsoever.`,\n fix: isRebaseManagedPolicy(snapshot, policy)\n ? managedPolicyFix(\n policy,\n `replace the rule that grants unconditional ${policy.command === \"SELECT\" ? \"reads\" : \"access\"} ` +\n `with one that scopes the rows — \\`ownerField\\`, \\`roles\\`, or a \\`condition\\``\n )\n : `-- Replace the constant with the scoping you intended, e.g.:\\n` +\n `ALTER POLICY ${qi(policy.name)} ON ${qrel(policy.schema, policy.table)}\\n` +\n ` ${clauses.includes(\"USING\") ? `USING (user_id = ${uidCall})` : `WITH CHECK (user_id = ${uidCall})`};\\n` +\n `-- or, if unconditional access really is intended, drop the policy and say so\\n` +\n `-- with an explicit grant instead:\\n` +\n `-- DROP POLICY ${qi(policy.name)} ON ${qrel(policy.schema, policy.table)};`\n })\n );\n }\n\n return findings;\n }\n};\n\n/** Name of a RESTRICTIVE policy that is ANDed with this one, if any. */\nfunction restrictiveGate(snapshot: DbSnapshot, policy: DbPolicy): string | null {\n const overlapping = snapshot.policies.find(\n (p) =>\n !p.permissive &&\n p.schema === policy.schema &&\n p.table === policy.table &&\n (p.command === \"ALL\" || policy.command === \"ALL\" || p.command === policy.command)\n );\n return overlapping?.name ?? null;\n}\n","/**\n * The contract between the three layers of `rls-check`:\n *\n * introspect.ts — reads the catalogs into a {@link DbSnapshot}. Talks to Postgres.\n * checks/*.ts — pure functions, snapshot in, {@link Finding}s out. No I/O.\n * report.ts — Findings to text or JSON. No knowledge of Postgres.\n *\n * Checks being pure is the point: every one of them is unit-testable against a\n * hand-written snapshot, so the test suite does not need a live database to\n * cover the interesting cases (it has a Docker-backed suite too, for the\n * introspection layer, which is the only part that can lie).\n */\n\n/** Ordered least → most severe; the CLI's `--fail-on` compares by index. */\nexport const SEVERITIES = [\"info\", \"low\", \"medium\", \"high\", \"critical\"] as const;\n\nexport type Severity = (typeof SEVERITIES)[number];\n\n/** What a finding points at. `table` is absent for database-wide findings. */\nexport interface FindingTarget {\n schema: string;\n table?: string;\n /** Policy name, for policy-level findings. */\n policy?: string;\n view?: string;\n routine?: string;\n column?: string;\n}\n\nexport interface Finding {\n /**\n * Stable, kebab-case check id — `rls-disabled`, `permissive-tautology`.\n * Stable because people put it in `--skip` lists and CI baselines; treat a\n * rename as a breaking change.\n */\n id: string;\n severity: Severity;\n /** One line, specific: names the object and what is wrong with it. */\n title: string;\n target: FindingTarget;\n /**\n * Why this is wrong, in concrete terms. Not \"RLS is recommended\" — say what\n * the database will actually do when a request arrives.\n */\n detail: string;\n /**\n * What an unauthenticated or wrong-tenant caller gets out of it. This is the\n * line people screenshot, so it must be true and not inflated: if reachability\n * depends on the API layer, say \"if this table is exposed over an API\".\n */\n impact: string;\n /** Copy-pasteable SQL, or a precise instruction when SQL cannot express it. */\n fix?: string;\n /** Anchor on https://rebase.pro/docs/rls-check#<id>, filled in by the check. */\n docs?: string;\n /**\n * How sure the check is. Heuristic checks (junction inference, unqualified\n * column detection) MUST set `\"heuristic\"` — the report separates them, and\n * a false positive in the confident bucket is what makes a tool like this\n * get uninstalled.\n */\n confidence: \"certain\" | \"heuristic\";\n}\n\n// ---------------------------------------------------------------------------\n// Snapshot — the read-only picture of the database the checks reason about.\n// ---------------------------------------------------------------------------\n\n/**\n * `pg_class.relkind`, spelled out.\n *\n * Named `RelationKind` until that collided with `RelationKind` in\n * `@rebasepro/types`, which is the cardinality of a *collection* relation\n * (`belongsTo` | `hasOne` | `hasMany` | `manyToMany` | `via`). Unrelated\n * meanings, and the shared word is \"relation\" in two different senses —\n * Postgres's (any table-like object) and the data model's (a link between\n * collections).\n */\nexport type PgRelationKind = \"table\" | \"partitioned_table\" | \"view\" | \"materialized_view\" | \"foreign_table\";\n\nexport interface DbRelation {\n schema: string;\n name: string;\n kind: PgRelationKind;\n owner: string;\n /** `pg_class.relrowsecurity`. Always false for views. */\n rlsEnabled: boolean;\n /** `pg_class.relforcerowsecurity` — without it the owner bypasses RLS. */\n rlsForced: boolean;\n columns: DbColumn[];\n /** Estimated live rows (`pg_class.reltuples`), -1 when never analyzed. */\n estimatedRows: number;\n}\n\nexport interface DbColumn {\n name: string;\n type: string;\n notNull: boolean;\n isPrimaryKey: boolean;\n}\n\nexport type PolicyCommand = \"ALL\" | \"SELECT\" | \"INSERT\" | \"UPDATE\" | \"DELETE\";\n\nexport interface DbPolicy {\n schema: string;\n table: string;\n name: string;\n /** PERMISSIVE policies OR together; RESTRICTIVE ones AND. */\n permissive: boolean;\n /** Roles in the TO clause. `[\"public\"]` means every role. */\n roles: string[];\n command: PolicyCommand;\n /** `pg_policies.qual` — the USING expression, as Postgres rewrote it. */\n using: string | null;\n /** `pg_policies.with_check`. */\n withCheck: string | null;\n}\n\nexport interface DbRole {\n name: string;\n canLogin: boolean;\n superuser: boolean;\n /** BYPASSRLS — every policy is a no-op for this role. */\n bypassRls: boolean;\n /** Roles this one is a member of, transitively resolved. */\n memberOf: string[];\n}\n\n/** One privilege grant on a relation, from `information_schema.role_table_grants`. */\nexport interface DbGrant {\n schema: string;\n table: string;\n grantee: string;\n privileges: (\"SELECT\" | \"INSERT\" | \"UPDATE\" | \"DELETE\" | \"TRUNCATE\" | \"REFERENCES\" | \"TRIGGER\")[];\n}\n\nexport interface DbView {\n schema: string;\n name: string;\n owner: string;\n /**\n * PG15+ `security_invoker=true`. When false, the view runs with the *owner's*\n * privileges, so it reads straight past the RLS on its base tables.\n * `null` on servers older than 15, where the option does not exist at all.\n */\n securityInvoker: boolean | null;\n /** Base relations the view reads, resolved through `pg_depend`. */\n dependsOn: { schema: string; table: string }[];\n}\n\nexport interface DbForeignKey {\n schema: string;\n table: string;\n columns: string[];\n refSchema: string;\n refTable: string;\n refColumns: string[];\n}\n\nexport interface DbRoutine {\n schema: string;\n name: string;\n owner: string;\n /** SECURITY DEFINER runs as the owner and can bypass RLS. */\n securityDefiner: boolean;\n /** True when `search_path` is NOT pinned in the routine's config. */\n mutableSearchPath: boolean;\n}\n\nexport interface DbSnapshot {\n /** `server_version_num`, e.g. 160004. */\n serverVersionNum: number;\n serverVersion: string;\n /** The role the scan itself connected as. */\n currentRole: string;\n /** True when the scanning role cannot be constrained by RLS — see report caveat. */\n scannerIsPrivileged: boolean;\n /** Schemas actually scanned, after `--schema` and the system-schema filter. */\n schemas: string[];\n /**\n * Roles that are plausibly reachable by an untrusted caller: Supabase's\n * `anon` / `authenticated`, PostgREST's `web_anon`, Rebase's `rebase_user`,\n * plus `PUBLIC` — and anything named with `--role`. Checks use this to\n * decide whether an exposure is real, so a stack whose app role is not on\n * that list must name it or the checks have nothing to gate on.\n */\n exposedRoles: string[];\n /** Detected platform, which changes the wording of several findings. */\n platform: \"supabase\" | \"neon\" | \"rebase\" | \"postgrest\" | \"unknown\";\n relations: DbRelation[];\n policies: DbPolicy[];\n roles: DbRole[];\n grants: DbGrant[];\n views: DbView[];\n foreignKeys: DbForeignKey[];\n routines: DbRoutine[];\n}\n\n// ---------------------------------------------------------------------------\n// Checks\n// ---------------------------------------------------------------------------\n\nexport interface Check {\n id: string;\n /** Shown in `--list-checks`. */\n title: string;\n /** One sentence on what the check looks for. */\n description: string;\n run(snapshot: DbSnapshot): Finding[];\n}\n\nexport interface ScanResult {\n /** ISO timestamp, stamped by the caller — checks stay pure. */\n scannedAt: string;\n /** Host and database only. NEVER the user, password or full URL. */\n database: { host: string; name: string };\n serverVersion: string;\n platform: DbSnapshot[\"platform\"];\n scannerIsPrivileged: boolean;\n /**\n * The roles every check gated on. Reported because it is the single fact\n * that decides whether a clean run means anything: a check only calls a\n * table exposed when one of these can reach it, so a reader who does not\n * see their own app role here knows the run did not cover their API.\n */\n exposedRoles: string[];\n stats: {\n schemas: number;\n tables: number;\n policies: number;\n tablesWithoutRls: number;\n checksRun: number;\n };\n findings: Finding[];\n /**\n * What the scan could not read.\n *\n * `introspect` records every catalogue query that failed, and `introspect()`\n * threw the record away before `scan` saw it — `ScanResult` had no field for\n * it, and neither the report nor the exit code mentioned it. A single failed\n * grants read silently disables `rls-disabled`, both view checks and\n * `anonymous-write-allowed`, and the run then prints \"✓ No unexpected RLS\n * findings\" and exits 0.\n *\n * A scanner that cannot say \"I could not look\" is worse than no scanner: it\n * answers the question it was asked with the wrong word.\n */\n diagnostics: {\n /** Catalogue reads that failed, and why. Non-empty means degraded. */\n degraded: { what: string; error: string }[];\n /** TLS certificate verification was turned off to connect. */\n tlsVerificationDisabled: boolean;\n /** Schemas left out of the scan, and why. */\n excludedSchemas: { schema: string; reason: \"system\" | \"platform\" | \"not-requested\" }[];\n /**\n * Roles holding read or write privileges on scanned tables that the scan\n * neither recognises as exposed nor can explain as trusted. Non-empty\n * means the exposed-role set may be incomplete, and every check gates on\n * that set — so this is the difference between \"clean\" and \"clean as far\n * as I could tell\".\n */\n unrecognizedGrantees: string[];\n /**\n * The role the scan connected as, when RLS constrains it and it was\n * therefore treated as exposed. `null` or absent when the scan connected\n * as a superuser, an owner or a BYPASSRLS role — the case\n * `scannerIsPrivileged` already describes.\n */\n scanningAsExposedRole?: string | null;\n };\n}\n","import type { Check, DbPolicy, DbSnapshot, Finding, Severity } from \"../types\";\nimport { SEVERITIES } from \"../types\";\n\nimport {\n callerIdCall,\n finding,\n isRebaseManagedPolicy,\n listAnd,\n managedPolicyFix,\n policyTargetsExposedRole,\n qi,\n qrel\n} from \"./util\";\n\nconst ID = \"policy-anonymous-tautology\";\n\n/**\n * `auth.uid() IS NOT NULL` and its relatives.\n *\n * What this expression *means* depends entirely on the stack in front of the\n * database, and getting that wrong would fire a critical finding on essentially\n * every Supabase project in existence:\n *\n * - Supabase: `auth.uid()` reads a JWT claim and returns NULL for an anonymous\n * caller, so the expression is a legitimate \"signed in\" test. Its only real\n * failing is that it does not scope rows to their owner — worth `low`, worded\n * as a design observation rather than a vulnerability.\n * - Rebase / PostgREST-style stacks that coerce a missing id to a sentinel\n * (`'anonymous'`, `''`): the expression is true for signed-out callers, so it\n * is a straight authentication bypass. This exact policy shipped in this\n * project and granted anonymous access for weeks.\n * - Anything else: it comes down to whether the stack coerces, which the\n * database cannot tell us. `medium`, and say so out loud.\n *\n * A guard that excludes the sentinel is the corrected form and clears the policy.\n * A guard that excludes *something else* is the interesting case — see\n * {@link matchTautology}.\n */\nexport const policyAnonymousTautology: Check = {\n id: ID,\n title: \"Policy only checks that a caller id exists\",\n description:\n \"A policy whose expression is `auth.uid() IS NOT NULL`-shaped: it separates signed-in \" +\n \"from signed-out callers but scopes no rows.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const uidCall = callerIdCall(snapshot);\n const findings: Finding[] = [];\n const { severity: baseSeverity, meaning, impactSuffix } = platformReading(snapshot.platform);\n\n for (const policy of snapshot.policies) {\n if (!snapshot.schemas.includes(policy.schema)) continue;\n if (!policy.permissive) continue;\n\n const exposed = policyTargetsExposedRole(snapshot, policy);\n if (exposed.length === 0) continue;\n\n const clauses: string[] = [];\n const usingMatch = matchTautology(policy.using);\n const checkMatch = matchTautology(policy.withCheck);\n if (usingMatch) clauses.push(\"USING\");\n if (checkMatch) clauses.push(\"WITH CHECK\");\n if (clauses.length === 0) continue;\n\n const shape = usingMatch?.shape ?? checkMatch?.shape ?? \"the caller id\";\n const decoys = [\n ...new Set([...(usingMatch?.decoyGuards ?? []), ...(checkMatch?.decoyGuards ?? [])])\n ];\n const severity = forCommand(baseSeverity, policy.command);\n const written = decoys.map((d) => `\\`${shape} <> '${d}'\\``);\n\n findings.push(\n finding({\n id: ID,\n severity,\n confidence: \"heuristic\",\n title:\n decoys.length > 0\n ? `Policy \"${policy.name}\" on ${policy.schema}.${policy.table} excludes ` +\n `${listAnd(decoys.map((d) => `'${d}'`))}, which is not the anonymous sentinel`\n : `Policy \"${policy.name}\" on ${policy.schema}.${policy.table} only checks ` +\n `that ${shape} is not null`,\n target: { schema: policy.schema, table: policy.table, policy: policy.name },\n detail:\n (decoys.length > 0\n ? `The ${listAnd(clauses)} expression of this ${policy.command} policy reads as ` +\n `\"signed in\": it tests that ${shape} is non-null and excludes ` +\n `${listAnd(written)}. But the id a signed-out caller actually arrives with is ` +\n `${listAnd(CLEARING_SENTINELS.map(describeSentinel))}, and neither is ` +\n `${listAnd(decoys.map((d) => `'${d}'`))} — so the guard excludes nobody and the ` +\n `null test stands on its own. `\n : `The ${listAnd(clauses)} expression of this ${policy.command} policy tests only ` +\n `that ${shape} is non-null. `) +\n `It does not compare anything to a column, so every row of the table satisfies it ` +\n `equally — the policy distinguishes signed-in from signed-out callers and nothing ` +\n `else. ${meaning}` +\n (policy.command === \"ALL\" || policy.command === \"UPDATE\" || policy.command === \"DELETE\"\n ? ` This policy governs ${policy.command === \"ALL\" ? \"every command, writes included\" : policy.command}, ` +\n `so the same expression decides who may change rows, not only who may read them.`\n : \"\"),\n impact:\n `Any caller for whom ${shape} is non-null can reach every row this policy covers, ` +\n `including rows belonging to other users or tenants. ${impactSuffix}` +\n (decoys.length > 0\n ? ` A policy in this shape reads as safe on review, which is why it survives: the ` +\n `guard is present, spelled plausibly, and matches nothing.`\n : \"\"),\n fix: isRebaseManagedPolicy(snapshot, policy)\n ? managedPolicyFix(\n policy,\n \"scope the rule to the row's owner (an `ownerField`) rather than to the existence \" +\n \"of an id — `access: \\\"authenticated\\\"` compiled to exactly this shape before 1.0, \" +\n \"so a database pushed then still carries it\"\n )\n : `-- Scope the policy to the row's owner rather than to the existence of an id:\\n` +\n `ALTER POLICY ${qi(policy.name)} ON ${qrel(policy.schema, policy.table)}\\n` +\n ` USING (user_id = ${uidCall});\\n` +\n `-- If the intent really is \"any signed-in user\", exclude every id your stack has\\n` +\n `-- ever used for \"nobody\" — one literal is not enough:\\n` +\n `-- USING (${uidCall} IS NOT NULL AND ${uidCall} <> ALL (ARRAY['anonymous', 'anon']));`\n })\n );\n }\n\n return findings;\n }\n};\n\n/**\n * Writes are worse than reads.\n *\n * The platform decides whether this expression is a bypass at all; the command\n * decides what it costs when it is. A `FOR ALL` policy in this shape governed\n * `UPDATE` and `DELETE` on a live users table — an anonymous PATCH setting\n * `roles: [\"admin\"]` was reachable through it — while the same predicate under\n * `FOR SELECT` would only have leaked. One step, not two: the platform reading\n * is still the dominant term.\n */\nfunction forCommand(base: Severity, command: DbPolicy[\"command\"]): Severity {\n if (command !== \"ALL\" && command !== \"UPDATE\" && command !== \"DELETE\") return base;\n return SEVERITIES[Math.min(SEVERITIES.indexOf(base) + 1, SEVERITIES.length - 1)];\n}\n\nconst describeSentinel = (s: string) => (s === \"\" ? \"the empty string\" : `'${s}'`);\n\nfunction platformReading(platform: DbSnapshot[\"platform\"]): {\n severity: Severity;\n meaning: string;\n impactSuffix: string;\n} {\n switch (platform) {\n case \"supabase\":\n return {\n severity: \"low\",\n meaning:\n `On Supabase, \\`auth.uid()\\` returns NULL for an anonymous request, so this is a ` +\n `working authenticated-only check rather than a bypass. It is listed because it ` +\n `only distinguishes signed-in from signed-out; it does not scope rows to their owner.`,\n impactSuffix:\n `Anonymous callers are correctly excluded on Supabase, so this is a data-scoping ` +\n `gap between signed-in users, not an anonymous-access hole.`\n };\n case \"rebase\":\n case \"postgrest\":\n return {\n severity: \"critical\",\n meaning:\n `On this stack a request without a session is given a sentinel id (an 'anonymous' ` +\n `string rather than NULL), so this expression is true for signed-out callers too — ` +\n `it authorises everyone.`,\n impactSuffix:\n `Because signed-out requests arrive with a sentinel id rather than NULL, this ` +\n `includes unauthenticated callers.`\n };\n default:\n return {\n severity: \"medium\",\n meaning:\n `Whether this excludes anonymous callers depends on the layer in front of the ` +\n `database: if it leaves the setting unset for signed-out requests the expression is ` +\n `false and this is merely loose; if it coerces them to a sentinel id (an empty ` +\n `string or 'anonymous') the expression is true and this authorises everyone.`,\n impactSuffix:\n `Whether unauthenticated callers are included depends on whether your stack coerces ` +\n `a missing caller id to a sentinel value — check that before judging the severity.`\n };\n }\n}\n\n/**\n * Both schema spellings, deliberately.\n *\n * This runs over policy bodies read back from a live database, and those outlive\n * the release that wrote them: Rebase moved its helpers from `auth` to `rebase`\n * at 1.0, so a database mid-migration holds both, and a Supabase database holds\n * `auth.*` for good. A security check that stops recognising a dangerous clause\n * because a schema was renamed is a check that silently turned itself off.\n */\nconst CALLER_ID_CALLS: { re: RegExp; label: (m: RegExpExecArray) => string }[] = [\n { re: /(?:rebase|auth)\\.uid\\s*\\(\\s*\\)/g, label: (m) => m[0].replace(/\\s+/g, \"\") },\n { re: /auth\\.role\\s*\\(\\s*\\)/g, label: () => \"auth.role()\" },\n { re: /(?:rebase|auth)\\.roles\\s*\\(\\s*\\)/g, label: (m) => m[0].replace(/\\s+/g, \"\") },\n { re: /(?:rebase|auth)\\.jwt\\s*\\(\\s*\\)/g, label: (m) => m[0].replace(/\\s+/g, \"\") },\n { re: /current_setting\\s*\\(\\s*('[^']*')\\s*(?:,\\s*[a-z]+\\s*)?\\)/g, label: (m) => `current_setting(${m[1]})` }\n];\n\n/**\n * The ids that a signed-out caller can actually arrive with, so excluding one of\n * them is a real guard.\n *\n * `'anonymous'` is Rebase's `ANONYMOUS_USER_ID`; the empty string is what a\n * PostgREST-shaped stack leaves an unset claim as. Deliberately **not** the whole\n * of Rebase's `ANONYMOUS_USER_IDS`: that list also carries `'anon'`, the id the\n * request path reported before the sentinel was unified, and a policy excluding\n * only `'anon'` does not exclude anyone on any server shipping today. Treating\n * `'anon'` as clearing is exactly the mistake this check now exists to catch.\n */\nconst CLEARING_SENTINELS = [\"anonymous\", \"\"];\n\nexport interface TautologyMatch {\n /** How to name the caller-id expression in prose, e.g. `auth.uid()`. */\n shape: string;\n /** Literals the policy excludes that exclude nobody. Empty for a bare null test. */\n decoyGuards: string[];\n /**\n * Whether the clause excludes an id a signed-out caller can actually arrive\n * with — the *corrected* form of this shape.\n *\n * This is the fork between two findings. `false` is\n * {@link policyAnonymousTautology}: the null test stands on its own, so\n * signed-out callers satisfy it. `true` is `policy-authenticated-tautology`:\n * signed-out callers really are excluded, and still no row is scoped, so\n * every registered account reads every row — the shape that left a\n * customer's `users` table readable by anyone who could sign up.\n *\n * Returning the distinction rather than treating the guarded form as a clean\n * bill of health is the whole point: the previous version answered `null`\n * here, which is why the policy that actually shipped passed silently.\n */\n guardsSentinel: boolean;\n}\n\n/**\n * Recognise \"the policy tests that a caller id exists, and nothing that narrows\n * which rows\" — and report which no-op guards it wears while doing so.\n *\n * Two rules keep this honest.\n *\n * **Every conjunct has to be accounted for.** `auth.uid() IS NOT NULL AND user_id\n * = auth.uid()` contains the shape and is a perfectly scoped policy; a substring\n * match would flag it, and flagging correct Supabase policies is the fastest way\n * to get this tool deleted. So the expression is split on `AND`, each conjunct is\n * classified, and a single conjunct this function does not recognise means it\n * stays quiet. An `OR` anywhere means the same — the shape no longer describes\n * what the policy admits.\n *\n * **A guard only clears if it excludes an id somebody can actually arrive with.**\n * The version before this one bailed on the literal string `<> 'anonymous'`, which\n * meant `<> 'anon'` fell past the bail and then failed to match the bare-null-test\n * shape, so the function returned null and the check said nothing at all. That is\n * not a near miss: it is the precise predicate that left a production `users`\n * table — password hashes included — readable by the entire internet for three and\n * a half weeks, and this tool was run against that database and reported clean.\n * A guard naming the wrong literal is now the *loudest* case, not the silent one,\n * because it is the one that survives code review.\n */\nexport function callerIdOnlyClause(clause: string | null | undefined): TautologyMatch | null {\n if (!clause) return null;\n\n const flat = clause\n .toLowerCase()\n // Casts first: `'anonymous'::text`, `array[...]::text[]`.\n .replace(/::\\s*[a-z0-9_]+(?:\\s*\\[\\s*\\])*/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n\n for (const { re, label } of CALLER_ID_CALLS) {\n const first = new RegExp(re.source).exec(flat);\n if (!first) continue;\n\n const substituted = flat.replace(new RegExp(re.source, \"g\"), \" callerid \");\n // `OR` changes what the policy admits; this shape no longer describes it.\n if (/\\bor\\b/.test(substituted)) return null;\n\n let sawNullTest = false;\n const decoys: string[] = [];\n let cleared = false;\n\n for (const raw of splitTopLevelAnd(substituted)) {\n const conjunct = canon(raw);\n if (conjunct === \"\") continue;\n\n if (conjunct === \"callerid is not null\") {\n sawNullTest = true;\n continue;\n }\n\n const excluded = excludedLiterals(conjunct);\n if (!excluded) return null; // something we do not understand: stay quiet\n if (excluded.some((lit) => CLEARING_SENTINELS.includes(lit))) cleared = true;\n decoys.push(...excluded);\n }\n\n if (!sawNullTest) continue;\n if (cleared) {\n return { shape: label(first), decoyGuards: [...new Set(decoys)], guardsSentinel: true };\n }\n\n return { shape: label(first), decoyGuards: [...new Set(decoys)], guardsSentinel: false };\n }\n\n return null;\n}\n\n/**\n * The subset of {@link callerIdOnlyClause} that *this* check reports: a bare\n * null test wearing no guard that excludes a real signed-out id.\n *\n * A clause that does exclude the sentinel is not clean, it is a different\n * finding with a different severity and a different fix, and\n * `policy-authenticated-tautology` reports it from the same parse.\n */\nfunction matchTautology(clause: string | null | undefined): TautologyMatch | null {\n const matched = callerIdOnlyClause(clause);\n return matched && !matched.guardsSentinel ? matched : null;\n}\n\n/**\n * Split on `AND`, counting parens.\n *\n * A plain `.split(/\\band\\b/)` is what the first attempt used and it does not\n * survive contact with Postgres, which reads an expression back fully\n * parenthesised: `((uid() IS NOT NULL) AND (uid() <> 'anon'))` splits into two\n * fragments with unbalanced parens, neither of which matches anything, so the\n * check goes quiet on precisely the input it exists for. Depth is tracked, quoted\n * literals are skipped so an `and` inside a string is not a separator, and each\n * part is re-split so nested conjunctions flatten.\n */\nfunction splitTopLevelAnd(expr: string): string[] {\n const s = peel(expr);\n const parts: string[] = [];\n let depth = 0;\n let inQuote = false;\n let start = 0;\n\n for (let i = 0; i < s.length; i++) {\n const ch = s[i];\n if (ch === \"'\") {\n inQuote = !inQuote;\n continue;\n }\n if (inQuote) continue;\n if (ch === \"(\") depth++;\n else if (ch === \")\") depth--;\n else if (depth === 0 && s.startsWith(\"and\", i) && isWord(s, i, 3)) {\n parts.push(s.slice(start, i));\n i += 2;\n start = i + 1;\n }\n }\n parts.push(s.slice(start));\n\n return parts.length === 1 ? parts : parts.flatMap(splitTopLevelAnd);\n}\n\n/** `and` as a whole word, not the tail of `brand` or the head of `android`. */\nfunction isWord(s: string, at: number, length: number): boolean {\n const before = at === 0 ? \"\" : s[at - 1];\n const after = s[at + length] ?? \"\";\n return !/[a-z0-9_]/.test(before) && !/[a-z0-9_]/.test(after);\n}\n\n/** Remove balanced wrapping parens: `((x))` -> `x`, but `(a) and (b)` is left alone. */\nfunction peel(fragment: string): string {\n let s = fragment.trim();\n while (s.startsWith(\"(\") && s.endsWith(\")\") && balanced(s.slice(1, -1))) {\n s = s.slice(1, -1).trim();\n }\n return s;\n}\n\n/** Strip the noise Postgres adds when it rewrites an expression. */\nfunction canon(fragment: string): string {\n let s = peel(\n fragment\n .replace(/\\bselect\\b/g, \" \")\n // `( SELECT rebase.uid() AS uid)` is how a wrapped call reads back.\n .replace(/\\bas [a-z0-9_]+/g, \" \")\n .replace(/\\s+/g, \" \")\n );\n\n let previous: string;\n do {\n previous = s;\n s = peel(s.replace(/\\(\\s*(callerid)\\s*\\)/g, \"$1\").trim());\n } while (s !== previous);\n\n return s.replace(/\\s+/g, \" \").trim();\n}\n\nfunction balanced(s: string): boolean {\n let depth = 0;\n for (const ch of s) {\n if (ch === \"(\") depth++;\n else if (ch === \")\" && --depth < 0) return false;\n }\n return depth === 0;\n}\n\n/**\n * The literals a conjunct excludes the caller id from, or `null` if the conjunct\n * is not an exclusion at all.\n *\n * `null` is the conservative answer and the common one: `user_id = callerid`\n * lands here and silences the whole check, which is correct, because that\n * conjunct scopes rows.\n */\nfunction excludedLiterals(conjunct: string): string[] | null {\n let m = /^callerid (?:<>|!=) '([^']*)'$/.exec(conjunct);\n if (m) return [m[1]];\n\n m = /^'([^']*)' (?:<>|!=) callerid$/.exec(conjunct);\n if (m) return [m[1]];\n\n m = /^callerid (?:<>|!=) all \\( ?array \\[(.*)\\] ?\\)$/.exec(conjunct);\n if (m) return literalList(m[1]);\n\n m = /^callerid not in \\((.*)\\)$/.exec(conjunct);\n if (m) return literalList(m[1]);\n\n return null;\n}\n\n/** `'a', 'b'` -> `[\"a\", \"b\"]`; `null` if any element is not a plain literal. */\nfunction literalList(inner: string): string[] | null {\n const out: string[] = [];\n for (const part of inner.split(\",\")) {\n const m = /^ ?'([^']*)' ?$/.exec(part);\n if (!m) return null;\n out.push(m[1]);\n }\n return out.length > 0 ? out : null;\n}\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport {\n callerIdCall,\n finding,\n isRebaseManagedPolicy,\n listAnd,\n managedPolicyFix,\n policyTargetsExposedRole,\n qi,\n qrel\n} from \"./util\";\nimport { callerIdOnlyClause } from \"./policy-anonymous-tautology\";\n\nconst ID = \"policy-authenticated-tautology\";\n\n/**\n * `auth.uid() IS NOT NULL AND auth.uid() <> 'anonymous'` — and nothing else.\n *\n * This is the *corrected* form of the anonymous tautology, and correcting that\n * one is where people stop. It genuinely does exclude signed-out callers. What\n * it does not do is scope any rows: what remains is \"every registered account\n * may read every row of this table\", which is a different sentence from the one\n * the person writing it usually means.\n *\n * It is the shape that leaked a customer's `users` table — every email address\n * on the platform, and the columns beside them, readable by anyone who could\n * sign up, which on a product with open registration is anyone at all. The\n * scanner watched for the anonymous form and treated the sentinel guard as a\n * clean bill of health, so the policy that actually shipped passed silently.\n *\n * `high`, not `critical`: it costs an account. On a table with open\n * registration that is a formality, and the wording says so rather than\n * pretending the distinction is comforting.\n *\n * Not folded into {@link policyAnonymousTautology}: check ids appear in\n * `--skip`, in CI baselines and in people's runbooks, so two findings with\n * different fixes and different severities have to be two ids. Someone who has\n * decided their `countries` table really is world-readable should be able to\n * silence that without also silencing \"signed-out callers can read it\".\n */\nexport const policyAuthenticatedTautology: Check = {\n id: ID,\n title: \"Policy admits every signed-in caller to every row\",\n description:\n \"A policy whose expression is only \\\"the caller is signed in and not anonymous\\\": it excludes \" +\n \"signed-out callers correctly and scopes no rows between accounts.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const uidCall = callerIdCall(snapshot);\n const findings: Finding[] = [];\n\n for (const policy of snapshot.policies) {\n if (!snapshot.schemas.includes(policy.schema)) continue;\n if (!policy.permissive) continue;\n\n const exposed = policyTargetsExposedRole(snapshot, policy);\n if (exposed.length === 0) continue;\n\n const usingMatch = callerIdOnlyClause(policy.using);\n const checkMatch = callerIdOnlyClause(policy.withCheck);\n\n const clauses: string[] = [];\n if (usingMatch?.guardsSentinel) clauses.push(\"USING\");\n if (checkMatch?.guardsSentinel) clauses.push(\"WITH CHECK\");\n if (clauses.length === 0) continue;\n\n const shape = (usingMatch?.guardsSentinel ? usingMatch.shape : checkMatch?.shape) ?? \"the caller id\";\n\n findings.push(\n finding({\n id: ID,\n severity: \"high\",\n confidence: \"heuristic\",\n title:\n `Policy \"${policy.name}\" on ${policy.schema}.${policy.table} admits every ` +\n \"signed-in caller to every row\",\n target: { schema: policy.schema, table: policy.table, policy: policy.name },\n detail:\n `The ${listAnd(clauses)} expression of this ${policy.command} policy tests that ` +\n `${shape} exists and is not the anonymous sentinel, and tests nothing else. That ` +\n \"correctly excludes signed-out callers — and it compares nothing to a column, so \" +\n \"every row of the table satisfies it equally for every account that does sign in.\",\n impact:\n \"Any user with an account reaches every row this policy covers, including rows \" +\n \"belonging to other users and other tenants. Where registration is open, \\\"any \" +\n \"user with an account\\\" is anybody who fills in a form. This is the shape that \" +\n \"makes a `users` table — every address on the platform — readable by its own \" +\n \"members.\",\n fix: isRebaseManagedPolicy(snapshot, policy)\n ? managedPolicyFix(\n policy,\n \"scope the rule to the row rather than to the existence of a session — \" +\n \"an `ownerField`, or a `condition` naming the group whose members may share rows\"\n )\n : \"-- Scope the policy to the row, rather than to the existence of a session:\\n\" +\n `ALTER POLICY ${qi(policy.name)} ON ${qrel(policy.schema, policy.table)}\\n` +\n ` USING (user_id = ${uidCall});\\n` +\n \"-- Or, where members of a shared group really may see each other's rows, say\\n\" +\n \"-- which group:\\n\" +\n `-- USING (EXISTS (SELECT 1 FROM memberships m\\n` +\n `-- WHERE m.org_id = ${policy.table}.org_id AND m.user_id = ${uidCall}));\\n` +\n \"-- If the table genuinely is readable by every account, keep this policy and\\n\" +\n `-- skip the finding: rls-check --skip ${ID}`\n })\n );\n }\n\n return findings;\n }\n};\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport {\n finding,\n isPublicRole,\n isRebaseManagedPolicy,\n managedPolicyFix,\n policiesFor,\n qi,\n qrel,\n sameRole,\n scannedTables\n} from \"./util\";\n\nconst ID = \"policy-role-unreachable\";\n\n/**\n * Every policy on a table names roles that nothing can arrive as.\n *\n * The symptom is a table that reads as empty while its policies look perfectly\n * correct. It happens most often when policies are copied from Supabase\n * documentation — `TO authenticated` — onto a database where requests arrive as\n * some other role entirely. Postgres accepts the policy, it simply never\n * applies, and RLS with no *applicable* policy denies everything.\n *\n * This project's own demo database sat in exactly this state for weeks.\n *\n * Reachability is judged from `pg_auth_members`, not from what a superuser could\n * theoretically SET ROLE to. A superuser can become any role without membership,\n * so counting that would make this check unable to fire at all — and it would be\n * answering the wrong question anyway, which is \"do the requests my application\n * sends land on these policies?\"\n */\nexport const policyRoleUnreachable: Check = {\n id: ID,\n title: \"Policies target roles nothing connects as\",\n description:\n \"Every policy on a table names roles that do not exist, cannot log in, and that no \" +\n \"login role inherits — so no policy ever applies and the table reads as empty.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const rel of scannedTables(snapshot)) {\n if (!rel.rlsEnabled) continue;\n\n const policies = policiesFor(snapshot, rel.schema, rel.name);\n if (policies.length === 0) continue; // rls-enabled-no-policies owns this\n\n const named = [...new Set(policies.flatMap((p) => p.roles))];\n if (named.some((role) => isReachable(snapshot, role))) continue;\n\n const missing = named.filter((role) => !snapshot.roles.some((r) => sameRole(r.name, role)));\n\n findings.push(\n finding({\n id: ID,\n severity: \"medium\",\n confidence: \"certain\",\n title:\n `All ${policies.length} ${policies.length === 1 ? \"policy\" : \"policies\"} on ` +\n `${rel.schema}.${rel.name} ${policies.length === 1 ? \"targets\" : \"target\"} unreachable ${named.length === 1 ? \"role\" : \"roles\"} ` +\n `(${named.join(\", \")})`,\n target: { schema: rel.schema, table: rel.name },\n detail:\n `Row-level security is enabled on this table and every policy names only ` +\n `${named.join(\", \")}. ` +\n (missing.length > 0\n ? `${missing.join(\", \")} ${missing.length === 1 ? \"does\" : \"do\"} not exist in ` +\n `pg_roles at all. `\n : \"\") +\n `None of these roles can log in, and no login role is a member of ` +\n `${named.length === 1 ? \"it\" : \"any of them\"}, so no session ever has these ` +\n `policies applied. RLS with no applicable policy denies every row.`,\n impact:\n `Reads of this table return zero rows for every application role, and writes are ` +\n `rejected. Nothing is exposed — the data is invisible instead, and it looks ` +\n `identical to an empty table, which is why this usually goes unnoticed for a long ` +\n `time. The owner (${rel.owner}) still sees everything` +\n `${rel.rlsForced ? \" unless FORCE ROW LEVEL SECURITY changes that\" : \", since FORCE ROW LEVEL SECURITY is not set\"}.`,\n fix: policies.every((p) => isRebaseManagedPolicy(snapshot, p))\n ? managedPolicyFix(\n policies[0],\n `name the role your requests actually arrive as in the rules' \\`roles\\` — ` +\n `confirm it with \\`SELECT current_user\\` from the application's own connection`\n )\n : `-- Point the policies at the role your requests actually arrive as. Confirm it with:\\n` +\n `-- SELECT current_user; -- run this from your application's connection\\n` +\n `ALTER POLICY ${qi(policies[0].name)} ON ${qrel(rel.schema, rel.name)} TO <that role>;\\n` +\n `-- Alternatively, if ${named[0]} is meant to be reachable, grant membership:\\n` +\n `-- GRANT ${qi(named[0])} TO <your login role>;`\n })\n );\n }\n\n return findings;\n }\n};\n\nfunction isReachable(snapshot: DbSnapshot, role: string): boolean {\n if (isPublicRole(role)) return true;\n\n const def = snapshot.roles.find((r) => sameRole(r.name, role));\n if (!def) return false;\n if (def.canLogin) return true;\n\n return snapshot.roles.some(\n (r) => r.canLogin && r.memberOf.some((m) => sameRole(m, role))\n );\n}\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { callerIdCall, DML, exposedGrantees, finding, listAnd, qrel, qrole, rowsPhrase, scannedTables } from \"./util\";\n\nconst ID = \"rls-disabled\";\n\n/**\n * A table with RLS off *and* a DML grant to a role an untrusted caller arrives as.\n *\n * The grant half is not a formality. Most databases contain plenty of tables\n * with RLS off that nothing outside the owner can address — migration bookkeeping,\n * lookup tables reachable only by the service role. Flagging those is how a\n * scanner produces forty findings on a healthy database and gets ignored, so a\n * table nobody exposed produces no finding at all.\n */\nexport const rlsDisabled: Check = {\n id: ID,\n title: \"Table exposed without row-level security\",\n description:\n \"A table with RLS disabled that also grants SELECT/INSERT/UPDATE/DELETE to a role \" +\n \"an unauthenticated or untrusted caller can reach.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const uidCall = callerIdCall(snapshot);\n const findings: Finding[] = [];\n\n for (const rel of scannedTables(snapshot)) {\n if (rel.rlsEnabled) continue;\n\n const exposed = exposedGrantees(snapshot, rel.schema, rel.name, DML);\n if (exposed.length === 0) continue;\n\n const roles = exposed.map((e) => e.role);\n const privileges = [...new Set(exposed.flatMap((e) => e.privileges))].sort();\n const canRead = privileges.includes(\"SELECT\");\n const writes = privileges.filter((p) => p !== \"SELECT\");\n\n const reach: string[] = [];\n if (canRead) reach.push(`read every row${rowsPhrase(rel)}`);\n if (writes.length > 0) reach.push(`${listAnd(writes.map((w) => w.toLowerCase()))} any row`);\n\n findings.push(\n finding({\n id: ID,\n severity: \"critical\",\n confidence: \"certain\",\n title: `${rel.schema}.${rel.name} has row-level security disabled and is granted to ${listAnd(roles)}`,\n target: { schema: rel.schema, table: rel.name },\n detail:\n `Row-level security is not enabled on this table, so Postgres applies no ` +\n `per-row filter at all — policies, if any exist, are never consulted. ` +\n `${listAnd(roles)} ${roles.length > 1 ? \"hold\" : \"holds\"} ` +\n `${listAnd(privileges)} on it.`,\n impact:\n `If this table is reachable over an API that connects as ${listAnd(roles)}, ` +\n `a caller can ${listAnd(reach)}, with no tenant or owner scoping.`,\n fix:\n `ALTER TABLE ${qrel(rel.schema, rel.name)} ENABLE ROW LEVEL SECURITY;\\n` +\n `-- Enabling RLS with no policies denies every row to everyone but the owner,\\n` +\n `-- so add the policy you intend in the same migration, for example:\\n` +\n `-- CREATE POLICY ${JSON.stringify(`${rel.name}_owner_select`)} ON ${qrel(rel.schema, rel.name)}\\n` +\n `-- FOR SELECT TO ${qrole(roles[0])} USING (user_id = ${uidCall});`\n })\n );\n }\n\n return findings;\n }\n};\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { callerIdCall, finding, policiesFor, qrel, scannedTables } from \"./util\";\n\nconst ID = \"rls-enabled-no-policies\";\n\n/**\n * RLS on, zero policies — deny-all for everyone except the table's owner.\n *\n * This is not a hole, it is a *correctness* bug, and an invisible one: the API\n * returns `[]` and an empty table is indistinguishable from a filtered one. It\n * is worth a finding precisely because nothing else reports it — a database in\n * this state passes every \"is RLS enabled?\" audit while serving nothing.\n */\nexport const rlsEnabledNoPolicies: Check = {\n id: ID,\n title: \"RLS enabled with no policies (denies everything)\",\n description: \"A table with row-level security enabled but not a single policy defined.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const uidCall = callerIdCall(snapshot);\n const findings: Finding[] = [];\n\n for (const rel of scannedTables(snapshot)) {\n if (!rel.rlsEnabled) continue;\n if (policiesFor(snapshot, rel.schema, rel.name).length > 0) continue;\n\n const ownerNote = rel.rlsForced\n ? \"FORCE ROW LEVEL SECURITY is also set, so even the owner sees nothing.\"\n : `The owner (${rel.owner}) still reads and writes the table normally, because ` +\n `FORCE ROW LEVEL SECURITY is not set — which is why this often looks fine from ` +\n `psql and returns nothing through the application.`;\n\n findings.push(\n finding({\n id: ID,\n severity: \"medium\",\n confidence: \"certain\",\n title: `${rel.schema}.${rel.name} has row-level security enabled but no policies`,\n target: { schema: rel.schema, table: rel.name },\n detail:\n `With RLS enabled and no policy defined, Postgres denies every row to every ` +\n `role. ${ownerNote}`,\n impact:\n `Reads of this table return zero rows and writes are rejected for every ` +\n `non-owner role. No data is exposed; data is silently missing instead, and ` +\n `an empty result is indistinguishable from a correctly filtered one.`,\n fix:\n `-- Either define the policy you intended:\\n` +\n `CREATE POLICY ${JSON.stringify(`${rel.name}_select`)} ON ${qrel(rel.schema, rel.name)}\\n` +\n ` FOR SELECT TO authenticated USING (user_id = ${uidCall});\\n` +\n `-- or, if this table is genuinely meant to be unreadable, drop the grants\\n` +\n `-- instead of relying on an empty policy set:\\n` +\n `-- REVOKE ALL ON ${qrel(rel.schema, rel.name)} FROM PUBLIC;`\n })\n );\n }\n\n return findings;\n }\n};\n","import type { Check, DbSnapshot, Finding, Severity } from \"../types\";\n\nimport { finding, qrel, sameRole, scannedTables } from \"./util\";\n\nconst ID = \"rls-enabled-not-forced\";\n\n/**\n * RLS on, FORCE off — the table owner is exempt from its own policies.\n *\n * How much that matters is entirely a question of *who the owner is*, and the\n * severity has to follow that or the check becomes noise:\n *\n * - Owner cannot log in: a provisioning role nothing connects as. Informational.\n * - Owner can log in and is otherwise ordinary: anything using that connection\n * string reads the whole table. This is the case worth waking up for.\n * - Owner is a superuser or has BYPASSRLS: FORCE would not help either way,\n * because those attributes skip RLS before ownership is even considered.\n * Reporting `high` here would be misleading — the fix is \"do not connect as\n * this role\", not \"set FORCE\".\n */\nexport const rlsEnabledNotForced: Check = {\n id: ID,\n title: \"RLS enabled but not forced for the table owner\",\n description: \"A table with RLS enabled where the owning role is exempt from its own policies.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const rel of scannedTables(snapshot)) {\n if (!rel.rlsEnabled || rel.rlsForced) continue;\n\n const owner = snapshot.roles.find((r) => sameRole(r.name, rel.owner));\n const bypasses = Boolean(owner?.superuser || owner?.bypassRls);\n const canLogin = Boolean(owner?.canLogin);\n\n let severity: Severity = \"medium\";\n let detail: string;\n let impact: string;\n let fix: string;\n\n if (bypasses) {\n severity = \"medium\";\n detail =\n `Policies on this table do not apply to its owner, ${rel.owner}, because ` +\n `FORCE ROW LEVEL SECURITY is not set. That role is additionally ` +\n `${owner?.superuser ? \"a superuser\" : \"marked BYPASSRLS\"}, so it skips row-level ` +\n `security on every table regardless of this setting — FORCE would not constrain it.`;\n impact =\n `Any connection made as ${rel.owner} reads and writes every row of this table, ` +\n `ignoring all policies. This is expected for an administrative role and dangerous ` +\n `only if an application connects with it.`;\n fix =\n `-- FORCE cannot constrain this role. Connect your application as a role that is\\n` +\n `-- neither the owner nor BYPASSRLS, and keep ${rel.owner} for migrations only.\\n` +\n `ALTER TABLE ${qrel(rel.schema, rel.name)} FORCE ROW LEVEL SECURITY; -- still worth setting`;\n } else if (canLogin) {\n severity = \"high\";\n detail =\n `FORCE ROW LEVEL SECURITY is not set, so the owning role ${rel.owner} is exempt ` +\n `from every policy on this table. ${rel.owner} can log in directly, which means a ` +\n `connection string for it bypasses all row filtering.`;\n impact =\n `Anything connecting as ${rel.owner} — including an application that was handed ` +\n `the owner's connection string, which is the default in most quick-start setups — ` +\n `reads and writes every row, ignoring the policies on this table.`;\n fix = `ALTER TABLE ${qrel(rel.schema, rel.name)} FORCE ROW LEVEL SECURITY;`;\n } else {\n severity = \"medium\";\n detail =\n `FORCE ROW LEVEL SECURITY is not set, so the owning role ${rel.owner} is exempt ` +\n `from every policy on this table. That role cannot log in` +\n `${owner ? \"\" : \" (it was not found in pg_roles, so it may have been dropped)\"}, ` +\n `so nothing connects as it directly today.`;\n impact =\n `No caller bypasses policies through ownership right now. If ${rel.owner} is ever ` +\n `granted LOGIN, or a role that can SET ROLE to it is used by an application, every ` +\n `policy on this table stops applying to that session.`;\n fix = `ALTER TABLE ${qrel(rel.schema, rel.name)} FORCE ROW LEVEL SECURITY;`;\n }\n\n findings.push(\n finding({\n id: ID,\n severity,\n confidence: \"certain\",\n title: `${rel.schema}.${rel.name}: policies do not apply to its owner ${rel.owner}`,\n target: { schema: rel.schema, table: rel.name },\n detail,\n impact,\n fix\n })\n );\n }\n\n return findings;\n }\n};\n","import type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { finding } from \"./util\";\n\nconst ID = \"security-definer-mutable-search-path\";\n\n/**\n * A SECURITY DEFINER routine whose `search_path` is not pinned.\n *\n * The routine executes as its owner — frequently a superuser or the schema owner\n * — while the *caller* decides what `public.some_table` or `now()` resolves to.\n * Anyone who can create objects in a schema on the caller's search_path can put\n * a shadowing function or table in front of one the routine uses, and it runs\n * with the owner's privileges. That includes reading and writing tables whose\n * RLS policies would otherwise have applied.\n *\n * This one is `certain`: it is a property of the catalog, not an inference.\n * Whether it is exploitable depends on who can create objects, which is why the\n * impact line says so rather than claiming a working escalation.\n */\nexport const securityDefinerMutableSearchPath: Check = {\n id: ID,\n title: \"SECURITY DEFINER routine with a mutable search_path\",\n description:\n \"A SECURITY DEFINER function or procedure that does not pin search_path, so the caller \" +\n \"controls how its identifiers resolve.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const routine of snapshot.routines) {\n if (!snapshot.schemas.includes(routine.schema)) continue;\n if (!routine.securityDefiner || !routine.mutableSearchPath) continue;\n\n findings.push(\n finding({\n id: ID,\n severity: \"medium\",\n confidence: \"certain\",\n title:\n `${routine.schema}.${routine.name} is SECURITY DEFINER and does not pin ` +\n `search_path`,\n target: { schema: routine.schema, routine: routine.name },\n detail:\n `This routine runs with the privileges of its owner (${routine.owner}) but ` +\n `resolves unqualified names using the *caller's* search_path, because no ` +\n `\\`SET search_path\\` is attached to it. A caller who can create objects in any ` +\n `schema on that path can shadow a table or function the routine uses, and their ` +\n `object then runs as ${routine.owner}.`,\n impact:\n `Any role that can both execute this routine and create objects in a schema on ` +\n `its search_path can have arbitrary SQL run as ${routine.owner} — which bypasses ` +\n `row-level security on every table ${routine.owner} can reach. Without CREATE on ` +\n `some schema this is not directly exploitable, so check who holds CREATE ` +\n `(commonly PUBLIC on \\`public\\` in older databases).`,\n fix:\n `-- Pin the search_path on every overload of this routine:\\n` +\n `DO $$\\n` +\n `DECLARE r record;\\n` +\n `BEGIN\\n` +\n ` FOR r IN\\n` +\n ` SELECT p.oid::regprocedure AS sig\\n` +\n ` FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace\\n` +\n ` WHERE n.nspname = ${literal(routine.schema)} AND p.proname = ${literal(routine.name)}\\n` +\n ` LOOP\\n` +\n ` EXECUTE format('ALTER ROUTINE %s SET search_path = pg_catalog, pg_temp', r.sig);\\n` +\n ` END LOOP;\\n` +\n `END $$;\\n` +\n `-- Then schema-qualify every identifier in the body, since nothing but\\n` +\n `-- pg_catalog is on the path any more.`\n })\n );\n }\n\n return findings;\n }\n};\n\nconst literal = (value: string): string => `'${value.replace(/'/g, \"''\")}'`;\n","import type { Check, DbPolicy, DbRelation, DbSnapshot, Finding } from \"../types\";\n\nimport { SQL_KEYWORDS, findSubqueries, isBareIdent, matchParen, tokenize, type SqlToken } from \"./sql\";\nimport { finding, hasColumn, isRebaseManagedPolicy, managedPolicyFix, qi, qrel, relationAt } from \"./util\";\n\nconst ID = \"unqualified-column-in-subquery\";\n\n/**\n * A bare column reference inside a policy subquery that resolves to the *inner*\n * relation when the outer row was meant.\n *\n * This is the bug this whole tool exists for. Postgres resolves an unqualified\n * name against the innermost scope that has it, silently, with no warning — so\n *\n * USING (EXISTS (SELECT 1 FROM org_members\n * WHERE user_id = auth.uid() AND organization_id = id))\n *\n * where the author meant `organizations.id` compares `org_members.organization_id`\n * to `org_members.id`. The correlation to the outer row disappears and the\n * predicate becomes something completely different — in the case this codebase\n * actually shipped, one that denied every row to everyone. Fail-open variants of\n * the same mistake are equally possible.\n *\n * Two deliberate restrictions keep the false-positive rate near zero:\n *\n * - Only the subquery's *predicate* is examined. A bare name in the select\n * list (`IN (SELECT org_id FROM members)`) is correct by construction.\n * - The bare name has to be compared against something that could be a column.\n * `WHERE user_id = auth.uid()` binds to the inner table on purpose, and\n * flagging it merely because the outer table also has a `user_id` would fire\n * on a large fraction of correct policies.\n *\n * Confidence is always heuristic, and the detail says why an absence proves\n * nothing: `pg_policies.qual` is Postgres's re-rendering of the parse tree, and\n * it normally re-qualifies references. A finding here therefore means the\n * ambiguity survived that rewrite, which is strong evidence — but a clean scan\n * is not proof that the original SQL was unambiguous.\n */\nexport const unqualifiedColumnInSubquery: Check = {\n id: ID,\n title: \"Unqualified column inside a policy subquery\",\n description:\n \"A bare column name in an EXISTS/IN subquery that exists on both the inner relation and \" +\n \"the policy's own table, so Postgres binds it to the inner one.\",\n\n run(snapshot: DbSnapshot): Finding[] {\n const findings: Finding[] = [];\n\n for (const policy of snapshot.policies) {\n if (!snapshot.schemas.includes(policy.schema)) continue;\n\n const outer = relationAt(snapshot, policy.schema, policy.table);\n if (!outer) continue;\n\n for (const clause of [\"USING\", \"WITH CHECK\"] as const) {\n const expr = clause === \"USING\" ? policy.using : policy.withCheck;\n if (!expr) continue;\n\n for (const hit of scanExpression(snapshot, outer, expr)) {\n findings.push(buildFinding(snapshot, policy, outer, clause, hit));\n }\n }\n }\n\n return findings;\n }\n};\n\ninterface Ambiguity {\n /** The bare name as written. */\n column: string;\n /** The relation Postgres binds it to. */\n inner: string;\n /** What it is compared against, for the report. */\n comparedTo: string;\n}\n\ninterface FromItem {\n schema?: string;\n name: string;\n alias?: string;\n}\n\nconst FROM_TERMINATORS = new Set([\n \"where\", \"group\", \"having\", \"order\", \"limit\", \"offset\", \"union\", \"intersect\",\n \"except\", \"window\", \"fetch\", \"returning\"\n]);\nconst JOIN_WORDS = new Set([\"join\", \"inner\", \"left\", \"right\", \"full\", \"outer\", \"cross\", \"natural\", \"lateral\"]);\nconst COMPARISONS = new Set([\"=\", \"<>\", \"!=\", \"<\", \">\", \"<=\", \">=\", \"~~\", \"!~\", \"is\", \"like\", \"ilike\", \"in\"]);\n\nfunction scanExpression(snapshot: DbSnapshot, outer: DbRelation, expr: string): Ambiguity[] {\n const tokens = tokenize(expr);\n const subqueries = findSubqueries(tokens);\n const seen = new Set<string>();\n const out: Ambiguity[] = [];\n\n for (const sub of subqueries) {\n // A subquery's own tokens exclude anything belonging to a subquery nested\n // inside it — those are analysed on their own pass, against their own FROM.\n const nested = subqueries.filter((s) => s !== sub && s.open > sub.open && s.close < sub.close);\n const own: number[] = [];\n for (let i = sub.open + 1; i < sub.close; i++) {\n if (nested.some((n) => i >= n.open && i <= n.close)) continue;\n own.push(i);\n }\n\n const from = parseFrom(tokens, own);\n if (from.length === 0) continue;\n\n const relations = from\n .map((item) => resolveRelation(snapshot, outer.schema, item))\n .filter((r): r is DbRelation => Boolean(r))\n // A self-reference has no \"outer vs inner\" distinction worth warning\n // about: the author selected from this very table, so binding to the\n // inner copy is the ordinary reading.\n .filter((r) => !(r.schema === outer.schema && r.name === outer.name));\n if (relations.length === 0) continue;\n\n const aliases = new Set(from.map((f) => f.alias).filter((a): a is string => Boolean(a)));\n\n for (const idx of predicateIndices(tokens, own)) {\n const token = tokens[idx];\n if (token.kind !== \"ident\" || token.quoted) continue;\n if (SQL_KEYWORDS.has(token.value) || aliases.has(token.value)) continue;\n if (!isBareIdent(tokens, idx)) continue;\n if (!hasColumn(outer, token.value)) continue;\n\n const inner = relations.find((r) => hasColumn(r, token.value));\n if (!inner) continue;\n\n const partner = comparisonPartner(tokens, own, idx);\n if (!partner) continue;\n\n const key = `${token.value}|${inner.schema}.${inner.name}`;\n if (seen.has(key)) continue;\n seen.add(key);\n\n out.push({ column: token.value, inner: `${inner.schema}.${inner.name}`, comparedTo: partner });\n }\n }\n\n return out;\n}\n\n/** Relation references in the subquery's FROM list, with their aliases. */\nfunction parseFrom(tokens: SqlToken[], own: number[]): FromItem[] {\n const start = own.findIndex((i) => tokens[i].kind === \"ident\" && tokens[i].value === \"from\");\n if (start === -1) return [];\n\n const items: FromItem[] = [];\n let expectRelation = true;\n\n for (let k = start + 1; k < own.length; k++) {\n const t = tokens[own[k]];\n\n if (t.kind === \"ident\" && FROM_TERMINATORS.has(t.value)) break;\n if (t.kind === \"punct\" && t.value === \",\") {\n expectRelation = true;\n continue;\n }\n if (t.kind === \"ident\" && JOIN_WORDS.has(t.value)) {\n expectRelation = true;\n continue;\n }\n // `ON <predicate>` / `USING (...)` — skip to the next join or comma.\n if (t.kind === \"ident\" && (t.value === \"on\" || t.value === \"using\")) {\n expectRelation = false;\n continue;\n }\n if (!expectRelation || t.kind !== \"ident\") continue;\n\n // schema.name[.…] — take the last two parts.\n const parts: string[] = [t.value];\n let k2 = k;\n while (\n tokens[own[k2 + 1]]?.kind === \"punct\" &&\n tokens[own[k2 + 1]]?.value === \".\" &&\n tokens[own[k2 + 2]]?.kind === \"ident\"\n ) {\n parts.push(tokens[own[k2 + 2]].value);\n k2 += 2;\n }\n\n const item: FromItem = parts.length > 1\n ? { schema: parts[parts.length - 2], name: parts[parts.length - 1] }\n : { name: parts[0] };\n\n // Optional alias, with or without AS.\n let after = tokens[own[k2 + 1]];\n if (after?.kind === \"ident\" && after.value === \"as\") {\n k2 += 1;\n after = tokens[own[k2 + 1]];\n }\n if (after?.kind === \"ident\" && !SQL_KEYWORDS.has(after.value) && !JOIN_WORDS.has(after.value)) {\n item.alias = after.value;\n k2 += 1;\n }\n\n items.push(item);\n k = k2;\n expectRelation = false;\n }\n\n return items;\n}\n\n/** Token indices that sit in a WHERE or JOIN … ON predicate of this subquery. */\nfunction predicateIndices(tokens: SqlToken[], own: number[]): number[] {\n const out: number[] = [];\n let inPredicate = false;\n\n for (let k = 0; k < own.length; k++) {\n const t = tokens[own[k]];\n if (t.kind === \"ident\") {\n if (t.value === \"where\" || t.value === \"on\") {\n inPredicate = true;\n continue;\n }\n if (FROM_TERMINATORS.has(t.value) && t.value !== \"where\") {\n inPredicate = false;\n continue;\n }\n if (JOIN_WORDS.has(t.value)) {\n inPredicate = false;\n continue;\n }\n }\n if (inPredicate) out.push(own[k]);\n }\n\n return out;\n}\n\n/**\n * What the bare name is compared with, or null when it is not compared with\n * something that could be a column.\n *\n * Literals, function calls and NULL tests are excluded on purpose: those\n * predicates mean what they say when bound to the inner relation, and the outer\n * table happening to share the column name is a coincidence, not a bug.\n */\nfunction comparisonPartner(tokens: SqlToken[], own: number[], idx: number): string | null {\n const pos = own.indexOf(idx);\n if (pos === -1) return null;\n\n const at = (offset: number): SqlToken | undefined => tokens[own[pos + offset]];\n\n let partnerStart: number;\n if (at(1)?.kind === \"op\" && COMPARISONS.has(at(1)!.value)) partnerStart = pos + 2;\n else if (at(1)?.kind === \"ident\" && COMPARISONS.has(at(1)!.value)) partnerStart = pos + 2;\n else if (at(-1)?.kind === \"op\" && COMPARISONS.has(at(-1)!.value)) partnerStart = pos - 2;\n else if (at(-1)?.kind === \"ident\" && COMPARISONS.has(at(-1)!.value)) partnerStart = pos - 2;\n else return null;\n\n const step = partnerStart > pos ? 1 : -1;\n const head = tokens[own[partnerStart]];\n if (!head || head.kind !== \"ident\") return null;\n if (head.value === \"null\" || head.value === \"true\" || head.value === \"false\") return null;\n\n // Walk the dotted name in whichever direction the partner lies.\n const parts: string[] = [head.value];\n let cursor = partnerStart;\n while (\n tokens[own[cursor + step]]?.kind === \"punct\" &&\n tokens[own[cursor + step]]?.value === \".\" &&\n tokens[own[cursor + 2 * step]]?.kind === \"ident\"\n ) {\n const next = tokens[own[cursor + 2 * step]].value;\n if (step > 0) parts.push(next);\n else parts.unshift(next);\n cursor += 2 * step;\n }\n\n // A trailing `(` makes it a function call, not a column.\n const tail = step > 0 ? tokens[own[cursor + 1]] : tokens[own[partnerStart + 1]];\n if (tail?.kind === \"punct\" && tail.value === \"(\") return null;\n\n return parts.join(\".\");\n}\n\nfunction resolveRelation(\n snapshot: DbSnapshot,\n policySchema: string,\n item: FromItem\n): DbRelation | undefined {\n if (item.schema) return relationAt(snapshot, item.schema, item.name);\n\n const local = relationAt(snapshot, policySchema, item.name);\n if (local) return local;\n\n // Unqualified and not in the policy's schema: only accept an unambiguous hit.\n const candidates = snapshot.relations.filter(\n (r) => r.name === item.name && snapshot.schemas.includes(r.schema)\n );\n return candidates.length === 1 ? candidates[0] : undefined;\n}\n\nfunction buildFinding(\n snapshot: DbSnapshot,\n policy: DbPolicy,\n outer: DbRelation,\n clause: \"USING\" | \"WITH CHECK\",\n hit: Ambiguity\n): Finding {\n return finding({\n id: ID,\n severity: \"high\",\n confidence: \"heuristic\",\n title:\n `Policy \"${policy.name}\" on ${policy.schema}.${policy.table}: does \\`${hit.column}\\` in ` +\n `the subquery mean ${outer.name}.${hit.column} or ${hit.inner}.${hit.column}?`,\n target: {\n schema: policy.schema,\n table: policy.table,\n policy: policy.name,\n column: hit.column\n },\n detail:\n `In the ${clause} expression, \\`${hit.column}\\` is written unqualified inside a subquery ` +\n `over ${hit.inner}, compared against \\`${hit.comparedTo}\\`. Both ${hit.inner} and ` +\n `${policy.schema}.${policy.table} have a column named \\`${hit.column}\\`, and Postgres ` +\n `resolves the bare name against the innermost scope that has it — so it binds to ` +\n `${hit.inner}.${hit.column}, not to the outer row. If the intent was to correlate the ` +\n `subquery with the row being checked, that correlation is not happening.\\n\\n` +\n `Note that \\`pg_policies\\` shows Postgres's own re-rendering of the policy, which usually ` +\n `re-qualifies column references. A match here means the ambiguity survived that rewrite, ` +\n `so it is strong evidence — but the absence of a match on other policies is not proof ` +\n `that they are unambiguous.`,\n impact:\n `The predicate does not mean what it reads like. Depending on the data it either matches ` +\n `far more rows than intended — exposing other users' or tenants' rows to anyone the policy ` +\n `applies to — or, if the inner comparison is never satisfiable, matches none, and the table ` +\n `silently returns empty results.`,\n fix: isRebaseManagedPolicy(snapshot, policy)\n ? managedPolicyFix(\n policy,\n `qualify every reference in the rule's condition so the binding is explicit — ` +\n `\\`${hit.inner}.${hit.column}\\` and \\`${policy.table}.${hit.column}\\` are different columns, ` +\n `and the bare name binds to the inner one`\n )\n : `-- Qualify every reference so the binding is explicit:\\n` +\n `ALTER POLICY ${qi(policy.name)} ON ${qrel(policy.schema, policy.table)}\\n` +\n ` ${clause === \"USING\" ? \"USING\" : \"WITH CHECK\"} (EXISTS (\\n` +\n ` SELECT 1 FROM ${hit.inner}\\n` +\n ` WHERE ${hit.inner}.${hit.comparedTo.includes(\".\") ? hit.comparedTo.split(\".\").pop() : hit.comparedTo}\\n` +\n ` = ${policy.table}.${hit.column}\\n` +\n ` ));\\n` +\n `-- Verify the intended direction first — this rewrite assumes the outer row was meant.`\n });\n}\n","/**\n * The check registry.\n *\n * Check ids are a public API: they appear in `--skip`, in CI baselines and in\n * people's documentation, so renaming one is a breaking change. Adding a check\n * is not, which is why the list is ordered by severity rather than alphabetically\n * — it is read by humans deciding what to look at first.\n */\nimport type { Check, DbSnapshot, Finding } from \"../types\";\n\nimport { anonymousWriteAllowed } from \"./anonymous-write-allowed\";\nimport { currentSettingThrows } from \"./current-setting-throws\";\nimport { grantToPublic } from \"./grant-to-public\";\nimport { junctionTableUnprotected } from \"./junction-table-unprotected\";\nimport { matviewBypassesRls } from \"./matview-bypasses-rls\";\nimport { policyAlwaysTrue } from \"./policy-always-true\";\nimport { policyAnonymousTautology } from \"./policy-anonymous-tautology\";\nimport { policyAuthenticatedTautology } from \"./policy-authenticated-tautology\";\nimport { policyRoleUnreachable } from \"./policy-role-unreachable\";\nimport { rlsDisabled } from \"./rls-disabled\";\nimport { rlsEnabledNoPolicies } from \"./rls-enabled-no-policies\";\nimport { rlsEnabledNotForced } from \"./rls-enabled-not-forced\";\nimport { securityDefinerMutableSearchPath } from \"./security-definer-mutable-search-path\";\nimport { unqualifiedColumnInSubquery } from \"./unqualified-column-in-subquery\";\nimport { viewBypassesRls } from \"./view-bypasses-rls\";\nimport { SEVERITY_ORDER, relationAt } from \"./util\";\n\nexport const CHECKS: Check[] = [\n rlsDisabled,\n policyAlwaysTrue,\n policyAnonymousTautology,\n policyAuthenticatedTautology,\n viewBypassesRls,\n matviewBypassesRls,\n anonymousWriteAllowed,\n unqualifiedColumnInSubquery,\n junctionTableUnprotected,\n rlsEnabledNotForced,\n rlsEnabledNoPolicies,\n policyRoleUnreachable,\n grantToPublic,\n securityDefinerMutableSearchPath,\n currentSettingThrows\n];\n\nexport interface RunOptions {\n /** Run only these check ids. */\n only?: string[];\n /** Skip these check ids. */\n skip?: string[];\n}\n\n/**\n * Run every selected check and return the findings, worst first.\n *\n * Never throws. A check that blows up on some shape of catalog nobody\n * anticipated degrades to a check that found nothing — reporting thirteen\n * findings is strictly better than reporting a crash, and this is a tool people\n * point at production and expect to be inert.\n */\nexport function runChecks(snapshot: DbSnapshot, opts: RunOptions = {}): Finding[] {\n const only = opts.only && opts.only.length > 0 ? new Set(opts.only) : null;\n const skip = new Set(opts.skip ?? []);\n\n const findings: Finding[] = [];\n for (const check of CHECKS) {\n if (only && !only.has(check.id)) continue;\n if (skip.has(check.id)) continue;\n\n try {\n findings.push(...check.run(snapshot));\n } catch {\n // Deliberately silent: `report.ts` owns what the user sees, and a\n // check that failed produced no findings, which is what the caller\n // is told by its absence.\n }\n }\n\n return sortFindings(snapshot, findings);\n}\n\n/**\n * Worst first, then biggest blast radius first.\n *\n * `estimatedRows` orders equally-severe findings so the table with a million\n * rows is read before the one with four. It never influences severity — a\n * never-analyzed table reports `reltuples = -1` and would otherwise be\n * systematically buried.\n */\nexport function sortFindings(snapshot: DbSnapshot, findings: Finding[]): Finding[] {\n const rows = (f: Finding): number => {\n if (!f.target.table) return -1;\n return relationAt(snapshot, f.target.schema, f.target.table)?.estimatedRows ?? -1;\n };\n\n return [...findings].sort((a, b) => {\n const bySeverity = SEVERITY_ORDER.indexOf(b.severity) - SEVERITY_ORDER.indexOf(a.severity);\n if (bySeverity !== 0) return bySeverity;\n\n const byRows = rows(b) - rows(a);\n if (byRows !== 0) return byRows;\n\n return (\n a.target.schema.localeCompare(b.target.schema) ||\n (a.target.table ?? \"\").localeCompare(b.target.table ?? \"\") ||\n a.id.localeCompare(b.id) ||\n a.title.localeCompare(b.title)\n );\n });\n}\n","/**\n * Connection-string redaction.\n *\n * This tool is pointed at production databases by people who have never run it\n * before, and its output ends up in CI logs, GitHub issues and screenshots. The\n * credential must therefore never survive into *any* output — not the report,\n * not an error, not a debug line. Everything printed goes through here.\n *\n * The parser is deliberately hand-rolled rather than `new URL()`: real-world\n * Postgres URLs carry passwords with unencoded `@`, `:` and `/` in them, which\n * `URL` either rejects or silently mis-splits. Getting that wrong once means\n * printing a password, so the splitting rules are explicit:\n *\n * - the authority ends at the first `/`, `?` or `#` after `://`\n * - the userinfo ends at the LAST `@` in the authority\n * - the user ends at the FIRST `:` in the userinfo\n *\n * A libpq keyword string (`host=... password=...`) is understood too, because\n * `psql \"$PGSTRING\"` users paste those. Understood here means *parsed*: `pg`\n * itself cannot read that form, so anything that opens a connection has to go\n * through {@link parseKeywordConnectionString} and build the `Client` options\n * by hand. Parsing it here and handing the raw string to the driver would\n * report failures against a host the user never named.\n */\n\n/** What replaces every credential. */\nexport const REDACTED = \"***\";\n\nexport interface ConnectionTarget {\n /** `postgres` / `postgresql`, or `postgresql` for keyword strings. */\n scheme: string;\n /** Hostname only, no port. `localhost` when the string omits it. */\n host: string;\n port: number | null;\n /** Database name, empty string when the string omits it. */\n database: string;\n user: string | null;\n password: string | null;\n}\n\n/** Query parameters worth keeping in the redacted form: informative, never secret. */\nconst SAFE_PARAMS = new Set([\"sslmode\", \"application_name\", \"connect_timeout\", \"target_session_attrs\"]);\n\nfunction decode(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\nfunction splitHostPort(hostport: string): { host: string; port: number | null } {\n // IPv6 literals are bracketed: [::1]:5432\n if (hostport.startsWith(\"[\")) {\n const close = hostport.indexOf(\"]\");\n if (close !== -1) {\n const host = hostport.slice(0, close + 1);\n const rest = hostport.slice(close + 1);\n const port = rest.startsWith(\":\") ? Number.parseInt(rest.slice(1), 10) : NaN;\n\n return { host, port: Number.isFinite(port) ? port : null };\n }\n }\n\n const colon = hostport.lastIndexOf(\":\");\n if (colon === -1) return { host: hostport, port: null };\n\n const port = Number.parseInt(hostport.slice(colon + 1), 10);\n if (!Number.isFinite(port)) return { host: hostport, port: null };\n\n return { host: hostport.slice(0, colon), port };\n}\n\n/**\n * The libpq keyword/value form (`host=… dbname=…`) as a map of lowercased\n * keyword to unquoted value, or `null` when the string is not that form.\n *\n * Exported because `pg` cannot read this form at all: `pg-connection-string`\n * only understands URLs, so a `Client({ connectionString: \"host=127.0.0.1\n * port=1 …\" })` connects to the *default* host and reports a failure against an\n * endpoint nobody asked for. Whoever opens the connection has to translate\n * these keywords into `Client` options itself — see `connect()` in\n * `introspect.ts`.\n */\nexport function parseKeywordConnectionString(raw: string): Map<string, string> | null {\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n // A `scheme://` string is a URL, whatever else it contains.\n if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\//.test(trimmed)) return null;\n\n // host=localhost port=5432 dbname=app user=me password='s3 cret'\n const pairs = trimmed.match(/(\\w+)\\s*=\\s*('(?:[^']|'')*'|[^\\s]+)/g);\n if (!pairs || pairs.length === 0) return null;\n\n const map = new Map<string, string>();\n for (const pair of pairs) {\n const eq = pair.indexOf(\"=\");\n const key = pair.slice(0, eq).trim().toLowerCase();\n let value = pair.slice(eq + 1).trim();\n if (value.startsWith(\"'\") && value.endsWith(\"'\") && value.length >= 2) {\n value = value.slice(1, -1).replace(/''/g, \"'\");\n }\n map.set(key, value);\n }\n\n if (!map.has(\"host\") && !map.has(\"dbname\") && !map.has(\"user\")) return null;\n\n return map;\n}\n\nfunction parseKeywordString(raw: string): ConnectionTarget | null {\n const map = parseKeywordConnectionString(raw);\n if (!map) return null;\n\n const port = map.has(\"port\") ? Number.parseInt(map.get(\"port\")!, 10) : NaN;\n\n return {\n scheme: \"postgresql\",\n host: map.get(\"host\") ?? \"localhost\",\n port: Number.isFinite(port) ? port : null,\n database: map.get(\"dbname\") ?? \"\",\n user: map.get(\"user\") ?? null,\n password: map.get(\"password\") ?? null\n };\n}\n\n/**\n * A hostname, an IPv4 literal, a bracketed IPv6 literal, or a Unix socket path.\n * Anything else means the split went wrong.\n */\nconst PLAUSIBLE_HOST = /^(\\[[0-9A-Fa-f:.]+\\]|[A-Za-z0-9._-]+|\\/[^\\s?#@]*)$/;\n\n/** A database name in a URL: no delimiters, because they were not encoded. */\nconst PLAUSIBLE_DATABASE = /^[^\\s:@?#/]*$/;\n\n/**\n * Split a connection string into its parts. Returns `null` when the input is\n * not recognisably a connection string — callers must treat that as \"unknown\",\n * never as \"safe to print\".\n */\nexport function parseConnectionString(raw: string): ConnectionTarget | null {\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n\n const schemeMatch = /^([a-zA-Z][a-zA-Z0-9+.-]*):\\/\\//.exec(trimmed);\n if (!schemeMatch) return parseKeywordString(trimmed);\n\n const scheme = schemeMatch[1];\n const afterScheme = trimmed.slice(schemeMatch[0].length);\n\n const pathStart = afterScheme.search(/[/?#]/);\n const authority = pathStart === -1 ? afterScheme : afterScheme.slice(0, pathStart);\n const remainder = pathStart === -1 ? \"\" : afterScheme.slice(pathStart);\n\n let user: string | null = null;\n let password: string | null = null;\n let hostport = authority;\n\n const at = authority.lastIndexOf(\"@\");\n if (at !== -1) {\n const userinfo = authority.slice(0, at);\n hostport = authority.slice(at + 1);\n const colon = userinfo.indexOf(\":\");\n if (colon === -1) {\n user = decode(userinfo);\n } else {\n user = decode(userinfo.slice(0, colon));\n password = decode(userinfo.slice(colon + 1));\n }\n }\n\n const { host, port } = splitHostPort(hostport);\n\n let database = \"\";\n if (remainder.startsWith(\"/\")) {\n const end = remainder.search(/[?#]/);\n database = decode(end === -1 ? remainder.slice(1) : remainder.slice(1, end));\n }\n\n // A password can also arrive as a query parameter, which is why the\n // redacted form only ever re-emits an allowlist of parameters.\n const queryStart = remainder.search(/[?#]/);\n if (queryStart !== -1 && password === null) {\n const params = new URLSearchParams(remainder.slice(queryStart + 1));\n const fromQuery = params.get(\"password\");\n if (fromQuery) password = fromQuery;\n if (user === null && params.get(\"user\")) user = params.get(\"user\");\n }\n\n // A password containing an unencoded `/`, `?` or `@` makes the authority\n // boundary ambiguous, and the split lands somewhere inside the credential.\n // The tell is a host or database that does not look like one — at which\n // point the only safe answer is \"unparseable\", because printing the pieces\n // would print fragments of the password. libpq is no happier with these\n // than we are; the fix on the user's side is percent-encoding.\n const normalisedHost = host.length > 0 ? host : \"localhost\";\n if (!PLAUSIBLE_HOST.test(normalisedHost) || !PLAUSIBLE_DATABASE.test(database)) return null;\n\n return {\n scheme,\n host: normalisedHost,\n port,\n database,\n user,\n password\n };\n}\n\n/** `host:port` if a port is known, otherwise just the host. Never a credential. */\nexport function formatEndpoint(target: ConnectionTarget | null): string {\n if (!target) return \"unknown host\";\n\n return target.port === null ? target.host : `${target.host}:${target.port}`;\n}\n\n/**\n * The display form of a connection string: scheme, host, port and database\n * kept; user and password replaced. Safe to print anywhere.\n */\nexport function redactConnectionString(raw: string): string {\n const target = parseConnectionString(raw);\n if (!target) return \"<connection string>\";\n\n const credential = target.user !== null || target.password !== null ? `${REDACTED}:${REDACTED}@` : \"\";\n const endpoint = formatEndpoint(target);\n const database = target.database.length > 0 ? `/${target.database}` : \"\";\n\n let query = \"\";\n const queryStart = raw.indexOf(\"?\");\n if (queryStart !== -1) {\n const kept: string[] = [];\n for (const [key, value] of new URLSearchParams(raw.slice(queryStart + 1))) {\n if (SAFE_PARAMS.has(key.toLowerCase())) kept.push(`${key}=${value}`);\n }\n if (kept.length > 0) query = `?${kept.join(\"&\")}`;\n }\n\n return `${target.scheme}://${credential}${endpoint}${database}${query}`;\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Scrub credentials out of arbitrary text — driver error messages, stack\n * traces, anything we did not author. Two passes:\n *\n * 1. exact: the connection string we were given, and its password, wherever\n * they appear, however they were quoted;\n * 2. generic: any `scheme://user:pass@host` shaped substring, which catches\n * strings we were never told about (a `pg` error quoting `PGPASSWORD`, a\n * nested cause carrying another URL).\n *\n * The generic pass is what makes this safe by default: forgetting to pass\n * `connectionString` degrades the redaction, it does not disable it.\n */\nexport function redactSecrets(text: string, connectionString?: string): string {\n let out = text;\n\n if (connectionString && connectionString.trim().length > 0) {\n const raw = connectionString.trim();\n out = out.split(raw).join(redactConnectionString(raw));\n\n const target = parseConnectionString(raw);\n\n // `password authentication failed for user \"postgres\"` — the driver\n // quotes the role name back at us. Only the quoted form is replaced,\n // because a bare global replace of a name like `app` would shred the\n // sentence around it.\n if (target?.user && target.user.length > 0) {\n out = out.replace(new RegExp(`\"${escapeRegExp(target.user)}\"`, \"g\"), `\"${REDACTED}\"`);\n }\n\n // Short passwords are left alone: a global replace of \"a\" would shred\n // the message and tell an attacker more than it hides.\n if (target?.password && target.password.length >= 3) {\n out = out.replace(new RegExp(escapeRegExp(target.password), \"g\"), REDACTED);\n const encoded = encodeURIComponent(target.password);\n if (encoded !== target.password) {\n out = out.replace(new RegExp(escapeRegExp(encoded), \"g\"), REDACTED);\n }\n }\n }\n\n // scheme://userinfo@host — the userinfo is always a credential.\n out = out.replace(\n /\\b([a-zA-Z][a-zA-Z0-9+.-]*):\\/\\/([^\\s\"'<>]*?)@/g,\n (_match, scheme: string) => `${scheme}://${REDACTED}:${REDACTED}@`\n );\n\n // libpq keyword form, in whatever quoting the message used.\n out = out.replace(/\\bpassword\\s*=\\s*('(?:[^']|'')*'|\"[^\"]*\"|\\S+)/gi, `password=${REDACTED}`);\n\n return out;\n}\n\n/**\n * Is this endpoint a local proxy or tunnel rather than the database itself?\n *\n * Matters for diagnosis: cloud-sql-proxy, an SSH -L forward and a local pooler\n * all accept the TCP connection and then hang up when *their* upstream auth\n * fails, which reaches the client as a bare ECONNRESET. Advice about TLS is\n * wrong there — the proxy terminates TLS itself, and the real cause is only in\n * its log.\n *\n * Takes the display form produced by `formatEndpoint`, so `host`, `host:port`\n * and `[::1]:5432` all work.\n */\nexport function isLoopbackEndpoint(endpoint: string): boolean {\n const { host } = splitHostPort(endpoint.trim());\n const bare = host.replace(/^\\[|]$/g, \"\").toLowerCase();\n\n if (bare === \"localhost\" || bare.endsWith(\".localhost\")) return true;\n // The whole 127.0.0.0/8 block is loopback, not just 127.0.0.1.\n if (/^127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(bare)) return true;\n\n return bare === \"::1\" || bare === \"0:0:0:0:0:0:0:1\";\n}\n","/**\n * Read the catalogs into a {@link DbSnapshot}.\n *\n * This is the only part of the tool that talks to a database, and it is written\n * on the assumption that it will be pointed at production by someone who has\n * never heard of us. So:\n *\n * - The session is opened `default_transaction_read_only` and the work runs\n * inside `BEGIN READ ONLY`. Not as a convention — as a guarantee, so that a\n * bug here cannot write.\n * - `statement_timeout` is set before anything else runs, so a scan of a large\n * catalog cannot pin a connection.\n * - Privileges are read from `pg_class.relacl` via `aclexplode`, not from\n * `information_schema.role_table_grants`. The information_schema views only\n * show grants involving roles the caller is a member of, so a non-superuser\n * scanning a Supabase database would not see the grants to `anon` at all —\n * it would report a clean bill of health on a wide-open table.\n * - Every optional query is guarded: a server that lacks a catalog degrades\n * that one fact, not the whole scan.\n */\nimport { Client } from \"pg\";\nimport type { ClientConfig } from \"pg\";\n\nimport { parseKeywordConnectionString } from \"./redact\";\nimport type {\n DbColumn,\n DbForeignKey,\n DbGrant,\n DbPolicy,\n DbRelation,\n DbRole,\n DbRoutine,\n DbSnapshot,\n DbView,\n PolicyCommand,\n PgRelationKind\n} from \"./types\";\n\nexport interface ConnectOptions {\n connectionString: string;\n /** Explicit schema allowlist. Default: every non-system schema. */\n schemas?: string[];\n /** Default 15000. Applied as statement_timeout. */\n statementTimeoutMs?: number;\n /**\n * Roles an untrusted caller arrives as, named by whoever runs the scan.\n *\n * Added to {@link CANDIDATE_EXPOSED_ROLES} rather than replacing it: a\n * union can only widen what the checks consider, and for a security scanner\n * the safe direction of a wrong guess is more findings, not fewer.\n */\n roles?: string[];\n}\n\n/**\n * Facts about *the scan* rather than about the database.\n *\n * `Finding`/`DbSnapshot` have nowhere to put these, and the report needs both:\n * a TLS downgrade must be disclosed, and \"we skipped 11 schemas\" is the\n * difference between a clean result and a misleading one.\n */\nexport interface IntrospectDiagnostics {\n /**\n * True when the server demanded TLS, presented a certificate this machine\n * could not verify, and the scan retried with verification off. Common with\n * managed Postgres behind a pooler — and never done silently.\n */\n tlsVerificationDisabled: boolean;\n /** Schemas that exist but were not scanned, with why. */\n excludedSchemas: { schema: string; reason: \"system\" | \"platform\" | \"not-requested\" }[];\n /** Catalog queries that failed and the fact they cost us. */\n degraded: { what: string; error: string }[];\n /**\n * Roles holding DML on scanned tables that the scan does not treat as\n * exposed, and cannot explain as trusted.\n *\n * The exposed-role set is a list of names this tool happens to know\n * ({@link CANDIDATE_EXPOSED_ROLES}). A database served by a role under any\n * other name — `app_user`, `api`, `hasura` — matches nothing, every check\n * gates on a grant to an exposed role, and the run prints \"No findings\" on\n * a wide-open database. That is a false negative, which is the one failure\n * mode a security scanner must not have quietly.\n *\n * So: anything that holds DML and is neither recognised nor demonstrably\n * privileged is reported, with the `--role` flag as the fix. The scan still\n * cannot know whether such a role is reachable — but it can refuse to\n * imply that it looked.\n */\n unrecognizedGrantees: string[];\n /**\n * The connecting role, when it is not privileged and was therefore added to\n * the exposed set. Disclosed rather than silent: it changes which findings\n * appear, so a reader comparing two runs has to be able to see it.\n */\n scanningAsExposedRole?: string | null;\n}\n\nexport interface IntrospectResult {\n snapshot: DbSnapshot;\n diagnostics: IntrospectDiagnostics;\n}\n\n/**\n * A `--role` that is not in `pg_roles`.\n *\n * Same reasoning as an unknown `--skip` id: a typo here does not widen the scan\n * and get noticed, it *narrows* it silently. `exposedRolesFor` drops the name,\n * every check that gates on a grant to an exposed role stops matching, and the\n * run prints \"No findings\" on a database nobody looked at properly. So it is an\n * error, not a warning.\n */\nexport class UnknownRoleError extends Error {\n readonly roles: string[];\n\n constructor(roles: string[]) {\n super(`Unknown role${roles.length === 1 ? \"\" : \"s\"}: ${roles.join(\", \")}.`);\n this.name = \"UnknownRoleError\";\n this.roles = roles;\n }\n}\n\nconst SYSTEM_SCHEMAS = [\"pg_catalog\", \"information_schema\", \"pg_toast\"];\n\n/**\n * Schemas owned by the platform rather than by the person running the scan.\n *\n * Excluding these is the single biggest signal decision in the tool. Supabase's\n * `auth` and `storage` schemas contain dozens of tables with grants and no RLS\n * by design; a scan that reports forty findings inside them buries the one\n * finding in `public` that actually matters, and the reader concludes the tool\n * is wrong. They are still scannable — `--schema auth` opts back in.\n *\n * `rebase` is deliberately NOT on this list, though it is as much \"ours\" as\n * `auth` is Supabase's. It held refresh-token hashes, TOTP secrets and API-key\n * rows with RLS off and full DML granted to `rebase_user` — the exact shape\n * `rls-disabled` exists to catch — and this exclusion is why no scan ever said\n * so. Those grants are revoked at creation now (`revokeInternalTableSql`), so\n * scanning the schema is quiet; if a new internal table ships without its\n * revoke, `pnpm rls:check` fails instead of a reviewer having to notice. The\n * checks all gate on a grant to a reachable role, so an unexposed table still\n * produces nothing.\n */\nconst PLATFORM_SCHEMAS = [\n // Supabase\n \"auth\", \"storage\", \"realtime\", \"_realtime\", \"vault\", \"extensions\",\n \"graphql\", \"graphql_public\", \"supabase_functions\", \"supabase_migrations\",\n \"net\", \"cron\", \"pgsodium\", \"pgsodium_masks\",\n // Rebase\n \"drizzle\"\n];\n\nconst RELATION_KINDS: Record<string, PgRelationKind> = {\n r: \"table\",\n p: \"partitioned_table\",\n v: \"view\",\n m: \"materialized_view\",\n f: \"foreign_table\"\n};\n\nconst SCANNED_RELKINDS = [\"r\", \"p\", \"v\", \"m\", \"f\"];\n\n/**\n * Roles an untrusted request plausibly arrives as. `service_role` is the\n * trusted bypass.\n *\n * These are recognised *by name*, which is the one place this tool is not\n * framework-agnostic: it knows Supabase's pair, PostgREST's `web_anon` and\n * Rebase's `rebase_user`, and nothing else. Every check gates on a grant to an\n * exposed role, so a database served by `app_user` used to scan clean no\n * matter how open it was.\n *\n * Two things close that hole, and neither is a guess about your schema:\n * `--role` names the role explicitly, and `unrecognizedGranteesFor` reports\n * write-holding roles that matched nothing here, so an unrecognised setup\n * produces a caveat instead of a green checkmark.\n */\nconst CANDIDATE_EXPOSED_ROLES = [\"anon\", \"authenticated\", \"web_anon\", \"rebase_user\"];\n\nexport async function introspect(opts: ConnectOptions): Promise<DbSnapshot> {\n return (await introspectWithDiagnostics(opts)).snapshot;\n}\n\nexport async function introspectWithDiagnostics(opts: ConnectOptions): Promise<IntrospectResult> {\n const diagnostics: IntrospectDiagnostics = {\n tlsVerificationDisabled: false,\n excludedSchemas: [],\n degraded: [],\n unrecognizedGrantees: []\n };\n\n const { client, tlsVerificationDisabled } = await connect(opts.connectionString);\n diagnostics.tlsVerificationDisabled = tlsVerificationDisabled;\n\n try {\n const timeout = Math.max(1000, Math.floor(opts.statementTimeoutMs ?? 15000));\n await client.query(`SET statement_timeout = ${timeout}`);\n await client.query(\"SET default_transaction_read_only = on\");\n await client.query(\"BEGIN READ ONLY\");\n\n try {\n const snapshot = await readSnapshot(client, opts, diagnostics);\n await client.query(\"COMMIT\");\n return { snapshot, diagnostics };\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n throw error;\n }\n } finally {\n await client.end().catch(() => undefined);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Connecting\n// ---------------------------------------------------------------------------\n\ntype SslOption = false | { rejectUnauthorized: boolean };\n\n/**\n * libpq keywords this tool can express as `pg` `Client` options.\n *\n * The list is an allowlist rather than a \"map what we know and drop the rest\",\n * because every keyword left out changes where the connection goes or how it is\n * verified: dropping `sslrootcert` would connect with weaker verification than\n * was asked for, and dropping `hostaddr` would connect to a different machine.\n * Silently doing either in a security scanner is worse than refusing.\n */\nconst SUPPORTED_KEYWORDS = new Set([\n \"host\",\n \"port\",\n \"dbname\",\n \"user\",\n \"password\",\n \"sslmode\",\n \"application_name\",\n \"connect_timeout\",\n \"options\"\n]);\n\n/**\n * The keywords in a libpq keyword string that this tool cannot honour, in the\n * order they were written. Empty for a URL, and empty for a keyword string it\n * can translate in full.\n */\nexport function unsupportedConnectionKeywords(connectionString: string): string[] {\n const keywords = parseKeywordConnectionString(connectionString);\n if (!keywords) return [];\n\n return [...keywords.keys()].filter((keyword) => !SUPPORTED_KEYWORDS.has(keyword));\n}\n\n/**\n * `Client` options for a libpq keyword string. Throws on a keyword it cannot\n * honour. Exported for the test that pins the translation: getting `host` or\n * `port` wrong here sends a production scan somewhere nobody asked for, and the\n * failure would still be reported against the host the user typed.\n */\nexport function clientConfigFromKeywords(keywords: Map<string, string>): ClientConfig {\n const unsupported = [...keywords.keys()].filter((keyword) => !SUPPORTED_KEYWORDS.has(keyword));\n if (unsupported.length > 0) {\n throw new Error(\n `Unsupported connection keyword${unsupported.length === 1 ? \"\" : \"s\"}: ${unsupported.join(\", \")}. Use a postgresql:// URL instead.`\n );\n }\n\n const port = keywords.has(\"port\") ? Number.parseInt(keywords.get(\"port\")!, 10) : NaN;\n const timeoutSeconds = keywords.has(\"connect_timeout\")\n ? Number.parseInt(keywords.get(\"connect_timeout\")!, 10)\n : NaN;\n\n const config: ClientConfig = {\n host: keywords.get(\"host\") ?? \"localhost\",\n database: keywords.get(\"dbname\"),\n user: keywords.get(\"user\"),\n password: keywords.get(\"password\"),\n application_name: keywords.get(\"application_name\"),\n options: keywords.get(\"options\")\n };\n if (Number.isFinite(port)) config.port = port;\n if (Number.isFinite(timeoutSeconds)) config.connectionTimeoutMillis = timeoutSeconds * 1000;\n\n return config;\n}\n\n/**\n * Connect, negotiating TLS the way libpq would.\n *\n * `pg` lets the connection string override an explicit `ssl` option, so the\n * `sslmode` parameter is read and removed before the attempts are built —\n * otherwise the retry would silently reuse the setting that just failed.\n *\n * `pg` also cannot read the libpq keyword form (`host=… dbname=…`) at all — it\n * would connect to the default host and then report the failure against the\n * host the user *did* name, which is the worst of both. So that form is\n * translated into explicit `Client` options here, or refused.\n */\nasync function connect(connectionString: string): Promise<{ client: Client; tlsVerificationDisabled: boolean }> {\n const keywords = parseKeywordConnectionString(connectionString);\n const sslmode = keywords ? keywords.get(\"sslmode\")?.toLowerCase() : readParam(connectionString, \"sslmode\");\n const base: ClientConfig = keywords\n ? clientConfigFromKeywords(keywords)\n : { connectionString: stripParam(connectionString, \"sslmode\") };\n\n // Each entry is (ssl option, did we give up verification to get here?).\n let attempts: { ssl: SslOption; downgraded: boolean }[];\n switch (sslmode) {\n case \"disable\":\n attempts = [{ ssl: false, downgraded: false }];\n break;\n case \"require\":\n case \"no-verify\":\n // libpq's `require` encrypts without verifying. Honouring that is not\n // a downgrade — it is what the connection string asked for.\n attempts = [{ ssl: { rejectUnauthorized: false }, downgraded: false }];\n break;\n case \"verify-ca\":\n case \"verify-full\":\n // An explicit request to verify is never quietly relaxed.\n attempts = [{ ssl: { rejectUnauthorized: true }, downgraded: false }];\n break;\n default:\n attempts = [\n { ssl: false, downgraded: false },\n { ssl: { rejectUnauthorized: true }, downgraded: false },\n { ssl: { rejectUnauthorized: false }, downgraded: true }\n ];\n }\n\n let lastError: unknown;\n for (const attempt of attempts) {\n const client = new Client({ ...base, ssl: attempt.ssl });\n try {\n await client.connect();\n return { client, tlsVerificationDisabled: attempt.downgraded };\n } catch (error) {\n lastError = error;\n await client.end().catch(() => undefined);\n if (!isTlsNegotiationError(error)) throw error;\n }\n }\n\n throw lastError;\n}\n\n/** Errors that mean \"try a different TLS posture\", as opposed to a real failure. */\nfunction isTlsNegotiationError(error: unknown): boolean {\n const err = error as { code?: string; message?: string };\n const code = String(err?.code ?? \"\");\n const message = String(err?.message ?? \"\");\n\n if (\n [\n \"DEPTH_ZERO_SELF_SIGNED_CERT\",\n \"SELF_SIGNED_CERT_IN_CHAIN\",\n \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\",\n \"UNABLE_TO_GET_ISSUER_CERT_LOCALLY\",\n \"CERT_HAS_EXPIRED\",\n \"ERR_TLS_CERT_ALTNAME_INVALID\"\n ].includes(code)\n ) {\n return true;\n }\n\n return /SSL|TLS|certificate|self.signed|pg_hba\\.conf entry .*SSL|sslmode/i.test(message);\n}\n\nfunction readParam(connectionString: string, name: string): string | undefined {\n const match = new RegExp(`[?&]${name}=([^&]*)`, \"i\").exec(connectionString);\n return match ? decodeURIComponent(match[1]).toLowerCase() : undefined;\n}\n\nfunction stripParam(connectionString: string, name: string): string {\n return connectionString\n .replace(new RegExp(`([?&])${name}=[^&]*&?`, \"gi\"), \"$1\")\n .replace(/[?&]$/, \"\");\n}\n\n// ---------------------------------------------------------------------------\n// Reading\n// ---------------------------------------------------------------------------\n\ninterface Reader {\n query<R extends Record<string, unknown>>(\n what: string,\n text: string,\n values?: unknown[]\n ): Promise<R[]>;\n}\n\nfunction reader(client: Client, diagnostics: IntrospectDiagnostics): Reader {\n return {\n async query(what, text, values) {\n try {\n const result = await client.query(text, values);\n return result.rows;\n } catch (error) {\n // One catalog we could not read is a degraded fact, not a failed\n // scan — a server missing `pg_policies` should still tell us\n // which tables have RLS off.\n diagnostics.degraded.push({\n what,\n error: error instanceof Error ? error.message : String(error)\n });\n await client.query(\"ROLLBACK\").catch(() => undefined);\n await client.query(\"BEGIN READ ONLY\").catch(() => undefined);\n return [];\n }\n }\n };\n}\n\nasync function readSnapshot(\n client: Client,\n opts: ConnectOptions,\n diagnostics: IntrospectDiagnostics\n): Promise<DbSnapshot> {\n const db = reader(client, diagnostics);\n\n const [server] = await db.query<{\n version_num: string | number;\n version: string;\n role_name: string;\n is_super: boolean | null;\n bypass_rls: boolean | null;\n }>(\n \"server version and current role\",\n `SELECT current_setting('server_version_num') AS version_num,\n current_setting('server_version') AS version,\n current_user AS role_name,\n (SELECT rolsuper FROM pg_roles WHERE rolname = current_user) AS is_super,\n (SELECT rolbypassrls FROM pg_roles WHERE rolname = current_user) AS bypass_rls`\n );\n\n const serverVersionNum = Number(server?.version_num ?? 0);\n const serverVersion = String(server?.version ?? \"unknown\");\n const currentRole = String(server?.role_name ?? \"unknown\");\n\n const allSchemas = (\n await db.query<{ nspname: string }>(\n \"schema list\",\n \"SELECT nspname FROM pg_namespace ORDER BY nspname\"\n )\n ).map((r) => r.nspname);\n\n const schemas = selectSchemas(allSchemas, opts.schemas, diagnostics);\n\n const roles = await readRoles(db);\n assertRolesExist(roles, opts.roles, diagnostics);\n\n const relations = await readRelations(db, schemas);\n const policies = await readPolicies(db, schemas);\n const grants = await readGrants(db, schemas);\n const views = await readViews(db, schemas, serverVersionNum, relations);\n const foreignKeys = await readForeignKeys(db, schemas);\n const routines = await readRoutines(db, schemas);\n\n const scannerIsPrivileged = isPrivileged(currentRole, server, roles, relations);\n\n // An unprivileged connecting role IS an exposed role for the purposes of\n // this scan: RLS constrains it, it holds whatever the API role holds, and\n // the overwhelmingly common way to run this tool without a superuser\n // password is to run it as the application's own role. Leaving it out meant\n // scanning as `app_user` on a table granted only to `app_user` reported\n // nothing at all. `scannerIsPrivileged` already gates the other direction:\n // a superuser or owner is not \"exposed\", it is the reason the caveat exists.\n const connectingRole = !scannerIsPrivileged && currentRole !== \"unknown\" ? currentRole : undefined;\n const exposedRoles = exposedRolesFor(roles, opts.roles, connectingRole);\n diagnostics.scanningAsExposedRole = connectingRole && exposedRoles.includes(connectingRole)\n ? connectingRole\n : null;\n diagnostics.unrecognizedGrantees = unrecognizedGranteesFor(\n grants,\n roles,\n relations,\n exposedRoles,\n currentRole\n );\n\n return {\n serverVersionNum,\n serverVersion,\n currentRole,\n scannerIsPrivileged,\n schemas,\n exposedRoles,\n platform: detectPlatform(allSchemas, roles),\n relations,\n policies,\n roles,\n grants,\n views,\n foreignKeys,\n routines\n };\n}\n\n/**\n * Which of the database's schemas this scan covers.\n *\n * Exported for its own test: which schemas are in scope is a security decision,\n * not an implementation detail — a schema quietly added to\n * {@link PLATFORM_SCHEMAS} silently stops being checked.\n */\nexport function selectSchemas(\n all: string[],\n requested: string[] | undefined,\n diagnostics: IntrospectDiagnostics\n): string[] {\n const isSystem = (s: string) =>\n SYSTEM_SCHEMAS.includes(s) || /^pg_temp(_\\d+)?$/.test(s) || /^pg_toast_temp(_\\d+)?$/.test(s);\n\n if (requested && requested.length > 0) {\n const wanted = new Set(requested);\n const kept: string[] = [];\n for (const schema of all) {\n if (wanted.has(schema)) kept.push(schema);\n else diagnostics.excludedSchemas.push({ schema, reason: \"not-requested\" });\n }\n return kept;\n }\n\n const kept: string[] = [];\n for (const schema of all) {\n if (isSystem(schema)) {\n diagnostics.excludedSchemas.push({ schema, reason: \"system\" });\n continue;\n }\n // `public` is never platform-internal, whatever it is named alongside.\n if (schema !== \"public\" && PLATFORM_SCHEMAS.includes(schema)) {\n diagnostics.excludedSchemas.push({ schema, reason: \"platform\" });\n continue;\n }\n kept.push(schema);\n }\n return kept;\n}\n\nasync function readRoles(db: Reader): Promise<DbRole[]> {\n const rows = await db.query<{\n rolname: string;\n rolcanlogin: boolean;\n rolsuper: boolean;\n rolbypassrls: boolean | null;\n }>(\n \"roles\",\n \"SELECT rolname, rolcanlogin, rolsuper, rolbypassrls FROM pg_roles ORDER BY rolname\"\n );\n\n const edges = await db.query<{ role: string; member: string }>(\n \"role memberships\",\n `SELECT r.rolname AS role, m.rolname AS member\n FROM pg_auth_members am\n JOIN pg_roles r ON r.oid = am.roleid\n JOIN pg_roles m ON m.oid = am.member`\n );\n\n // Direct memberships, then a fixpoint. A grant to a role that `anon` inherits\n // is a grant to `anon`; a checker that compares grantee names literally\n // reports \"clean\" on exactly the databases whose owners took the most care.\n const direct = new Map<string, Set<string>>();\n for (const edge of edges) {\n if (!direct.has(edge.member)) direct.set(edge.member, new Set());\n direct.get(edge.member)!.add(edge.role);\n }\n\n const closure = new Map<string, Set<string>>();\n for (const role of rows) closure.set(role.rolname, new Set(direct.get(role.rolname) ?? []));\n\n let changed = true;\n while (changed) {\n changed = false;\n for (const [member, parents] of closure) {\n for (const parent of [...parents]) {\n for (const grandparent of closure.get(parent) ?? []) {\n if (grandparent === member || parents.has(grandparent)) continue;\n parents.add(grandparent);\n changed = true;\n }\n }\n }\n }\n\n return rows.map((r) => ({\n name: r.rolname,\n canLogin: Boolean(r.rolcanlogin),\n superuser: Boolean(r.rolsuper),\n bypassRls: Boolean(r.rolbypassrls),\n memberOf: [...(closure.get(r.rolname) ?? [])].sort()\n }));\n}\n\nasync function readRelations(db: Reader, schemas: string[]): Promise<DbRelation[]> {\n const rows = await db.query<{\n schema: string;\n name: string;\n kind: string;\n owner: string;\n rls_enabled: boolean;\n rls_forced: boolean;\n estimated_rows: string | number;\n }>(\n \"relations\",\n `SELECT n.nspname AS schema, c.relname AS name, c.relkind::text AS kind,\n pg_get_userbyid(c.relowner) AS owner,\n c.relrowsecurity AS rls_enabled,\n c.relforcerowsecurity AS rls_forced,\n c.reltuples::float8 AS estimated_rows\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE c.relkind::text = ANY($2) AND n.nspname = ANY($1)\n ORDER BY n.nspname, c.relname`,\n [schemas, SCANNED_RELKINDS]\n );\n\n const columns = await readColumns(db, schemas);\n\n return rows.map((r) => ({\n schema: r.schema,\n name: r.name,\n kind: RELATION_KINDS[r.kind] ?? \"table\",\n owner: r.owner,\n // Postgres reports these false for views; do not infer anything from it.\n rlsEnabled: Boolean(r.rls_enabled),\n rlsForced: Boolean(r.rls_forced),\n columns: columns.get(`${r.schema}.${r.name}`) ?? [],\n estimatedRows: Number(r.estimated_rows ?? -1)\n }));\n}\n\nasync function readColumns(db: Reader, schemas: string[]): Promise<Map<string, DbColumn[]>> {\n const rows = await db.query<{\n schema: string;\n table_name: string;\n name: string;\n type: string;\n not_null: boolean;\n is_pk: boolean | null;\n }>(\n \"columns\",\n `SELECT n.nspname AS schema, c.relname AS table_name, a.attname AS name,\n format_type(a.atttypid, a.atttypmod) AS type,\n a.attnotnull AS not_null,\n EXISTS (\n SELECT 1 FROM pg_index i\n WHERE i.indrelid = c.oid AND i.indisprimary AND a.attnum = ANY(i.indkey)\n ) AS is_pk\n FROM pg_attribute a\n JOIN pg_class c ON c.oid = a.attrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE a.attnum > 0 AND NOT a.attisdropped\n AND c.relkind::text = ANY($2) AND n.nspname = ANY($1)\n ORDER BY n.nspname, c.relname, a.attnum`,\n [schemas, SCANNED_RELKINDS]\n );\n\n const map = new Map<string, DbColumn[]>();\n for (const row of rows) {\n const key = `${row.schema}.${row.table_name}`;\n if (!map.has(key)) map.set(key, []);\n map.get(key)!.push({\n name: row.name,\n type: row.type,\n notNull: Boolean(row.not_null),\n isPrimaryKey: Boolean(row.is_pk)\n });\n }\n return map;\n}\n\nasync function readPolicies(db: Reader, schemas: string[]): Promise<DbPolicy[]> {\n const rows = await db.query<{\n schemaname: string;\n tablename: string;\n policyname: string;\n permissive: string;\n roles: string[] | string;\n cmd: string;\n qual: string | null;\n with_check: string | null;\n }>(\n \"policies\",\n `SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check\n FROM pg_policies\n WHERE schemaname = ANY($1)\n ORDER BY schemaname, tablename, policyname`,\n [schemas]\n );\n\n return rows.map((r) => ({\n schema: r.schemaname,\n table: r.tablename,\n name: r.policyname,\n permissive: String(r.permissive ?? \"PERMISSIVE\").toUpperCase() !== \"RESTRICTIVE\",\n // node-postgres yields text[] as an array; some drivers hand back \"{a,b}\".\n roles: Array.isArray(r.roles)\n ? r.roles\n : String(r.roles ?? \"\")\n .replace(/^\\{|\\}$/g, \"\")\n .split(\",\")\n .map((s) => s.trim().replace(/^\"|\"$/g, \"\"))\n .filter(Boolean),\n command: (String(r.cmd ?? \"ALL\").toUpperCase() as PolicyCommand) ?? \"ALL\",\n using: r.qual,\n withCheck: r.with_check\n }));\n}\n\nasync function readGrants(db: Reader, schemas: string[]): Promise<DbGrant[]> {\n const rows = await db.query<{\n schema: string;\n table_name: string;\n grantee: string;\n privilege_type: string;\n }>(\n \"table privileges\",\n `SELECT n.nspname AS schema, c.relname AS table_name,\n CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE pg_get_userbyid(a.grantee) END AS grantee,\n a.privilege_type\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n CROSS JOIN LATERAL aclexplode(COALESCE(c.relacl, acldefault('r'::\"char\", c.relowner))) a\n WHERE c.relkind::text = ANY($2) AND n.nspname = ANY($1)`,\n [schemas, SCANNED_RELKINDS]\n );\n\n const known = new Set([\"SELECT\", \"INSERT\", \"UPDATE\", \"DELETE\", \"TRUNCATE\", \"REFERENCES\", \"TRIGGER\"]);\n const map = new Map<string, DbGrant>();\n for (const row of rows) {\n // PG17 added MAINTAIN; anything unknown is not exposure and is dropped.\n if (!known.has(row.privilege_type)) continue;\n const key = `${row.schema}.${row.table_name}.${row.grantee}`;\n if (!map.has(key)) {\n map.set(key, {\n schema: row.schema,\n table: row.table_name,\n grantee: row.grantee,\n privileges: []\n });\n }\n map.get(key)!.privileges.push(row.privilege_type as DbGrant[\"privileges\"][number]);\n }\n return [...map.values()];\n}\n\nasync function readViews(\n db: Reader,\n schemas: string[],\n serverVersionNum: number,\n relations: DbRelation[]\n): Promise<DbView[]> {\n const rows = await db.query<{\n schema: string;\n name: string;\n owner: string;\n kind: string;\n reloptions: string[] | null;\n }>(\n \"views\",\n `SELECT n.nspname AS schema, c.relname AS name, pg_get_userbyid(c.relowner) AS owner,\n c.relkind::text AS kind, c.reloptions\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE c.relkind = ANY(ARRAY['v','m']) AND n.nspname = ANY($1)\n ORDER BY n.nspname, c.relname`,\n [schemas]\n );\n\n const dependencies = await readViewDependencies(db, schemas, relations);\n\n return rows.map((r) => ({\n schema: r.schema,\n name: r.name,\n owner: r.owner,\n // The option does not exist before PG 15, and never applies to a\n // materialized view. `null` means \"the question does not apply here\",\n // which is what the dependent check keys its wording off.\n securityInvoker:\n serverVersionNum < 150000 || r.kind === \"m\"\n ? null\n : (r.reloptions ?? []).some((o) => /^security_invoker=(true|on)$/i.test(o)),\n dependsOn: dependencies.get(`${r.schema}.${r.name}`) ?? []\n }));\n}\n\n/**\n * Base relations of each view, resolved through `pg_depend` → `pg_rewrite`.\n *\n * Deliberately transitive: a view over a view over a protected table is still a\n * path to that table, and the intermediate view's own options do not save it\n * (the outer view still executes as its own owner). Base tables are not filtered\n * to the scanned schemas — a view in `public` reading a hidden schema is exactly\n * the case worth catching.\n */\nasync function readViewDependencies(\n db: Reader,\n schemas: string[],\n relations: DbRelation[]\n): Promise<Map<string, { schema: string; table: string }[]>> {\n const rows = await db.query<{\n view_schema: string;\n view_name: string;\n ref_schema: string;\n ref_name: string;\n }>(\n \"view dependencies\",\n `SELECT DISTINCT dn.nspname AS view_schema, dc.relname AS view_name,\n rn.nspname AS ref_schema, rc.relname AS ref_name\n FROM pg_depend d\n JOIN pg_rewrite rw ON rw.oid = d.objid\n JOIN pg_class dc ON dc.oid = rw.ev_class\n JOIN pg_namespace dn ON dn.oid = dc.relnamespace\n JOIN pg_class rc ON rc.oid = d.refobjid\n JOIN pg_namespace rn ON rn.oid = rc.relnamespace\n WHERE d.classid = 'pg_rewrite'::regclass\n AND d.refclassid = 'pg_class'::regclass\n AND dc.oid <> rc.oid\n AND dc.relkind = ANY(ARRAY['v','m'])\n AND rc.relkind::text = ANY($2)\n AND dn.nspname = ANY($1)`,\n [schemas, SCANNED_RELKINDS]\n );\n\n const direct = new Map<string, Set<string>>();\n for (const row of rows) {\n const key = `${row.view_schema}.${row.view_name}`;\n if (!direct.has(key)) direct.set(key, new Set());\n direct.get(key)!.add(`${row.ref_schema}.${row.ref_name}`);\n }\n\n const isView = new Set(\n relations\n .filter((r) => r.kind === \"view\" || r.kind === \"materialized_view\")\n .map((r) => `${r.schema}.${r.name}`)\n );\n\n const out = new Map<string, { schema: string; table: string }[]>();\n for (const key of direct.keys()) {\n const seen = new Set<string>();\n const queue = [...(direct.get(key) ?? [])];\n while (queue.length > 0) {\n const next = queue.shift()!;\n if (next === key || seen.has(next)) continue;\n seen.add(next);\n if (isView.has(next)) queue.push(...(direct.get(next) ?? []));\n }\n out.set(\n key,\n [...seen].map((ref) => {\n const dot = ref.indexOf(\".\");\n return { schema: ref.slice(0, dot), table: ref.slice(dot + 1) };\n })\n );\n }\n return out;\n}\n\nasync function readForeignKeys(db: Reader, schemas: string[]): Promise<DbForeignKey[]> {\n const rows = await db.query<{\n schema: string;\n table_name: string;\n ref_schema: string;\n ref_table: string;\n columns: string[] | null;\n ref_columns: string[] | null;\n }>(\n \"foreign keys\",\n `SELECT n.nspname AS schema, c.relname AS table_name,\n rn.nspname AS ref_schema, rc.relname AS ref_table,\n -- The ::text cast is load-bearing: attname is of type name, so\n -- array_agg yields name[] (OID 1003), which node-pg has no parser\n -- for and hands back as the raw literal \"{post_id}\". The declared\n -- string[] then lies, every column comparison silently fails to\n -- match, and junction-table-unprotected could never fire.\n (SELECT array_agg(att.attname::text ORDER BY k.ord)\n FROM unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord)\n JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = k.attnum) AS columns,\n -- The ::text cast is load-bearing: attname is of type name, so\n -- array_agg yields name[] (OID 1003), which node-pg has no parser\n -- for and hands back as the raw literal \"{post_id}\". The declared\n -- string[] then lies, every column comparison silently fails to\n -- match, and junction-table-unprotected could never fire.\n (SELECT array_agg(att.attname::text ORDER BY k.ord)\n FROM unnest(con.confkey) WITH ORDINALITY AS k(attnum, ord)\n JOIN pg_attribute att ON att.attrelid = con.confrelid AND att.attnum = k.attnum) AS ref_columns\n FROM pg_constraint con\n JOIN pg_class c ON c.oid = con.conrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n JOIN pg_class rc ON rc.oid = con.confrelid\n JOIN pg_namespace rn ON rn.oid = rc.relnamespace\n WHERE con.contype = 'f' AND n.nspname = ANY($1)\n ORDER BY n.nspname, c.relname, con.conname`,\n [schemas]\n );\n\n return rows.map((r) => ({\n schema: r.schema,\n table: r.table_name,\n columns: r.columns ?? [],\n refSchema: r.ref_schema,\n refTable: r.ref_table,\n refColumns: r.ref_columns ?? []\n }));\n}\n\nasync function readRoutines(db: Reader, schemas: string[]): Promise<DbRoutine[]> {\n const rows = await db.query<{\n schema: string;\n name: string;\n owner: string;\n security_definer: boolean;\n proconfig: string[] | null;\n }>(\n \"routines\",\n `SELECT n.nspname AS schema, p.proname AS name, pg_get_userbyid(p.proowner) AS owner,\n p.prosecdef AS security_definer, p.proconfig\n FROM pg_proc p\n JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = ANY($1) AND p.prokind = ANY(ARRAY['f','p'])\n ORDER BY n.nspname, p.proname`,\n [schemas]\n );\n\n return rows.map((r) => ({\n schema: r.schema,\n name: r.name,\n owner: r.owner,\n securityDefiner: Boolean(r.security_definer),\n mutableSearchPath: !(r.proconfig ?? []).some((c) => /^search_path=/i.test(c))\n }));\n}\n\n// ---------------------------------------------------------------------------\n// Derived facts\n// ---------------------------------------------------------------------------\n\n/**\n * Can the scanning role see past RLS?\n *\n * Reported rather than corrected. A privileged scanner reads the true catalog,\n * which is what makes the findings accurate — but every finding then describes\n * what some *other* role gets, and the report has to say so or a reader will\n * assume the tool tested its own access.\n */\nfunction isPrivileged(\n currentRole: string,\n server: { is_super?: boolean | null; bypass_rls?: boolean | null } | undefined,\n roles: DbRole[],\n relations: DbRelation[]\n): boolean {\n if (server?.is_super || server?.bypass_rls) return true;\n\n const me = roles.find((r) => r.name === currentRole);\n if (me?.superuser || me?.bypassRls) return true;\n\n // Membership in a privileged role is the same power, one SET ROLE away.\n const inherited = new Set(me?.memberOf ?? []);\n if (roles.some((r) => inherited.has(r.name) && (r.superuser || r.bypassRls))) return true;\n\n // Ownership exempts from non-FORCE RLS on the owned tables.\n return relations.some((r) => r.owner === currentRole || inherited.has(r.owner));\n}\n\n/**\n * Which platform is in front of this database.\n *\n * Cheap and total: it reads the schema and role lists that were already\n * fetched, so a database missing any of these catalogs falls through to\n * `unknown` rather than erroring. The answer only ever changes the *wording* and\n * severity of `policy-anonymous-tautology`, so guessing wrong degrades a message\n * rather than inventing or hiding a finding.\n */\nfunction detectPlatform(allSchemas: string[], roles: DbRole[]): DbSnapshot[\"platform\"] {\n const schema = new Set(allSchemas);\n const role = new Set(roles.map((r) => r.name));\n\n if (\n schema.has(\"auth\") &&\n schema.has(\"storage\") &&\n (role.has(\"anon\") || role.has(\"authenticated\") || role.has(\"service_role\"))\n ) {\n return \"supabase\";\n }\n\n if (role.has(\"rebase_user\") || schema.has(\"rebase\")) return \"rebase\";\n if (role.has(\"web_anon\")) return \"postgrest\";\n if (role.has(\"neon_superuser\") || schema.has(\"neon\")) return \"neon\";\n\n return \"unknown\";\n}\n\n/**\n * Roles an untrusted caller plausibly arrives as.\n *\n * `service_role` is excluded on purpose. It exists, and it is in `roles`, but it\n * is the *trusted* bypass identity: treating it as exposed would flag every\n * table in every Supabase project as critical, which is both useless and wrong.\n *\n * `connecting` is the role the scan came in as, passed only when it is not\n * privileged — see the call site.\n */\nfunction exposedRolesFor(roles: DbRole[], explicit?: string[], connecting?: string): string[] {\n const present = new Set(roles.map((r) => r.name));\n const named = [...CANDIDATE_EXPOSED_ROLES, ...(explicit ?? []), ...(connecting ? [connecting] : [])];\n // A role named with `--role` that does not exist never reaches here:\n // `assertRolesExist` refuses the scan, because dropping it would narrow the\n // run without saying so. The filter still stands for the built-in\n // candidates, most of which are absent on any given database.\n return [\"PUBLIC\", ...new Set(named.filter((r) => present.has(r)))];\n}\n\n/**\n * Refuse a `--role` that is not in `pg_roles`.\n *\n * Not a warning, for the reason spelled out on {@link UnknownRoleError}. The\n * one exception is a degraded `roles` read: with no catalogue to check against,\n * every name would look unknown, and turning a partial catalogue read into\n * \"your flag is wrong\" would send the reader after the wrong problem.\n */\nfunction assertRolesExist(roles: DbRole[], requested: string[] | undefined, diagnostics: IntrospectDiagnostics): void {\n if (!requested || requested.length === 0) return;\n if (diagnostics.degraded.some((entry) => entry.what === \"roles\")) return;\n\n const present = new Set(roles.map((r) => r.name));\n const unknown = [...new Set(requested.filter((role) => !present.has(role)))];\n if (unknown.length > 0) throw new UnknownRoleError(unknown);\n}\n\n/**\n * Roles that can read or write a scanned table and are neither exposed nor\n * trusted.\n *\n * \"Trusted\" is deliberately generous — every role this can explain is one the\n * user does not have to think about. A grantee is explained when it is a\n * superuser, holds BYPASSRLS (RLS is a no-op for it, so no policy this tool\n * checks would constrain it anyway), owns the relation, is the role the scan\n * connected as, or is platform bookkeeping (`pg_*`, `cloudsql*`, Supabase's\n * `service_role`, which is the documented trusted bypass).\n *\n * SELECT alone counts. It used to be filtered out as \"a normal, deliberate\n * grant on a reference table\", which reads the risk backwards: the finding this\n * whole tool exists for is `rls-disabled`, and what an RLS-disabled table\n * hands a role holding nothing but SELECT is every row in it. A read-only\n * reporting role reachable from the internet is the textbook leak, and the\n * caveat that exists to stop a false negative was skipping exactly that shape.\n *\n * What is left is the interesting set: a named role with data access that this\n * tool has no opinion about. It may be a service account nothing can\n * authenticate as — `rebase_user` is NOLOGIN by design — or it may be exactly\n * the role the API connects as. The scan cannot tell from the catalog, so it\n * names them and lets the reader decide.\n */\nfunction unrecognizedGranteesFor(\n grants: DbGrant[],\n roles: DbRole[],\n relations: DbRelation[],\n exposed: string[],\n currentRole: string\n): string[] {\n const exposedSet = new Set(exposed.map((r) => r.toLowerCase()));\n const byName = new Map(roles.map((r) => [r.name.toLowerCase(), r]));\n const owners = new Set(relations.map((r) => r.owner.toLowerCase()));\n // TRUNCATE, REFERENCES and TRIGGER are not data access, so a grant of only\n // those does not make a role worth a sentence here.\n const dataAccess = new Set([\"SELECT\", \"INSERT\", \"UPDATE\", \"DELETE\"]);\n\n const out = new Set<string>();\n for (const grant of grants) {\n const name = grant.grantee;\n const key = name.toLowerCase();\n\n if (exposedSet.has(key) || isPublicName(name)) continue;\n if (key === currentRole.toLowerCase()) continue;\n if (owners.has(key)) continue;\n if (key.startsWith(\"pg_\") || key.startsWith(\"cloudsql\") || key === \"service_role\") continue;\n\n const role = byName.get(key);\n if (role?.superuser || role?.bypassRls) continue;\n\n if (!grant.privileges.some((p) => dataAccess.has(p))) continue;\n\n out.add(name);\n }\n return [...out].sort();\n}\n\nfunction isPublicName(name: string): boolean {\n return name.toUpperCase() === \"PUBLIC\";\n}\n","/**\n * Rendering: {@link ScanResult} in, text or JSON out. No Postgres, no I/O.\n *\n * Two hard rules shape everything here.\n *\n * 1. Colour is decoration, never information. The plain-text form is what ends\n * up pasted into a CI log or a GitHub issue, so every severity is spelled\n * out as a word and every section has a textual heading. Turning colour off\n * must not remove a single fact.\n *\n * 2. Heuristic findings are quarantined. They render in their own section,\n * after the confident ones, behind a sentence that says they need human\n * judgement. A tool that mixes \"your table is public\" with \"this might be a\n * junction table\" trains people to distrust both.\n */\n\nimport type { Check, Finding, ScanResult, Severity } from \"./types\";\nimport { SEVERITIES } from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Severity ordering\n// ---------------------------------------------------------------------------\n\n/** Higher is worse. `info` = 0 … `critical` = 4. */\nexport function severityRank(severity: Severity): number {\n return SEVERITIES.indexOf(severity);\n}\n\n/** The worst severity present, or `null` for an empty list. */\nexport function maxSeverity(findings: readonly Finding[]): Severity | null {\n let worst: Severity | null = null;\n for (const finding of findings) {\n if (worst === null || severityRank(finding.severity) > severityRank(worst)) {\n worst = finding.severity;\n }\n }\n\n return worst;\n}\n\n/**\n * Does this result trip `--fail-on`? Heuristic findings count: a caller who\n * does not want them can `--skip` the check, but silently discounting them\n * would make the exit code disagree with the report.\n */\nexport function exceedsThreshold(findings: readonly Finding[], failOn: Severity | \"none\"): boolean {\n if (failOn === \"none\") return false;\n const threshold = severityRank(failOn);\n\n return findings.some((finding) => severityRank(finding.severity) >= threshold);\n}\n\n// ---------------------------------------------------------------------------\n// Colour\n// ---------------------------------------------------------------------------\n\ntype Paint = (value: string) => string;\n\nconst identity: Paint = (value) => value;\n\nconst code = (open: number, close: number): Paint => (value) => `\\u001B[${open}m${value}\\u001B[${close}m`;\n\ninterface Style {\n bold: Paint;\n dim: Paint;\n red: Paint;\n boldRed: Paint;\n yellow: Paint;\n cyan: Paint;\n green: Paint;\n}\n\nconst PLAIN: Style = {\n bold: identity,\n dim: identity,\n red: identity,\n boldRed: identity,\n yellow: identity,\n cyan: identity,\n green: identity\n};\n\nconst COLOUR: Style = {\n bold: code(1, 22),\n dim: code(2, 22),\n red: code(31, 39),\n boldRed: (value) => `\\u001B[1m\\u001B[31m${value}\\u001B[39m\\u001B[22m`,\n yellow: code(33, 39),\n cyan: code(36, 39),\n green: code(32, 39)\n};\n\nfunction severityPaint(style: Style, severity: Severity): Paint {\n switch (severity) {\n case \"critical\":\n return style.boldRed;\n case \"high\":\n return style.red;\n case \"medium\":\n return style.yellow;\n case \"low\":\n return style.cyan;\n case \"info\":\n return style.dim;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Text helpers\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_WIDTH = 88;\nconst MIN_WIDTH = 48;\nconst MAX_WIDTH = 100;\n\nexport function clampWidth(columns: number | undefined): number {\n if (!columns || !Number.isFinite(columns)) return DEFAULT_WIDTH;\n\n return Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, Math.floor(columns)));\n}\n\n/** Word-wrap, preserving deliberate newlines in the source text. */\nfunction wrap(text: string, width: number): string[] {\n const lines: string[] = [];\n for (const paragraph of text.split(\"\\n\")) {\n const words = paragraph.trim().split(/\\s+/).filter((word) => word.length > 0);\n if (words.length === 0) {\n lines.push(\"\");\n continue;\n }\n let current = \"\";\n for (const word of words) {\n if (current.length === 0) {\n current = word;\n } else if (current.length + 1 + word.length <= width) {\n current += ` ${word}`;\n } else {\n lines.push(current);\n current = word;\n }\n }\n if (current.length > 0) lines.push(current);\n }\n\n return lines;\n}\n\nfunction indent(lines: readonly string[], pad: string): string[] {\n return lines.map((line) => (line.length === 0 ? \"\" : pad + line));\n}\n\n/**\n * Human-readable object path for a finding. Kept boring on purpose — people\n * grep these out of CI logs.\n */\nexport function formatTarget(target: Finding[\"target\"]): string {\n const parts: string[] = [];\n\n if (target.table) {\n parts.push(target.column ? `${target.schema}.${target.table}.${target.column}` : `${target.schema}.${target.table}`);\n } else if (target.view) {\n parts.push(`${target.schema}.${target.view}`);\n } else if (target.routine) {\n parts.push(`${target.schema}.${target.routine}()`);\n } else {\n parts.push(`schema ${target.schema}`);\n }\n\n if (target.policy) parts.push(`policy \"${target.policy}\"`);\n\n return parts.join(\" · \");\n}\n\n/** Sort key: worst first, then a stable schema/table/id ordering. */\nfunction compareFindings(a: Finding, b: Finding): number {\n const bySeverity = severityRank(b.severity) - severityRank(a.severity);\n if (bySeverity !== 0) return bySeverity;\n\n const bySchema = a.target.schema.localeCompare(b.target.schema);\n if (bySchema !== 0) return bySchema;\n\n const aName = a.target.table ?? a.target.view ?? a.target.routine ?? \"\";\n const bName = b.target.table ?? b.target.view ?? b.target.routine ?? \"\";\n const byName = aName.localeCompare(bName);\n if (byName !== 0) return byName;\n\n const byId = a.id.localeCompare(b.id);\n if (byId !== 0) return byId;\n\n return (a.target.policy ?? \"\").localeCompare(b.target.policy ?? \"\");\n}\n\nconst PLATFORM_LABEL: Record<ScanResult[\"platform\"], string> = {\n supabase: \"Supabase\",\n neon: \"Neon\",\n rebase: \"Rebase\",\n postgrest: \"PostgREST\",\n unknown: \"PostgreSQL (no platform markers)\"\n};\n\n// ---------------------------------------------------------------------------\n// Report\n// ---------------------------------------------------------------------------\n\nexport interface RenderOptions {\n color: boolean;\n /** Findings only: no banner, no summary. The privilege caveat still prints. */\n quiet?: boolean;\n /** Echoed in the summary so the exit code is never a mystery. */\n failOn: Severity | \"none\";\n /** Terminal width; clamped into a readable range. */\n width?: number;\n /**\n * `host:port` for the header. {@link ScanResult} deliberately carries no\n * port (it is a redaction surface), so the CLI passes the display form in.\n */\n endpoint?: string;\n /** Shown in the banner. */\n version?: string;\n}\n\nconst RULE = \"─\";\n\n/**\n * What the scan could not read, and what that costs.\n *\n * Named per failed read rather than summarised, because which catalogue failed\n * decides which checks went quiet — and the reader is the only one who can\n * judge whether the missing ones mattered.\n */\nfunction renderDegradedCaveat(\n degraded: { what: string; error: string }[],\n style: Style,\n width: number\n): string[] {\n const out: string[] = [];\n out.push(style.yellow(`⚠ This scan was incomplete: ${degraded.length} catalogue read(s) failed.`));\n out.push(...wrap(\n \"Checks that depend on what could not be read report nothing, which looks exactly \" +\n \"like finding nothing. Treat this run as inconclusive rather than clean.\",\n width\n ));\n for (const item of degraded) {\n out.push(` • ${item.what}: ${item.error}`);\n }\n return out;\n}\n\nexport function renderReport(result: ScanResult, options: RenderOptions): string {\n const style = options.color ? COLOUR : PLAIN;\n const width = clampWidth(options.width);\n const out: string[] = [];\n\n const certain = result.findings.filter((finding) => finding.confidence !== \"heuristic\").sort(compareFindings);\n const heuristic = result.findings.filter((finding) => finding.confidence === \"heuristic\").sort(compareFindings);\n\n if (!options.quiet) {\n out.push(...renderHeader(result, style, width, options));\n }\n\n // The privilege caveat survives --quiet. Suppressing it would let someone\n // read a clean report as \"we are fine\" from a scan that could not possibly\n // have observed what a low-privilege caller sees.\n if (result.scannerIsPrivileged) {\n out.push(...renderPrivilegeCaveat(style, width));\n out.push(\"\");\n }\n\n // The other half of the same disclosure: when the scan came in as a role RLS\n // does constrain, that role was added to the exposed set, which changes\n // which findings appear. Two runs of the same database as different roles\n // are allowed to disagree, and the report has to say why.\n const scanningAs = result.diagnostics?.scanningAsExposedRole;\n if (scanningAs) {\n out.push(...renderConnectingRoleNote(scanningAs, style, width));\n out.push(\"\");\n }\n\n // Same reasoning as the privilege caveat above, and it survives --quiet for\n // the same reason: a check whose catalogue read failed returns no findings,\n // which is indistinguishable in the output from a check that found nothing.\n // One failed grants read silently disables `rls-disabled`, both view checks\n // and `anonymous-write-allowed`.\n const degraded = result.diagnostics?.degraded ?? [];\n if (degraded.length > 0) {\n out.push(...renderDegradedCaveat(degraded, style, width));\n out.push(\"\");\n }\n\n // Also survives --quiet, and for the sharpest version of the same reason:\n // every check gates on a grant to an exposed role, and the exposed set is\n // recognised by name. A database served by a role this tool has never heard\n // of produces no findings at all — a green report on an open database.\n const unrecognized = result.diagnostics?.unrecognizedGrantees ?? [];\n if (unrecognized.length > 0) {\n out.push(...renderUnrecognizedRolesCaveat(unrecognized, style, width));\n out.push(\"\");\n }\n\n if (certain.length === 0 && heuristic.length === 0) {\n out.push(degraded.length > 0\n // Not \"no findings\": the scan did not finish looking.\n ? style.yellow(\"No findings from the checks that ran — but the scan was incomplete. See above.\")\n : style.green(\"No findings. Every table, view and policy in scope passed all checks.\"));\n out.push(\"\");\n }\n\n if (certain.length > 0) {\n out.push(...renderFindingSections(certain, style, width));\n }\n\n if (heuristic.length > 0) {\n out.push(style.bold(\"WORTH CHECKING\"));\n out.push(\n ...indent(\n wrap(\n \"These are heuristics, not proofs. They match a shape that is usually a mistake, \" +\n \"but each one may be deliberate in your schema — read them and decide. They are \" +\n \"listed separately so nothing above needs a second opinion.\",\n width - 2\n ),\n \" \"\n ).map(style.dim)\n );\n out.push(\"\");\n out.push(...renderFindingSections(heuristic, style, width, { headings: false }));\n }\n\n if (!options.quiet) {\n out.push(...renderSummary(result, certain, heuristic, style, width, options));\n }\n\n return `${out.join(\"\\n\").replace(/\\n{3,}/g, \"\\n\\n\").trimEnd()}\\n`;\n}\n\nfunction renderHeader(result: ScanResult, style: Style, width: number, options: RenderOptions): string[] {\n const out: string[] = [];\n const title = options.version ? `rls-check ${options.version}` : \"rls-check\";\n\n out.push(`${style.bold(title)}${style.dim(\" · read-only Row-Level Security audit\")}`);\n out.push(style.dim(RULE.repeat(width)));\n out.push(\"\");\n\n const endpoint = options.endpoint ?? result.database.host;\n\n // Some servers report a bare `18.4`, others a full `PostgreSQL 15.6 on …`.\n // A header row reading \"Server 18.4\" tells the reader nothing.\n const serverVersion = /^[\\d.]/.test(result.serverVersion.trim())\n ? `PostgreSQL ${result.serverVersion}`\n : result.serverVersion;\n\n // Every check gates on this set: a table is only \"exposed\" when one of these\n // roles can reach it. Printing it in the header is the difference between a\n // reader taking \"No findings\" at face value and noticing their own API role\n // is not in the list.\n const exposed = result.exposedRoles ?? [];\n\n const rows: [string, string][] = [\n [\"Database\", `${endpoint}/${result.database.name}`],\n [\"Server\", serverVersion],\n [\"Platform\", PLATFORM_LABEL[result.platform]],\n [\"Exposed\", `${exposed.length > 0 ? exposed.join(\", \") : \"PUBLIC\"} (add yours with --role)`],\n [\n \"Scanned\",\n [\n `${result.stats.schemas} ${plural(result.stats.schemas, \"schema\", \"schemas\")}`,\n `${result.stats.tables} ${plural(result.stats.tables, \"table\", \"tables\")}`,\n `${result.stats.policies} ${plural(result.stats.policies, \"policy\", \"policies\")}`,\n `${result.stats.checksRun} ${plural(result.stats.checksRun, \"check\", \"checks\")}`\n ].join(\" · \")\n ]\n ];\n\n for (const [label, value] of rows) {\n out.push(`${style.dim(label.padEnd(10))}${value}`);\n }\n out.push(\"\");\n\n return out;\n}\n\nfunction renderPrivilegeCaveat(style: Style, width: number): string[] {\n const body = wrap(\n \"This scan connected as a role that row-level security cannot constrain — a superuser, \" +\n \"a table owner, or a role with BYPASSRLS. That is why it can read the true catalog, \" +\n \"and it is also why nothing below describes what this connection experiences. \" +\n \"The findings are about what OTHER roles get.\",\n width - 8\n );\n\n const out: string[] = [];\n out.push(`${style.yellow(\"Note\")} ${body[0] ?? \"\"}`);\n for (const line of body.slice(1)) out.push(` ${line}`);\n\n return out;\n}\n\n/**\n * The scan connected as a role row-level security constrains, so that role was\n * treated as exposed.\n *\n * The counterpart to {@link renderPrivilegeCaveat}: one says \"nothing below\n * describes this connection\", the other says \"this connection is one of the\n * things below\". Both are disclosures about what the exposed set contains,\n * which is the only reason any of the findings say what they say.\n */\nfunction renderConnectingRoleNote(role: string, style: Style, width: number): string[] {\n const body = wrap(\n `This scan connected as \"${role}\", which row-level security does constrain — no superuser, ` +\n \"no BYPASSRLS, no ownership of the scanned tables. It has therefore been treated as a role \" +\n \"an untrusted caller can arrive as, and the findings below include what it reaches. \" +\n \"Scanning the same database as a privileged role will report a different set.\",\n width - 8\n );\n\n const out: string[] = [];\n out.push(`${style.yellow(\"Note\")} ${body[0] ?? \"\"}`);\n for (const line of body.slice(1)) out.push(` ${line}`);\n\n return out;\n}\n\n/**\n * The caveat that keeps a false negative from reading as a pass.\n *\n * Deliberately not a finding: the scan has no evidence that any of these roles\n * is reachable, and inventing a critical for every service account would be the\n * mirror of the bug it exists to prevent. It is a caveat with an exact remedy —\n * one flag, named roles — so the reader can convert it into real coverage.\n */\nfunction renderUnrecognizedRolesCaveat(roles: string[], style: Style, width: number): string[] {\n const shown = roles.slice(0, 6);\n const rest = roles.length - shown.length;\n const list = shown.map((r) => `\"${r}\"`).join(\", \") + (rest > 0 ? `, and ${rest} more` : \"\");\n\n const body = wrap(\n `${list} can read or write scanned tables here, and ` +\n \"this scan does not know whether requests arrive as \" +\n `${plural(roles.length, \"it\", \"them\")}. The checks only report a table as exposed when an ` +\n \"exposed role can reach it, so anything served through \" +\n `${plural(roles.length, \"this role\", \"these roles\")} was NOT assessed. If your ` +\n \"application connects as one of them, re-run naming it — \" +\n `for example: --role ${shown[0] ?? \"app_user\"}`,\n width - 8\n );\n\n const out: string[] = [];\n out.push(`${style.yellow(\"Note\")} ${body[0] ?? \"\"}`);\n for (const line of body.slice(1)) out.push(` ${line}`);\n\n return out;\n}\n\nfunction renderFindingSections(\n findings: readonly Finding[],\n style: Style,\n width: number,\n opts: { headings?: boolean } = {}\n): string[] {\n const out: string[] = [];\n const showHeadings = opts.headings !== false;\n\n for (const severity of [...SEVERITIES].reverse()) {\n const group = findings.filter((finding) => finding.severity === severity);\n if (group.length === 0) continue;\n\n if (showHeadings) {\n const paint = severityPaint(style, severity);\n out.push(\n `${paint(severity.toUpperCase())}${style.dim(\n ` · ${group.length} ${plural(group.length, \"finding\", \"findings\")}`\n )}`\n );\n out.push(style.dim(RULE.repeat(width)));\n out.push(\"\");\n }\n\n for (const finding of group) {\n out.push(...renderFinding(finding, style, width));\n out.push(\"\");\n }\n }\n\n return out;\n}\n\nfunction renderFinding(finding: Finding, style: Style, width: number): string[] {\n const out: string[] = [];\n const paint = severityPaint(style, finding.severity);\n const body = width - 6;\n\n out.push(` ${paint(`[${finding.severity}]`)} ${style.bold(finding.id)} ${style.cyan(formatTarget(finding.target))}`);\n out.push(...indent(wrap(finding.title, body), \" \"));\n\n if (finding.detail.trim().length > 0) {\n // A blank line, because without colour the title and the detail are the\n // same shape, and this is the form that ends up in a bug report.\n out.push(\"\");\n out.push(...indent(wrap(finding.detail, body), \" \").map(style.dim));\n }\n\n if (finding.impact.trim().length > 0) {\n const impact = wrap(finding.impact, body - 8);\n out.push(` ${style.bold(\"Impact\")} ${impact[0] ?? \"\"}`);\n for (const line of impact.slice(1)) out.push(` ${line}`);\n }\n\n if (finding.fix && finding.fix.trim().length > 0) {\n out.push(` ${style.bold(\"Fix\")}`);\n // Never wrapped: the point of this block is that it survives a copy.\n for (const line of finding.fix.replace(/\\s+$/, \"\").split(\"\\n\")) {\n out.push(line.length === 0 ? \"\" : ` ${style.green(line)}`);\n }\n }\n\n if (finding.docs) {\n out.push(` ${style.dim(\"Docs\")} ${style.dim(finding.docs)}`);\n }\n\n return out;\n}\n\nfunction renderSummary(\n result: ScanResult,\n certain: readonly Finding[],\n heuristic: readonly Finding[],\n style: Style,\n width: number,\n options: RenderOptions\n): string[] {\n const out: string[] = [];\n\n out.push(style.dim(RULE.repeat(width)));\n out.push(style.bold(\"Summary\"));\n out.push(\"\");\n\n const counts = [...SEVERITIES]\n .reverse()\n .map((severity) => {\n const count = result.findings.filter((finding) => finding.severity === severity).length;\n const text = `${severity} ${count}`;\n\n return count === 0 ? style.dim(text) : severityPaint(style, severity)(text);\n })\n .join(style.dim(\" \"));\n out.push(` ${counts}`);\n\n out.push(\n ` ${style.dim(\n `${certain.length} confirmed · ${heuristic.length} worth checking · ` +\n `${result.stats.checksRun} ${plural(result.stats.checksRun, \"check\", \"checks\")} run against ` +\n `${result.stats.tables} ${plural(result.stats.tables, \"table\", \"tables\")} in ` +\n `${result.stats.schemas} ${plural(result.stats.schemas, \"schema\", \"schemas\")}`\n )}`\n );\n\n if (result.stats.tablesWithoutRls > 0) {\n out.push(\n ` ${style.dim(\n `${result.stats.tablesWithoutRls} of ${result.stats.tables} ${plural(\n result.stats.tables,\n \"table has\",\n \"tables have\"\n )} row-level security disabled`\n )}`\n );\n }\n out.push(\"\");\n\n const failing = exceedsThreshold(result.findings, options.failOn);\n if (options.failOn === \"none\") {\n out.push(` ${style.dim(\"Exit code 0 — --fail-on none, so findings never fail the run.\")}`);\n } else if (failing) {\n out.push(\n ` ${style.bold(\"Exit code 1\")} ${style.dim(\n `— at least one finding is \"${options.failOn}\" or worse (--fail-on ${options.failOn}).`\n )}`\n );\n } else {\n out.push(\n ` ${style.bold(\"Exit code 0\")} ${style.dim(\n `— nothing at or above \"${options.failOn}\" (--fail-on ${options.failOn}).`\n )}`\n );\n }\n out.push(` ${style.dim(`Scanned ${result.scannedAt} · read-only, and nothing left this machine.`)}`);\n out.push(\"\");\n out.push(style.dim(\"rls-check is free and maintained by the team behind Rebase — https://rebase.pro\"));\n\n return out;\n}\n\nfunction plural(count: number, one: string, many: string): string {\n return count === 1 ? one : many;\n}\n\n// ---------------------------------------------------------------------------\n// JSON\n// ---------------------------------------------------------------------------\n\n/**\n * Exactly the {@link ScanResult} shape, pretty-printed, nothing else. This is\n * the contract other people build on: stable keys, no ANSI, no wrapper object,\n * no timing noise that would make two identical scans diff.\n */\nexport function renderJson(result: ScanResult): string {\n const sorted: ScanResult = {\n ...result,\n findings: [...result.findings].sort(compareFindings)\n };\n\n return JSON.stringify(sorted, null, 2);\n}\n\n// ---------------------------------------------------------------------------\n// Catalog\n// ---------------------------------------------------------------------------\n\n/**\n * `--list-checks`. Ordered by the severity the check *can* emit is tempting,\n * but a check has no fixed severity — so this is plain alphabetical by id,\n * which is also the order people write `--skip` lists in.\n */\nexport function renderCheckCatalog(checks: readonly Check[], options: { color: boolean; width?: number }): string {\n const style = options.color ? COLOUR : PLAIN;\n const width = clampWidth(options.width);\n const sorted = [...checks].sort((a, b) => a.id.localeCompare(b.id));\n const idWidth = sorted.reduce((max, check) => Math.max(max, check.id.length), 0);\n\n const out: string[] = [];\n out.push(style.bold(`${sorted.length} ${plural(sorted.length, \"check\", \"checks\")}`));\n out.push(style.dim(RULE.repeat(width)));\n out.push(\"\");\n\n for (const check of sorted) {\n out.push(` ${style.bold(check.id.padEnd(idWidth))} ${check.title}`);\n const description = wrap(check.description, Math.max(MIN_WIDTH, width - idWidth - 6));\n for (const line of description) {\n out.push(` ${\" \".repeat(idWidth)} ${style.dim(line)}`);\n }\n out.push(\"\");\n }\n\n out.push(style.dim(\"Use --only <id> or --skip <id> (repeatable, or comma-separated) to select checks.\"));\n\n return `${out.join(\"\\n\")}\\n`;\n}\n","/**\n * Rendering: {@link ScanResult} in, one self-contained HTML file out. No\n * Postgres, no I/O — the same contract `report.ts` holds to.\n *\n * This exists because the text report is written for a terminal and the person\n * who needs to act on it is usually not the person who ran it. A `--fail-on`\n * exit code stops a pipeline; it does not survive being forwarded to whoever\n * owns the database. So the HTML form is the artifact: one file, attachable to\n * a ticket, readable by someone who will never install this tool.\n *\n * Four rules shape everything here, and three of them are inherited from\n * `report.ts` on purpose — a second renderer that quietly relaxed them would\n * make the two disagree about the same scan.\n *\n * 1. **Colour is decoration, never information.** Every severity is spelled out\n * as a word next to its swatch, every section has a textual heading. The\n * report has to survive being printed in greyscale, which is exactly what\n * happens when it reaches a compliance review.\n *\n * 2. **Heuristic findings are quarantined.** Their own section, after the\n * confident ones, behind a sentence saying they need human judgement.\n *\n * 3. **The caveats outrank the findings.** A scan by a privileged role, or one\n * whose catalogue reads failed, renders its warning above everything — for\n * the same reason the text report does it: a check that could not run\n * returns no findings, which looks identical to a check that found nothing.\n *\n * 4. **Nothing leaves the machine, including at read time.** No stylesheet\n * link, no font host, no script, no image URL — a security report that makes\n * a network request when opened is not one. It also has to render from\n * `file://` on a laptop with no connection, which is where it will be read.\n *\n * The security property that matters most here is escaping. Every string in a\n * {@link ScanResult} that is not our own prose — table names, policy names,\n * roles, the `fix` SQL, the server version banner — comes from the scanned\n * database, and this file turns them into markup. A table called\n * `<img src=x onerror=…>` is a legal Postgres identifier. {@link escapeHtml} is\n * applied at every interpolation without exception, including inside `<pre>`,\n * and `renderHtml` never accepts pre-built markup from a caller.\n */\n\nimport type { Finding, ScanResult, Severity } from \"./types\";\nimport { SEVERITIES } from \"./types\";\nimport { formatTarget, severityRank } from \"./report\";\n\n/**\n * The one primitive this whole file rests on.\n *\n * Escapes the five characters that can change the meaning of markup, in both\n * element and attribute position, so a single function covers every\n * interpolation site and there is no \"safe here, unsafe there\" judgement to get\n * wrong. Ampersand goes first or it double-escapes the replacements.\n */\nexport function escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\nexport interface HtmlRenderOptions {\n /** Echoed in the summary so the exit code is never a mystery. */\n failOn: Severity | \"none\";\n /**\n * `host:port` for the header. {@link ScanResult} deliberately carries no\n * port (it is a redaction surface), so the CLI passes the display form in.\n */\n endpoint?: string;\n /** Shown in the header. */\n version?: string;\n}\n\nconst PLATFORM_LABEL: Record<ScanResult[\"platform\"], string> = {\n supabase: \"Supabase\",\n neon: \"Neon\",\n rebase: \"Rebase\",\n postgrest: \"PostgREST\",\n unknown: \"PostgreSQL (no platform markers)\"\n};\n\n/** Sort key: worst first, then a stable schema/table/id ordering. */\nfunction compareFindings(a: Finding, b: Finding): number {\n const bySeverity = severityRank(b.severity) - severityRank(a.severity);\n if (bySeverity !== 0) return bySeverity;\n\n const bySchema = a.target.schema.localeCompare(b.target.schema);\n if (bySchema !== 0) return bySchema;\n\n const aName = a.target.table ?? a.target.view ?? a.target.routine ?? \"\";\n const bName = b.target.table ?? b.target.view ?? b.target.routine ?? \"\";\n const byName = aName.localeCompare(bName);\n if (byName !== 0) return byName;\n\n const byId = a.id.localeCompare(b.id);\n if (byId !== 0) return byId;\n\n return (a.target.policy ?? \"\").localeCompare(b.target.policy ?? \"\");\n}\n\nfunction plural(count: number, one: string, many: string): string {\n return count === 1 ? one : many;\n}\n\n/**\n * Only `https://` and `http://` links are emitted, and only as the whole href.\n *\n * `Finding.docs` is set by our own checks today, so this is defence against a\n * future check that builds the URL from something the database said — a\n * `javascript:` href would be a scripting vector that escaping alone does not\n * close, because the escaped text is perfectly valid inside an attribute.\n */\nfunction safeHref(url: string): string | null {\n return /^https?:\\/\\//i.test(url) ? url : null;\n}\n\n// ---------------------------------------------------------------------------\n// Styles\n// ---------------------------------------------------------------------------\n\n/**\n * Inline, because rule 4 forbids a stylesheet link, and a system font stack\n * because a font host is a network request too.\n *\n * The dark palette is defined only in a `prefers-color-scheme` block over a\n * complete light `:root`, so a viewer whose browser reports no preference gets\n * the light set rather than half of each. Print rules flatten the surfaces to\n * white and keep the severity words, which are what carry the meaning once the\n * colour is gone.\n */\nconst STYLES = `\n:root{\n --ground:#F7F8FA; --surface:#FFFFFF; --surface-2:#EEF1F5;\n --ink:#12161C; --ink-2:#454E5C; --ink-3:#6C7583;\n --line:#DEE3EA; --line-strong:#C3CAD5;\n --brand:#0070F4;\n --critical:#B3261E; --high:#C2410C; --medium:#9A6207; --low:#1F6FA8; --info:#6C7583;\n --critical-bg:rgba(179,38,30,.09); --high-bg:rgba(194,65,12,.09);\n --medium-bg:rgba(154,98,7,.10); --low-bg:rgba(31,111,168,.09); --info-bg:rgba(108,117,131,.09);\n --ok:#0E7A55; --ok-bg:rgba(14,122,85,.10);\n --warn:#9A6207; --warn-bg:rgba(154,98,7,.10);\n}\n@media (prefers-color-scheme:dark){\n :root{\n --ground:#0D1014; --surface:#151A21; --surface-2:#1C232C;\n --ink:#EAEEF3; --ink-2:#AAB4C1; --ink-3:#7B8592;\n --line:#232B35; --line-strong:#333D49;\n --brand:#5AA6FF;\n --critical:#FF7B72; --high:#FFA657; --medium:#E3B341; --low:#79C0FF; --info:#8B949E;\n --critical-bg:rgba(255,123,114,.12); --high-bg:rgba(255,166,87,.12);\n --medium-bg:rgba(227,179,65,.12); --low-bg:rgba(121,192,255,.12); --info-bg:rgba(139,148,158,.12);\n --ok:#56D4A0; --ok-bg:rgba(86,212,160,.13);\n --warn:#E3B341; --warn-bg:rgba(227,179,65,.13);\n }\n}\n*{box-sizing:border-box}\nbody{\n margin:0; background:var(--ground); color:var(--ink);\n font:16px/1.6 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif;\n -webkit-font-smoothing:antialiased;\n}\ncode,pre,.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,\"Liberation Mono\",monospace}\n.wrap{max-width:940px;margin:0 auto;padding:40px 24px 80px}\na{color:var(--brand)}\n\nheader.top{border-bottom:1px solid var(--line-strong);padding-bottom:24px}\n.tool{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);font-family:ui-monospace,monospace}\nh1{font-size:30px;line-height:1.15;letter-spacing:-.02em;margin:12px 0 0;font-weight:700}\n.facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:14px 28px;margin-top:22px}\n.fact .k{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3);font-family:ui-monospace,monospace}\n.fact .v{margin-top:3px;font-size:14px;color:var(--ink-2);word-break:break-word}\n\n.verdict{margin:28px 0 0;padding:16px 20px;border-radius:6px;border:1px solid var(--line);background:var(--surface)}\n.verdict.clean{background:var(--ok-bg);border-color:var(--ok)}\n.verdict.warn{background:var(--warn-bg);border-color:var(--warn)}\n.verdict.bad{background:var(--critical-bg);border-color:var(--critical)}\n.verdict h2{margin:0 0 4px;font-size:17px;letter-spacing:-.01em}\n.verdict p{margin:0;font-size:14.5px;color:var(--ink-2)}\n\n.caveat{margin-top:18px;padding:16px 20px;border-radius:6px;background:var(--warn-bg);border:1px solid var(--warn)}\n.caveat h3{margin:0 0 6px;font-size:14px;letter-spacing:.02em}\n.caveat p{margin:0 0 8px;font-size:14px;color:var(--ink-2)}\n.caveat ul{margin:8px 0 0;padding-left:20px;font-size:13.5px;color:var(--ink-2)}\n.caveat li{margin-bottom:4px}\n.caveat li code{font-size:12.5px}\n\n.counts{display:flex;flex-wrap:wrap;gap:8px;margin-top:26px}\n.count{display:flex;align-items:baseline;gap:7px;padding:7px 12px;border-radius:5px;border:1px solid var(--line);background:var(--surface);font-size:13px}\n.count b{font-family:ui-monospace,monospace;font-size:15px;font-variant-numeric:tabular-nums}\n.count.zero{opacity:.5}\n\nh2.section{font-size:13px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);\n font-family:ui-monospace,monospace;margin:44px 0 4px;padding-bottom:8px;border-bottom:1px solid var(--line-strong)}\n.lede{font-size:14px;color:var(--ink-2);margin:12px 0 0}\n\n.finding{margin-top:16px;background:var(--surface);border:1px solid var(--line);border-radius:6px;overflow:hidden}\n.finding > .head{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid var(--line)}\n.sev{display:inline-flex;align-items:center;gap:6px;font-family:ui-monospace,monospace;font-size:11px;\n font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:3px 8px;border-radius:4px}\n.sev::before{content:\"\";width:7px;height:7px;border-radius:50%;background:currentColor;flex:none}\n.sev.critical{color:var(--critical);background:var(--critical-bg)}\n.sev.high{color:var(--high);background:var(--high-bg)}\n.sev.medium{color:var(--medium);background:var(--medium-bg)}\n.sev.low{color:var(--low);background:var(--low-bg)}\n.sev.info{color:var(--info);background:var(--info-bg)}\n.cid{font-family:ui-monospace,monospace;font-size:12.5px;font-weight:600}\n.target{font-family:ui-monospace,monospace;font-size:12.5px;color:var(--ink-3);word-break:break-all}\n.finding > .body{padding:16px 18px}\n.finding h3{margin:0 0 10px;font-size:16px;line-height:1.35;letter-spacing:-.01em}\n.finding p{margin:0 0 12px;font-size:14.5px;color:var(--ink-2)}\n.row{display:grid;grid-template-columns:74px 1fr;gap:12px;margin-top:12px;align-items:start}\n.row .k{font-family:ui-monospace,monospace;font-size:10.5px;letter-spacing:.11em;text-transform:uppercase;\n color:var(--ink-3);padding-top:3px}\n.row .v{font-size:14.5px;color:var(--ink-2)}\npre{margin:0;padding:12px 14px;background:var(--surface-2);border:1px solid var(--line);border-radius:5px;\n overflow-x:auto;font-size:13px;line-height:1.5;color:var(--ink);white-space:pre}\n\nfooter{margin-top:56px;padding-top:20px;border-top:1px solid var(--line-strong);font-size:13px;color:var(--ink-3)}\nfooter p{margin:0 0 6px}\n\n@media print{\n :root{--ground:#FFF;--surface:#FFF;--surface-2:#F4F4F4;--ink:#000;--ink-2:#222;--ink-3:#555;\n --line:#CCC;--line-strong:#999}\n body{font-size:11pt}\n .wrap{max-width:none;padding:0}\n .finding{break-inside:avoid;page-break-inside:avoid}\n .verdict,.caveat{break-inside:avoid}\n}\n`.trim();\n\n// ---------------------------------------------------------------------------\n// Fragments\n// ---------------------------------------------------------------------------\n\nfunction renderFacts(result: ScanResult, options: HtmlRenderOptions): string {\n const endpoint = options.endpoint ?? result.database.host;\n\n // Some servers report a bare `18.4`, others a full `PostgreSQL 15.6 on …`.\n // A row reading \"Server 18.4\" tells the reader nothing.\n const serverVersion = /^[\\d.]/.test(result.serverVersion.trim())\n ? `PostgreSQL ${result.serverVersion}`\n : result.serverVersion;\n\n const scanned = [\n `${result.stats.schemas} ${plural(result.stats.schemas, \"schema\", \"schemas\")}`,\n `${result.stats.tables} ${plural(result.stats.tables, \"table\", \"tables\")}`,\n `${result.stats.policies} ${plural(result.stats.policies, \"policy\", \"policies\")}`,\n `${result.stats.checksRun} ${plural(result.stats.checksRun, \"check\", \"checks\")}`\n ].join(\" · \");\n\n const facts: [string, string][] = [\n [\"Database\", `${endpoint}/${result.database.name}`],\n [\"Server\", serverVersion],\n [\"Platform\", PLATFORM_LABEL[result.platform]],\n [\"Scanned\", scanned]\n ];\n\n return `<div class=\"facts\">${facts\n .map(\n ([key, value]) =>\n `<div class=\"fact\"><div class=\"k\">${escapeHtml(key)}</div><div class=\"v\">${escapeHtml(value)}</div></div>`\n )\n .join(\"\")}</div>`;\n}\n\n/**\n * The one-sentence answer, above everything.\n *\n * \"Inconclusive\" is its own state and outranks \"clean\": a scan whose catalogue\n * reads failed has not earned the word clean, and a reader who skims one line\n * of this report must not be told otherwise.\n */\nfunction renderVerdict(result: ScanResult, certain: readonly Finding[], heuristic: readonly Finding[]): string {\n const degraded = result.diagnostics?.degraded ?? [];\n\n if (degraded.length > 0) {\n return `<div class=\"verdict warn\"><h2>Inconclusive</h2><p>${escapeHtml(\n `${degraded.length} catalogue ${plural(degraded.length, \"read\", \"reads\")} failed, so some checks could not run. ` +\n \"Findings below are real, but their absence proves nothing.\"\n )}</p></div>`;\n }\n\n if (certain.length === 0 && heuristic.length === 0) {\n return `<div class=\"verdict clean\"><h2>No findings</h2><p>${escapeHtml(\n \"Every table, view and policy in scope passed all checks.\"\n )}</p></div>`;\n }\n\n const worst = certain[0]?.severity;\n const tone = worst === \"critical\" || worst === \"high\" ? \"bad\" : \"warn\";\n const parts: string[] = [];\n if (certain.length > 0) parts.push(`${certain.length} confirmed ${plural(certain.length, \"finding\", \"findings\")}`);\n if (heuristic.length > 0) parts.push(`${heuristic.length} worth checking`);\n\n return `<div class=\"verdict ${tone}\"><h2>${escapeHtml(parts.join(\" · \"))}</h2><p>${escapeHtml(\n certain.length > 0\n ? \"Confirmed findings are listed first. Each one names the object, what the database will actually do, and how to fix it.\"\n : \"Nothing confirmed. The heuristics below match a shape that is usually a mistake — read them and decide.\"\n )}</p></div>`;\n}\n\nfunction renderPrivilegeCaveat(): string {\n return `<div class=\"caveat\"><h3>This scan ran as a role RLS cannot constrain</h3><p>${escapeHtml(\n \"The connection was a superuser, a table owner, or a role with BYPASSRLS. That is why it could read the true \" +\n \"catalog, and it is also why nothing below describes what this connection experiences. The findings are \" +\n \"about what OTHER roles get.\"\n )}</p></div>`;\n}\n\nfunction renderDegradedCaveat(degraded: readonly { what: string; error: string }[]): string {\n const items = degraded\n .map((item) => `<li><code>${escapeHtml(item.what)}</code> — ${escapeHtml(item.error)}</li>`)\n .join(\"\");\n\n return `<div class=\"caveat\"><h3>${escapeHtml(\n `This scan was incomplete: ${degraded.length} catalogue ${plural(degraded.length, \"read\", \"reads\")} failed`\n )}</h3><p>${escapeHtml(\n \"Checks that depend on what could not be read report nothing, which looks exactly like finding nothing. \" +\n \"Treat this run as inconclusive rather than clean.\"\n )}</p><ul>${items}</ul></div>`;\n}\n\nfunction renderCounts(result: ScanResult): string {\n const counts = [...SEVERITIES]\n .reverse()\n .map((severity) => {\n const count = result.findings.filter((finding) => finding.severity === severity).length;\n\n return `<div class=\"count${count === 0 ? \" zero\" : \"\"}\"><span class=\"sev ${severity}\">${escapeHtml(\n severity\n )}</span><b>${count}</b></div>`;\n })\n .join(\"\");\n\n return `<div class=\"counts\">${counts}</div>`;\n}\n\nfunction renderFinding(finding: Finding): string {\n const out: string[] = [];\n\n out.push('<article class=\"finding\">');\n out.push(\n `<div class=\"head\"><span class=\"sev ${finding.severity}\">${escapeHtml(\n finding.severity\n )}</span><span class=\"cid\">${escapeHtml(finding.id)}</span><span class=\"target\">${escapeHtml(\n formatTarget(finding.target)\n )}</span></div>`\n );\n out.push('<div class=\"body\">');\n out.push(`<h3>${escapeHtml(finding.title)}</h3>`);\n\n if (finding.detail.trim().length > 0) {\n out.push(`<p>${escapeHtml(finding.detail)}</p>`);\n }\n\n if (finding.impact.trim().length > 0) {\n out.push(`<div class=\"row\"><div class=\"k\">Impact</div><div class=\"v\">${escapeHtml(finding.impact)}</div></div>`);\n }\n\n if (finding.fix && finding.fix.trim().length > 0) {\n // `<pre>` and never wrapped: the point of this block is that it survives\n // a copy into a psql session exactly as written.\n out.push(\n `<div class=\"row\"><div class=\"k\">Fix</div><div class=\"v\"><pre>${escapeHtml(\n finding.fix.replace(/\\s+$/, \"\")\n )}</pre></div></div>`\n );\n }\n\n const href = finding.docs ? safeHref(finding.docs) : null;\n if (href) {\n out.push(\n `<div class=\"row\"><div class=\"k\">Docs</div><div class=\"v\"><a href=\"${escapeHtml(\n href\n )}\">${escapeHtml(href)}</a></div></div>`\n );\n }\n\n out.push(\"</div></article>\");\n\n return out.join(\"\");\n}\n\n/** Findings grouped under a textual severity heading, worst group first. */\nfunction renderFindingGroups(findings: readonly Finding[], opts: { headings?: boolean } = {}): string {\n const out: string[] = [];\n const showHeadings = opts.headings !== false;\n\n for (const severity of [...SEVERITIES].reverse()) {\n const group = findings.filter((finding) => finding.severity === severity);\n if (group.length === 0) continue;\n\n if (showHeadings) {\n out.push(\n `<h2 class=\"section\">${escapeHtml(severity)} · ${group.length} ${escapeHtml(\n plural(group.length, \"finding\", \"findings\")\n )}</h2>`\n );\n }\n for (const finding of group) out.push(renderFinding(finding));\n }\n\n return out.join(\"\");\n}\n\nfunction renderFooter(result: ScanResult, options: HtmlRenderOptions): string {\n const failing = options.failOn !== \"none\" &&\n result.findings.some((finding) => severityRank(finding.severity) >= severityRank(options.failOn as Severity));\n\n const exitLine =\n options.failOn === \"none\"\n ? \"Exit code 0 — --fail-on none, so findings never fail the run.\"\n : failing\n ? `Exit code 1 — at least one finding is \"${options.failOn}\" or worse (--fail-on ${options.failOn}).`\n : `Exit code 0 — nothing at or above \"${options.failOn}\" (--fail-on ${options.failOn}).`;\n\n return `<footer><p>${escapeHtml(exitLine)}</p><p>${escapeHtml(\n `Scanned ${result.scannedAt} · read-only, and nothing left this machine. This file makes no network requests.`\n )}</p><p>rls-check is free and maintained by the team behind Rebase — <a href=\"https://rebase.pro\">rebase.pro</a></p></footer>`;\n}\n\n// ---------------------------------------------------------------------------\n// Document\n// ---------------------------------------------------------------------------\n\n/**\n * The whole report as one HTML document, ready to write to a file.\n *\n * Deterministic: two scans of an unchanged database differ only by\n * `scannedAt`, which keeps the output diffable when someone commits it.\n */\nexport function renderHtml(result: ScanResult, options: HtmlRenderOptions): string {\n const certain = result.findings.filter((finding) => finding.confidence !== \"heuristic\").sort(compareFindings);\n const heuristic = result.findings.filter((finding) => finding.confidence === \"heuristic\").sort(compareFindings);\n const degraded = result.diagnostics?.degraded ?? [];\n\n const title = options.version ? `rls-check ${options.version}` : \"rls-check\";\n const endpoint = options.endpoint ?? result.database.host;\n\n const body: string[] = [];\n\n body.push('<div class=\"wrap\">');\n body.push('<header class=\"top\">');\n body.push(`<div class=\"tool\">${escapeHtml(title)} · read-only Row-Level Security audit</div>`);\n body.push(`<h1>${escapeHtml(`${endpoint}/${result.database.name}`)}</h1>`);\n body.push(renderFacts(result, options));\n body.push(\"</header>\");\n\n body.push(renderVerdict(result, certain, heuristic));\n\n // Both caveats outrank the findings, and both survive every other option,\n // for the same reason: a check that could not run is indistinguishable in\n // the output from a check that found nothing.\n if (result.scannerIsPrivileged) body.push(renderPrivilegeCaveat());\n if (degraded.length > 0) body.push(renderDegradedCaveat(degraded));\n\n body.push(renderCounts(result));\n\n if (certain.length > 0) {\n body.push(renderFindingGroups(certain));\n }\n\n if (heuristic.length > 0) {\n body.push('<h2 class=\"section\">Worth checking</h2>');\n body.push(\n `<p class=\"lede\">${escapeHtml(\n \"These are heuristics, not proofs. They match a shape that is usually a mistake, but each one may be \" +\n \"deliberate in your schema — read them and decide. They are listed separately so nothing above \" +\n \"needs a second opinion.\"\n )}</p>`\n );\n body.push(renderFindingGroups(heuristic, { headings: false }));\n }\n\n body.push(renderFooter(result, options));\n body.push(\"</div>\");\n\n return [\n \"<!doctype html>\",\n '<html lang=\"en\">',\n \"<head>\",\n '<meta charset=\"utf-8\">',\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">',\n // Belt and braces against rule 4: even if a future edit introduced an\n // external reference, the document is not permitted to fetch it.\n `<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; style-src 'unsafe-inline'\">`,\n `<title>${escapeHtml(`RLS audit · ${endpoint}/${result.database.name}`)}</title>`,\n `<style>${STYLES}</style>`,\n \"</head>\",\n \"<body>\",\n body.join(\"\\n\"),\n \"</body>\",\n \"</html>\",\n \"\"\n ].join(\"\\n\");\n}\n","/**\n * The command line: argument parsing, connection resolution, error translation\n * and exit codes. Everything a stranger's first thirty seconds with this tool\n * touches.\n *\n * Three invariants, in order of importance:\n *\n * 1. {@link runCli} never throws and never calls `process.exit`. `bin/rls-check.js`\n * assigns its return value to `process.exitCode`, so an escaping exception\n * would surface as a stack trace and exit code 1 — which CI would read as\n * \"findings\", not \"broken\".\n *\n * 2. Exit 1 and exit 2 are never conflated. 1 means the database has a problem;\n * 2 means the scan did not happen. A pipeline that treats a DNS failure as a\n * clean bill of health is worse than no tool at all.\n *\n * 3. The connection string never reaches an output stream. Every string that\n * could have come from `pg`, Node or the user goes through `redactSecrets`\n * on its way out.\n */\n\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport { CHECKS, runChecks } from \"./checks\";\nimport { introspectWithDiagnostics, UnknownRoleError, unsupportedConnectionKeywords } from \"./introspect\";\nimport { formatEndpoint, isLoopbackEndpoint, parseConnectionString, redactSecrets } from \"./redact\";\nimport { exceedsThreshold, renderCheckCatalog, renderJson, renderReport } from \"./report\";\nimport { renderHtml } from \"./report-html\";\nimport type { DbSnapshot, Finding, ScanResult, Severity } from \"./types\";\nimport { SEVERITIES } from \"./types\";\n\n/** Clean, or nothing at or above `--fail-on`. */\nexport const EXIT_OK = 0;\n/** Findings at or above `--fail-on`. */\nexport const EXIT_FINDINGS = 1;\n/** The scan did not happen: bad arguments, bad connection, timeout. */\nexport const EXIT_ERROR = 2;\n\nconst DEFAULT_TIMEOUT_MS = 15_000;\nconst DEFAULT_FAIL_ON: Severity = \"high\";\n\nconst TABLE_KINDS = new Set([\"table\", \"partitioned_table\"]);\n\n// ---------------------------------------------------------------------------\n// Programmatic API\n// ---------------------------------------------------------------------------\n\nexport interface ScanOptions {\n connectionString: string;\n /** Restrict to these schemas. Empty or omitted means \"every user schema\". */\n schemas?: string[];\n /**\n * Additional roles an untrusted caller can arrive as, on top of the names\n * this tool recognises. Needed by any stack whose app role is not called\n * `anon`, `authenticated`, `web_anon` or `rebase_user`.\n */\n roles?: string[];\n /** Run only these check ids. */\n only?: string[];\n /** Skip these check ids. */\n skip?: string[];\n statementTimeoutMs?: number;\n /** Injectable so callers (and tests) control the `scannedAt` stamp. */\n now?: Date;\n}\n\n/**\n * Connect, introspect, run the checks, and assemble a {@link ScanResult}.\n *\n * The one impure step in the pipeline. Everything downstream of `introspect`\n * is a pure function of the snapshot, which is why the checks have unit tests\n * and this has an integration test.\n */\nexport async function scan(options: ScanOptions): Promise<ScanResult> {\n // `introspectWithDiagnostics`, not `introspect`: the latter drops the record\n // of what could not be read, and a check whose inputs went missing returns\n // no findings — which is indistinguishable from a clean table.\n const { snapshot, diagnostics } = await introspectWithDiagnostics({\n connectionString: options.connectionString,\n schemas: options.schemas && options.schemas.length > 0 ? options.schemas : undefined,\n roles: options.roles && options.roles.length > 0 ? options.roles : undefined,\n statementTimeoutMs: options.statementTimeoutMs ?? DEFAULT_TIMEOUT_MS\n });\n\n const findings = runChecks(snapshot, { only: options.only, skip: options.skip });\n\n return buildScanResult(snapshot, findings, {\n connectionString: options.connectionString,\n checksRun: selectCheckIds(options).length,\n scannedAt: (options.now ?? new Date()).toISOString(),\n diagnostics\n });\n}\n\n/**\n * The check ids `runChecks` will actually execute for these options.\n *\n * Recomputed here rather than reported back by `runChecks`, because\n * {@link ScanResult.stats} needs the count and the contract has no channel for\n * it. Same filter, same order, so the number in the report is the number that\n * ran — as long as `runChecks` keeps `only` as a whitelist and `skip` as a\n * blacklist applied after it.\n */\nexport function selectCheckIds(options: { only?: string[]; skip?: string[] }): string[] {\n const skip = new Set(options.skip ?? []);\n const only = options.only && options.only.length > 0 ? new Set(options.only) : null;\n\n return CHECKS.filter((check) => (only === null || only.has(check.id)) && !skip.has(check.id)).map(\n (check) => check.id\n );\n}\n\n/**\n * The verdict, as an exit code.\n *\n * Pulled out of `runCli` so it can be tested: `runCli` needs a database, and\n * the one line that decides whether CI goes red had no coverage at all —\n * deleting it broke no test.\n *\n * A degraded scan exits 2, the same code a crash uses, rather than 0. Checks\n * whose catalogue reads failed return no findings, which is indistinguishable\n * from finding none, so exiting 0 would have the scanner answer \"no problems\"\n * to a question it never managed to ask. Both codes mean the same thing here:\n * no verdict.\n */\nexport function exitCodeFor(result: ScanResult, failOn: Severity | \"none\"): number {\n if (result.diagnostics.degraded.length > 0) return EXIT_ERROR;\n return exceedsThreshold(result.findings, failOn) ? EXIT_FINDINGS : EXIT_OK;\n}\n\nfunction buildScanResult(\n snapshot: DbSnapshot,\n findings: Finding[],\n meta: {\n connectionString: string;\n checksRun: number;\n scannedAt: string;\n diagnostics: ScanResult[\"diagnostics\"];\n }\n): ScanResult {\n const target = parseConnectionString(meta.connectionString);\n const tables = snapshot.relations.filter((relation) => TABLE_KINDS.has(relation.kind));\n\n return {\n scannedAt: meta.scannedAt,\n // Host and database only — never the user, password or full URL.\n database: {\n host: target?.host ?? \"unknown\",\n name: target?.database && target.database.length > 0 ? target.database : \"unknown\"\n },\n serverVersion: snapshot.serverVersion,\n platform: snapshot.platform,\n scannerIsPrivileged: snapshot.scannerIsPrivileged,\n exposedRoles: snapshot.exposedRoles,\n stats: {\n schemas: snapshot.schemas.length,\n tables: tables.length,\n policies: snapshot.policies.length,\n tablesWithoutRls: tables.filter((relation) => !relation.rlsEnabled).length,\n checksRun: meta.checksRun\n },\n findings,\n diagnostics: meta.diagnostics\n };\n}\n\n// ---------------------------------------------------------------------------\n// Argument parsing\n// ---------------------------------------------------------------------------\n\nexport interface CliOptions {\n connectionString: string | null;\n json: boolean;\n /**\n * `--html <path>`: also write a self-contained HTML report there.\n *\n * \"Also\", not \"instead\": the terminal output is what a CI log keeps, and\n * the file is what gets forwarded to whoever owns the database. Making the\n * flag replace stdout would trade one audience for the other.\n */\n html: string | null;\n schemas: string[];\n /** Extra roles to treat as reachable by an untrusted caller. */\n roles: string[];\n failOn: Severity | \"none\";\n skip: string[];\n only: string[];\n listChecks: boolean;\n /** `null` = decide from NO_COLOR / TTY. */\n color: boolean | null;\n quiet: boolean;\n timeoutMs: number;\n help: boolean;\n version: boolean;\n}\n\nexport type ParseResult = { ok: true; options: CliOptions } | { ok: false; message: string };\n\nconst FAIL_ON_VALUES = [...SEVERITIES, \"none\"] as const;\n\nfunction emptyOptions(): CliOptions {\n return {\n connectionString: null,\n json: false,\n html: null,\n schemas: [],\n roles: [],\n failOn: DEFAULT_FAIL_ON,\n skip: [],\n only: [],\n listChecks: false,\n color: null,\n quiet: false,\n timeoutMs: DEFAULT_TIMEOUT_MS,\n help: false,\n version: false\n };\n}\n\n/** `--schema a,b --schema c` and `--schema a --schema b` mean the same thing. */\nfunction splitList(value: string): string[] {\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter((item) => item.length > 0);\n}\n\nexport function parseArgs(argv: readonly string[]): ParseResult {\n const options = emptyOptions();\n let positionals = 0;\n\n for (let index = 0; index < argv.length; index += 1) {\n const arg = argv[index];\n\n // Everything after `--` is positional, so a connection string that\n // somehow starts with a dash still works.\n if (arg === \"--\") {\n for (const rest of argv.slice(index + 1)) {\n positionals += 1;\n // Never echo the value: the extra argument is usually a second\n // connection string, and this message goes to a terminal.\n if (positionals > 1) {\n return { ok: false, message: \"Unexpected extra argument. Only one connection string is accepted.\" };\n }\n options.connectionString = rest;\n }\n break;\n }\n\n if (!arg.startsWith(\"-\")) {\n positionals += 1;\n if (positionals > 1) {\n return {\n ok: false,\n message: `Unexpected extra argument. Only one connection string is accepted; quote it if it contains a \"?\" or \"&\".`\n };\n }\n options.connectionString = arg;\n continue;\n }\n\n // --flag=value and --flag value are both accepted.\n const eq = arg.indexOf(\"=\");\n const flag = eq === -1 ? arg : arg.slice(0, eq);\n const inlineValue = eq === -1 ? null : arg.slice(eq + 1);\n\n const takeValue = (): string | null => {\n if (inlineValue !== null) return inlineValue;\n const next = argv[index + 1];\n if (next === undefined || (next.startsWith(\"-\") && next.length > 1)) return null;\n index += 1;\n\n return next;\n };\n\n switch (flag) {\n case \"-h\":\n case \"--help\":\n options.help = true;\n break;\n case \"-v\":\n case \"--version\":\n options.version = true;\n break;\n case \"--json\":\n options.json = true;\n break;\n case \"--quiet\":\n case \"-q\":\n options.quiet = true;\n break;\n case \"--list-checks\":\n options.listChecks = true;\n break;\n case \"--no-color\":\n case \"--no-colour\":\n options.color = false;\n break;\n case \"--color\":\n case \"--colour\":\n options.color = true;\n break;\n case \"--html\": {\n const value = takeValue();\n if (value === null) {\n return { ok: false, message: \"--html needs a file path, e.g. --html rls-report.html.\" };\n }\n options.html = value;\n break;\n }\n case \"--schema\": {\n const value = takeValue();\n if (value === null) return { ok: false, message: \"--schema needs a schema name.\" };\n options.schemas.push(...splitList(value));\n break;\n }\n case \"--role\": {\n const value = takeValue();\n if (value === null) {\n return { ok: false, message: \"--role needs a role name, e.g. --role app_user.\" };\n }\n options.roles.push(...splitList(value));\n break;\n }\n case \"--skip\": {\n const value = takeValue();\n if (value === null) return { ok: false, message: \"--skip needs a check id. See --list-checks.\" };\n options.skip.push(...splitList(value));\n break;\n }\n case \"--only\": {\n const value = takeValue();\n if (value === null) return { ok: false, message: \"--only needs a check id. See --list-checks.\" };\n options.only.push(...splitList(value));\n break;\n }\n case \"--fail-on\": {\n const value = takeValue();\n if (value === null) {\n return { ok: false, message: `--fail-on needs one of: ${FAIL_ON_VALUES.join(\", \")}.` };\n }\n const normalised = value.toLowerCase();\n if (!(FAIL_ON_VALUES as readonly string[]).includes(normalised)) {\n return {\n ok: false,\n message: `--fail-on ${value} is not a severity. Use one of: ${FAIL_ON_VALUES.join(\", \")}.`\n };\n }\n options.failOn = normalised as Severity | \"none\";\n break;\n }\n case \"--timeout\": {\n const value = takeValue();\n if (value === null) return { ok: false, message: \"--timeout needs a number of milliseconds.\" };\n const ms = Number(value);\n if (!Number.isFinite(ms) || ms <= 0) {\n return { ok: false, message: `--timeout ${value} is not a positive number of milliseconds.` };\n }\n options.timeoutMs = Math.floor(ms);\n break;\n }\n default:\n return { ok: false, message: `Unknown option: ${flag}. Run --help for the full list.` };\n }\n }\n\n return { ok: true, options };\n}\n\n// ---------------------------------------------------------------------------\n// Connection resolution\n// ---------------------------------------------------------------------------\n\nexport interface ResolvedConnection {\n connectionString: string;\n /** Where it came from, for the \"using DATABASE_URL from .env\" line. */\n source: string;\n}\n\n/**\n * A three-line .env reader. Deliberately not `dotenv`: the whole promise of\n * this package is that `npx` installs `pg` and nothing else, and a transitive\n * dependency is a supply-chain surface on a tool people point at production.\n *\n * Handles what a Postgres URL actually needs: `export` prefixes, `#` comments,\n * and single or double quotes. It does not do variable interpolation.\n */\nexport function readDotEnv(directory: string): Map<string, string> {\n const values = new Map<string, string>();\n\n let raw: string;\n try {\n raw = readFileSync(join(directory, \".env\"), \"utf8\");\n } catch {\n return values;\n }\n\n for (const line of raw.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (trimmed.length === 0 || trimmed.startsWith(\"#\")) continue;\n\n const body = trimmed.startsWith(\"export \") ? trimmed.slice(\"export \".length).trim() : trimmed;\n const eq = body.indexOf(\"=\");\n if (eq <= 0) continue;\n\n const key = body.slice(0, eq).trim();\n let value = body.slice(eq + 1).trim();\n\n const quoted =\n (value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"));\n if (quoted && value.length >= 2) {\n value = value.slice(1, -1);\n } else {\n // An unquoted trailing comment. `#` inside a password is why this\n // only strips a `#` that follows whitespace.\n const comment = value.search(/\\s#/);\n if (comment !== -1) value = value.slice(0, comment).trim();\n }\n\n if (key.length > 0) values.set(key, value);\n }\n\n return values;\n}\n\nexport function resolveConnection(\n positional: string | null,\n env: NodeJS.ProcessEnv,\n cwd: string\n): ResolvedConnection | null {\n if (positional && positional.trim().length > 0) {\n return { connectionString: positional.trim(), source: \"the command line\" };\n }\n\n if (env.DATABASE_URL && env.DATABASE_URL.trim().length > 0) {\n return { connectionString: env.DATABASE_URL.trim(), source: \"$DATABASE_URL\" };\n }\n\n if (env.POSTGRES_URL && env.POSTGRES_URL.trim().length > 0) {\n return { connectionString: env.POSTGRES_URL.trim(), source: \"$POSTGRES_URL\" };\n }\n\n const dotenv = readDotEnv(cwd);\n for (const key of [\"DATABASE_URL\", \"POSTGRES_URL\"]) {\n const value = dotenv.get(key);\n if (value && value.trim().length > 0) {\n return { connectionString: value.trim(), source: `${key} in .env` };\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Error translation\n// ---------------------------------------------------------------------------\n\nexport interface FriendlyError {\n headline: string;\n hint?: string;\n /** The driver's own words, redacted. Kept short — this is not a stack trace. */\n detail?: string;\n}\n\n/** `pg` sets `code` to either an errno string or a five-character SQLSTATE. */\nfunction errorCode(error: unknown): string | undefined {\n let current: unknown = error;\n for (let depth = 0; depth < 5 && current !== null && current !== undefined; depth += 1) {\n const code = (current as { code?: unknown }).code;\n if (typeof code === \"string\" && code.length > 0) return code;\n current = (current as { cause?: unknown }).cause;\n }\n\n return undefined;\n}\n\nfunction errorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n\n return String(error);\n}\n\n/**\n * Turn a driver failure into one sentence a human can act on.\n *\n * Deliberately covers the failures that account for nearly every first run:\n * wrong host, wrong password, wrong database, TLS, and a firewall that drops\n * rather than refuses. Anything unmapped still prints a redacted one-liner\n * rather than a stack trace.\n */\nexport function explainError(\n error: unknown,\n context: { endpoint: string; timeoutMs: number; connectionString?: string }\n): FriendlyError {\n const code = errorCode(error);\n const raw = errorMessage(error);\n const message = redactSecrets(raw, context.connectionString).replace(/\\s+/g, \" \").trim();\n const detail = code ? `${code}: ${message}` : message;\n const at = context.endpoint;\n\n const friendly = (headline: string, hint?: string): FriendlyError => ({ headline, hint, detail });\n\n switch (code) {\n case \"ECONNREFUSED\":\n return friendly(\n `Nothing is accepting connections at ${at}.`,\n \"Check the host and port, that the server is running, and that you are on the network it allows — a VPN, an SSH tunnel or an IP allowlist is the usual cause.\"\n );\n case \"ENOTFOUND\":\n case \"EAI_AGAIN\":\n return friendly(\n `The host in the connection string does not resolve (${at}).`,\n \"Check it for a typo. A Supabase host looks like db.<project-ref>.supabase.co; a Neon host ends in .neon.tech.\"\n );\n case \"ETIMEDOUT\":\n case \"ENETUNREACH\":\n case \"EHOSTUNREACH\":\n return friendly(\n `Timed out reaching ${at}.`,\n \"Packets are being dropped rather than refused, which almost always means a firewall or an IP allowlist. Add this machine's address to the database's allowed list.\"\n );\n case \"ECONNRESET\":\n // A loopback endpoint is a proxy or tunnel, never the database: it\n // accepted the TCP connection and then hung up because its own\n // upstream auth failed. sslmode cannot help — the proxy terminates\n // TLS itself — and the reason is only in the proxy's log.\n return isLoopbackEndpoint(at)\n ? friendly(\n `A local proxy or tunnel on ${at} closed the connection before the handshake finished.`,\n \"Something is listening on that port and hung up — cloud-sql-proxy, an SSH -L forward or a local pooler. The real cause is in its log, not in your TLS mode.\"\n )\n : friendly(\n `${at} closed the connection before the handshake finished.`,\n \"Most often TLS. Try appending ?sslmode=require to the connection string; managed providers reject plaintext.\"\n );\n case \"EPROTO\":\n case \"DEPTH_ZERO_SELF_SIGNED_CERT\":\n case \"SELF_SIGNED_CERT_IN_CHAIN\":\n case \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\":\n case \"ERR_TLS_CERT_ALTNAME_INVALID\":\n return friendly(\n `The TLS certificate presented by ${at} could not be verified.`,\n \"Append ?sslmode=no-verify to connect without verifying it, or point sslrootcert at your provider's CA bundle if you would rather keep verification on.\"\n );\n case \"28P01\":\n return friendly(\n `Password authentication failed on ${at}.`,\n \"Check the password. If it contains / ? or #, it has to be percent-encoded inside a URL — that is the single most common cause of this error. An @ or a : needs no encoding: the userinfo is split at the last @ and the user at the first :.\"\n );\n case \"28000\":\n return friendly(\n `${at} refused the connection for this role.`,\n \"Either the role does not exist, or pg_hba.conf has no rule matching this host, user and database combination.\"\n );\n case \"3D000\":\n return friendly(\n \"That database does not exist on the server.\",\n \"The database name is the last path segment of the connection string. On Supabase it is usually `postgres`.\"\n );\n case \"42501\":\n return friendly(\n \"The connected role is not allowed to read the catalogs this scan needs.\",\n \"Run it as the database owner or another role that can read pg_policies and pg_class. The scan itself only issues SELECTs.\"\n );\n case \"53300\":\n return friendly(\n `${at} has no connection slots left.`,\n \"Retry when the pool frees up, or point the scan at a direct connection rather than a saturated pooler.\"\n );\n case \"57014\":\n return friendly(\n `A catalog query exceeded the ${context.timeoutMs}ms statement timeout.`,\n \"Databases with tens of thousands of relations need longer: re-run with --timeout 60000.\"\n );\n case \"08006\":\n case \"08001\":\n case \"08004\":\n // Same trap as ECONNRESET above: on loopback there is no TLS mode\n // to get wrong, because the thing that refused you is a proxy.\n return isLoopbackEndpoint(at)\n ? friendly(\n `Could not establish a connection through the local proxy or tunnel on ${at}.`,\n \"The port answered but the session never came up. That is the proxy's upstream failing, not your TLS mode — its log has the reason.\"\n )\n : friendly(\n `Could not establish a connection to ${at}.`,\n \"Check the host, port and TLS mode. Managed providers usually need ?sslmode=require.\"\n );\n default:\n break;\n }\n\n if (/does not support SSL/i.test(raw)) {\n return friendly(\n `${at} is not configured for TLS.`,\n \"Append ?sslmode=disable if this is a local database you trust the network path to.\"\n );\n }\n if (/self.signed certificate|certificate/i.test(raw) && /SSL|TLS|certificate/i.test(raw)) {\n return friendly(\n `The TLS certificate presented by ${at} could not be verified.`,\n \"Append ?sslmode=no-verify, or point sslrootcert at your provider's CA bundle.\"\n );\n }\n if (/timeout/i.test(raw)) {\n return friendly(\n `Timed out talking to ${at}.`,\n `The scan uses a ${context.timeoutMs}ms statement timeout; raise it with --timeout <ms>, or check for a firewall dropping the connection.`\n );\n }\n if (/Connection terminated/i.test(raw)) {\n return friendly(\n `The connection to ${at} was closed unexpectedly.`,\n \"A pooler or proxy in front of the database is the usual cause. Try a direct connection.\"\n );\n }\n\n return friendly(`The scan against ${at} failed.`);\n}\n\n// ---------------------------------------------------------------------------\n// Presentation of operational failures\n// ---------------------------------------------------------------------------\n\nfunction formatFriendlyError(error: FriendlyError, color: boolean): string {\n const bold = (value: string): string => (color ? `\\u001B[1m${value}\\u001B[22m` : value);\n const dim = (value: string): string => (color ? `\\u001B[2m${value}\\u001B[22m` : value);\n const red = (value: string): string => (color ? `\\u001B[31m${value}\\u001B[39m` : value);\n\n const lines = [`${red(\"Error\")} ${bold(error.headline)}`];\n if (error.hint) lines.push(` ${error.hint}`);\n if (error.detail) lines.push(` ${dim(truncate(error.detail, 240))}`);\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction truncate(value: string, max: number): string {\n return value.length <= max ? value : `${value.slice(0, max - 1)}…`;\n}\n\n// ---------------------------------------------------------------------------\n// Static text\n// ---------------------------------------------------------------------------\n\nfunction helpText(version: string): string {\n return `rls-check ${version} — audit Row-Level Security on any PostgreSQL database.\n\nUsage\n DATABASE_URL=\"postgresql://user:pass@host:5432/db\" npx @rebasepro/rls-check\n\nThe connection string is taken from, in order: the argument, $DATABASE_URL,\n$POSTGRES_URL, then DATABASE_URL in a .env file in the current directory.\n\nPrefer the environment. A connection string passed as an argument is echoed back\nby npm before this program starts, and is written to your shell history — both\nwith the password in it, and neither is something rls-check can redact after the\nfact. Its own output redacts the password everywhere.\n\nOptions\n --json Machine-readable ScanResult on stdout, and nothing else.\n --html <path> Also write a self-contained HTML report to <path>. One file,\n no network requests, safe to attach to a ticket.\n --schema <name> Restrict the scan to a schema. Repeatable or comma-separated.\n --role <name> Treat this role as one an untrusted caller arrives as, in\n addition to anon, authenticated, web_anon and rebase_user.\n Name the role your application connects as — the checks\n only report a table as exposed when an exposed role can\n reach it. Repeatable or comma-separated.\n --fail-on <severity> Exit 1 at or above this severity: info, low, medium, high,\n critical, or none to never fail. Default: high.\n --only <id> Run only these checks. Repeatable or comma-separated.\n --skip <id> Skip these checks. Repeatable or comma-separated.\n --list-checks Print the check catalog and exit.\n --timeout <ms> Statement timeout for each catalog query. Default: 15000.\n --quiet Findings only: no banner, no summary.\n --no-color Disable ANSI colour. NO_COLOR and a non-TTY stdout are\n honoured automatically; --color forces it back on.\n -h, --help Show this text.\n -v, --version Print the version.\n\nExit codes\n 0 Clean, or nothing at or above --fail-on.\n 1 Findings at or above --fail-on.\n 2 The scan did not run: bad arguments, connection refused, auth failed, timeout.\n\nExamples\n DATABASE_URL=\"postgresql://user:pass@db.abcdef.supabase.co:5432/postgres\" \\\\\n npx @rebasepro/rls-check\n npx @rebasepro/rls-check --schema public --schema billing --fail-on medium\n npx @rebasepro/rls-check --json > rls-report.json\n npx @rebasepro/rls-check --html rls-report.html\n\nIt is read-only: it issues SELECTs against the system catalogs, writes nothing,\nand sends nothing anywhere.\n`;\n}\n\nfunction usageText(): string {\n return `rls-check needs a PostgreSQL connection string.\n\n DATABASE_URL=\"postgresql://user:password@host:5432/database\" npx @rebasepro/rls-check\n\nOr put DATABASE_URL in a .env file in this directory, or set POSTGRES_URL. It\nalso accepts the string as an argument, but npm echoes the command line before\nthis program starts and your shell records it, so the password ends up in two\nplaces rls-check cannot reach.\n\nIt is read-only — it reads the system catalogs and writes nothing. Run with\n--help for all options.\n`;\n}\n\n/**\n * Read the version out of the package manifest at runtime rather than inlining\n * it at build time, so a published tarball and a `pnpm build` never disagree.\n * `../package.json` is correct from both `src/cli.ts` and `dist/index.es.js`.\n */\nexport function resolveVersion(): string {\n for (const candidate of [\"../package.json\", \"../../package.json\"]) {\n try {\n const raw = readFileSync(new URL(candidate, import.meta.url), \"utf8\");\n const parsed = JSON.parse(raw) as { name?: string; version?: string };\n if (parsed.name === \"@rebasepro/rls-check\" && typeof parsed.version === \"string\") {\n return parsed.version;\n }\n } catch {\n // Fall through: a missing manifest must never break a scan.\n }\n }\n\n return \"unknown\";\n}\n\n// ---------------------------------------------------------------------------\n// runCli\n// ---------------------------------------------------------------------------\n\nexport interface CliIo {\n stdout(text: string): void;\n stderr(text: string): void;\n /**\n * Write a file, for `--html`. Injected rather than imported so the CLI\n * tests can exercise the whole path without touching a disk.\n */\n writeFile?(path: string, contents: string): void;\n env: NodeJS.ProcessEnv;\n cwd: string;\n /** Whether stdout is a terminal; drives colour when nothing else decides. */\n isTty: boolean;\n columns?: number;\n}\n\nfunction defaultIo(): CliIo {\n return {\n stdout: (text) => process.stdout.write(text),\n stderr: (text) => process.stderr.write(text),\n writeFile: (path, contents) => writeFileSync(path, contents, \"utf8\"),\n env: process.env,\n cwd: process.cwd(),\n isTty: Boolean(process.stdout.isTTY),\n columns: process.stdout.columns\n };\n}\n\n/**\n * Colour is on only when every signal agrees: not JSON, not NO_COLOR, not a\n * dumb terminal, and either a TTY or an explicit --color.\n */\nexport function resolveColor(explicit: boolean | null, json: boolean, env: NodeJS.ProcessEnv, isTty: boolean): boolean {\n if (json) return false;\n if (explicit !== null) return explicit;\n if (typeof env.NO_COLOR === \"string\" && env.NO_COLOR !== \"\") return false;\n if (env.TERM === \"dumb\") return false;\n if (typeof env.FORCE_COLOR === \"string\" && env.FORCE_COLOR !== \"\" && env.FORCE_COLOR !== \"0\") return true;\n\n return isTty;\n}\n\n/**\n * The entry point `bin/rls-check.js` calls. Returns the process exit code and\n * never throws — an unexpected failure becomes exit 2 with a redacted message.\n */\nexport async function runCli(argv: readonly string[], io: CliIo = defaultIo()): Promise<number> {\n let connectionString: string | undefined;\n\n try {\n const parsed = parseArgs(argv);\n if (!parsed.ok) {\n io.stderr(formatFriendlyError({ headline: parsed.message }, resolveColor(null, false, io.env, io.isTty)));\n\n return EXIT_ERROR;\n }\n\n const options = parsed.options;\n const color = resolveColor(options.color, options.json, io.env, io.isTty);\n const version = resolveVersion();\n\n if (options.help) {\n io.stdout(helpText(version));\n\n return EXIT_OK;\n }\n\n if (options.version) {\n io.stdout(`${version}\\n`);\n\n return EXIT_OK;\n }\n\n if (options.listChecks) {\n if (options.json) {\n io.stdout(\n `${JSON.stringify(\n CHECKS.map((check) => ({ id: check.id, title: check.title, description: check.description })),\n null,\n 2\n )}\\n`\n );\n } else {\n io.stdout(renderCheckCatalog(CHECKS, { color, width: io.columns }));\n }\n\n return EXIT_OK;\n }\n\n const unknownIds = [...options.only, ...options.skip].filter(\n (id) => !CHECKS.some((check) => check.id === id)\n );\n if (unknownIds.length > 0) {\n io.stderr(\n formatFriendlyError(\n {\n headline: `Unknown check ${unknownIds.length === 1 ? \"id\" : \"ids\"}: ${unknownIds.join(\", \")}.`,\n hint: \"Run --list-checks for the catalog. Ids are stable, so a typo here silently weakens the scan rather than failing it — which is why this is an error.\"\n },\n color\n )\n );\n\n return EXIT_ERROR;\n }\n\n const resolved = resolveConnection(options.connectionString, io.env, io.cwd);\n if (!resolved) {\n io.stderr(usageText());\n\n return EXIT_ERROR;\n }\n connectionString = resolved.connectionString;\n\n const target = parseConnectionString(connectionString);\n const endpoint = formatEndpoint(target);\n\n if (!target) {\n io.stderr(\n formatFriendlyError(\n {\n headline: \"That does not look like a PostgreSQL connection string.\",\n hint: \"Expected postgresql://user:password@host:5432/database, or a libpq keyword string carrying at least one of host= dbname= user= (port, password, sslmode, application_name, connect_timeout and options are honoured too). A password containing / ? or # is the usual cause: those end the authority, so the split lands inside the credential and neither this tool nor libpq can tell where it ends — percent-encode them. An @ or a : inside a password is fine, and needs no encoding.\"\n },\n color\n )\n );\n\n return EXIT_ERROR;\n }\n\n // A keyword string this tool cannot translate in full is refused rather\n // than connected with the missing keyword dropped: every one of them\n // changes where the connection goes or how it is verified.\n const unsupported = unsupportedConnectionKeywords(connectionString);\n if (unsupported.length > 0) {\n io.stderr(\n formatFriendlyError(\n {\n headline: `This connection string uses ${unsupported.length === 1 ? \"a keyword\" : \"keywords\"} the scan cannot honour: ${unsupported.join(\", \")}.`,\n hint: \"Rewrite it as a URL — postgresql://user:password@host:5432/database?sslmode=require. Connecting with those keywords dropped would send the scan somewhere you did not ask for, or verify TLS less strictly than you asked for.\"\n },\n color\n )\n );\n\n return EXIT_ERROR;\n }\n\n // There is no way to abort an in-flight libpq query from Node, so the\n // handler exits directly rather than unwinding. 130 is the shell's\n // convention for SIGINT and keeps it distinct from 1 and 2.\n const onSigint = (): void => {\n io.stderr(\"\\nInterrupted. Nothing was written — the scan is read-only.\\n\");\n process.exit(130);\n };\n process.on(\"SIGINT\", onSigint);\n\n let result: ScanResult;\n try {\n result = await scan({\n connectionString,\n schemas: options.schemas,\n roles: options.roles,\n only: options.only,\n skip: options.skip,\n statementTimeoutMs: options.timeoutMs\n });\n } catch (error) {\n // A typo in `--role` is the user's mistake, not the database's, and\n // `explainError` would bury it under \"the scan failed\".\n if (error instanceof UnknownRoleError) {\n io.stderr(\n formatFriendlyError(\n {\n headline: `No such role on this database: ${error.roles.join(\", \")}.`,\n hint: \"Check the spelling against `SELECT rolname FROM pg_roles`. This is an error rather than a warning for the same reason an unknown --skip id is: a name that matches nothing silently narrows the scan, and the run then prints a clean report of a database nobody looked at.\"\n },\n color\n )\n );\n\n return EXIT_ERROR;\n }\n\n io.stderr(\n formatFriendlyError(explainError(error, { endpoint, timeoutMs: options.timeoutMs, connectionString }), color)\n );\n\n return EXIT_ERROR;\n } finally {\n process.removeListener(\"SIGINT\", onSigint);\n }\n\n if (options.json) {\n io.stdout(`${renderJson(result)}\\n`);\n } else {\n io.stdout(\n renderReport(result, {\n color,\n quiet: options.quiet,\n failOn: options.failOn,\n width: io.columns,\n endpoint,\n version\n })\n );\n }\n\n // The artifact is the whole point of asking for it, so failing to write\n // it is a failed run even though the scan itself succeeded — exit 2,\n // the \"no verdict\" code, rather than letting a missing report look like\n // a clean one. The findings are already on stdout either way.\n if (options.html !== null) {\n const write = io.writeFile;\n if (!write) {\n io.stderr(\n formatFriendlyError(\n { headline: \"--html is not available: this environment cannot write files.\" },\n color\n )\n );\n\n return EXIT_ERROR;\n }\n try {\n write(options.html, renderHtml(result, { failOn: options.failOn, endpoint, version }));\n io.stderr(`Wrote ${options.html}\\n`);\n } catch (error) {\n io.stderr(\n formatFriendlyError(\n {\n headline: `Could not write ${options.html}.`,\n hint: \"Check the directory exists and is writable.\",\n detail: redactSecrets(errorMessage(error), connectionString)\n },\n color\n )\n );\n\n return EXIT_ERROR;\n }\n }\n\n // A degraded scan is not a clean scan. Checks whose catalogue reads\n // failed return nothing, which is indistinguishable from finding\n // nothing — so reporting EXIT_OK here would be the scanner answering\n // \"no problems\" to a question it never managed to ask. Exit 2, the same\n // code an outright crash uses, because both mean \"no verdict\".\n return exitCodeFor(result, options.failOn);\n } catch (error) {\n // Anything that escapes the expected paths. Exit 2, never 1: a crash is\n // not a clean bill of health and it is not a finding either.\n io.stderr(\n formatFriendlyError(\n {\n headline: \"rls-check failed before it could report.\",\n hint: \"This is a bug — please open an issue at https://github.com/rebasepro/rebase/issues.\",\n detail: redactSecrets(errorMessage(error), connectionString)\n },\n false\n )\n );\n\n return EXIT_ERROR;\n }\n}\n"],"mappings":"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"}
|