@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.
@@ -1,3 +1,4 @@
1
+ import type { ClientConfig } from "pg";
1
2
  import type { DbSnapshot } from "./types.js";
2
3
  export interface ConnectOptions {
3
4
  connectionString: string;
@@ -55,13 +56,45 @@ export interface IntrospectDiagnostics {
55
56
  * imply that it looked.
56
57
  */
57
58
  unrecognizedGrantees: string[];
59
+ /**
60
+ * The connecting role, when it is not privileged and was therefore added to
61
+ * the exposed set. Disclosed rather than silent: it changes which findings
62
+ * appear, so a reader comparing two runs has to be able to see it.
63
+ */
64
+ scanningAsExposedRole?: string | null;
58
65
  }
59
66
  export interface IntrospectResult {
60
67
  snapshot: DbSnapshot;
61
68
  diagnostics: IntrospectDiagnostics;
62
69
  }
70
+ /**
71
+ * A `--role` that is not in `pg_roles`.
72
+ *
73
+ * Same reasoning as an unknown `--skip` id: a typo here does not widen the scan
74
+ * and get noticed, it *narrows* it silently. `exposedRolesFor` drops the name,
75
+ * every check that gates on a grant to an exposed role stops matching, and the
76
+ * run prints "No findings" on a database nobody looked at properly. So it is an
77
+ * error, not a warning.
78
+ */
79
+ export declare class UnknownRoleError extends Error {
80
+ readonly roles: string[];
81
+ constructor(roles: string[]);
82
+ }
63
83
  export declare function introspect(opts: ConnectOptions): Promise<DbSnapshot>;
64
84
  export declare function introspectWithDiagnostics(opts: ConnectOptions): Promise<IntrospectResult>;
85
+ /**
86
+ * The keywords in a libpq keyword string that this tool cannot honour, in the
87
+ * order they were written. Empty for a URL, and empty for a keyword string it
88
+ * can translate in full.
89
+ */
90
+ export declare function unsupportedConnectionKeywords(connectionString: string): string[];
91
+ /**
92
+ * `Client` options for a libpq keyword string. Throws on a keyword it cannot
93
+ * honour. Exported for the test that pins the translation: getting `host` or
94
+ * `port` wrong here sends a production scan somewhere nobody asked for, and the
95
+ * failure would still be reported against the host the user typed.
96
+ */
97
+ export declare function clientConfigFromKeywords(keywords: Map<string, string>): ClientConfig;
65
98
  /**
66
99
  * Which of the database's schemas this scan covers.
67
100
  *
package/dist/redact.d.ts CHANGED
@@ -16,7 +16,11 @@
16
16
  * - the user ends at the FIRST `:` in the userinfo
17
17
  *
18
18
  * A libpq keyword string (`host=... password=...`) is understood too, because
19
- * `psql "$PGSTRING"` users paste those.
19
+ * `psql "$PGSTRING"` users paste those. Understood here means *parsed*: `pg`
20
+ * itself cannot read that form, so anything that opens a connection has to go
21
+ * through {@link parseKeywordConnectionString} and build the `Client` options
22
+ * by hand. Parsing it here and handing the raw string to the driver would
23
+ * report failures against a host the user never named.
20
24
  */
21
25
  /** What replaces every credential. */
22
26
  export declare const REDACTED = "***";
@@ -31,6 +35,18 @@ export interface ConnectionTarget {
31
35
  user: string | null;
32
36
  password: string | null;
33
37
  }
38
+ /**
39
+ * The libpq keyword/value form (`host=… dbname=…`) as a map of lowercased
40
+ * keyword to unquoted value, or `null` when the string is not that form.
41
+ *
42
+ * Exported because `pg` cannot read this form at all: `pg-connection-string`
43
+ * only understands URLs, so a `Client({ connectionString: "host=127.0.0.1
44
+ * port=1 …" })` connects to the *default* host and reports a failure against an
45
+ * endpoint nobody asked for. Whoever opens the connection has to translate
46
+ * these keywords into `Client` options itself — see `connect()` in
47
+ * `introspect.ts`.
48
+ */
49
+ export declare function parseKeywordConnectionString(raw: string): Map<string, string> | null;
34
50
  /**
35
51
  * Split a connection string into its parts. Returns `null` when the input is
36
52
  * not recognisably a connection string — callers must treat that as "unknown",
package/dist/types.d.ts CHANGED
@@ -198,6 +198,13 @@ export interface ScanResult {
198
198
  serverVersion: string;
199
199
  platform: DbSnapshot["platform"];
200
200
  scannerIsPrivileged: boolean;
201
+ /**
202
+ * The roles every check gated on. Reported because it is the single fact
203
+ * that decides whether a clean run means anything: a check only calls a
204
+ * table exposed when one of these can reach it, so a reader who does not
205
+ * see their own app role here knows the run did not cover their API.
206
+ */
207
+ exposedRoles: string[];
201
208
  stats: {
202
209
  schemas: number;
203
210
  tables: number;
@@ -233,11 +240,19 @@ export interface ScanResult {
233
240
  reason: "system" | "platform" | "not-requested";
234
241
  }[];
235
242
  /**
236
- * Roles holding write privileges that the scan neither recognises as
237
- * exposed nor can explain as trusted. Non-empty means the exposed-role
238
- * set may be incomplete, and every check gates on that set — so this is
239
- * the difference between "clean" and "clean as far as I could tell".
243
+ * Roles holding read or write privileges on scanned tables that the scan
244
+ * neither recognises as exposed nor can explain as trusted. Non-empty
245
+ * means the exposed-role set may be incomplete, and every check gates on
246
+ * that set — so this is the difference between "clean" and "clean as far
247
+ * as I could tell".
240
248
  */
241
249
  unrecognizedGrantees: string[];
250
+ /**
251
+ * The role the scan connected as, when RLS constrains it and it was
252
+ * therefore treated as exposed. `null` or absent when the scan connected
253
+ * as a superuser, an owner or a BYPASSRLS role — the case
254
+ * `scannerIsPrivileged` already describes.
255
+ */
256
+ scanningAsExposedRole?: string | null;
242
257
  };
243
258
  }
