@rebasepro/rls-check 0.22.0 → 0.24.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,5 +1,5 @@
1
1
  import type { ClientConfig } from "pg";
2
- import type { DbSnapshot } from "./types.js";
2
+ import type { DbForeignKey, DbGrant, DbRelation, DbSnapshot, DbView } from "./types.js";
3
3
  export interface ConnectOptions {
4
4
  connectionString: string;
5
5
  /** Explicit schema allowlist. Default: every non-system schema. */
@@ -80,8 +80,24 @@ export declare class UnknownRoleError extends Error {
80
80
  readonly roles: string[];
81
81
  constructor(roles: string[]);
82
82
  }
83
+ /**
84
+ * A `--schema` that is not in `pg_namespace`.
85
+ *
86
+ * The same failure as an unknown `--role`, and worse: a typo such as
87
+ * `--schema pubic`, or `--schema Public` for a schema named `public`, scanned
88
+ * nothing at all. Zero tables, "No findings", exit 0, for as long as the typo
89
+ * sat in a CI job. A name that matches nothing only ever narrows the scan, so
90
+ * it is an error.
91
+ */
92
+ export declare class UnknownSchemaError extends Error {
93
+ readonly schemas: string[];
94
+ constructor(schemas: string[]);
95
+ }
83
96
  export declare function introspect(opts: ConnectOptions): Promise<DbSnapshot>;
84
97
  export declare function introspectWithDiagnostics(opts: ConnectOptions): Promise<IntrospectResult>;
98
+ type SslOption = false | {
99
+ rejectUnauthorized: boolean;
100
+ };
85
101
  /**
86
102
  * The keywords in a libpq keyword string that this tool cannot honour, in the
87
103
  * order they were written. Empty for a URL, and empty for a keyword string it
@@ -95,6 +111,17 @@ export declare function unsupportedConnectionKeywords(connectionString: string):
95
111
  * failure would still be reported against the host the user typed.
96
112
  */
97
113
  export declare function clientConfigFromKeywords(keywords: Map<string, string>): ClientConfig;
114
+ /**
115
+ * The TLS postures to try, in order, for an `sslmode`. Each entry says whether
116
+ * certificate verification had to be given up to get there.
117
+ */
118
+ export declare function connectionAttempts(sslmode: string | undefined): {
119
+ ssl: SslOption;
120
+ downgraded: boolean;
121
+ }[];
122
+ export interface Reader {
123
+ query<R extends Record<string, unknown>>(what: string, text: string, values?: unknown[]): Promise<R[]>;
124
+ }
98
125
  /**
99
126
  * Which of the database's schemas this scan covers.
100
127
  *
@@ -103,3 +130,76 @@ export declare function clientConfigFromKeywords(keywords: Map<string, string>):
103
130
  * {@link PLATFORM_SCHEMAS} silently stops being checked.
104
131
  */
105
132
  export declare function selectSchemas(all: string[], requested: string[] | undefined, diagnostics: IntrospectDiagnostics): string[];
133
+ /**
134
+ * Relations a scanned view reads or a scanned foreign key points at that the
135
+ * scan did not read, each named once.
136
+ *
137
+ * View dependencies are read across schemas on purpose, and foreign keys
138
+ * always were — but a dependency on a relation that is not in the snapshot
139
+ * resolves to nothing. `CREATE VIEW public.files AS SELECT * FROM
140
+ * storage.objects`, granted to anon, produced no finding on a default scan
141
+ * (which leaves `storage` out), and a junction table pointing at `auth.users`
142
+ * was never considered.
143
+ */
144
+ export declare function outOfScopeReferences(relations: DbRelation[], views: DbView[], foreignKeys: DbForeignKey[]): {
145
+ schema: string;
146
+ name: string;
147
+ }[];
148
+ /**
149
+ * Read the relations {@link outOfScopeReferences} names. Columns are not read:
150
+ * nothing judges a referenced relation by its columns, only by whether it has
151
+ * row-level security.
152
+ */
153
+ export declare function readReferencedRelations(db: Reader, refs: {
154
+ schema: string;
155
+ name: string;
156
+ }[]): Promise<DbRelation[]>;
157
+ /**
158
+ * Privileges on each scanned relation, from the relation's ACL and from its
159
+ * columns' ACLs.
160
+ *
161
+ * A column privilege is a privilege on the relation for what these checks ask:
162
+ * `GRANT SELECT (id, email) ON users TO anon` on a table with RLS off hands
163
+ * anon those columns of every row, and a column-level UPDATE grant next to a
164
+ * `USING (true)` policy lets anon rewrite them. Reading `relacl` alone missed
165
+ * both, so `rls-disabled` and `anonymous-write-allowed` said nothing.
166
+ */
167
+ export declare function readGrants(db: Reader, schemas: string[]): Promise<DbGrant[]>;
168
+ /**
169
+ * Who holds USAGE on each schema. Every schema, not only the scanned ones: a
170
+ * view or a foreign key can point outside the scan.
171
+ *
172
+ * `undefined`, not `[]`, when the read failed — an empty list would say no role
173
+ * can use any schema and silence every check that gates on a grant, which is
174
+ * the wrong direction for a scanner to fail in.
175
+ */
176
+ export declare function readSchemaUsage(db: Reader, diagnostics: IntrospectDiagnostics): Promise<{
177
+ schema: string;
178
+ grantee: string;
179
+ }[] | undefined>;
180
+ export declare function readViews(db: Reader, schemas: string[], serverVersionNum: number): Promise<DbView[]>;
181
+ /**
182
+ * Postgres's `parse_bool`: `true`/`false`, `yes`/`no`, `on`/`off`, `1`/`0`,
183
+ * case-insensitively, and any unambiguous prefix of the words — `t`, `ye`,
184
+ * `of` — but not `o`, which could be either. `null` for anything else.
185
+ */
186
+ export declare function parsePgBool(raw: string): boolean | null;
187
+ export interface ViewDependencyRow extends Record<string, unknown> {
188
+ view_schema: string;
189
+ view_name: string;
190
+ ref_schema: string;
191
+ ref_name: string;
192
+ }
193
+ /**
194
+ * The transitive closure of direct view dependencies, keyed by
195
+ * `JSON.stringify([schema, name])`.
196
+ *
197
+ * Every view with a dependency of its own is expanded, wherever it lives, and
198
+ * names are kept as pairs: `schema.name` split at the first dot is wrong for a
199
+ * schema whose name has a dot in it.
200
+ */
201
+ export declare function resolveViewDependencies(rows: ViewDependencyRow[]): Map<string, {
202
+ schema: string;
203
+ table: string;
204
+ }[]>;
205
+ export {};
package/dist/report.d.ts CHANGED
@@ -24,6 +24,38 @@ export declare function maxSeverity(findings: readonly Finding[]): Severity | nu
24
24
  * would make the exit code disagree with the report.
25
25
  */
26
26
  export declare function exceedsThreshold(findings: readonly Finding[], failOn: Severity | "none"): boolean;
27
+ /** Clean, or nothing at or above `--fail-on`. */
28
+ export declare const EXIT_OK = 0;
29
+ /** Findings at or above `--fail-on`. */
30
+ export declare const EXIT_FINDINGS = 1;
31
+ /** The scan did not happen: bad arguments, bad connection, timeout. */
32
+ export declare const EXIT_ERROR = 2;
33
+ /**
34
+ * The verdict, as an exit code.
35
+ *
36
+ * Pulled out of `runCli` so it can be tested: `runCli` needs a database, and
37
+ * the one line that decides whether CI goes red had no coverage at all —
38
+ * deleting it broke no test.
39
+ *
40
+ * A degraded scan exits 2, the same code a crash uses, rather than 0. Checks
41
+ * whose catalogue reads failed return no findings, which is indistinguishable
42
+ * from finding none, so exiting 0 would have the scanner answer "no problems"
43
+ * to a question it never managed to ask. Both codes mean the same thing here:
44
+ * no verdict.
45
+ */
46
+ export declare function exitCodeFor(result: ScanResult, failOn: Severity | "none"): number;
47
+ /**
48
+ * The summary's last word: the code the process exits with, and why.
49
+ *
50
+ * Both reports print it, and both used to work it out for themselves from the
51
+ * findings alone, so a degraded scan printed "Exit code 0" or "Exit code 1"
52
+ * and then exited 2. It comes from {@link exitCodeFor} now, the same function
53
+ * the CLI exits with.
54
+ */
55
+ export declare function exitVerdict(result: ScanResult, failOn: Severity | "none"): {
56
+ code: number;
57
+ reason: string;
58
+ };
27
59
  export declare function clampWidth(columns: number | undefined): number;
28
60
  /**
29
61
  * Human-readable object path for a finding. Kept boring on purpose — people
package/dist/types.d.ts CHANGED
@@ -175,6 +175,18 @@ export interface DbSnapshot {
175
175
  policies: DbPolicy[];
176
176
  roles: DbRole[];
177
177
  grants: DbGrant[];
178
+ /**
179
+ * Who holds USAGE on each schema, from `pg_namespace.nspacl` (`PUBLIC` for
180
+ * a grant to everyone). A role without it cannot name anything in the
181
+ * schema, whatever it holds on the tables there. Absent when it could not
182
+ * be read, and a schema with no entries here counts as unknown: either way
183
+ * every role is assumed to have USAGE, which widens a scan rather than
184
+ * narrowing it.
185
+ */
186
+ schemaUsage?: {
187
+ schema: string;
188
+ grantee: string;
189
+ }[];
178
190
  views: DbView[];
179
191
  foreignKeys: DbForeignKey[];
180
192
  routines: DbRoutine[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/rls-check",
3
- "version": "0.22.0",
3
+ "version": "0.24.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
5
  "keywords": [
6
6
  "postgres",
@@ -59,7 +59,7 @@
59
59
  "@types/pg": "^8.11.10",
60
60
  "typescript": "^6.0.3",
61
61
  "vite": "^8.1.5",
62
- "vitest": "^4.1.10"
62
+ "vitest": "^4.1.11"
63
63
  },
64
64
  "scripts": {
65
65
  "test": "vitest run",