@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
package/perf/explain.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Proof by EXPLAIN (`--explain`).
|
|
4
|
+
*
|
|
5
|
+
* The `X*` rules infer from the catalog that a query *shape* has no index to
|
|
6
|
+
* serve it. This turns the inference into evidence: for each finding that has
|
|
7
|
+
* a probe shape, plan the query the finding is about and read the plan back.
|
|
8
|
+
*
|
|
9
|
+
* The planner is the authority, so the interesting outcome is disagreement —
|
|
10
|
+
* a finding whose probe plans as an index scan is **refuted** (some index the
|
|
11
|
+
* catalog rules didn't credit does serve it) and stops counting against the
|
|
12
|
+
* score. The reverse is not symmetrical: an empty or unanalyzed table always
|
|
13
|
+
* seq-scans, so a seq scan only *confirms* a finding above the row floor;
|
|
14
|
+
* below it the probe is inconclusive and the finding is left exactly as it
|
|
15
|
+
* was.
|
|
16
|
+
*
|
|
17
|
+
* Probes use `EXPLAIN (GENERIC_PLAN)` — parameters stay parameters, nothing
|
|
18
|
+
* is executed, and no value has to be invented for a column whose domain we
|
|
19
|
+
* know nothing about. That requires PostgreSQL 16+.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.proveFindings = proveFindings;
|
|
23
|
+
/** The minimum server version supporting `EXPLAIN (GENERIC_PLAN)`. */
|
|
24
|
+
const MIN_SERVER_VERSION = 160000;
|
|
25
|
+
/**
|
|
26
|
+
* Plan a probe for every probeable perf finding and annotate it in place.
|
|
27
|
+
* Findings the planner refutes are acknowledged (reported, unscored).
|
|
28
|
+
*/
|
|
29
|
+
async function proveFindings(exec, findings, tables, options = {}) {
|
|
30
|
+
const minRows = options.minRows ?? 1000;
|
|
31
|
+
const report = { probed: 0, confirmed: 0, refuted: 0, inconclusive: 0 };
|
|
32
|
+
const { rows: versionRows } = await exec.query(`SELECT setting FROM pg_settings WHERE name = 'server_version_num'`);
|
|
33
|
+
const version = versionRows[0];
|
|
34
|
+
if (Number(version?.setting ?? 0) < MIN_SERVER_VERSION) {
|
|
35
|
+
report.unavailable =
|
|
36
|
+
`EXPLAIN (GENERIC_PLAN) requires PostgreSQL 16 or later (server is ${version?.setting ?? 'unknown'}) — no findings were probed`;
|
|
37
|
+
return report;
|
|
38
|
+
}
|
|
39
|
+
for (const finding of findings) {
|
|
40
|
+
if (!finding.schema || !finding.table)
|
|
41
|
+
continue;
|
|
42
|
+
const table = tables.get(`${finding.schema}.${finding.table}`);
|
|
43
|
+
if (!table)
|
|
44
|
+
continue;
|
|
45
|
+
const probe = buildProbe(finding, table);
|
|
46
|
+
if (!probe)
|
|
47
|
+
continue;
|
|
48
|
+
const plan = await planProbe(exec, probe.sql);
|
|
49
|
+
if (!plan)
|
|
50
|
+
continue;
|
|
51
|
+
report.probed += 1;
|
|
52
|
+
const nodes = collectNodeTypes(plan);
|
|
53
|
+
const servedByIndex = nodes.some((n) => n.includes('Index'));
|
|
54
|
+
const predicted = probe.expect === 'sort'
|
|
55
|
+
? nodes.includes('Sort') || nodes.includes('Incremental Sort')
|
|
56
|
+
: nodes.includes('Seq Scan');
|
|
57
|
+
const evidence = {
|
|
58
|
+
source: 'explain',
|
|
59
|
+
probe: probe.sql,
|
|
60
|
+
plan: nodes.join(' → '),
|
|
61
|
+
status: 'inconclusive'
|
|
62
|
+
};
|
|
63
|
+
if (servedByIndex && !predicted) {
|
|
64
|
+
evidence.status = 'refuted';
|
|
65
|
+
report.refuted += 1;
|
|
66
|
+
finding.acknowledged = true;
|
|
67
|
+
finding.severity = 'info';
|
|
68
|
+
finding.message += ' — refuted by EXPLAIN (the planner serves this shape with an index)';
|
|
69
|
+
finding.hint =
|
|
70
|
+
'The planner chose an index for the probe query, so this finding does not describe a real plan. Reported for review, excluded from the perf score.';
|
|
71
|
+
}
|
|
72
|
+
else if (predicted && estimatedRows(table) >= minRows) {
|
|
73
|
+
evidence.status = 'confirmed';
|
|
74
|
+
report.confirmed += 1;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
report.inconclusive += 1;
|
|
78
|
+
evidence.note = estimatedRows(table) < minRows
|
|
79
|
+
? `table has ~${Math.max(estimatedRows(table), 0)} estimated rows (< ${minRows}); a sequential scan is the correct plan at this size, so the probe proves nothing`
|
|
80
|
+
: 'the probe plan matched neither an index scan nor the predicted scan';
|
|
81
|
+
}
|
|
82
|
+
finding.evidence = evidence;
|
|
83
|
+
}
|
|
84
|
+
return report;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The query a finding is a claim about. `null` for findings with no probeable
|
|
88
|
+
* shape (X5 redundant indexes, X6 missing PK, S*, P*) — nothing is planned
|
|
89
|
+
* speculatively, so `--explain` never invents work.
|
|
90
|
+
*/
|
|
91
|
+
function buildProbe(finding, table) {
|
|
92
|
+
const relation = `${quote(table.schema)}.${quote(table.name)}`;
|
|
93
|
+
const context = finding.context ?? {};
|
|
94
|
+
switch (finding.code) {
|
|
95
|
+
case 'X1': {
|
|
96
|
+
const columns = asStringArray(context.columns);
|
|
97
|
+
if (columns.length === 0)
|
|
98
|
+
return null;
|
|
99
|
+
const predicates = columns
|
|
100
|
+
.map((name, i) => equality(table, name, i + 1))
|
|
101
|
+
.filter((p) => p !== null);
|
|
102
|
+
if (predicates.length !== columns.length)
|
|
103
|
+
return null;
|
|
104
|
+
return { sql: `SELECT 1 FROM ${relation} WHERE ${predicates.join(' AND ')}`, expect: 'seq-scan' };
|
|
105
|
+
}
|
|
106
|
+
case 'X2': {
|
|
107
|
+
const predicate = typeof context.column === 'string'
|
|
108
|
+
? equality(table, context.column, 1)
|
|
109
|
+
: null;
|
|
110
|
+
if (!predicate)
|
|
111
|
+
return null;
|
|
112
|
+
return { sql: `SELECT 1 FROM ${relation} WHERE ${predicate}`, expect: 'seq-scan' };
|
|
113
|
+
}
|
|
114
|
+
case 'X7': {
|
|
115
|
+
const column = typeof context.column === 'string' ? context.column : null;
|
|
116
|
+
if (!column)
|
|
117
|
+
return null;
|
|
118
|
+
const type = columnType(table, column);
|
|
119
|
+
if (!type)
|
|
120
|
+
return null;
|
|
121
|
+
if (type === 'tsvector') {
|
|
122
|
+
return {
|
|
123
|
+
sql: `SELECT 1 FROM ${relation} WHERE ${quote(column)} @@ $1::tsquery`,
|
|
124
|
+
expect: 'seq-scan'
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Vector search is an ordering, not a filter: the index either serves
|
|
128
|
+
// the distance order or the plan sorts every row.
|
|
129
|
+
return {
|
|
130
|
+
sql: `SELECT 1 FROM ${relation} ORDER BY ${quote(column)} <-> $1::${type} LIMIT 10`,
|
|
131
|
+
expect: 'sort'
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
case 'X8': {
|
|
135
|
+
const column = typeof context.column === 'string' ? context.column : null;
|
|
136
|
+
if (!column)
|
|
137
|
+
return null;
|
|
138
|
+
return {
|
|
139
|
+
sql: `SELECT 1 FROM ${relation} ORDER BY ${quote(column)} DESC LIMIT 100`,
|
|
140
|
+
expect: 'sort'
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
default:
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function planProbe(exec, sql) {
|
|
148
|
+
try {
|
|
149
|
+
const { rows } = await exec.query(`EXPLAIN (GENERIC_PLAN, FORMAT JSON) ${sql}`);
|
|
150
|
+
const first = rows[0];
|
|
151
|
+
if (!first)
|
|
152
|
+
return null;
|
|
153
|
+
// node-postgres returns the single JSON column under 'QUERY PLAN'.
|
|
154
|
+
const value = first['QUERY PLAN'] ?? Object.values(first)[0];
|
|
155
|
+
return typeof value === 'string' ? JSON.parse(value) : value;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// A probe that cannot be planned (missing operator class, permissions,
|
|
159
|
+
// an exotic type) is simply not evidence.
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Plan node types, outermost first. */
|
|
164
|
+
function collectNodeTypes(plan) {
|
|
165
|
+
const out = [];
|
|
166
|
+
const visit = (node) => {
|
|
167
|
+
if (Array.isArray(node)) {
|
|
168
|
+
for (const item of node)
|
|
169
|
+
visit(item);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!node || typeof node !== 'object')
|
|
173
|
+
return;
|
|
174
|
+
const record = node;
|
|
175
|
+
if (typeof record['Node Type'] === 'string')
|
|
176
|
+
out.push(record['Node Type']);
|
|
177
|
+
for (const key of ['Plan', 'Plans']) {
|
|
178
|
+
if (key in record)
|
|
179
|
+
visit(record[key]);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
visit(plan);
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
function equality(table, column, param) {
|
|
186
|
+
const type = columnType(table, column);
|
|
187
|
+
if (!type)
|
|
188
|
+
return null;
|
|
189
|
+
return `${quote(column)} = $${param}::${type}`;
|
|
190
|
+
}
|
|
191
|
+
function columnType(table, column) {
|
|
192
|
+
return table.columns.find((c) => c.name === column)?.type ?? null;
|
|
193
|
+
}
|
|
194
|
+
function estimatedRows(table) {
|
|
195
|
+
return table.estimatedRows;
|
|
196
|
+
}
|
|
197
|
+
function asStringArray(value) {
|
|
198
|
+
if (!Array.isArray(value))
|
|
199
|
+
return [];
|
|
200
|
+
return value.filter((v) => typeof v === 'string');
|
|
201
|
+
}
|
|
202
|
+
function quote(identifier) {
|
|
203
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
204
|
+
}
|
package/pg/acl.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-graph and schema-ACL introspection for the grant/policy lattice.
|
|
3
|
+
*
|
|
4
|
+
* The table snapshot reports grants exactly as the catalog stores them —
|
|
5
|
+
* `GRANT TO PUBLIC` is one row, membership in a granted role is zero rows.
|
|
6
|
+
* The lattice rules reason about *effective* access, which needs:
|
|
7
|
+
*
|
|
8
|
+
* - the role inheritance closure (`pg_auth_members`, INHERIT-respecting);
|
|
9
|
+
* - which roles bypass RLS entirely (superuser / BYPASSRLS);
|
|
10
|
+
* - schema USAGE ACLs (an object grant without USAGE on its schema is
|
|
11
|
+
* unreachable);
|
|
12
|
+
* - whether a schema holds any function the role can EXECUTE (so "dead
|
|
13
|
+
* schema USAGE" doesn't false-positive on function-only schemas).
|
|
14
|
+
*/
|
|
15
|
+
import type { QueryExecutor } from './introspect';
|
|
16
|
+
export interface RoleAttributes {
|
|
17
|
+
name: string;
|
|
18
|
+
/** Superuser or BYPASSRLS — policies never apply. */
|
|
19
|
+
bypassRls: boolean;
|
|
20
|
+
/** Superuser — ACL checks never apply either. */
|
|
21
|
+
isSuper: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Roles this role inherits privileges from, transitively. Only memberships
|
|
24
|
+
* that actually confer inheritance (INHERIT) are followed.
|
|
25
|
+
*/
|
|
26
|
+
inheritsFrom: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Roles this role can assume with `SET ROLE`, transitively. Distinct from
|
|
29
|
+
* `inheritsFrom`: on PG16+ a membership can confer the ability to `SET ROLE`
|
|
30
|
+
* (`pg_auth_members.set_option`) *without* passive inheritance, so a role
|
|
31
|
+
* that reaches nothing by inheritance can still execute with a target
|
|
32
|
+
* role's full privileges by assuming it. Excludes the role itself.
|
|
33
|
+
*/
|
|
34
|
+
canSetRole: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface SchemaAclGrant {
|
|
37
|
+
role: string;
|
|
38
|
+
privilege: 'USAGE' | 'CREATE';
|
|
39
|
+
}
|
|
40
|
+
export interface SchemaAclInfo {
|
|
41
|
+
schema: string;
|
|
42
|
+
owner: string;
|
|
43
|
+
grants: SchemaAclGrant[];
|
|
44
|
+
/** Roles (incl. 'PUBLIC') holding EXECUTE on at least one function in the schema. */
|
|
45
|
+
executeRoles: string[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Every non-system role with its RLS-bypass flags, its transitive
|
|
49
|
+
* INHERIT-following membership closure, and its transitive `SET ROLE`
|
|
50
|
+
* (`set_option`) closure.
|
|
51
|
+
*/
|
|
52
|
+
export declare function introspectRoleGraph(exec: QueryExecutor): Promise<Map<string, RoleAttributes>>;
|
|
53
|
+
export interface SchemaAclOptions {
|
|
54
|
+
schemas?: string[];
|
|
55
|
+
excludeSchemas?: string[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Schema ACLs plus, per schema, the roles holding EXECUTE on at least one of
|
|
59
|
+
* its functions. A NULL `nspacl` means the default ACL (owner-only + nothing
|
|
60
|
+
* for PUBLIC on non-`public` schemas); a NULL `proacl` means EXECUTE for
|
|
61
|
+
* PUBLIC, which is why the EXECUTE aggregation has to account for it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function introspectSchemaAcls(exec: QueryExecutor, options?: SchemaAclOptions): Promise<SchemaAclInfo[]>;
|
|
64
|
+
/**
|
|
65
|
+
* The role can enter the schema, so a grant on something inside it is reach.
|
|
66
|
+
*
|
|
67
|
+
* The same gate L3 applies to a table grant, applied to a schema's contents:
|
|
68
|
+
* without `USAGE` the object cannot be named, and Postgres's default
|
|
69
|
+
* EXECUTE-to-PUBLIC would otherwise make every function in an internal schema
|
|
70
|
+
* look reachable by every role in the database.
|
|
71
|
+
*/
|
|
72
|
+
export declare function canEnterSchema(schema: string, role: string, graph: Map<string, RoleAttributes>, schemaAcls: Map<string, SchemaAclInfo>): boolean;
|
package/pg/acl.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Role-graph and schema-ACL introspection for the grant/policy lattice.
|
|
4
|
+
*
|
|
5
|
+
* The table snapshot reports grants exactly as the catalog stores them —
|
|
6
|
+
* `GRANT TO PUBLIC` is one row, membership in a granted role is zero rows.
|
|
7
|
+
* The lattice rules reason about *effective* access, which needs:
|
|
8
|
+
*
|
|
9
|
+
* - the role inheritance closure (`pg_auth_members`, INHERIT-respecting);
|
|
10
|
+
* - which roles bypass RLS entirely (superuser / BYPASSRLS);
|
|
11
|
+
* - schema USAGE ACLs (an object grant without USAGE on its schema is
|
|
12
|
+
* unreachable);
|
|
13
|
+
* - whether a schema holds any function the role can EXECUTE (so "dead
|
|
14
|
+
* schema USAGE" doesn't false-positive on function-only schemas).
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.introspectRoleGraph = introspectRoleGraph;
|
|
18
|
+
exports.introspectSchemaAcls = introspectSchemaAcls;
|
|
19
|
+
exports.canEnterSchema = canEnterSchema;
|
|
20
|
+
/**
|
|
21
|
+
* Every non-system role with its RLS-bypass flags, its transitive
|
|
22
|
+
* INHERIT-following membership closure, and its transitive `SET ROLE`
|
|
23
|
+
* (`set_option`) closure.
|
|
24
|
+
*/
|
|
25
|
+
async function introspectRoleGraph(exec) {
|
|
26
|
+
const { rows: roleRows } = await exec.query(`
|
|
27
|
+
SELECT rolname, rolsuper, rolbypassrls, rolinherit
|
|
28
|
+
FROM pg_roles
|
|
29
|
+
WHERE rolname NOT LIKE 'pg\\_%'
|
|
30
|
+
ORDER BY rolname
|
|
31
|
+
`);
|
|
32
|
+
let memberRows;
|
|
33
|
+
try {
|
|
34
|
+
const { rows } = await exec.query(`
|
|
35
|
+
SELECT m.rolname AS member, p.rolname AS parent,
|
|
36
|
+
am.inherit_option AS inherits, am.set_option AS "canSet"
|
|
37
|
+
FROM pg_auth_members am
|
|
38
|
+
JOIN pg_roles m ON m.oid = am.member
|
|
39
|
+
JOIN pg_roles p ON p.oid = am.roleid
|
|
40
|
+
WHERE m.rolname NOT LIKE 'pg\\_%' AND p.rolname NOT LIKE 'pg\\_%'
|
|
41
|
+
`);
|
|
42
|
+
memberRows = rows;
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
if (!isUndefinedColumn(e))
|
|
46
|
+
throw e;
|
|
47
|
+
const { rows } = await exec.query(`
|
|
48
|
+
SELECT m.rolname AS member, p.rolname AS parent,
|
|
49
|
+
m.rolinherit AS inherits, true AS "canSet"
|
|
50
|
+
FROM pg_auth_members am
|
|
51
|
+
JOIN pg_roles m ON m.oid = am.member
|
|
52
|
+
JOIN pg_roles p ON p.oid = am.roleid
|
|
53
|
+
WHERE m.rolname NOT LIKE 'pg\\_%' AND p.rolname NOT LIKE 'pg\\_%'
|
|
54
|
+
`);
|
|
55
|
+
memberRows = rows;
|
|
56
|
+
}
|
|
57
|
+
const parents = new Map();
|
|
58
|
+
const setTargets = new Map();
|
|
59
|
+
for (const m of memberRows) {
|
|
60
|
+
if (m.inherits) {
|
|
61
|
+
const list = parents.get(m.member) ?? [];
|
|
62
|
+
list.push(m.parent);
|
|
63
|
+
parents.set(m.member, list);
|
|
64
|
+
}
|
|
65
|
+
if (m.canSet) {
|
|
66
|
+
const list = setTargets.get(m.member) ?? [];
|
|
67
|
+
list.push(m.parent);
|
|
68
|
+
setTargets.set(m.member, list);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const graph = new Map();
|
|
72
|
+
for (const r of roleRows) {
|
|
73
|
+
graph.set(r.rolname, {
|
|
74
|
+
name: r.rolname,
|
|
75
|
+
bypassRls: r.rolsuper || r.rolbypassrls,
|
|
76
|
+
isSuper: r.rolsuper,
|
|
77
|
+
inheritsFrom: closure(r.rolname, parents),
|
|
78
|
+
canSetRole: closure(r.rolname, setTargets)
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return graph;
|
|
82
|
+
}
|
|
83
|
+
/** Transitive parents of `role`, excluding itself, cycle-safe. */
|
|
84
|
+
function closure(role, parents) {
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
const stack = [...(parents.get(role) ?? [])];
|
|
87
|
+
while (stack.length > 0) {
|
|
88
|
+
const next = stack.pop();
|
|
89
|
+
if (next === role || seen.has(next))
|
|
90
|
+
continue;
|
|
91
|
+
seen.add(next);
|
|
92
|
+
stack.push(...(parents.get(next) ?? []));
|
|
93
|
+
}
|
|
94
|
+
return [...seen].sort();
|
|
95
|
+
}
|
|
96
|
+
function isUndefinedColumn(e) {
|
|
97
|
+
return e.code === '42703';
|
|
98
|
+
}
|
|
99
|
+
const DEFAULT_EXCLUDES = ['pg_catalog', 'information_schema', 'pg_toast'];
|
|
100
|
+
/**
|
|
101
|
+
* Schema ACLs plus, per schema, the roles holding EXECUTE on at least one of
|
|
102
|
+
* its functions. A NULL `nspacl` means the default ACL (owner-only + nothing
|
|
103
|
+
* for PUBLIC on non-`public` schemas); a NULL `proacl` means EXECUTE for
|
|
104
|
+
* PUBLIC, which is why the EXECUTE aggregation has to account for it.
|
|
105
|
+
*/
|
|
106
|
+
async function introspectSchemaAcls(exec, options = {}) {
|
|
107
|
+
const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
|
|
108
|
+
const schemaFilter = options.schemas && options.schemas.length > 0
|
|
109
|
+
? `AND n.nspname = ANY($1::text[])`
|
|
110
|
+
: `AND NOT (n.nspname = ANY($2::text[]))`;
|
|
111
|
+
const sql = `
|
|
112
|
+
WITH _params AS (
|
|
113
|
+
SELECT $1::text[] AS include_schemas, $2::text[] AS exclude_schemas
|
|
114
|
+
),
|
|
115
|
+
ns AS (
|
|
116
|
+
SELECT n.oid, n.nspname, pg_get_userbyid(n.nspowner) AS owner, n.nspacl
|
|
117
|
+
FROM pg_namespace n
|
|
118
|
+
WHERE n.nspname NOT LIKE 'pg\\_%'
|
|
119
|
+
${schemaFilter}
|
|
120
|
+
),
|
|
121
|
+
schema_grants AS (
|
|
122
|
+
SELECT
|
|
123
|
+
ns.oid,
|
|
124
|
+
CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE rol.rolname END AS grantee,
|
|
125
|
+
a.privilege_type
|
|
126
|
+
FROM ns, aclexplode(ns.nspacl) a
|
|
127
|
+
LEFT JOIN pg_roles rol ON rol.oid = a.grantee
|
|
128
|
+
WHERE ns.nspacl IS NOT NULL
|
|
129
|
+
),
|
|
130
|
+
fn_execute AS (
|
|
131
|
+
SELECT
|
|
132
|
+
p.pronamespace AS oid,
|
|
133
|
+
CASE
|
|
134
|
+
WHEN p.proacl IS NULL THEN 'PUBLIC'
|
|
135
|
+
WHEN a.grantee = 0 THEN 'PUBLIC'
|
|
136
|
+
ELSE rol.rolname
|
|
137
|
+
END AS grantee
|
|
138
|
+
FROM pg_proc p
|
|
139
|
+
LEFT JOIN LATERAL aclexplode(p.proacl) a
|
|
140
|
+
ON p.proacl IS NOT NULL AND a.privilege_type = 'EXECUTE'
|
|
141
|
+
LEFT JOIN pg_roles rol ON rol.oid = a.grantee
|
|
142
|
+
WHERE p.pronamespace IN (SELECT oid FROM ns)
|
|
143
|
+
)
|
|
144
|
+
SELECT
|
|
145
|
+
ns.nspname AS schema_name,
|
|
146
|
+
ns.owner,
|
|
147
|
+
COALESCE(
|
|
148
|
+
(SELECT jsonb_agg(DISTINCT jsonb_build_object('role', g.grantee, 'privilege', g.privilege_type))
|
|
149
|
+
FROM schema_grants g WHERE g.oid = ns.oid),
|
|
150
|
+
'[]'::jsonb
|
|
151
|
+
) AS grants,
|
|
152
|
+
COALESCE(
|
|
153
|
+
(SELECT array_agg(DISTINCT f.grantee) FROM fn_execute f
|
|
154
|
+
WHERE f.oid = ns.oid AND f.grantee IS NOT NULL),
|
|
155
|
+
ARRAY[]::text[]
|
|
156
|
+
) AS execute_roles
|
|
157
|
+
FROM ns
|
|
158
|
+
ORDER BY ns.nspname
|
|
159
|
+
`;
|
|
160
|
+
const { rows } = await exec.query(sql, [options.schemas ?? [], excludes]);
|
|
161
|
+
return rows.map((r) => ({
|
|
162
|
+
schema: r.schema_name,
|
|
163
|
+
owner: r.owner,
|
|
164
|
+
grants: r.grants
|
|
165
|
+
.filter((g) => g.privilege === 'USAGE' || g.privilege === 'CREATE')
|
|
166
|
+
.map((g) => ({ role: g.role, privilege: g.privilege })),
|
|
167
|
+
executeRoles: r.execute_roles
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The role can enter the schema, so a grant on something inside it is reach.
|
|
172
|
+
*
|
|
173
|
+
* The same gate L3 applies to a table grant, applied to a schema's contents:
|
|
174
|
+
* without `USAGE` the object cannot be named, and Postgres's default
|
|
175
|
+
* EXECUTE-to-PUBLIC would otherwise make every function in an internal schema
|
|
176
|
+
* look reachable by every role in the database.
|
|
177
|
+
*/
|
|
178
|
+
function canEnterSchema(schema, role, graph, schemaAcls) {
|
|
179
|
+
const acl = schemaAcls.get(schema);
|
|
180
|
+
if (!acl)
|
|
181
|
+
return true; // not introspected: assume reachable rather than silently drop
|
|
182
|
+
const attrs = graph.get(role);
|
|
183
|
+
if (attrs?.isSuper)
|
|
184
|
+
return true;
|
|
185
|
+
if (role === acl.owner)
|
|
186
|
+
return true;
|
|
187
|
+
const usage = new Set(acl.grants.filter((g) => g.privilege === 'USAGE').map((g) => g.role));
|
|
188
|
+
if (usage.has('PUBLIC') || usage.has(role))
|
|
189
|
+
return true;
|
|
190
|
+
return (attrs?.inheritsFrom ?? []).some((a) => usage.has(a) || a === acl.owner);
|
|
191
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostGraphile *behaviors*, read from the object comments they are declared in.
|
|
3
|
+
*
|
|
4
|
+
* A behavior string is the schema author stating which parts of the generated
|
|
5
|
+
* API an object participates in — `@behavior -list -connection` on a foreign
|
|
6
|
+
* key constraint says the reverse relation is not exposed. That is a different
|
|
7
|
+
* kind of evidence from anything else safegres collects: `pg_class.reltuples`
|
|
8
|
+
* and `pg_stat_user_indexes.idx_scan` are *measurements*, and safegres grades
|
|
9
|
+
* an ephemeral CI database that has never held data, so both read zero at
|
|
10
|
+
* exactly the moment they would have to mean something. A declaration reads the
|
|
11
|
+
* same in CI as in production.
|
|
12
|
+
*
|
|
13
|
+
* Read from the comment rather than from a running Graphile instance, or from
|
|
14
|
+
* whatever tables the author generated the comment out of:
|
|
15
|
+
*
|
|
16
|
+
* - `@behavior` in a comment is the PostGraphile v5 convention, so this works
|
|
17
|
+
* on any Graphile database rather than one project's metadata schema.
|
|
18
|
+
* - The comment is the composed value — one tag per object — so it is already
|
|
19
|
+
* the author's resolved intent.
|
|
20
|
+
* - A live instance would additionally resolve preset defaults, but requires an
|
|
21
|
+
* API to be running. safegres grades a database.
|
|
22
|
+
*
|
|
23
|
+
* The consequence of that last point is the rule this module exists to enforce:
|
|
24
|
+
* **only an explicit negative fragment is evidence.** Presets grant most
|
|
25
|
+
* behaviors by default, so the *absence* of `+list` says nothing whatsoever. A
|
|
26
|
+
* scanner that reads silence as denial is a scanner that tells you to drop an
|
|
27
|
+
* index a live API is using.
|
|
28
|
+
*/
|
|
29
|
+
import type { IntrospectOptions, QueryExecutor } from './introspect';
|
|
30
|
+
/** One `[+|-]scope` term of a behavior string. */
|
|
31
|
+
export interface BehaviorFragment {
|
|
32
|
+
modifier: '+' | '-';
|
|
33
|
+
/**
|
|
34
|
+
* The scope as written, e.g. `list`, `resource:connection`, `*`. Scope paths
|
|
35
|
+
* are `:`-separated and increasingly specific left to right.
|
|
36
|
+
*/
|
|
37
|
+
scope: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The three behavior tags a relation can carry.
|
|
41
|
+
*
|
|
42
|
+
* `@behavior` applies to *both* directions of a foreign key. Graphile also
|
|
43
|
+
* accepts `@forwardBehavior` and `@backwardBehavior`, which apply to only one
|
|
44
|
+
* — and the distinction is not cosmetic: denying `list` with a plain
|
|
45
|
+
* `@behavior` removes the forward `post.author` field as well as the reverse
|
|
46
|
+
* `author.posts` list, which is almost never what the author meant.
|
|
47
|
+
*/
|
|
48
|
+
export interface ConstraintBehaviors {
|
|
49
|
+
/** `@behavior` — applies to both directions. */
|
|
50
|
+
both?: string;
|
|
51
|
+
/** `@forwardBehavior` — the referencing table's field pointing at the parent. */
|
|
52
|
+
forward?: string;
|
|
53
|
+
/** `@backwardBehavior` — the referenced table's field listing the children. */
|
|
54
|
+
backward?: string;
|
|
55
|
+
}
|
|
56
|
+
/** Behaviors found on each kind of object, keyed as described on each map. */
|
|
57
|
+
export interface BehaviorSnapshot {
|
|
58
|
+
/** `schema.table` → behavior string. */
|
|
59
|
+
tables: Map<string, string>;
|
|
60
|
+
/** `schema.table.column` → behavior string. */
|
|
61
|
+
columns: Map<string, string>;
|
|
62
|
+
/** `schema.table.constraint` → behavior string. */
|
|
63
|
+
constraints: Map<string, string>;
|
|
64
|
+
/** `schema.table.constraint` → the directional tags on that constraint. */
|
|
65
|
+
constraintDirections: Map<string, ConstraintBehaviors>;
|
|
66
|
+
}
|
|
67
|
+
export declare function emptyBehaviorSnapshot(): BehaviorSnapshot;
|
|
68
|
+
/**
|
|
69
|
+
* The behavior governing one direction of a relation: the directional tag when
|
|
70
|
+
* present, else the undirected one. They do not merge — Graphile resolves the
|
|
71
|
+
* specific tag against the preset, and treating `@behavior -list` as an extra
|
|
72
|
+
* fragment on top of `@backwardBehavior +list` would invert the author's
|
|
73
|
+
* override.
|
|
74
|
+
*/
|
|
75
|
+
export declare function directionalBehavior(behaviors: ConstraintBehaviors | undefined, direction: 'forward' | 'backward'): string | undefined;
|
|
76
|
+
/** Catalog schemas no API surface ever contains. */
|
|
77
|
+
export declare const SYSTEM_SCHEMAS: string[];
|
|
78
|
+
/**
|
|
79
|
+
* Extract the `@behavior` smart tag from an object comment.
|
|
80
|
+
*
|
|
81
|
+
* Smart tags occupy the leading lines of the comment, `@tag [value]`, and stop
|
|
82
|
+
* at the first line that is not one; everything after is the description. Only
|
|
83
|
+
* `@behavior` is returned — `@omit` is deliberately not translated here, since
|
|
84
|
+
* its v4 semantics ("remove from the schema") are not the same question as a
|
|
85
|
+
* behavior fragment and conflating them would put a guess in the evidence.
|
|
86
|
+
*/
|
|
87
|
+
export declare function parseBehaviorTag(comment: string | null | undefined): string | null;
|
|
88
|
+
/** Every behavior tag on a comment, by direction. */
|
|
89
|
+
export declare function parseBehaviorTags(comment: string | null | undefined): ConstraintBehaviors;
|
|
90
|
+
/** Split a behavior string into fragments. A bare scope is a grant, as in PostGraphile. */
|
|
91
|
+
export declare function parseFragments(behavior: string | null | undefined): BehaviorFragment[];
|
|
92
|
+
/**
|
|
93
|
+
* Resolve one ability against a behavior string.
|
|
94
|
+
*
|
|
95
|
+
* `undefined` means *undeclared*, and is the answer that matters: it is not
|
|
96
|
+
* `false`. Later fragments win, matching PostGraphile — `-* +list` grants
|
|
97
|
+
* `list` and denies the rest.
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveAbility(behavior: string | null | undefined, ability: string): boolean | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* True when *every* one of `abilities` is explicitly denied.
|
|
102
|
+
*
|
|
103
|
+
* Every, not some: a relation reachable as a single record is still reachable,
|
|
104
|
+
* and one undeclared ability is enough to leave the question open.
|
|
105
|
+
*/
|
|
106
|
+
export declare function deniesAll(behavior: string | null | undefined, abilities: string[]): boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Behavior tags on every table, column and constraint in scope.
|
|
109
|
+
*
|
|
110
|
+
* Extension-owned objects are not filtered here: the maps are only ever
|
|
111
|
+
* consulted for relations that survived the scan's own filtering, so an extra
|
|
112
|
+
* entry costs a map slot and changes nothing.
|
|
113
|
+
*/
|
|
114
|
+
export declare function introspectBehaviors(exec: QueryExecutor, options?: Pick<IntrospectOptions, 'schemas' | 'excludeSchemas'>): Promise<BehaviorSnapshot>;
|