package/package.json CHANGED
@@ -1,12 +1,41 @@
1
1
  {
2
2
  "name": "@rebasepro/rls-check",
3
- "version": "0.17.3",
3
+ "version": "0.18.0",
4
4
  "description": "Audit Row-Level Security on any PostgreSQL database — finds tables served without RLS, permissive tautologies, unqualified columns in policy subqueries, RLS-bypassing views and unprotected junction tables. Read-only.",
5
+ "keywords": [
6
+ "postgres",
7
+ "postgresql",
8
+ "rls",
9
+ "row-level-security",
10
+ "security",
11
+ "audit",
12
+ "linter",
13
+ "supabase",
14
+ "policy"
15
+ ],
16
+ "homepage": "https://rebase.pro",
17
+ "bugs": {
18
+ "url": "https://github.com/rebasepro/rebase/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/rebasepro/rebase.git",
23
+ "directory": "packages/rls-check"
24
+ },
25
+ "license": "MIT",
26
+ "author": "rebase.pro",
5
27
  "main": "./dist/index.es.js",
6
28
  "module": "./dist/index.es.js",
7
29
  "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.es.js",
34
+ "default": "./dist/index.es.js"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
8
38
  "type": "module",
9
- "source": "src/index.ts",
10
39
  "bin": {
11
40
  "rls-check": "bin/rls-check.js"
12
41
  },
@@ -19,27 +48,9 @@
19
48
  "bin",
20
49
  "README.md"
21
50
  ],
22
- "repository": {
23
- "type": "git",
24
- "url": "https://github.com/rebasepro/rebase.git",
25
- "directory": "packages/rls-check"
26
- },
27
51
  "engines": {
28
- "node": ">=20"
52
+ "node": ">=22.22.0"
29
53
  },
30
- "keywords": [
31
- "postgres",
32
- "postgresql",
33
- "rls",
34
- "row-level-security",
35
- "security",
36
- "audit",
37
- "linter",
38
- "supabase",
39
- "policy"
40
- ],
41
- "author": "rebase.pro",
42
- "license": "MIT",
43
54
  "dependencies": {
44
55
  "pg": "^8.22.0"
45
56
  },
@@ -52,6 +63,7 @@
52
63
  },
53
64
  "scripts": {
54
65
  "test": "vitest run",
66
+ "test:watch": "vitest",
55
67
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
56
68
  "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../tooling/scripts/add-dts-extensions.mjs dist && node ../../tooling/scripts/assert-build-output.mjs",
57
69
  "clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f",