safegres 1.30.0 → 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.
- package/README.md +42 -13
- package/package.json +3 -3
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/
|
|
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
|
-
|
|
122
|
-
|
|
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 (
|
|
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
|
-
†
|
|
162
|
-
`"R1": ["critical", { "roles": ["anonymous"] }]
|
|
163
|
-
|
|
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` —
|
|
184
|
-
`
|
|
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
|
-
|
|
214
|
-
|
|
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
|
|
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.
|
|
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",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"@types/node": "^22.0.0",
|
|
84
84
|
"@types/pg": "^8.20.4",
|
|
85
85
|
"makage": "^0.8.0",
|
|
86
|
-
"pgsql-test": "^5.20.
|
|
86
|
+
"pgsql-test": "^5.20.1"
|
|
87
87
|
},
|
|
88
|
-
"gitHead": "
|
|
88
|
+
"gitHead": "a88c9ad6d2a2ae670f1feb00978f43cc8ab6f969"
|
|
89
89
|
}
|