@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/esm/pg/paths.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access-path signals: independent, individually-auditable pieces of evidence
|
|
3
|
+
* about whether anything reads a foreign key.
|
|
4
|
+
*
|
|
5
|
+
* X1 ("foreign key with no covering index") is right about the mechanics — a
|
|
6
|
+
* `DELETE` on the parent really does scan the child — but it assumes the child
|
|
7
|
+
* is a relation somebody traverses. On a provisioning-config table that
|
|
8
|
+
* assumption is false, and acting on the finding buys a write penalty on every
|
|
9
|
+
* insert in exchange for speeding up a scan of one row.
|
|
10
|
+
*
|
|
11
|
+
* The tempting gate, `pg_class.reltuples`, is useless here: safegres grades an
|
|
12
|
+
* ephemeral CI database that has never held data, so every row estimate is 0
|
|
13
|
+
* at exactly the moment we grade. Worse, row count is the wrong question — a
|
|
14
|
+
* huge append-only log nobody joins on wants no FK index, and a tiny lookup
|
|
15
|
+
* table hammered by every request does. The property we need is *reachability*.
|
|
16
|
+
*
|
|
17
|
+
* Reachability is not one boolean, and this module deliberately does not
|
|
18
|
+
* compute one. It collects signals, each of which points in one direction and
|
|
19
|
+
* says why, and leaves the verdict to the caller:
|
|
20
|
+
*
|
|
21
|
+
* - `policy-read` and `view-read` are **reads**. They are decisive: the
|
|
22
|
+
* database itself traverses the column, so no amount of contrary evidence
|
|
23
|
+
* makes the path unreachable.
|
|
24
|
+
* - `write-once-pointer` and `config-record` are **shape**. They are not
|
|
25
|
+
* evidence of unreachability, only of a schema idiom — a `NOT NULL` key
|
|
26
|
+
* defaulting to the nil UUID is a slot a provisioner fills in, and a table
|
|
27
|
+
* with several of them looks like a config record rather than a relation.
|
|
28
|
+
*
|
|
29
|
+
* Shape alone must never suppress a finding. A generated API can expose a
|
|
30
|
+
* reverse relation over any foreign key regardless of how its default is
|
|
31
|
+
* written, and if it does, the path is reachable and the index is wanted.
|
|
32
|
+
*
|
|
33
|
+
* - `behavior-hidden` is the third kind, **declared**: the schema author has
|
|
34
|
+
* said the reverse relation is not in the generated API. It comes from the
|
|
35
|
+
* same behavior tags exposure reach reads, so the two axes agree about what
|
|
36
|
+
* the API contains rather than each guessing separately. It is reported and
|
|
37
|
+
* not acted on — a hidden relation is one missing path, not proof that
|
|
38
|
+
* nothing traverses the key, since the referential-integrity scan on a
|
|
39
|
+
* parent `DELETE` runs whatever the API exposes.
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_MIN_POINTERS = 2;
|
|
42
|
+
/**
|
|
43
|
+
* Defaults that pin a key column to one value for every row the application
|
|
44
|
+
* does not overwrite — the nil UUID, a literal, a constant of any scalar type.
|
|
45
|
+
* A call to anything else (`uuidv7()`, `now()`, a sequence) is excluded: those
|
|
46
|
+
* produce a real distribution of keys.
|
|
47
|
+
*/
|
|
48
|
+
const CONSTANT_DEFAULT = /^(uuid_nil\(\)|'(?:[^']|'')*'(?:::[\w .]+(?:\[\])?)?|-?\d+(?:\.\d+)?|true|false)$/i;
|
|
49
|
+
export function isConstantDefault(defaultExpr) {
|
|
50
|
+
if (!defaultExpr)
|
|
51
|
+
return false;
|
|
52
|
+
return CONSTANT_DEFAULT.test(defaultExpr.trim());
|
|
53
|
+
}
|
|
54
|
+
/** Key for {@link AccessPath} lookups: the relation plus the constraint name. */
|
|
55
|
+
export function pathKey(schema, table, constraint) {
|
|
56
|
+
return `${schema}.${table}.${constraint}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* One real reader ends the discussion, so a `read` signal always wins. Absent
|
|
60
|
+
* one, the shape has to hold at the *table* level before it means anything: a
|
|
61
|
+
* lone defaulted column is a habit, whereas a table built entirely out of them
|
|
62
|
+
* is an idiom. `write-once-pointer` on its own is therefore reported and not
|
|
63
|
+
* acted on.
|
|
64
|
+
*/
|
|
65
|
+
export function assess(signals) {
|
|
66
|
+
if (signals.some((s) => s.direction === 'read'))
|
|
67
|
+
return 'read';
|
|
68
|
+
if (signals.some((s) => s.name === 'config-record'))
|
|
69
|
+
return 'write-once-shaped';
|
|
70
|
+
return 'unknown';
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Collect the signals for every foreign key in the snapshot.
|
|
74
|
+
*
|
|
75
|
+
* `tables` supplies the RLS policy predicates (as SQL text) and `viewBodies`
|
|
76
|
+
* the view definitions. Both are matched as whole-word tokens across the
|
|
77
|
+
* *whole* database rather than per relation — deliberately over-eager, because
|
|
78
|
+
* a spurious `read` signal costs one retained finding while a missing one
|
|
79
|
+
* could cost a dropped index.
|
|
80
|
+
*/
|
|
81
|
+
export function classifyPaths(indexSnapshot, tables, viewBodies, options = {}) {
|
|
82
|
+
const minPointers = options.minPointers ?? DEFAULT_MIN_POINTERS;
|
|
83
|
+
const hidden = options.hiddenBackwardRelations ?? new Set();
|
|
84
|
+
const policyTokens = identifierTokens(tables.flatMap((t) => t.policies.flatMap((p) => [p.using, p.withCheck])));
|
|
85
|
+
const viewTokens = identifierTokens(viewBodies);
|
|
86
|
+
const paths = new Map();
|
|
87
|
+
for (const table of indexSnapshot) {
|
|
88
|
+
const defaults = new Map(table.columns.map((c) => [c.attnum, c.defaultExpr]));
|
|
89
|
+
const readBy = (tokens, columns) => columns.filter((c) => tokens.has(c.toLowerCase()));
|
|
90
|
+
const pointers = table.foreignKeys.filter((fk) => fk.columns.length > 0 &&
|
|
91
|
+
fk.columns.every((a) => isConstantDefault(defaults.get(a) ?? null)) &&
|
|
92
|
+
readBy(policyTokens, fk.columnNames).length === 0 &&
|
|
93
|
+
readBy(viewTokens, fk.columnNames).length === 0);
|
|
94
|
+
for (const fk of table.foreignKeys) {
|
|
95
|
+
const signals = [];
|
|
96
|
+
const inPolicy = readBy(policyTokens, fk.columnNames);
|
|
97
|
+
if (inPolicy.length > 0) {
|
|
98
|
+
signals.push({
|
|
99
|
+
name: 'policy-read',
|
|
100
|
+
direction: 'read',
|
|
101
|
+
detail: `an RLS policy predicate names ${inPolicy.join(', ')}`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const inView = readBy(viewTokens, fk.columnNames);
|
|
105
|
+
if (inView.length > 0) {
|
|
106
|
+
signals.push({
|
|
107
|
+
name: 'view-read',
|
|
108
|
+
direction: 'read',
|
|
109
|
+
detail: `a view or materialized view names ${inView.join(', ')}`
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (pointers.some((p) => p.name === fk.name)) {
|
|
113
|
+
signals.push({
|
|
114
|
+
name: 'write-once-pointer',
|
|
115
|
+
direction: 'shape',
|
|
116
|
+
detail: `every column of ${fk.name} has a constant default`
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (pointers.length >= minPointers && inPolicy.length === 0 && inView.length === 0) {
|
|
120
|
+
signals.push({
|
|
121
|
+
name: 'config-record',
|
|
122
|
+
direction: 'shape',
|
|
123
|
+
detail: `${table.name} carries ${pointers.length} write-once pointers`
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (hidden.has(pathKey(table.schema, table.name, fk.name))) {
|
|
127
|
+
signals.push({
|
|
128
|
+
name: 'behavior-hidden',
|
|
129
|
+
direction: 'declared',
|
|
130
|
+
detail: `a behavior declares the reverse relation over ${fk.name} absent from the API`
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
paths.set(pathKey(table.schema, table.name, fk.name), {
|
|
134
|
+
schema: table.schema,
|
|
135
|
+
table: table.name,
|
|
136
|
+
columns: fk.columnNames,
|
|
137
|
+
constraint: fk.name,
|
|
138
|
+
signals,
|
|
139
|
+
assessment: assess(signals)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return paths;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Every identifier-shaped token appearing in the given SQL, lowercased.
|
|
147
|
+
* Tokenising rather than substring-matching keeps `schema_id` from being
|
|
148
|
+
* "found" inside `private_schema_id`.
|
|
149
|
+
*/
|
|
150
|
+
function identifierTokens(sources) {
|
|
151
|
+
const out = new Set();
|
|
152
|
+
for (const sql of sources) {
|
|
153
|
+
if (!sql)
|
|
154
|
+
continue;
|
|
155
|
+
for (const token of sql.toLowerCase().match(/[a-z_][a-z0-9_$]*/g) ?? [])
|
|
156
|
+
out.add(token);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
package/esm/pg/proc.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { QueryExecutor } from './introspect';
|
|
2
|
+
/** `pg_proc.provolatile`: `i` immutable, `s` stable, `v` volatile. */
|
|
3
|
+
export type Volatility = 'i' | 's' | 'v';
|
|
4
|
+
export interface ProcVolatility {
|
|
5
|
+
/** Schema-qualified name, or bare name if in `pg_catalog` / a common system schema. */
|
|
6
|
+
name: string;
|
|
7
|
+
volatility: Volatility;
|
|
8
|
+
isSecurityDefiner: boolean;
|
|
9
|
+
/** `pg_proc.proleakproof` — a leakproof function can be pushed below a security qual. */
|
|
10
|
+
isLeakproof: boolean;
|
|
11
|
+
/** True if this is a built-in / system function (we usually allow-list these). */
|
|
12
|
+
isSystem: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Look up `(schema, name)` tuples in pg_proc and return their volatility.
|
|
16
|
+
* Returns a map keyed by the two names the AST walker might see:
|
|
17
|
+
* - `schema.name` (schema-qualified)
|
|
18
|
+
* - `name` (unqualified; only used when the function lives in `pg_catalog`)
|
|
19
|
+
*/
|
|
20
|
+
export declare function lookupVolatility(exec: QueryExecutor, names: Array<{
|
|
21
|
+
schema?: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}>): Promise<Map<string, ProcVolatility>>;
|
package/esm/pg/proc.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const SYSTEM_SCHEMAS = new Set([
|
|
2
|
+
'pg_catalog',
|
|
3
|
+
'information_schema',
|
|
4
|
+
'pg_toast'
|
|
5
|
+
]);
|
|
6
|
+
/**
|
|
7
|
+
* Look up `(schema, name)` tuples in pg_proc and return their volatility.
|
|
8
|
+
* Returns a map keyed by the two names the AST walker might see:
|
|
9
|
+
* - `schema.name` (schema-qualified)
|
|
10
|
+
* - `name` (unqualified; only used when the function lives in `pg_catalog`)
|
|
11
|
+
*/
|
|
12
|
+
export async function lookupVolatility(exec, names) {
|
|
13
|
+
if (names.length === 0)
|
|
14
|
+
return new Map();
|
|
15
|
+
// Dedupe input.
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
const deduped = names.filter((n) => {
|
|
18
|
+
const key = `${n.schema ?? ''}.${n.name}`;
|
|
19
|
+
if (seen.has(key))
|
|
20
|
+
return false;
|
|
21
|
+
seen.add(key);
|
|
22
|
+
return true;
|
|
23
|
+
});
|
|
24
|
+
const { rows } = await exec.query(`
|
|
25
|
+
SELECT
|
|
26
|
+
n.nspname AS schema,
|
|
27
|
+
p.proname AS proname,
|
|
28
|
+
p.provolatile AS volatility,
|
|
29
|
+
p.prosecdef AS security_definer,
|
|
30
|
+
p.proleakproof AS leakproof
|
|
31
|
+
FROM pg_proc p
|
|
32
|
+
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
33
|
+
WHERE (n.nspname, p.proname) IN (
|
|
34
|
+
SELECT unnest($1::text[]), unnest($2::text[])
|
|
35
|
+
)
|
|
36
|
+
`, [
|
|
37
|
+
deduped.map((n) => n.schema ?? 'pg_catalog'),
|
|
38
|
+
deduped.map((n) => n.name)
|
|
39
|
+
]);
|
|
40
|
+
const map = new Map();
|
|
41
|
+
for (const r of rows) {
|
|
42
|
+
const info = {
|
|
43
|
+
name: `${r.schema}.${r.proname}`,
|
|
44
|
+
volatility: r.volatility,
|
|
45
|
+
isSecurityDefiner: r.security_definer,
|
|
46
|
+
isLeakproof: r.leakproof,
|
|
47
|
+
isSystem: SYSTEM_SCHEMAS.has(r.schema)
|
|
48
|
+
};
|
|
49
|
+
map.set(`${r.schema}.${r.proname}`, info);
|
|
50
|
+
if (r.schema === 'pg_catalog') {
|
|
51
|
+
map.set(r.proname, info);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return map;
|
|
55
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extra catalog inputs the revocable-grant rule (L21) needs and no other rule
|
|
3
|
+
* reads: the ordinary triggers on a relation, and the SQL expressions a write
|
|
4
|
+
* to it evaluates — column defaults, generated columns and CHECK constraints.
|
|
5
|
+
*
|
|
6
|
+
* All three matter for one reason: they run as the *querying* role, not the
|
|
7
|
+
* table owner. A function called from a column default or a trigger body is a
|
|
8
|
+
* function the writing role must be able to EXECUTE, exactly as if it had
|
|
9
|
+
* called it directly — so a grant reached only that way is load-bearing and
|
|
10
|
+
* must not be reported revocable. The per-table policy predicates L21 also
|
|
11
|
+
* closes over already live on {@link TableSnapshot.policies}; only these three
|
|
12
|
+
* were missing.
|
|
13
|
+
*/
|
|
14
|
+
import type { QueryExecutor } from './introspect';
|
|
15
|
+
/** A non-internal trigger on a relation, and the function it fires. */
|
|
16
|
+
export interface RelationTrigger {
|
|
17
|
+
name: string;
|
|
18
|
+
/** The write commands that fire it (TRUNCATE is dropped — it grants nothing). */
|
|
19
|
+
events: Array<'INSERT' | 'UPDATE' | 'DELETE'>;
|
|
20
|
+
/** `INSTEAD OF` (view) vs an ordinary BEFORE/AFTER trigger. */
|
|
21
|
+
instead: boolean;
|
|
22
|
+
functionSchema: string;
|
|
23
|
+
functionName: string;
|
|
24
|
+
}
|
|
25
|
+
/** A SQL expression a write to the relation evaluates, as the querying role. */
|
|
26
|
+
export interface RelationExpr {
|
|
27
|
+
kind: 'default' | 'generated' | 'check';
|
|
28
|
+
/** Column name for default/generated; constraint name for check. */
|
|
29
|
+
name: string;
|
|
30
|
+
expr: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ReachInputs {
|
|
33
|
+
/** Triggers per relation, keyed `schema.table`. */
|
|
34
|
+
triggers: Map<string, RelationTrigger[]>;
|
|
35
|
+
/** Write-time expressions per relation, keyed `schema.table`. */
|
|
36
|
+
expressions: Map<string, RelationExpr[]>;
|
|
37
|
+
}
|
|
38
|
+
export interface ReachInputsOptions {
|
|
39
|
+
schemas?: string[];
|
|
40
|
+
excludeSchemas?: string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The triggers and write-time expressions of every relation in scope, in two
|
|
44
|
+
* cheap queries. Keyed the same way the table snapshot is (`schema.table`), so
|
|
45
|
+
* the engine can look inputs up by the relation it is already iterating.
|
|
46
|
+
*/
|
|
47
|
+
export declare function introspectReachInputs(exec: QueryExecutor, options?: ReachInputsOptions): Promise<ReachInputs>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extra catalog inputs the revocable-grant rule (L21) needs and no other rule
|
|
3
|
+
* reads: the ordinary triggers on a relation, and the SQL expressions a write
|
|
4
|
+
* to it evaluates — column defaults, generated columns and CHECK constraints.
|
|
5
|
+
*
|
|
6
|
+
* All three matter for one reason: they run as the *querying* role, not the
|
|
7
|
+
* table owner. A function called from a column default or a trigger body is a
|
|
8
|
+
* function the writing role must be able to EXECUTE, exactly as if it had
|
|
9
|
+
* called it directly — so a grant reached only that way is load-bearing and
|
|
10
|
+
* must not be reported revocable. The per-table policy predicates L21 also
|
|
11
|
+
* closes over already live on {@link TableSnapshot.policies}; only these three
|
|
12
|
+
* were missing.
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_EXCLUDES = ['pg_catalog', 'information_schema', 'pg_toast'];
|
|
15
|
+
/**
|
|
16
|
+
* The triggers and write-time expressions of every relation in scope, in two
|
|
17
|
+
* cheap queries. Keyed the same way the table snapshot is (`schema.table`), so
|
|
18
|
+
* the engine can look inputs up by the relation it is already iterating.
|
|
19
|
+
*/
|
|
20
|
+
export async function introspectReachInputs(exec, options = {}) {
|
|
21
|
+
const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
|
|
22
|
+
const include = options.schemas ?? [];
|
|
23
|
+
// Both params are referenced unconditionally so the prepared statement's
|
|
24
|
+
// parameter count never depends on which branch of the include/exclude
|
|
25
|
+
// choice applies (`pg` binds by count, not by name).
|
|
26
|
+
const schemaFilter = 'AND CASE WHEN cardinality($1::text[]) > 0'
|
|
27
|
+
+ ' THEN n.nspname = ANY($1::text[])'
|
|
28
|
+
+ ' ELSE NOT (n.nspname = ANY($2::text[])) END';
|
|
29
|
+
const triggers = await introspectTriggers(exec, schemaFilter, include, excludes);
|
|
30
|
+
const expressions = await introspectExpressions(exec, schemaFilter, include, excludes);
|
|
31
|
+
return { triggers, expressions };
|
|
32
|
+
}
|
|
33
|
+
async function introspectTriggers(exec, schemaFilter, include, excludes) {
|
|
34
|
+
// tgtype bits (see access/tableam / commands/trigger.h): 1 ROW, 2 BEFORE,
|
|
35
|
+
// 4 INSERT, 8 DELETE, 16 UPDATE, 32 TRUNCATE, 64 INSTEAD OF. Decoded here so
|
|
36
|
+
// the engine gets event names, not a bitmask.
|
|
37
|
+
const { rows } = await exec.query(`
|
|
38
|
+
SELECT
|
|
39
|
+
n.nspname AS schema,
|
|
40
|
+
c.relname AS relation,
|
|
41
|
+
t.tgname AS name,
|
|
42
|
+
(t.tgtype & 4) <> 0 AS fires_insert,
|
|
43
|
+
(t.tgtype & 16) <> 0 AS fires_update,
|
|
44
|
+
(t.tgtype & 8) <> 0 AS fires_delete,
|
|
45
|
+
(t.tgtype & 64) <> 0 AS instead,
|
|
46
|
+
fn.nspname AS fn_schema,
|
|
47
|
+
p.proname AS fn_name
|
|
48
|
+
FROM pg_trigger t
|
|
49
|
+
JOIN pg_class c ON c.oid = t.tgrelid
|
|
50
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
51
|
+
JOIN pg_proc p ON p.oid = t.tgfoid
|
|
52
|
+
JOIN pg_namespace fn ON fn.oid = p.pronamespace
|
|
53
|
+
WHERE NOT t.tgisinternal
|
|
54
|
+
AND n.nspname NOT LIKE 'pg\\_%'
|
|
55
|
+
${schemaFilter}
|
|
56
|
+
ORDER BY n.nspname, c.relname, t.tgname
|
|
57
|
+
`, [include, excludes]);
|
|
58
|
+
const out = new Map();
|
|
59
|
+
for (const r of rows) {
|
|
60
|
+
const events = [];
|
|
61
|
+
if (r.fires_insert)
|
|
62
|
+
events.push('INSERT');
|
|
63
|
+
if (r.fires_update)
|
|
64
|
+
events.push('UPDATE');
|
|
65
|
+
if (r.fires_delete)
|
|
66
|
+
events.push('DELETE');
|
|
67
|
+
const key = `${r.schema}.${r.relation}`;
|
|
68
|
+
const list = out.get(key) ?? [];
|
|
69
|
+
list.push({
|
|
70
|
+
name: r.name,
|
|
71
|
+
events,
|
|
72
|
+
instead: r.instead,
|
|
73
|
+
functionSchema: r.fn_schema,
|
|
74
|
+
functionName: r.fn_name
|
|
75
|
+
});
|
|
76
|
+
out.set(key, list);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
async function introspectExpressions(exec, schemaFilter, include, excludes) {
|
|
81
|
+
const out = new Map();
|
|
82
|
+
const add = (key, expr) => {
|
|
83
|
+
const list = out.get(key) ?? [];
|
|
84
|
+
list.push(expr);
|
|
85
|
+
out.set(key, list);
|
|
86
|
+
};
|
|
87
|
+
// Column defaults and generated-column expressions both live in pg_attrdef;
|
|
88
|
+
// a generated column is flagged by pg_attribute.attgenerated.
|
|
89
|
+
const { rows: defaults } = await exec.query(`
|
|
90
|
+
SELECT
|
|
91
|
+
n.nspname AS schema,
|
|
92
|
+
c.relname AS relation,
|
|
93
|
+
a.attname AS column,
|
|
94
|
+
a.attgenerated <> '' AS generated,
|
|
95
|
+
pg_get_expr(ad.adbin, ad.adrelid) AS expr
|
|
96
|
+
FROM pg_attrdef ad
|
|
97
|
+
JOIN pg_class c ON c.oid = ad.adrelid
|
|
98
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
99
|
+
JOIN pg_attribute a ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
|
|
100
|
+
WHERE n.nspname NOT LIKE 'pg\\_%'
|
|
101
|
+
${schemaFilter}
|
|
102
|
+
ORDER BY n.nspname, c.relname, a.attnum
|
|
103
|
+
`, [include, excludes]);
|
|
104
|
+
for (const r of defaults) {
|
|
105
|
+
if (!r.expr)
|
|
106
|
+
continue;
|
|
107
|
+
add(`${r.schema}.${r.relation}`, {
|
|
108
|
+
kind: r.generated ? 'generated' : 'default',
|
|
109
|
+
name: r.column,
|
|
110
|
+
expr: r.expr
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const { rows: checks } = await exec.query(`
|
|
114
|
+
SELECT
|
|
115
|
+
n.nspname AS schema,
|
|
116
|
+
c.relname AS relation,
|
|
117
|
+
con.conname AS name,
|
|
118
|
+
pg_get_expr(con.conbin, con.conrelid) AS expr
|
|
119
|
+
FROM pg_constraint con
|
|
120
|
+
JOIN pg_class c ON c.oid = con.conrelid
|
|
121
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
122
|
+
WHERE con.contype = 'c'
|
|
123
|
+
AND n.nspname NOT LIKE 'pg\\_%'
|
|
124
|
+
${schemaFilter}
|
|
125
|
+
ORDER BY n.nspname, c.relname, con.conname
|
|
126
|
+
`, [include, excludes]);
|
|
127
|
+
for (const r of checks) {
|
|
128
|
+
if (!r.expr)
|
|
129
|
+
continue;
|
|
130
|
+
add(`${r.schema}.${r.relation}`, { kind: 'check', name: r.name, expr: r.expr });
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { QueryExecutor } from './introspect';
|
|
2
|
+
/** A filterable PostgreSQL role for audit purposes. */
|
|
3
|
+
export interface RoleInfo {
|
|
4
|
+
name: string;
|
|
5
|
+
canLogin: boolean;
|
|
6
|
+
isSuper: boolean;
|
|
7
|
+
isSystem: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Enumerate every non-system role that could meaningfully appear as a
|
|
11
|
+
* grantee. By default we include every role — caller may pass `--roles`
|
|
12
|
+
* to narrow to e.g. `authenticated, anonymous`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function listAuditableRoles(exec: QueryExecutor): Promise<RoleInfo[]>;
|
|
15
|
+
export interface RoleResolution {
|
|
16
|
+
/** Final list of role names used for the audit. */
|
|
17
|
+
roles: string[];
|
|
18
|
+
/** Roles returned by pg_roles but filtered out. */
|
|
19
|
+
excluded: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Apply `--roles` / `--exclude-roles` filters against the full pg_roles list.
|
|
23
|
+
*
|
|
24
|
+
* - If `include` is non-empty, only those names survive.
|
|
25
|
+
* - Else, every non-system role survives.
|
|
26
|
+
* - `exclude` is applied on top.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveRoles(allRoles: RoleInfo[], include: string[] | undefined, exclude?: string[]): RoleResolution;
|
package/esm/pg/roles.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enumerate every non-system role that could meaningfully appear as a
|
|
3
|
+
* grantee. By default we include every role — caller may pass `--roles`
|
|
4
|
+
* to narrow to e.g. `authenticated, anonymous`.
|
|
5
|
+
*/
|
|
6
|
+
export async function listAuditableRoles(exec) {
|
|
7
|
+
const { rows } = await exec.query(`
|
|
8
|
+
SELECT rolname, rolcanlogin, rolsuper
|
|
9
|
+
FROM pg_roles
|
|
10
|
+
ORDER BY rolname
|
|
11
|
+
`);
|
|
12
|
+
return rows.map((r) => ({
|
|
13
|
+
name: r.rolname,
|
|
14
|
+
canLogin: r.rolcanlogin,
|
|
15
|
+
isSuper: r.rolsuper,
|
|
16
|
+
isSystem: r.rolname.startsWith('pg_')
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Apply `--roles` / `--exclude-roles` filters against the full pg_roles list.
|
|
21
|
+
*
|
|
22
|
+
* - If `include` is non-empty, only those names survive.
|
|
23
|
+
* - Else, every non-system role survives.
|
|
24
|
+
* - `exclude` is applied on top.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveRoles(allRoles, include, exclude = []) {
|
|
27
|
+
const excludeSet = new Set(exclude);
|
|
28
|
+
const includeSet = include && include.length > 0 ? new Set(include) : null;
|
|
29
|
+
const roles = [];
|
|
30
|
+
const excluded = [];
|
|
31
|
+
for (const r of allRoles) {
|
|
32
|
+
if (excludeSet.has(r.name)) {
|
|
33
|
+
excluded.push(r.name);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (includeSet) {
|
|
37
|
+
if (includeSet.has(r.name))
|
|
38
|
+
roles.push(r.name);
|
|
39
|
+
else
|
|
40
|
+
excluded.push(r.name);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (r.isSystem) {
|
|
44
|
+
excluded.push(r.name);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
roles.push(r.name);
|
|
48
|
+
}
|
|
49
|
+
return { roles, excluded };
|
|
50
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime statistics snapshot (`--stats`).
|
|
3
|
+
*
|
|
4
|
+
* Everything here reads the cumulative statistics views — `pg_stat_user_tables`,
|
|
5
|
+
* `pg_stat_user_indexes`, and (when installed) `pg_stat_statements`. Unlike the
|
|
6
|
+
* catalog checks, these numbers describe a *workload*: they are only as good as
|
|
7
|
+
* the traffic the database has seen since the counters were last reset, which is
|
|
8
|
+
* why the snapshot carries `statsReset` and why the checks take floors.
|
|
9
|
+
*/
|
|
10
|
+
import { type IntrospectOptions, type QueryExecutor } from './introspect';
|
|
11
|
+
export interface IndexUsage {
|
|
12
|
+
name: string;
|
|
13
|
+
/** `pg_stat_user_indexes.idx_scan` — times the planner chose this index. */
|
|
14
|
+
scans: number;
|
|
15
|
+
/** `pg_relation_size` of the index, in bytes. */
|
|
16
|
+
sizeBytes: number;
|
|
17
|
+
unique: boolean;
|
|
18
|
+
/** Backs a PRIMARY KEY / UNIQUE / EXCLUDE constraint — never droppable alone. */
|
|
19
|
+
constraint: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface TableUsage {
|
|
22
|
+
schema: string;
|
|
23
|
+
name: string;
|
|
24
|
+
seqScans: number;
|
|
25
|
+
seqTuplesRead: number;
|
|
26
|
+
/** Index scans across every index on the table. */
|
|
27
|
+
indexScans: number;
|
|
28
|
+
liveTuples: number;
|
|
29
|
+
deadTuples: number;
|
|
30
|
+
/** `pg_relation_size` of the heap, in bytes. */
|
|
31
|
+
sizeBytes: number;
|
|
32
|
+
/** Most recent vacuum, manual or auto (ISO 8601), null if never. */
|
|
33
|
+
lastVacuum: string | null;
|
|
34
|
+
/** Most recent analyze, manual or auto (ISO 8601), null if never. */
|
|
35
|
+
lastAnalyze: string | null;
|
|
36
|
+
indexes: IndexUsage[];
|
|
37
|
+
}
|
|
38
|
+
export interface StatementUsage {
|
|
39
|
+
/** Normalised query text (parameters replaced by `$n`). */
|
|
40
|
+
query: string;
|
|
41
|
+
calls: number;
|
|
42
|
+
/** Total execution time across all calls, in milliseconds. */
|
|
43
|
+
totalTimeMs: number;
|
|
44
|
+
meanTimeMs: number;
|
|
45
|
+
/** Rows returned/affected across all calls. */
|
|
46
|
+
rows: number;
|
|
47
|
+
}
|
|
48
|
+
export interface StatsSnapshot {
|
|
49
|
+
tables: TableUsage[];
|
|
50
|
+
/** Present only when `pg_stat_statements` is installed and readable. */
|
|
51
|
+
statements?: StatementUsage[];
|
|
52
|
+
/**
|
|
53
|
+
* When the cumulative counters were last reset (ISO 8601), from
|
|
54
|
+
* `pg_stat_database`. A recent reset means the numbers describe a short
|
|
55
|
+
* window and the checks should be read as provisional.
|
|
56
|
+
*/
|
|
57
|
+
statsReset: string | null;
|
|
58
|
+
/** Why `statements` is absent, for the report's `notes`. */
|
|
59
|
+
statementsUnavailable?: string;
|
|
60
|
+
}
|
|
61
|
+
/** Statements slower than this in aggregate are worth reporting at all. */
|
|
62
|
+
export declare const DEFAULT_STATEMENT_LIMIT = 20;
|
|
63
|
+
export declare function introspectStats(exec: QueryExecutor, options?: Pick<IntrospectOptions, 'schemas' | 'excludeSchemas' | 'extensions'> & {
|
|
64
|
+
statementLimit?: number;
|
|
65
|
+
}): Promise<StatsSnapshot>;
|