safegres 1.30.1 → 1.30.2

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.
Files changed (2) hide show
  1. package/README.md +42 -13
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -87,7 +87,7 @@ Three analyses over three different representations. None of them run your queri
87
87
  grants, RLS flags, policies with their expressions, indexes, ownership.
88
88
 
89
89
  **2. Policy predicates, as ASTs.** Every `USING` / `WITH CHECK` expression is parsed with
90
- [`pgsql-parser`](https://github.com/launchql/pgsql-parser) (libpg_query — the *actual* PostgreSQL
90
+ [`pgsql-parser`](https://github.com/constructive-io/pgsql-parser) (libpg_query — the *actual* PostgreSQL
91
91
  grammar, not a regex) and traversed. That is what makes the interesting rules possible: a literal
92
92
  `true` in a write policy (A7), a cast wrapping the policy's own column so no plain index can serve
93
93
  it (X3), a non-`IMMUTABLE` call whose arguments reference no column of the row and which sits
@@ -118,10 +118,11 @@ trust boundaries on the way.
118
118
 
119
119
  ## What it checks
120
120
 
121
- 34 rules across two dimensions, plus a source-level convention linter. The prefix letter is a
122
- family, **not** the dimension: `P1`/`P1b` are performance, `P5` is security.
121
+ 45 rules across two dimensions — 34 security, 11 performance — plus four opt-in runtime-statistics
122
+ rules and a four-rule source-level convention linter. The prefix letter is a family, **not** the
123
+ dimension: `P1`/`P1b` are performance, `P5` is security.
123
124
 
124
- ### Security (19 rules)
125
+ ### Security (34 rules)
125
126
 
126
127
  | Code | Severity | Direction | Check |
127
128
  | --- | --- | --- | --- |
@@ -143,6 +144,7 @@ family, **not** the dimension: `P1`/`P1b` are performance, `P5` is security.
143
144
  | L4 | info | neutral | **Dead schema `USAGE`** — reaches no relation and no function |
144
145
  | L5 | info | fail-open | An untrusted role reaches an **RLS-off table** via PUBLIC/inheritance † |
145
146
  | L6 | info | neutral | **Unaddressable grant** — an API role holds privileges on a relation its API cannot name ‡ |
147
+ | L7 | info | fail-open | **Role escalation** — an untrusted role can `SET ROLE` to a role with more reach † |
146
148
  | L8 | info | fail-open | **DEFINER view bypass** — an untrusted role reads a base relation, and named columns of it, as the view's owner † |
147
149
  | L9 | info | fail-open | **DEFINER view write** — an auto-updatable definer view writes a base relation as its owner † |
148
150
  | L10 | info | fail-open | **Rewrite-rule bypass** — a rule on a view writes a relation as the view's owner, `security_invoker` notwithstanding † |
@@ -156,11 +158,13 @@ family, **not** the dimension: `P1`/`P1b` are performance, `P5` is security.
156
158
  | L18 | info | fail-open | **Writable filtering view without `WITH CHECK OPTION`** — an untrusted role writes rows the view's own filter excludes † |
157
159
  | L19 | info | fail-open | **Definer-function reach** — an untrusted role touches a relation by executing a `SECURITY DEFINER` function, which runs as its owner † |
158
160
  | L20 | info | fail-open | **`INSTEAD OF` trigger write** — a write against a view becomes a trigger function's body, and a definer one lands it as the function's owner † |
161
+ | L21 | info | fail-closed | **Revocable grant** — a role holds `EXECUTE` that no reachable path exercises † |
159
162
  | W1 | medium | — | **No exposure surface configured** — whole database assumed reachable, score capped |
160
163
 
161
- † R1/R2/L5 are no-ops until you name the untrusted roles:
162
- `"R1": ["critical", { "roles": ["anonymous"] }]`. They cost nothing on databases without an
163
- untrusted-role model; the `safegres:constructive` preset configures them for `anonymous`.
164
+ † The untrusted-role rules are no-ops until there is an untrusted role: named outright
165
+ (`"R1": ["critical", { "roles": ["anonymous"] }]`) or resolved from the exposure surface
166
+ (`rolesFrom: "anon"`, which `safegres:recommended` and the stack presets set — see
167
+ [Configuration](#configuration)). They cost nothing on databases without an untrusted-role model.
164
168
 
165
169
  ‡ L6 needs an adapter that can compute [API reach](#api-reach--the-relations-the-api-can-actually-name);
166
170
  without one nothing is unaddressable and it never fires.
@@ -180,9 +184,9 @@ availability and hygiene concern, not a leak. They contribute **zero** to the sc
180
184
  ### Convention (source-level lint, 4 rules — `safegres:constructive`)
181
185
 
182
186
  House-style rules that read function **definitions** (`pg_get_functiondef`), not the catalog. They
183
- are pure `source → findings` — no `pg` dependency in the lint module — and are **off outside the
184
- `safegres:constructive` preset**, which enables them. A function on a non-exposed schema costs
185
- nothing, like every other rule.
187
+ are pure `source → findings` — they come from [`@pgsql/lint`](https://www.npmjs.com/package/@pgsql/lint),
188
+ which has no `pg` dependency — and are **off outside the `safegres:constructive` preset**, which
189
+ enables them. A function on a non-exposed schema costs nothing, like every other rule.
186
190
 
187
191
  | Code | Severity | Direction | Check |
188
192
  | --- | --- | --- | --- |
@@ -210,8 +214,9 @@ EXECUTE 'REFRESH MATERIALIZED VIEW app.mv'; -- safegres-disable-line no-dynamic
210
214
  -- safegres-disable-file no-set-search-path -- vendored extension shim
211
215
  ```
212
216
 
213
- A directive with no rule id applies to every convention rule. Waived findings are **not dropped** —
214
- they surface as `acknowledged` (accepted-risk) findings carrying their reason, off the score.
217
+ The `pgsql-lint-` prefix (as written for the standalone `pgsql-lint` CLI) is read the same way, so
218
+ one waiver covers both tools. A directive with no rule id applies to every convention rule. Waived
219
+ findings are **not dropped** — they surface as `acknowledged` (accepted-risk) findings carrying their reason, off the score.
215
220
 
216
221
  ### Performance (11 rules, `--perf`)
217
222
 
@@ -227,7 +232,7 @@ they surface as `acknowledged` (accepted-risk) findings carrying their reason, o
227
232
  | X8 | info | **Sort-shaped column leads no index** (`timestamptz`/`date`) — heuristic, scores 0 |
228
233
  | X9 | medium | **Policy calls a STABLE function per row** — not wrapped in a scalar sub-select, so no InitPlan |
229
234
  | P1 | high | Policy body calls a **VOLATILE function** — re-evaluated per row |
230
- | P1b | medium | Policy body calls a **STABLE function** in a per-row position |
235
+ | P1b | medium | Policy body calls a **`SECURITY DEFINER` function** — the planner cannot inline it |
231
236
 
232
237
  X2/X3/X4/X9 are the checks a generic index linter structurally cannot make, because they require
233
238
  the policy predicate's AST. RLS quals are evaluated **before** user quals, on every candidate row,
@@ -693,6 +698,30 @@ differ by their gates and their baseline, not by their 19-entry `public.read` li
693
698
  "failOn": { "grade": "D" } }
694
699
  ```
695
700
 
701
+ ### Your own preset
702
+
703
+ A preset is a partial config file, so a team's own preset is just a file it `extends`. That is
704
+ a path inside one repo, or an npm package when several repos share it. A bare name that is not
705
+ a built-in is resolved as an installed package (its `main`, JSON or JS):
706
+
707
+ ```jsonc
708
+ // @acme/safegres-preset/safegres.json (package.json: "main": "safegres.json")
709
+ {
710
+ "extends": ["safegres:postgrest", "safegres:multi-tenant"],
711
+ "exposure": { "anonRoles": ["web_anon"] },
712
+ "rules": { "A5": "high" },
713
+ "failOn": { "grade": "B" }
714
+ }
715
+ ```
716
+
717
+ ```jsonc
718
+ // each project's .safegresrc.json
719
+ { "extends": "@acme/safegres-preset", "public": { "read": ["api.plans"] } }
720
+ ```
721
+
722
+ The project file wins over the preset, key by key; `overrides` unions across the chain.
723
+ `safegres print-config --explain` prints the merged result and where each value came from.
724
+
696
725
  A path in an inherited file resolves against **the file that wrote it**, so a base file's
697
726
  `"baseline": "ci/perf.json"` keeps meaning the base file's `ci/`, whichever job inherits it —
698
727
  the same rule as a discovered config, applied per key. Objects merge per key and arrays replace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safegres",
3
- "version": "1.30.1",
3
+ "version": "1.30.2",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "Postgres security and performance auditor: RLS, grants and effective-access analysis plus index and policy-cost checks, scored on two independent axes. One connection, no framework.",
6
6
  "main": "index.js",
@@ -85,5 +85,5 @@
85
85
  "makage": "^0.8.0",
86
86
  "pgsql-test": "^5.20.1"
87
87
  },
88
- "gitHead": "124df300f0afc15653854b50a25ac738c70261b7"
88
+ "gitHead": "a88c9ad6d2a2ae670f1feb00978f43cc8ab6f969"
89
89
  }