@safegres/test 0.0.1 → 1.30.7
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 +825 -412
- package/adapters.d.ts +8 -0
- package/adapters.js +18 -0
- package/ast/helpers.d.ts +16 -0
- package/ast/helpers.js +40 -0
- package/ast/parse.d.ts +31 -0
- package/ast/parse.js +87 -0
- package/ast/walk.d.ts +27 -0
- package/ast/walk.js +79 -0
- package/callgraph/baseline.d.ts +28 -0
- package/callgraph/baseline.js +55 -0
- package/callgraph/extract.d.ts +93 -0
- package/callgraph/extract.js +340 -0
- package/callgraph/graph.d.ts +82 -0
- package/callgraph/graph.js +276 -0
- package/checks/anti-patterns.d.ts +53 -0
- package/checks/anti-patterns.js +247 -0
- package/checks/column-grants.d.ts +20 -0
- package/checks/column-grants.js +74 -0
- package/checks/coverage.d.ts +43 -0
- package/checks/coverage.js +110 -0
- package/checks/definer-function.d.ts +91 -0
- package/checks/definer-function.js +458 -0
- package/checks/definer-view.d.ts +167 -0
- package/checks/definer-view.js +392 -0
- package/checks/indexes.d.ts +82 -0
- package/checks/indexes.js +263 -0
- package/checks/lattice.d.ts +179 -0
- package/checks/lattice.js +487 -0
- package/checks/object-acls.d.ts +32 -0
- package/checks/object-acls.js +129 -0
- package/checks/policy-index.d.ts +78 -0
- package/checks/policy-index.js +349 -0
- package/checks/revocable-grants.d.ts +140 -0
- package/checks/revocable-grants.js +377 -0
- package/checks/rls-flags.d.ts +25 -0
- package/checks/rls-flags.js +82 -0
- package/checks/role-reach.d.ts +329 -0
- package/checks/role-reach.js +277 -0
- package/checks/role-trust.d.ts +45 -0
- package/checks/role-trust.js +91 -0
- package/checks/set-role.d.ts +21 -0
- package/checks/set-role.js +66 -0
- package/checks/stats.d.ts +52 -0
- package/checks/stats.js +192 -0
- package/checks/view-exposure.d.ts +68 -0
- package/checks/view-exposure.js +236 -0
- package/checks/view-writes.d.ts +88 -0
- package/checks/view-writes.js +350 -0
- package/ci/baseline.d.ts +104 -0
- package/ci/baseline.js +111 -0
- package/ci/github.d.ts +47 -0
- package/ci/github.js +197 -0
- package/cli/audit.d.ts +3 -0
- package/cli/audit.js +560 -0
- package/cli/baseline.d.ts +3 -0
- package/cli/baseline.js +104 -0
- package/cli/commands.d.ts +2 -0
- package/cli/commands.js +60 -0
- package/cli/doctor.d.ts +3 -0
- package/cli/doctor.js +72 -0
- package/cli/eval.d.ts +3 -0
- package/cli/eval.js +153 -0
- package/cli/print-config.d.ts +3 -0
- package/cli/print-config.js +54 -0
- package/cli/shared.d.ts +43 -0
- package/cli/shared.js +153 -0
- package/cli.d.ts +2 -0
- package/cli.js +69 -0
- package/commands/audit.d.ts +65 -0
- package/commands/audit.js +779 -0
- package/commands/doctor.d.ts +19 -0
- package/commands/doctor.js +181 -0
- package/commands/eval.d.ts +67 -0
- package/commands/eval.js +93 -0
- package/config/fingerprint.d.ts +40 -0
- package/config/fingerprint.js +71 -0
- package/config/loader.d.ts +44 -0
- package/config/loader.js +210 -0
- package/config/presets.d.ts +95 -0
- package/config/presets.js +327 -0
- package/config/resolve.d.ts +47 -0
- package/config/resolve.js +192 -0
- package/config/schema.d.ts +54 -0
- package/config/schema.js +269 -0
- package/config/types.d.ts +562 -0
- package/config/validate.d.ts +14 -0
- package/config/validate.js +125 -0
- package/corpus/README.md +83 -0
- package/corpus/bootstrap.sql +14 -0
- package/corpus/cases/01-anon-write-grant/case.json +26 -0
- package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
- package/corpus/cases/02-grant-to-public/case.json +25 -0
- package/corpus/cases/02-grant-to-public/schema.sql +18 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
- package/corpus/cases/04-permissive-write-policy/case.json +30 -0
- package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
- package/corpus/cases/05-public-read-policy/case.json +25 -0
- package/corpus/cases/05-public-read-policy/schema.sql +16 -0
- package/corpus/cases/06-update-without-with-check/case.json +25 -0
- package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
- package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
- package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
- package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
- package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
- package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
- package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
- package/corpus/cases/10-rls-not-forced/case.json +24 -0
- package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
- package/corpus/cases/11-session-user-in-policy/case.json +24 -0
- package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
- package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
- package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
- package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
- package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
- package/corpus/cases/14-dead-policy/case.json +24 -0
- package/corpus/cases/14-dead-policy/schema.sql +18 -0
- package/corpus/cases/15-unreachable-grant/case.json +24 -0
- package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
- package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
- package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
- package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
- package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
- package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
- package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
- package/corpus/cases/19-policy-column-cast/case.json +24 -0
- package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
- package/corpus/cases/20-stable-function-per-row/case.json +24 -0
- package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
- package/corpus/cases/21-table-without-primary-key/case.json +24 -0
- package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
- package/corpus/cases/22-redundant-index/case.json +24 -0
- package/corpus/cases/22-redundant-index/schema.sql +16 -0
- package/corpus/cases/23-tsvector-without-index/case.json +24 -0
- package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
- package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
- package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
- package/corpus/cases/25-definer-view-bypass/case.json +31 -0
- package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
- package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
- package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
- package/corpus/cases/27-definer-view-write/case.json +36 -0
- package/corpus/cases/27-definer-view-write/schema.sql +40 -0
- package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
- package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
- package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
- package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
- package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
- package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
- package/corpus/cases/31-matview-snapshot/case.json +32 -0
- package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
- package/corpus/cases/33-leaky-filter-view/case.json +36 -0
- package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
- package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
- package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
- package/corpus/cases/35-column-grant-projection/case.json +27 -0
- package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
- package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
- package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
- package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
- package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
- package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
- package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
- package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
- package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
- package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
- package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
- package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
- package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
- package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
- package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
- package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
- package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
- package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
- package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
- package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
- package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
- package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
- package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
- package/corpus/cases/41-unreadable-view-body/case.json +33 -0
- package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
- package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
- package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
- package/corpus/cases/42-search-column-indexed/case.json +21 -0
- package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
- package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
- package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
- package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
- package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
- package/corpus/cases/45-anon-foreign-table/case.json +27 -0
- package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
- package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
- package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
- package/corpus/cases/47-checked-writable-view/case.json +37 -0
- package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
- package/corpus/cases/48-definer-function-reach/case.json +38 -0
- package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
- package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
- package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
- package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
- package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
- package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
- package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
- package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
- package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
- package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
- package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
- package/corpus/index.d.ts +79 -0
- package/corpus/index.js +125 -0
- package/esm/adapters.d.ts +8 -0
- package/esm/adapters.js +6 -0
- package/esm/ast/helpers.d.ts +16 -0
- package/esm/ast/helpers.js +36 -0
- package/esm/ast/parse.d.ts +31 -0
- package/esm/ast/parse.js +82 -0
- package/esm/ast/walk.d.ts +27 -0
- package/esm/ast/walk.js +72 -0
- package/esm/callgraph/baseline.d.ts +28 -0
- package/esm/callgraph/baseline.js +48 -0
- package/esm/callgraph/extract.d.ts +93 -0
- package/esm/callgraph/extract.js +333 -0
- package/esm/callgraph/graph.d.ts +82 -0
- package/esm/callgraph/graph.js +273 -0
- package/esm/checks/anti-patterns.d.ts +53 -0
- package/esm/checks/anti-patterns.js +240 -0
- package/esm/checks/column-grants.d.ts +20 -0
- package/esm/checks/column-grants.js +71 -0
- package/esm/checks/coverage.d.ts +43 -0
- package/esm/checks/coverage.js +104 -0
- package/esm/checks/definer-function.d.ts +91 -0
- package/esm/checks/definer-function.js +452 -0
- package/esm/checks/definer-view.d.ts +167 -0
- package/esm/checks/definer-view.js +382 -0
- package/esm/checks/indexes.d.ts +82 -0
- package/esm/checks/indexes.js +256 -0
- package/esm/checks/lattice.d.ts +179 -0
- package/esm/checks/lattice.js +475 -0
- package/esm/checks/object-acls.d.ts +32 -0
- package/esm/checks/object-acls.js +125 -0
- package/esm/checks/policy-index.d.ts +78 -0
- package/esm/checks/policy-index.js +342 -0
- package/esm/checks/revocable-grants.d.ts +140 -0
- package/esm/checks/revocable-grants.js +373 -0
- package/esm/checks/rls-flags.d.ts +25 -0
- package/esm/checks/rls-flags.js +77 -0
- package/esm/checks/role-reach.d.ts +329 -0
- package/esm/checks/role-reach.js +270 -0
- package/esm/checks/role-trust.d.ts +45 -0
- package/esm/checks/role-trust.js +86 -0
- package/esm/checks/set-role.d.ts +21 -0
- package/esm/checks/set-role.js +63 -0
- package/esm/checks/stats.d.ts +52 -0
- package/esm/checks/stats.js +184 -0
- package/esm/checks/view-exposure.d.ts +68 -0
- package/esm/checks/view-exposure.js +231 -0
- package/esm/checks/view-writes.d.ts +88 -0
- package/esm/checks/view-writes.js +344 -0
- package/esm/ci/baseline.d.ts +104 -0
- package/esm/ci/baseline.js +107 -0
- package/esm/ci/github.d.ts +47 -0
- package/esm/ci/github.js +161 -0
- package/esm/cli/audit.d.ts +3 -0
- package/esm/cli/audit.js +525 -0
- package/esm/cli/baseline.d.ts +3 -0
- package/esm/cli/baseline.js +102 -0
- package/esm/cli/commands.d.ts +2 -0
- package/esm/cli/commands.js +53 -0
- package/esm/cli/doctor.d.ts +3 -0
- package/esm/cli/doctor.js +67 -0
- package/esm/cli/eval.d.ts +3 -0
- package/esm/cli/eval.js +115 -0
- package/esm/cli/print-config.d.ts +3 -0
- package/esm/cli/print-config.js +52 -0
- package/esm/cli/shared.d.ts +43 -0
- package/esm/cli/shared.js +112 -0
- package/esm/cli.d.ts +2 -0
- package/esm/cli.js +67 -0
- package/esm/commands/audit.d.ts +65 -0
- package/esm/commands/audit.js +776 -0
- package/esm/commands/doctor.d.ts +19 -0
- package/esm/commands/doctor.js +178 -0
- package/esm/commands/eval.d.ts +67 -0
- package/esm/commands/eval.js +90 -0
- package/esm/config/fingerprint.d.ts +40 -0
- package/esm/config/fingerprint.js +68 -0
- package/esm/config/loader.d.ts +44 -0
- package/esm/config/loader.js +172 -0
- package/esm/config/presets.d.ts +95 -0
- package/esm/config/presets.js +324 -0
- package/esm/config/resolve.d.ts +47 -0
- package/esm/config/resolve.js +180 -0
- package/esm/config/schema.d.ts +54 -0
- package/esm/config/schema.js +265 -0
- package/esm/config/types.d.ts +562 -0
- package/esm/config/validate.d.ts +14 -0
- package/esm/config/validate.js +122 -0
- package/esm/corpus/index.d.ts +79 -0
- package/esm/corpus/index.js +85 -0
- package/esm/exposure/adapters.d.ts +152 -0
- package/esm/exposure/adapters.js +463 -0
- package/esm/exposure/planes.d.ts +59 -0
- package/esm/exposure/planes.js +144 -0
- package/esm/exposure/reach.d.ts +78 -0
- package/esm/exposure/reach.js +105 -0
- package/esm/exposure/routines.d.ts +36 -0
- package/esm/exposure/routines.js +54 -0
- package/esm/index.d.ts +96 -0
- package/esm/index.js +57 -6
- package/esm/lint/index.d.ts +11 -0
- package/esm/lint/index.js +10 -0
- package/esm/perf/baseline.d.ts +40 -0
- package/esm/perf/baseline.js +83 -0
- package/esm/perf/explain.d.ts +46 -0
- package/esm/perf/explain.js +201 -0
- package/esm/pg/acl.d.ts +72 -0
- package/esm/pg/acl.js +186 -0
- package/esm/pg/behaviors.d.ts +114 -0
- package/esm/pg/behaviors.js +206 -0
- package/esm/pg/exposure.d.ts +74 -0
- package/esm/pg/exposure.js +198 -0
- package/esm/pg/functions.d.ts +52 -0
- package/esm/pg/functions.js +128 -0
- package/esm/pg/indexes.d.ts +192 -0
- package/esm/pg/indexes.js +395 -0
- package/esm/pg/introspect.d.ts +134 -0
- package/esm/pg/introspect.js +274 -0
- package/esm/pg/objects.d.ts +37 -0
- package/esm/pg/objects.js +108 -0
- package/esm/pg/paths.d.ts +114 -0
- package/esm/pg/paths.js +159 -0
- package/esm/pg/proc.d.ts +23 -0
- package/esm/pg/proc.js +55 -0
- package/esm/pg/reach-inputs.d.ts +47 -0
- package/esm/pg/reach-inputs.js +133 -0
- package/esm/pg/roles.d.ts +28 -0
- package/esm/pg/roles.js +50 -0
- package/esm/pg/stats.d.ts +65 -0
- package/esm/pg/stats.js +129 -0
- package/esm/pgpm-test.d.ts +22 -0
- package/esm/pgpm-test.js +39 -0
- package/esm/report/callgraph.d.ts +9 -0
- package/esm/report/callgraph.js +74 -0
- package/esm/report/compare.d.ts +143 -0
- package/esm/report/compare.js +195 -0
- package/esm/report/github.d.ts +74 -0
- package/esm/report/github.js +290 -0
- package/esm/report/json.d.ts +4 -0
- package/esm/report/json.js +3 -0
- package/esm/report/markdown.d.ts +25 -0
- package/esm/report/markdown.js +352 -0
- package/esm/report/pretty.d.ts +14 -0
- package/esm/report/pretty.js +318 -0
- package/esm/report/sarif.d.ts +40 -0
- package/esm/report/sarif.js +189 -0
- package/esm/report/view.d.ts +82 -0
- package/esm/report/view.js +126 -0
- package/esm/rules/registry.d.ts +66 -0
- package/esm/rules/registry.js +562 -0
- package/esm/score/score.d.ts +84 -0
- package/esm/score/score.js +233 -0
- package/esm/score/scorecards.d.ts +68 -0
- package/esm/score/scorecards.js +157 -0
- package/esm/types.d.ts +292 -0
- package/esm/types.js +26 -0
- package/esm/version.d.ts +8 -0
- package/esm/version.js +9 -0
- package/exposure/adapters.d.ts +152 -0
- package/exposure/adapters.js +468 -0
- package/exposure/planes.d.ts +59 -0
- package/exposure/planes.js +151 -0
- package/exposure/reach.d.ts +78 -0
- package/exposure/reach.js +109 -0
- package/exposure/routines.d.ts +36 -0
- package/exposure/routines.js +57 -0
- package/index.d.ts +96 -6
- package/index.js +229 -6
- package/lint/index.d.ts +11 -0
- package/lint/index.js +19 -0
- package/package.json +54 -48
- package/perf/baseline.d.ts +40 -0
- package/perf/baseline.js +92 -0
- package/perf/explain.d.ts +46 -0
- package/perf/explain.js +204 -0
- package/pg/acl.d.ts +72 -0
- package/pg/acl.js +191 -0
- package/pg/behaviors.d.ts +114 -0
- package/pg/behaviors.js +217 -0
- package/pg/exposure.d.ts +74 -0
- package/pg/exposure.js +205 -0
- package/pg/functions.d.ts +52 -0
- package/pg/functions.js +131 -0
- package/pg/indexes.d.ts +192 -0
- package/pg/indexes.js +400 -0
- package/pg/introspect.d.ts +134 -0
- package/pg/introspect.js +280 -0
- package/pg/objects.d.ts +37 -0
- package/pg/objects.js +111 -0
- package/pg/paths.d.ts +114 -0
- package/pg/paths.js +166 -0
- package/pg/proc.d.ts +23 -0
- package/pg/proc.js +58 -0
- package/pg/reach-inputs.d.ts +47 -0
- package/pg/reach-inputs.js +136 -0
- package/pg/roles.d.ts +28 -0
- package/pg/roles.js +54 -0
- package/pg/stats.d.ts +65 -0
- package/pg/stats.js +133 -0
- package/pgpm-test.d.ts +22 -0
- package/pgpm-test.js +42 -0
- package/report/callgraph.d.ts +9 -0
- package/report/callgraph.js +81 -0
- package/report/compare.d.ts +143 -0
- package/report/compare.js +205 -0
- package/report/github.d.ts +74 -0
- package/report/github.js +333 -0
- package/report/json.d.ts +4 -0
- package/report/json.js +6 -0
- package/report/markdown.d.ts +25 -0
- package/report/markdown.js +355 -0
- package/report/pretty.d.ts +14 -0
- package/report/pretty.js +324 -0
- package/report/sarif.d.ts +40 -0
- package/report/sarif.js +193 -0
- package/report/view.d.ts +82 -0
- package/report/view.js +132 -0
- package/rules/registry.d.ts +66 -0
- package/rules/registry.js +569 -0
- package/schema/safegres.schema.json +1119 -0
- package/score/score.d.ts +84 -0
- package/score/score.js +238 -0
- package/score/scorecards.d.ts +68 -0
- package/score/scorecards.js +162 -0
- package/types.d.ts +292 -0
- package/types.js +32 -0
- package/version.d.ts +8 -0
- package/version.js +12 -0
- package/LICENSE +0 -23
- package/admin.d.ts +0 -26
- package/admin.js +0 -182
- package/connect.d.ts +0 -19
- package/connect.js +0 -83
- package/dist/README.md +0 -557
- package/dist/package.json +0 -72
- package/esm/admin.js +0 -178
- package/esm/connect.js +0 -78
- package/esm/manager.js +0 -136
- package/esm/roles.js +0 -32
- package/esm/seed/adapters.js +0 -23
- package/esm/seed/csv.js +0 -44
- package/esm/seed/index.js +0 -16
- package/esm/seed/json.js +0 -18
- package/esm/seed/launchql.js +0 -19
- package/esm/seed/sqitch.js +0 -17
- package/esm/stream.js +0 -43
- package/esm/test-client.js +0 -150
- package/manager.d.ts +0 -25
- package/manager.js +0 -140
- package/roles.d.ts +0 -17
- package/roles.js +0 -38
- package/seed/adapters.d.ts +0 -4
- package/seed/adapters.js +0 -28
- package/seed/csv.d.ts +0 -9
- package/seed/csv.js +0 -49
- package/seed/index.d.ts +0 -16
- package/seed/index.js +0 -33
- package/seed/json.d.ts +0 -6
- package/seed/json.js +0 -21
- package/seed/launchql.d.ts +0 -2
- package/seed/launchql.js +0 -22
- package/seed/sqitch.d.ts +0 -2
- package/seed/sqitch.js +0 -20
- package/seed/types.d.ts +0 -13
- package/stream.d.ts +0 -2
- package/stream.js +0 -46
- package/test-client.d.ts +0 -49
- package/test-client.js +0 -154
- /package/{seed → config}/types.js +0 -0
- /package/esm/{seed → config}/types.js +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PgAstNode } from '../ast/parse';
|
|
2
|
+
import type { PolicyInfo, TableSnapshot } from '../pg/introspect';
|
|
3
|
+
import type { ProcVolatility } from '../pg/proc';
|
|
4
|
+
import type { Finding } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* Anti-pattern P1: policy predicate invokes a VOLATILE function (per-row
|
|
7
|
+
* evaluation, breaks the planner's ability to hoist or cache).
|
|
8
|
+
*
|
|
9
|
+
* Also flags SECURITY DEFINER wrappers here (P1b): they can't be inlined and
|
|
10
|
+
* become per-row function calls.
|
|
11
|
+
*
|
|
12
|
+
* Expected input: a volatility map keyed by `schema.name` (see pg/proc.ts).
|
|
13
|
+
*/
|
|
14
|
+
export declare function checkVolatileFunctions(table: TableSnapshot, expr: PgAstNode, volatility: Map<string, ProcVolatility>, policyName: string): Finding[];
|
|
15
|
+
/**
|
|
16
|
+
* Anti-pattern P5: policy references `session_user`, `current_user`, or
|
|
17
|
+
* `pg_has_role(...)` for tenant gating. These bypass the role-and-JWT layer
|
|
18
|
+
* that the rest of the auth model is built on, and they silently evaluate
|
|
19
|
+
* against the *login* role instead of the `SET LOCAL role` role, which
|
|
20
|
+
* produces confusing RLS behavior across pooled connections.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkSessionUserGating(table: TableSnapshot, expr: PgAstNode, policyName: string): Finding[];
|
|
23
|
+
/**
|
|
24
|
+
* Anti-patterns A7 / A8: trivially-permissive policy body.
|
|
25
|
+
*
|
|
26
|
+
* A permissive policy whose body is the literal `true` adds zero security —
|
|
27
|
+
* it's equivalent to not having RLS at all for the covered command. This is
|
|
28
|
+
* different from an intentional *restrictive* `true` (which would require
|
|
29
|
+
* all rows to satisfy it). We only flag `permissive = true` here because
|
|
30
|
+
* Postgres defaults to PERMISSIVE and `USING (true)` is the most common
|
|
31
|
+
* accidental "fail-open" shape.
|
|
32
|
+
*
|
|
33
|
+
* The verb matters enormously:
|
|
34
|
+
* - A7 (critical): a WRITE verb (INSERT/UPDATE/DELETE/ALL) with literal
|
|
35
|
+
* `true` lets every applicable role mutate every row — and, because
|
|
36
|
+
* permissive policies OR together, it silently defeats any carefully
|
|
37
|
+
* scoped policies on the same table.
|
|
38
|
+
* - A8 (low): a SELECT-only `USING (true)` is the standard public-read
|
|
39
|
+
* reference-table pattern; flag it for confirmation, not alarm.
|
|
40
|
+
*
|
|
41
|
+
* Input: the parsed USING and WITH CHECK ASTs (may be null if empty).
|
|
42
|
+
*/
|
|
43
|
+
export declare function checkTriviallyPermissive(table: TableSnapshot, policy: PolicyInfo, usingAst: PgAstNode | null, withCheckAst: PgAstNode | null): Finding | null;
|
|
44
|
+
/**
|
|
45
|
+
* Walk a policy expression, collecting unique function `(schema, name)` tuples
|
|
46
|
+
* so we can resolve volatility in one batch query.
|
|
47
|
+
*/
|
|
48
|
+
export declare function collectFunctionNames(expr: PgAstNode): Array<{
|
|
49
|
+
schema?: string;
|
|
50
|
+
name: string;
|
|
51
|
+
}>;
|
|
52
|
+
/** Parse a policy expression or return `null` on empty input. Logs parse errors to stderr. */
|
|
53
|
+
export declare function parseOrNull(expr: string | null, context: string): Promise<PgAstNode | null>;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkVolatileFunctions = checkVolatileFunctions;
|
|
4
|
+
exports.checkSessionUserGating = checkSessionUserGating;
|
|
5
|
+
exports.checkTriviallyPermissive = checkTriviallyPermissive;
|
|
6
|
+
exports.collectFunctionNames = collectFunctionNames;
|
|
7
|
+
exports.parseOrNull = parseOrNull;
|
|
8
|
+
const helpers_1 = require("../ast/helpers");
|
|
9
|
+
const parse_1 = require("../ast/parse");
|
|
10
|
+
const walk_1 = require("../ast/walk");
|
|
11
|
+
/**
|
|
12
|
+
* Function names we consider "safe" (stable) for policy predicates, even when
|
|
13
|
+
* pg_proc marks them volatile. These are the well-known Postgres session
|
|
14
|
+
* primitives that the planner treats as constant for the duration of a query.
|
|
15
|
+
*
|
|
16
|
+
* This is intentionally narrow — everything else that's volatile gets flagged.
|
|
17
|
+
*/
|
|
18
|
+
const SAFE_VOLATILE_ALLOWLIST = new Set([
|
|
19
|
+
'current_setting', // only because PostgreSQL optimises this specially
|
|
20
|
+
'txid_current',
|
|
21
|
+
'clock_timestamp', // still flagged elsewhere — listed here to document intent
|
|
22
|
+
'now',
|
|
23
|
+
'statement_timestamp',
|
|
24
|
+
'transaction_timestamp'
|
|
25
|
+
]);
|
|
26
|
+
/**
|
|
27
|
+
* Anti-pattern P1: policy predicate invokes a VOLATILE function (per-row
|
|
28
|
+
* evaluation, breaks the planner's ability to hoist or cache).
|
|
29
|
+
*
|
|
30
|
+
* Also flags SECURITY DEFINER wrappers here (P1b): they can't be inlined and
|
|
31
|
+
* become per-row function calls.
|
|
32
|
+
*
|
|
33
|
+
* Expected input: a volatility map keyed by `schema.name` (see pg/proc.ts).
|
|
34
|
+
*/
|
|
35
|
+
function checkVolatileFunctions(table, expr, volatility, policyName) {
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const fc of (0, walk_1.findAll)(expr, 'FuncCall')) {
|
|
38
|
+
const { schema, name } = (0, walk_1.funcNameParts)(fc);
|
|
39
|
+
const qualified = (0, walk_1.funcNameQualified)(fc);
|
|
40
|
+
const lookupKey = schema ? `${schema}.${name}` : name;
|
|
41
|
+
const info = volatility.get(qualified) ?? volatility.get(lookupKey) ?? volatility.get(name);
|
|
42
|
+
if (!info)
|
|
43
|
+
continue; // unknown function (e.g. operator expansion) — skip silently
|
|
44
|
+
// A SECURITY DEFINER call is a plan fence whatever its volatility: the
|
|
45
|
+
// planner cannot inline it, so the qual never reaches an index. Checked
|
|
46
|
+
// before the volatility filter below, which would otherwise let every
|
|
47
|
+
// STABLE wrapper — the common shape — through unreported.
|
|
48
|
+
if (info.isSecurityDefiner) {
|
|
49
|
+
out.push({
|
|
50
|
+
code: 'P1b',
|
|
51
|
+
severity: 'medium',
|
|
52
|
+
category: 'anti-pattern',
|
|
53
|
+
schema: table.schema,
|
|
54
|
+
table: table.name,
|
|
55
|
+
policy: policyName,
|
|
56
|
+
message: `Policy "${policyName}" on ${table.schema}.${table.name} calls SECURITY DEFINER function ${info.name}`,
|
|
57
|
+
hint: 'SECURITY DEFINER wrappers can\'t be inlined by the planner — every row forces a function call. Convert to a STABLE SQL function or replace with a direct expression.',
|
|
58
|
+
context: { function: info.name }
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (info.volatility !== 'v')
|
|
62
|
+
continue;
|
|
63
|
+
if (SAFE_VOLATILE_ALLOWLIST.has(info.name.split('.').pop() ?? ''))
|
|
64
|
+
continue;
|
|
65
|
+
if (info.isSystem && SAFE_VOLATILE_ALLOWLIST.has(name))
|
|
66
|
+
continue;
|
|
67
|
+
out.push({
|
|
68
|
+
code: 'P1',
|
|
69
|
+
severity: 'high',
|
|
70
|
+
category: 'anti-pattern',
|
|
71
|
+
schema: table.schema,
|
|
72
|
+
table: table.name,
|
|
73
|
+
policy: policyName,
|
|
74
|
+
message: `Policy "${policyName}" on ${table.schema}.${table.name} calls VOLATILE function ${info.name}`,
|
|
75
|
+
hint: 'Volatile functions re-execute for every row considered by the planner. Mark the function STABLE if safe, or precompute the result in a CTE / subquery.',
|
|
76
|
+
context: { function: info.name, securityDefiner: info.isSecurityDefiner }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Anti-pattern P5: policy references `session_user`, `current_user`, or
|
|
83
|
+
* `pg_has_role(...)` for tenant gating. These bypass the role-and-JWT layer
|
|
84
|
+
* that the rest of the auth model is built on, and they silently evaluate
|
|
85
|
+
* against the *login* role instead of the `SET LOCAL role` role, which
|
|
86
|
+
* produces confusing RLS behavior across pooled connections.
|
|
87
|
+
*/
|
|
88
|
+
function checkSessionUserGating(table, expr, policyName) {
|
|
89
|
+
const out = [];
|
|
90
|
+
// session_user / current_user appear as CurrentOfExpr-adjacent nodes in some
|
|
91
|
+
// parser versions, but the common form is `SQLValueFunction` with `op:
|
|
92
|
+
// SVFOP_CURRENT_USER` (4) or SVFOP_SESSION_USER (6).
|
|
93
|
+
(0, walk_1.visitAll)(expr, (node, tag) => {
|
|
94
|
+
if (tag === 'SQLValueFunction') {
|
|
95
|
+
const op = node.op;
|
|
96
|
+
const opStr = typeof op === 'string' ? op : String(op);
|
|
97
|
+
if (/CURRENT_USER|SESSION_USER/i.test(opStr)) {
|
|
98
|
+
out.push({
|
|
99
|
+
code: 'P5',
|
|
100
|
+
severity: 'high',
|
|
101
|
+
category: 'anti-pattern',
|
|
102
|
+
schema: table.schema,
|
|
103
|
+
table: table.name,
|
|
104
|
+
policy: policyName,
|
|
105
|
+
message: `Policy "${policyName}" on ${table.schema}.${table.name} references ${opStr.replace('SVFOP_', '').toLowerCase()}`,
|
|
106
|
+
hint: 'Use a JWT-backed helper (`current_setting(\'jwt.claims...\')`) or a dedicated STABLE auth function. `current_user` / `session_user` bypass the app auth layer.'
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// pg_has_role(text, text, text)
|
|
112
|
+
for (const fc of (0, walk_1.findAll)(expr, 'FuncCall')) {
|
|
113
|
+
const { name } = (0, walk_1.funcNameParts)(fc);
|
|
114
|
+
if (name === 'pg_has_role') {
|
|
115
|
+
out.push({
|
|
116
|
+
code: 'P5',
|
|
117
|
+
severity: 'high',
|
|
118
|
+
category: 'anti-pattern',
|
|
119
|
+
schema: table.schema,
|
|
120
|
+
table: table.name,
|
|
121
|
+
policy: policyName,
|
|
122
|
+
message: `Policy "${policyName}" on ${table.schema}.${table.name} uses pg_has_role() for tenant gating`,
|
|
123
|
+
hint: 'pg_has_role() checks Postgres role grants — not application-level authorization. Consider a dedicated auth helper instead.'
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Also flag raw ColumnRef `current_user` — parser versions vary.
|
|
128
|
+
for (const cr of (0, walk_1.findAll)(expr, 'ColumnRef')) {
|
|
129
|
+
const path = (0, walk_1.columnRefPath)(cr);
|
|
130
|
+
if (path.length === 1 && (path[0] === 'current_user' || path[0] === 'session_user')) {
|
|
131
|
+
out.push({
|
|
132
|
+
code: 'P5',
|
|
133
|
+
severity: 'high',
|
|
134
|
+
category: 'anti-pattern',
|
|
135
|
+
schema: table.schema,
|
|
136
|
+
table: table.name,
|
|
137
|
+
policy: policyName,
|
|
138
|
+
message: `Policy "${policyName}" on ${table.schema}.${table.name} references ${path[0]}`,
|
|
139
|
+
hint: 'Use a JWT-backed helper or STABLE auth function instead of raw session/current role.'
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Anti-patterns A7 / A8: trivially-permissive policy body.
|
|
147
|
+
*
|
|
148
|
+
* A permissive policy whose body is the literal `true` adds zero security —
|
|
149
|
+
* it's equivalent to not having RLS at all for the covered command. This is
|
|
150
|
+
* different from an intentional *restrictive* `true` (which would require
|
|
151
|
+
* all rows to satisfy it). We only flag `permissive = true` here because
|
|
152
|
+
* Postgres defaults to PERMISSIVE and `USING (true)` is the most common
|
|
153
|
+
* accidental "fail-open" shape.
|
|
154
|
+
*
|
|
155
|
+
* The verb matters enormously:
|
|
156
|
+
* - A7 (critical): a WRITE verb (INSERT/UPDATE/DELETE/ALL) with literal
|
|
157
|
+
* `true` lets every applicable role mutate every row — and, because
|
|
158
|
+
* permissive policies OR together, it silently defeats any carefully
|
|
159
|
+
* scoped policies on the same table.
|
|
160
|
+
* - A8 (low): a SELECT-only `USING (true)` is the standard public-read
|
|
161
|
+
* reference-table pattern; flag it for confirmation, not alarm.
|
|
162
|
+
*
|
|
163
|
+
* Input: the parsed USING and WITH CHECK ASTs (may be null if empty).
|
|
164
|
+
*/
|
|
165
|
+
function checkTriviallyPermissive(table, policy, usingAst, withCheckAst) {
|
|
166
|
+
if (!policy.permissive)
|
|
167
|
+
return null;
|
|
168
|
+
const usingIsTrue = usingAst ? (0, helpers_1.boolConst)(usingAst) === true : false;
|
|
169
|
+
const withCheckIsTrue = withCheckAst ? (0, helpers_1.boolConst)(withCheckAst) === true : false;
|
|
170
|
+
// Figure out which clauses are "open".
|
|
171
|
+
// - If USING exists, it must be literal true to be "open" on the read side.
|
|
172
|
+
// - If WITH CHECK exists, it must also be literal true.
|
|
173
|
+
// - If a clause is absent, the command doesn't care about it (e.g. INSERT
|
|
174
|
+
// has no USING; SELECT has no WITH CHECK) — that's a separate concern
|
|
175
|
+
// covered by the coverage checks.
|
|
176
|
+
// We flag when *every* present clause is literal `true`.
|
|
177
|
+
const presentClauses = [];
|
|
178
|
+
const trivialClauses = [];
|
|
179
|
+
if (policy.using !== null) {
|
|
180
|
+
presentClauses.push('USING');
|
|
181
|
+
if (usingIsTrue)
|
|
182
|
+
trivialClauses.push('USING');
|
|
183
|
+
}
|
|
184
|
+
if (policy.withCheck !== null) {
|
|
185
|
+
presentClauses.push('WITH CHECK');
|
|
186
|
+
if (withCheckIsTrue)
|
|
187
|
+
trivialClauses.push('WITH CHECK');
|
|
188
|
+
}
|
|
189
|
+
if (presentClauses.length === 0)
|
|
190
|
+
return null;
|
|
191
|
+
if (trivialClauses.length !== presentClauses.length)
|
|
192
|
+
return null;
|
|
193
|
+
const clauseList = trivialClauses.join(' and ');
|
|
194
|
+
const isWrite = policy.cmd !== 'SELECT';
|
|
195
|
+
return isWrite
|
|
196
|
+
? {
|
|
197
|
+
code: 'A7',
|
|
198
|
+
severity: 'critical',
|
|
199
|
+
category: 'anti-pattern',
|
|
200
|
+
schema: table.schema,
|
|
201
|
+
table: table.name,
|
|
202
|
+
policy: policy.name,
|
|
203
|
+
message: `Policy "${policy.name}" (FOR ${policy.cmd}) on ${table.schema}.${table.name} is trivially permissive (${clauseList} = true) — every applicable role can write every row`,
|
|
204
|
+
hint: 'Permissive policies OR together, so a literal-true write policy defeats every scoped policy on the table. Tighten the predicate or drop the policy.',
|
|
205
|
+
context: { cmd: policy.cmd, clauses: trivialClauses }
|
|
206
|
+
}
|
|
207
|
+
: {
|
|
208
|
+
code: 'A8',
|
|
209
|
+
severity: 'low',
|
|
210
|
+
category: 'anti-pattern',
|
|
211
|
+
schema: table.schema,
|
|
212
|
+
table: table.name,
|
|
213
|
+
policy: policy.name,
|
|
214
|
+
message: `Policy "${policy.name}" (FOR SELECT) on ${table.schema}.${table.name} is trivially permissive (${clauseList} = true) — all rows readable by applicable roles`,
|
|
215
|
+
hint: 'This is the standard public-read reference-table pattern. Confirm the table holds no per-tenant data; otherwise tighten the predicate.',
|
|
216
|
+
context: { cmd: policy.cmd, clauses: trivialClauses }
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Walk a policy expression, collecting unique function `(schema, name)` tuples
|
|
221
|
+
* so we can resolve volatility in one batch query.
|
|
222
|
+
*/
|
|
223
|
+
function collectFunctionNames(expr) {
|
|
224
|
+
const out = [];
|
|
225
|
+
const seen = new Set();
|
|
226
|
+
for (const fc of (0, walk_1.findAll)(expr, 'FuncCall')) {
|
|
227
|
+
const parts = (0, walk_1.funcNameParts)(fc);
|
|
228
|
+
const key = `${parts.schema ?? ''}.${parts.name}`;
|
|
229
|
+
if (seen.has(key))
|
|
230
|
+
continue;
|
|
231
|
+
seen.add(key);
|
|
232
|
+
out.push(parts);
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
/** Parse a policy expression or return `null` on empty input. Logs parse errors to stderr. */
|
|
237
|
+
async function parseOrNull(expr, context) {
|
|
238
|
+
try {
|
|
239
|
+
const parsed = await (0, parse_1.parsePolicyExpression)(expr);
|
|
240
|
+
return parsed?.ast ?? null;
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
// eslint-disable-next-line no-console
|
|
244
|
+
console.warn(`[safegres-audit] could not parse ${context}: ${err.message}`);
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L13: reach that exists only in `pg_attribute.attacl`.
|
|
3
|
+
*
|
|
4
|
+
* `GRANT SELECT (secret) ON t TO anonymous` writes nothing to the relation's
|
|
5
|
+
* ACL. Every grant query in this package read `relacl` alone, so a role whose
|
|
6
|
+
* entire access to a table is column-scoped was reported as reaching *nothing*
|
|
7
|
+
* — no A2, no R1, no L-series, and "0 relation(s) accessible" in the role
|
|
8
|
+
* access report. The privilege is real: the role selects those columns, and
|
|
9
|
+
* when the table has no RLS it selects every row of them.
|
|
10
|
+
*
|
|
11
|
+
* The finding is deliberately about the *invisibility* as much as the access.
|
|
12
|
+
* A column grant is a legitimate and often good way to expose a projection;
|
|
13
|
+
* what is not fine is that nothing else in the audit grades it. So L13 reports
|
|
14
|
+
* the reach, states whether RLS mediates it, and — as everywhere else — never
|
|
15
|
+
* recommends revoking the grant it cannot prove unused.
|
|
16
|
+
*/
|
|
17
|
+
import type { TableSnapshot } from '../pg/introspect';
|
|
18
|
+
import type { Finding } from '../types';
|
|
19
|
+
import { type LatticeRoleOptions, type RoleGraph } from './lattice';
|
|
20
|
+
export declare function checkUntrustedColumnGrants(table: TableSnapshot, graph: RoleGraph, options?: LatticeRoleOptions): Finding[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* L13: reach that exists only in `pg_attribute.attacl`.
|
|
4
|
+
*
|
|
5
|
+
* `GRANT SELECT (secret) ON t TO anonymous` writes nothing to the relation's
|
|
6
|
+
* ACL. Every grant query in this package read `relacl` alone, so a role whose
|
|
7
|
+
* entire access to a table is column-scoped was reported as reaching *nothing*
|
|
8
|
+
* — no A2, no R1, no L-series, and "0 relation(s) accessible" in the role
|
|
9
|
+
* access report. The privilege is real: the role selects those columns, and
|
|
10
|
+
* when the table has no RLS it selects every row of them.
|
|
11
|
+
*
|
|
12
|
+
* The finding is deliberately about the *invisibility* as much as the access.
|
|
13
|
+
* A column grant is a legitimate and often good way to expose a projection;
|
|
14
|
+
* what is not fine is that nothing else in the audit grades it. So L13 reports
|
|
15
|
+
* the reach, states whether RLS mediates it, and — as everywhere else — never
|
|
16
|
+
* recommends revoking the grant it cannot prove unused.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.checkUntrustedColumnGrants = checkUntrustedColumnGrants;
|
|
20
|
+
const lattice_1 = require("./lattice");
|
|
21
|
+
/** SELECT/INSERT/UPDATE are the column privileges RLS has anything to say about. */
|
|
22
|
+
const MEDIATED = ['SELECT', 'INSERT', 'UPDATE'];
|
|
23
|
+
function checkUntrustedColumnGrants(table, graph, options = {}) {
|
|
24
|
+
const roles = options.roles ?? [];
|
|
25
|
+
if (roles.length === 0 || table.columnGrants.length === 0)
|
|
26
|
+
return [];
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const role of roles) {
|
|
29
|
+
const grants = (0, lattice_1.effectiveColumnGrants)(table, role, graph).filter((g) => MEDIATED.includes(g.privilege));
|
|
30
|
+
if (grants.length === 0)
|
|
31
|
+
continue;
|
|
32
|
+
const attrs = graph.get(role);
|
|
33
|
+
// Same exemption test the rest of the lattice uses: a role RLS does not
|
|
34
|
+
// apply to reads every row of the columns it is granted.
|
|
35
|
+
const rlsApplies = table.rlsEnabled
|
|
36
|
+
&& !attrs?.bypassRls
|
|
37
|
+
&& !attrs?.isSuper
|
|
38
|
+
&& !(role === table.owner && !table.rlsForced);
|
|
39
|
+
const columns = [...new Set(grants.flatMap((g) => g.columns))].sort();
|
|
40
|
+
const privileges = grants.map((g) => g.privilege).sort();
|
|
41
|
+
const via = grants[0].via;
|
|
42
|
+
out.push({
|
|
43
|
+
code: 'L13',
|
|
44
|
+
severity: 'info',
|
|
45
|
+
category: 'anti-pattern',
|
|
46
|
+
schema: table.schema,
|
|
47
|
+
table: table.name,
|
|
48
|
+
role,
|
|
49
|
+
privilege: privileges.join(', '),
|
|
50
|
+
message: `Untrusted role ${role} holds column-level ${privileges.join(', ')} on `
|
|
51
|
+
+ `${table.schema}.${table.name} (${columns.join(', ')})`
|
|
52
|
+
+ (via === 'direct' ? '' : ` via ${via}`)
|
|
53
|
+
+ (rlsApplies
|
|
54
|
+
? ' — mediated by RLS, but invisible to every rule that reads the relation ACL'
|
|
55
|
+
: ' — with no RLS to mediate it, so every row of those columns is readable'),
|
|
56
|
+
hint: `Column grants live in \`pg_attribute.attacl\`, not \`relacl\`: \`\\dp\` shows them in `
|
|
57
|
+
+ `the "Column privileges" column and nothing else in this audit graded them until now. `
|
|
58
|
+
+ (rlsApplies
|
|
59
|
+
? 'Confirm the policies that mediate this relation are the ones you would want applied '
|
|
60
|
+
+ 'to a projection of it.'
|
|
61
|
+
: 'If the projection is meant to be public this is correct as written; if not, enable '
|
|
62
|
+
+ 'RLS on the relation — the column grant restricts *which columns*, never which rows.')
|
|
63
|
+
+ ' Do not revoke the grant on the strength of this finding alone: nothing here proves it '
|
|
64
|
+
+ 'unused.',
|
|
65
|
+
context: {
|
|
66
|
+
columns,
|
|
67
|
+
via,
|
|
68
|
+
rlsMediated: rlsApplies,
|
|
69
|
+
rlsEnabled: table.rlsEnabled
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PgPrivilege, PolicyCmd, PolicyInfo, TableSnapshot } from '../pg/introspect';
|
|
2
|
+
import type { Finding } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Coverage rules for row-level security.
|
|
5
|
+
*
|
|
6
|
+
* The security posture of a table is determined per `(role, privilege)` — not
|
|
7
|
+
* per individual policy. A grant is "covered" iff at least one applicable
|
|
8
|
+
* *permissive* policy supplies the clause that privilege requires:
|
|
9
|
+
*
|
|
10
|
+
* - INSERT → a `WITH CHECK` clause (USING is not valid on FOR INSERT).
|
|
11
|
+
* - SELECT → a `USING` clause.
|
|
12
|
+
* - UPDATE → a `USING` clause (the FOR UPDATE visibility gate). If absent,
|
|
13
|
+
* writes against the row are impossible and the grant is dead.
|
|
14
|
+
* - DELETE → a `USING` clause.
|
|
15
|
+
*
|
|
16
|
+
* Postgres fails writes at runtime when the required clause is missing across
|
|
17
|
+
* *every* applicable permissive policy, so that's the only bar this check
|
|
18
|
+
* holds us to. Whether an UPDATE policy also pairs a WITH CHECK (defense in
|
|
19
|
+
* depth against row smuggling) is a separate, weaker concern that is
|
|
20
|
+
* covered by `checkUpdateWithCheckCoverage` below as an INFO-level finding.
|
|
21
|
+
*/
|
|
22
|
+
/** What clause kind satisfies coverage for a given privilege. */
|
|
23
|
+
export type ClauseKind = 'USING' | 'WITH CHECK';
|
|
24
|
+
export declare const CLAUSE_REQUIRED: Partial<Record<PgPrivilege, ClauseKind>>;
|
|
25
|
+
/** `polcmd` → the single verb the policy can satisfy (`ALL` satisfies every verb). */
|
|
26
|
+
export declare const POLICY_CMDS: Record<PolicyCmd, PgPrivilege[]>;
|
|
27
|
+
/**
|
|
28
|
+
* A4 / A5: Grant of INSERT/UPDATE/DELETE (A4, high) or SELECT (A5, medium) to
|
|
29
|
+
* a role but no applicable permissive policy supplies the clause that verb
|
|
30
|
+
* needs. A4 means "writes silently fail at runtime". A5 means "queries
|
|
31
|
+
* silently return 0 rows".
|
|
32
|
+
*/
|
|
33
|
+
export declare function checkCoverageGaps(table: TableSnapshot): Finding[];
|
|
34
|
+
/**
|
|
35
|
+
* A6 (informational): UPDATE coverage exists for a role but no applicable
|
|
36
|
+
* permissive UPDATE policy pairs a `WITH CHECK`. This is defense-in-depth —
|
|
37
|
+
* without WITH CHECK, updates can move rows out of the visible scope (a
|
|
38
|
+
* "row smuggling" pattern). Low severity because it does not cause runtime
|
|
39
|
+
* failures and in many designs is by construction safe (e.g. the columns
|
|
40
|
+
* that define scope are not writable, or a trigger re-asserts scope).
|
|
41
|
+
*/
|
|
42
|
+
export declare function checkUpdateWithCheckCoverage(table: TableSnapshot): Finding[];
|
|
43
|
+
export declare function policyProvidesClause(p: PolicyInfo, role: string, privilege: PgPrivilege, clause: ClauseKind): boolean;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POLICY_CMDS = exports.CLAUSE_REQUIRED = void 0;
|
|
4
|
+
exports.checkCoverageGaps = checkCoverageGaps;
|
|
5
|
+
exports.checkUpdateWithCheckCoverage = checkUpdateWithCheckCoverage;
|
|
6
|
+
exports.policyProvidesClause = policyProvidesClause;
|
|
7
|
+
exports.CLAUSE_REQUIRED = {
|
|
8
|
+
SELECT: 'USING',
|
|
9
|
+
INSERT: 'WITH CHECK',
|
|
10
|
+
UPDATE: 'USING',
|
|
11
|
+
DELETE: 'USING'
|
|
12
|
+
};
|
|
13
|
+
/** `polcmd` → the single verb the policy can satisfy (`ALL` satisfies every verb). */
|
|
14
|
+
exports.POLICY_CMDS = {
|
|
15
|
+
SELECT: ['SELECT'],
|
|
16
|
+
INSERT: ['INSERT'],
|
|
17
|
+
UPDATE: ['UPDATE'],
|
|
18
|
+
DELETE: ['DELETE'],
|
|
19
|
+
ALL: ['SELECT', 'INSERT', 'UPDATE', 'DELETE']
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A4 / A5: Grant of INSERT/UPDATE/DELETE (A4, high) or SELECT (A5, medium) to
|
|
23
|
+
* a role but no applicable permissive policy supplies the clause that verb
|
|
24
|
+
* needs. A4 means "writes silently fail at runtime". A5 means "queries
|
|
25
|
+
* silently return 0 rows".
|
|
26
|
+
*/
|
|
27
|
+
function checkCoverageGaps(table) {
|
|
28
|
+
if (!table.rlsEnabled)
|
|
29
|
+
return []; // A2 covers the "RLS off entirely" case.
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const grant of table.grants) {
|
|
32
|
+
if (grant.role === table.owner)
|
|
33
|
+
continue;
|
|
34
|
+
if (grant.role === 'PUBLIC')
|
|
35
|
+
continue;
|
|
36
|
+
if (grant.bypassRls)
|
|
37
|
+
continue; // Superusers and BYPASSRLS roles aren't subject to policies.
|
|
38
|
+
const requiredClause = exports.CLAUSE_REQUIRED[grant.privilege];
|
|
39
|
+
if (!requiredClause)
|
|
40
|
+
continue; // TRUNCATE / REFERENCES / TRIGGER don't participate in RLS.
|
|
41
|
+
const covered = table.policies.some((p) => policyProvidesClause(p, grant.role, grant.privilege, requiredClause));
|
|
42
|
+
if (covered)
|
|
43
|
+
continue;
|
|
44
|
+
const [code, severity] = grant.privilege === 'SELECT'
|
|
45
|
+
? ['A5', 'medium']
|
|
46
|
+
: ['A4', 'high'];
|
|
47
|
+
out.push({
|
|
48
|
+
code,
|
|
49
|
+
severity,
|
|
50
|
+
category: 'coverage',
|
|
51
|
+
schema: table.schema,
|
|
52
|
+
table: table.name,
|
|
53
|
+
role: grant.role,
|
|
54
|
+
privilege: grant.privilege,
|
|
55
|
+
message: `Role ${grant.role} has ${grant.privilege} grant on ${table.schema}.${table.name} but no permissive policy supplies ${requiredClause} for ${grant.privilege}`,
|
|
56
|
+
hint: grant.privilege === 'SELECT'
|
|
57
|
+
? 'Queries by this role will silently return 0 rows. Add a permissive policy FOR SELECT TO that role, or revoke the grant.'
|
|
58
|
+
: `${grant.privilege} by this role will fail at runtime. Add a permissive policy FOR ${grant.privilege} TO that role supplying ${requiredClause}, or revoke the grant.`
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A6 (informational): UPDATE coverage exists for a role but no applicable
|
|
65
|
+
* permissive UPDATE policy pairs a `WITH CHECK`. This is defense-in-depth —
|
|
66
|
+
* without WITH CHECK, updates can move rows out of the visible scope (a
|
|
67
|
+
* "row smuggling" pattern). Low severity because it does not cause runtime
|
|
68
|
+
* failures and in many designs is by construction safe (e.g. the columns
|
|
69
|
+
* that define scope are not writable, or a trigger re-asserts scope).
|
|
70
|
+
*/
|
|
71
|
+
function checkUpdateWithCheckCoverage(table) {
|
|
72
|
+
if (!table.rlsEnabled)
|
|
73
|
+
return [];
|
|
74
|
+
const out = [];
|
|
75
|
+
const updateGrants = table.grants.filter((g) => g.privilege === 'UPDATE'
|
|
76
|
+
&& g.role !== table.owner
|
|
77
|
+
&& g.role !== 'PUBLIC'
|
|
78
|
+
&& !g.bypassRls);
|
|
79
|
+
if (updateGrants.length === 0)
|
|
80
|
+
return [];
|
|
81
|
+
for (const grant of updateGrants) {
|
|
82
|
+
const usingCovered = table.policies.some((p) => policyProvidesClause(p, grant.role, 'UPDATE', 'USING'));
|
|
83
|
+
if (!usingCovered)
|
|
84
|
+
continue; // already reported as A4.
|
|
85
|
+
const withCheckCovered = table.policies.some((p) => policyProvidesClause(p, grant.role, 'UPDATE', 'WITH CHECK'));
|
|
86
|
+
if (withCheckCovered)
|
|
87
|
+
continue;
|
|
88
|
+
out.push({
|
|
89
|
+
code: 'A6',
|
|
90
|
+
severity: 'info',
|
|
91
|
+
category: 'coverage',
|
|
92
|
+
schema: table.schema,
|
|
93
|
+
table: table.name,
|
|
94
|
+
role: grant.role,
|
|
95
|
+
privilege: 'UPDATE',
|
|
96
|
+
message: `Role ${grant.role} has UPDATE on ${table.schema}.${table.name}; USING is supplied but no applicable permissive policy provides WITH CHECK`,
|
|
97
|
+
hint: 'Without a WITH CHECK on UPDATE, rows can be updated to values that move them outside the role\'s visible set. Add WITH CHECK if the scoped columns are writable by this role.'
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
function policyProvidesClause(p, role, privilege, clause) {
|
|
103
|
+
if (!p.permissive)
|
|
104
|
+
return false;
|
|
105
|
+
if (!exports.POLICY_CMDS[p.cmd].includes(privilege))
|
|
106
|
+
return false;
|
|
107
|
+
if (!p.roles.includes('PUBLIC') && !p.roles.includes(role))
|
|
108
|
+
return false;
|
|
109
|
+
return clause === 'USING' ? p.using != null : p.withCheck != null;
|
|
110
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L19 and L20: privilege that arrives through a *function body*.
|
|
3
|
+
*
|
|
4
|
+
* L8 modelled the view half of "SQL bodies confer privilege". This is the
|
|
5
|
+
* other half, and the larger one:
|
|
6
|
+
*
|
|
7
|
+
* - **L19, SECURITY DEFINER functions.** A definer function executes as its
|
|
8
|
+
* owner, so every relation its body touches is touched with the owner's
|
|
9
|
+
* privileges — including tables the caller holds nothing on, and, when
|
|
10
|
+
* the owner owns the table or bypasses RLS, without the row filter the
|
|
11
|
+
* table's policies would have applied. EXECUTE on the function is the
|
|
12
|
+
* only grant the caller needs, and no ACL on the base relation names it.
|
|
13
|
+
* Verified on PostgreSQL 18: an anonymous role with EXECUTE read every
|
|
14
|
+
* row of an RLS-protected table it had no grant on, while the invoker
|
|
15
|
+
* twin of the same function was denied.
|
|
16
|
+
*
|
|
17
|
+
* - **L20, `INSTEAD OF` triggers.** A write against a view carrying one
|
|
18
|
+
* never reaches a base relation: it *becomes* the trigger function's
|
|
19
|
+
* body. L9 suppressed those views because nothing followed that body —
|
|
20
|
+
* this rule follows it. The body is permission-checked against the
|
|
21
|
+
* function's effective user, so the escalation exists only when the
|
|
22
|
+
* trigger function is SECURITY DEFINER; the view's own owner and its
|
|
23
|
+
* `security_invoker` setting do not govern it. Both halves were probed on
|
|
24
|
+
* PG 18: the invoker trigger function was denied on the relation its body
|
|
25
|
+
* wrote, the definer one wrote it as its owner.
|
|
26
|
+
*
|
|
27
|
+
* The conservatism is L8's, unchanged. A body that cannot be read is unknown,
|
|
28
|
+
* not empty: its fragmentary access list is discarded and the function is
|
|
29
|
+
* reported as a coverage gap (L15) rather than scanned clean. And the fix is
|
|
30
|
+
* never a revoke — EXECUTE on the function is what the API serves; the defect
|
|
31
|
+
* is what the body does with the owner's rights.
|
|
32
|
+
*/
|
|
33
|
+
import { type SchemaAclInfo } from '../pg/acl';
|
|
34
|
+
import type { FunctionSnapshot } from '../pg/functions';
|
|
35
|
+
import type { ViewSnapshot } from '../pg/indexes';
|
|
36
|
+
import type { TableSnapshot } from '../pg/introspect';
|
|
37
|
+
import type { Finding } from '../types';
|
|
38
|
+
import { type SuppressedView } from './definer-view';
|
|
39
|
+
import { type LatticeRoleOptions, type RoleGraph } from './lattice';
|
|
40
|
+
import { type FunctionReachInput, type TriggerWriteInput } from './role-reach';
|
|
41
|
+
export interface FunctionBodyAnalysis {
|
|
42
|
+
/** Functions that execute as someone other than the caller, as reach inputs. */
|
|
43
|
+
functions: FunctionReachInput[];
|
|
44
|
+
/** `INSTEAD OF` triggers whose function re-owns the write. */
|
|
45
|
+
triggers: TriggerWriteInput[];
|
|
46
|
+
/** Bodies deliberately left out, with why — an unread body is not a clean bill. */
|
|
47
|
+
suppressed: SuppressedView[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Read every SECURITY DEFINER function's body and resolve what it reaches.
|
|
51
|
+
*
|
|
52
|
+
* Calls are followed: an invoker function called from a definer still runs
|
|
53
|
+
* with the definer's owner in force, while an inner definer switches the
|
|
54
|
+
* executing role again — the same rule as nested views, because it is the
|
|
55
|
+
* same rule. Views read from a body are followed too, through the existing
|
|
56
|
+
* view walk, so a definer function selecting from a definer view reaches that
|
|
57
|
+
* view's bases as well.
|
|
58
|
+
*/
|
|
59
|
+
export declare function analyzeFunctionBodies(functions: FunctionSnapshot[], views: ViewSnapshot[], tables: TableSnapshot[], auditedSchemas?: Iterable<string>): Promise<FunctionBodyAnalysis>;
|
|
60
|
+
/**
|
|
61
|
+
* L19: an untrusted role reaches a relation by executing a SECURITY DEFINER
|
|
62
|
+
* function.
|
|
63
|
+
*
|
|
64
|
+
* Fires once per (role, function, relation, privilege) where the role can
|
|
65
|
+
* EXECUTE the function, the body touches the relation as someone else, and
|
|
66
|
+
* the role holds no such privilege on the relation itself. The invoker twin
|
|
67
|
+
* of the same function produces nothing: its body runs as the caller, so the
|
|
68
|
+
* relation's own ACL and policies apply and there is no edge to report.
|
|
69
|
+
*/
|
|
70
|
+
export declare function checkDefinerFunctionReach(functions: FunctionReachInput[], tables: TableSnapshot[], graph: RoleGraph, schemaAcls: Map<string, SchemaAclInfo>, options?: LatticeRoleOptions): Finding[];
|
|
71
|
+
/**
|
|
72
|
+
* L20: an untrusted role writes a relation through an `INSTEAD OF` trigger
|
|
73
|
+
* whose function is SECURITY DEFINER.
|
|
74
|
+
*
|
|
75
|
+
* This is the suppression L9 has carried since it shipped. A write against a
|
|
76
|
+
* view with `INSTEAD OF` triggers never reaches a base relation — Postgres
|
|
77
|
+
* runs the trigger function instead — and where that write lands is in the
|
|
78
|
+
* body. Following it makes the edge provable; where the body is unreadable,
|
|
79
|
+
* or the trigger function runs as the caller, the suppression stands.
|
|
80
|
+
*/
|
|
81
|
+
export declare function checkInsteadOfTriggerWrite(triggers: TriggerWriteInput[], tables: TableSnapshot[], graph: RoleGraph, options?: LatticeRoleOptions): Finding[];
|
|
82
|
+
/**
|
|
83
|
+
* L15, asked of a function instead of a view: an untrusted role can execute a
|
|
84
|
+
* definer function whose body this analysis could not fully read.
|
|
85
|
+
*
|
|
86
|
+
* The same coverage statement the view producer makes, for the same reason —
|
|
87
|
+
* the execution is proven and the far end is unknown, so no grading rule saw
|
|
88
|
+
* it. Reporting the gap is the alternative to a silent clean bill; it is
|
|
89
|
+
* `info`, score-neutral, and recommends no revoke.
|
|
90
|
+
*/
|
|
91
|
+
export declare function checkUnreadableFunctionReach(functions: FunctionReachInput[], graph: RoleGraph, schemaAcls: Map<string, SchemaAclInfo>, options?: LatticeRoleOptions): Finding[];
|