@safegres/test 0.0.2 → 1.30.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +825 -412
- package/adapters.d.ts +8 -0
- package/adapters.js +18 -0
- package/ast/helpers.d.ts +16 -0
- package/ast/helpers.js +40 -0
- package/ast/parse.d.ts +31 -0
- package/ast/parse.js +87 -0
- package/ast/walk.d.ts +27 -0
- package/ast/walk.js +79 -0
- package/callgraph/baseline.d.ts +28 -0
- package/callgraph/baseline.js +55 -0
- package/callgraph/extract.d.ts +93 -0
- package/callgraph/extract.js +340 -0
- package/callgraph/graph.d.ts +82 -0
- package/callgraph/graph.js +276 -0
- package/checks/anti-patterns.d.ts +53 -0
- package/checks/anti-patterns.js +247 -0
- package/checks/column-grants.d.ts +20 -0
- package/checks/column-grants.js +74 -0
- package/checks/coverage.d.ts +43 -0
- package/checks/coverage.js +110 -0
- package/checks/definer-function.d.ts +91 -0
- package/checks/definer-function.js +458 -0
- package/checks/definer-view.d.ts +167 -0
- package/checks/definer-view.js +392 -0
- package/checks/indexes.d.ts +82 -0
- package/checks/indexes.js +263 -0
- package/checks/lattice.d.ts +179 -0
- package/checks/lattice.js +487 -0
- package/checks/object-acls.d.ts +32 -0
- package/checks/object-acls.js +129 -0
- package/checks/policy-index.d.ts +78 -0
- package/checks/policy-index.js +349 -0
- package/checks/revocable-grants.d.ts +140 -0
- package/checks/revocable-grants.js +377 -0
- package/checks/rls-flags.d.ts +25 -0
- package/checks/rls-flags.js +82 -0
- package/checks/role-reach.d.ts +329 -0
- package/checks/role-reach.js +277 -0
- package/checks/role-trust.d.ts +45 -0
- package/checks/role-trust.js +91 -0
- package/checks/set-role.d.ts +21 -0
- package/checks/set-role.js +66 -0
- package/checks/stats.d.ts +52 -0
- package/checks/stats.js +192 -0
- package/checks/view-exposure.d.ts +68 -0
- package/checks/view-exposure.js +236 -0
- package/checks/view-writes.d.ts +88 -0
- package/checks/view-writes.js +350 -0
- package/ci/baseline.d.ts +104 -0
- package/ci/baseline.js +111 -0
- package/ci/github.d.ts +47 -0
- package/ci/github.js +197 -0
- package/cli/audit.d.ts +3 -0
- package/cli/audit.js +560 -0
- package/cli/baseline.d.ts +3 -0
- package/cli/baseline.js +104 -0
- package/cli/commands.d.ts +2 -0
- package/cli/commands.js +60 -0
- package/cli/doctor.d.ts +3 -0
- package/cli/doctor.js +72 -0
- package/cli/eval.d.ts +3 -0
- package/cli/eval.js +153 -0
- package/cli/print-config.d.ts +3 -0
- package/cli/print-config.js +54 -0
- package/cli/shared.d.ts +43 -0
- package/cli/shared.js +153 -0
- package/cli.d.ts +2 -0
- package/cli.js +69 -0
- package/commands/audit.d.ts +65 -0
- package/commands/audit.js +779 -0
- package/commands/doctor.d.ts +19 -0
- package/commands/doctor.js +181 -0
- package/commands/eval.d.ts +67 -0
- package/commands/eval.js +93 -0
- package/config/fingerprint.d.ts +40 -0
- package/config/fingerprint.js +71 -0
- package/config/loader.d.ts +44 -0
- package/config/loader.js +210 -0
- package/config/presets.d.ts +95 -0
- package/config/presets.js +327 -0
- package/config/resolve.d.ts +47 -0
- package/config/resolve.js +192 -0
- package/config/schema.d.ts +54 -0
- package/config/schema.js +269 -0
- package/config/types.d.ts +562 -0
- package/config/validate.d.ts +14 -0
- package/config/validate.js +125 -0
- package/corpus/README.md +83 -0
- package/corpus/bootstrap.sql +14 -0
- package/corpus/cases/01-anon-write-grant/case.json +26 -0
- package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
- package/corpus/cases/02-grant-to-public/case.json +25 -0
- package/corpus/cases/02-grant-to-public/schema.sql +18 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
- package/corpus/cases/04-permissive-write-policy/case.json +30 -0
- package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
- package/corpus/cases/05-public-read-policy/case.json +25 -0
- package/corpus/cases/05-public-read-policy/schema.sql +16 -0
- package/corpus/cases/06-update-without-with-check/case.json +25 -0
- package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
- package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
- package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
- package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
- package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
- package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
- package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
- package/corpus/cases/10-rls-not-forced/case.json +24 -0
- package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
- package/corpus/cases/11-session-user-in-policy/case.json +24 -0
- package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
- package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
- package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
- package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
- package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
- package/corpus/cases/14-dead-policy/case.json +24 -0
- package/corpus/cases/14-dead-policy/schema.sql +18 -0
- package/corpus/cases/15-unreachable-grant/case.json +24 -0
- package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
- package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
- package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
- package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
- package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
- package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
- package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
- package/corpus/cases/19-policy-column-cast/case.json +24 -0
- package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
- package/corpus/cases/20-stable-function-per-row/case.json +24 -0
- package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
- package/corpus/cases/21-table-without-primary-key/case.json +24 -0
- package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
- package/corpus/cases/22-redundant-index/case.json +24 -0
- package/corpus/cases/22-redundant-index/schema.sql +16 -0
- package/corpus/cases/23-tsvector-without-index/case.json +24 -0
- package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
- package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
- package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
- package/corpus/cases/25-definer-view-bypass/case.json +31 -0
- package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
- package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
- package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
- package/corpus/cases/27-definer-view-write/case.json +36 -0
- package/corpus/cases/27-definer-view-write/schema.sql +40 -0
- package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
- package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
- package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
- package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
- package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
- package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
- package/corpus/cases/31-matview-snapshot/case.json +32 -0
- package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
- package/corpus/cases/33-leaky-filter-view/case.json +36 -0
- package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
- package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
- package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
- package/corpus/cases/35-column-grant-projection/case.json +27 -0
- package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
- package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
- package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
- package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
- package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
- package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
- package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
- package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
- package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
- package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
- package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
- package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
- package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
- package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
- package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
- package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
- package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
- package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
- package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
- package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
- package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
- package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
- package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
- package/corpus/cases/41-unreadable-view-body/case.json +33 -0
- package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
- package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
- package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
- package/corpus/cases/42-search-column-indexed/case.json +21 -0
- package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
- package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
- package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
- package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
- package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
- package/corpus/cases/45-anon-foreign-table/case.json +27 -0
- package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
- package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
- package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
- package/corpus/cases/47-checked-writable-view/case.json +37 -0
- package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
- package/corpus/cases/48-definer-function-reach/case.json +38 -0
- package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
- package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
- package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
- package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
- package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
- package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
- package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
- package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
- package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
- package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
- package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
- package/corpus/index.d.ts +79 -0
- package/corpus/index.js +125 -0
- package/esm/adapters.d.ts +8 -0
- package/esm/adapters.js +6 -0
- package/esm/ast/helpers.d.ts +16 -0
- package/esm/ast/helpers.js +36 -0
- package/esm/ast/parse.d.ts +31 -0
- package/esm/ast/parse.js +82 -0
- package/esm/ast/walk.d.ts +27 -0
- package/esm/ast/walk.js +72 -0
- package/esm/callgraph/baseline.d.ts +28 -0
- package/esm/callgraph/baseline.js +48 -0
- package/esm/callgraph/extract.d.ts +93 -0
- package/esm/callgraph/extract.js +333 -0
- package/esm/callgraph/graph.d.ts +82 -0
- package/esm/callgraph/graph.js +273 -0
- package/esm/checks/anti-patterns.d.ts +53 -0
- package/esm/checks/anti-patterns.js +240 -0
- package/esm/checks/column-grants.d.ts +20 -0
- package/esm/checks/column-grants.js +71 -0
- package/esm/checks/coverage.d.ts +43 -0
- package/esm/checks/coverage.js +104 -0
- package/esm/checks/definer-function.d.ts +91 -0
- package/esm/checks/definer-function.js +452 -0
- package/esm/checks/definer-view.d.ts +167 -0
- package/esm/checks/definer-view.js +382 -0
- package/esm/checks/indexes.d.ts +82 -0
- package/esm/checks/indexes.js +256 -0
- package/esm/checks/lattice.d.ts +179 -0
- package/esm/checks/lattice.js +475 -0
- package/esm/checks/object-acls.d.ts +32 -0
- package/esm/checks/object-acls.js +125 -0
- package/esm/checks/policy-index.d.ts +78 -0
- package/esm/checks/policy-index.js +342 -0
- package/esm/checks/revocable-grants.d.ts +140 -0
- package/esm/checks/revocable-grants.js +373 -0
- package/esm/checks/rls-flags.d.ts +25 -0
- package/esm/checks/rls-flags.js +77 -0
- package/esm/checks/role-reach.d.ts +329 -0
- package/esm/checks/role-reach.js +270 -0
- package/esm/checks/role-trust.d.ts +45 -0
- package/esm/checks/role-trust.js +86 -0
- package/esm/checks/set-role.d.ts +21 -0
- package/esm/checks/set-role.js +63 -0
- package/esm/checks/stats.d.ts +52 -0
- package/esm/checks/stats.js +184 -0
- package/esm/checks/view-exposure.d.ts +68 -0
- package/esm/checks/view-exposure.js +231 -0
- package/esm/checks/view-writes.d.ts +88 -0
- package/esm/checks/view-writes.js +344 -0
- package/esm/ci/baseline.d.ts +104 -0
- package/esm/ci/baseline.js +107 -0
- package/esm/ci/github.d.ts +47 -0
- package/esm/ci/github.js +161 -0
- package/esm/cli/audit.d.ts +3 -0
- package/esm/cli/audit.js +525 -0
- package/esm/cli/baseline.d.ts +3 -0
- package/esm/cli/baseline.js +102 -0
- package/esm/cli/commands.d.ts +2 -0
- package/esm/cli/commands.js +53 -0
- package/esm/cli/doctor.d.ts +3 -0
- package/esm/cli/doctor.js +67 -0
- package/esm/cli/eval.d.ts +3 -0
- package/esm/cli/eval.js +115 -0
- package/esm/cli/print-config.d.ts +3 -0
- package/esm/cli/print-config.js +52 -0
- package/esm/cli/shared.d.ts +43 -0
- package/esm/cli/shared.js +112 -0
- package/esm/cli.d.ts +2 -0
- package/esm/cli.js +67 -0
- package/esm/commands/audit.d.ts +65 -0
- package/esm/commands/audit.js +776 -0
- package/esm/commands/doctor.d.ts +19 -0
- package/esm/commands/doctor.js +178 -0
- package/esm/commands/eval.d.ts +67 -0
- package/esm/commands/eval.js +90 -0
- package/esm/config/fingerprint.d.ts +40 -0
- package/esm/config/fingerprint.js +68 -0
- package/esm/config/loader.d.ts +44 -0
- package/esm/config/loader.js +172 -0
- package/esm/config/presets.d.ts +95 -0
- package/esm/config/presets.js +324 -0
- package/esm/config/resolve.d.ts +47 -0
- package/esm/config/resolve.js +180 -0
- package/esm/config/schema.d.ts +54 -0
- package/esm/config/schema.js +265 -0
- package/esm/config/types.d.ts +562 -0
- package/esm/config/validate.d.ts +14 -0
- package/esm/config/validate.js +122 -0
- package/esm/corpus/index.d.ts +79 -0
- package/esm/corpus/index.js +85 -0
- package/esm/exposure/adapters.d.ts +152 -0
- package/esm/exposure/adapters.js +463 -0
- package/esm/exposure/planes.d.ts +59 -0
- package/esm/exposure/planes.js +144 -0
- package/esm/exposure/reach.d.ts +78 -0
- package/esm/exposure/reach.js +105 -0
- package/esm/exposure/routines.d.ts +36 -0
- package/esm/exposure/routines.js +54 -0
- package/esm/index.d.ts +96 -0
- package/esm/index.js +57 -6
- package/esm/lint/index.d.ts +11 -0
- package/esm/lint/index.js +10 -0
- package/esm/perf/baseline.d.ts +40 -0
- package/esm/perf/baseline.js +83 -0
- package/esm/perf/explain.d.ts +46 -0
- package/esm/perf/explain.js +201 -0
- package/esm/pg/acl.d.ts +72 -0
- package/esm/pg/acl.js +186 -0
- package/esm/pg/behaviors.d.ts +114 -0
- package/esm/pg/behaviors.js +206 -0
- package/esm/pg/exposure.d.ts +74 -0
- package/esm/pg/exposure.js +198 -0
- package/esm/pg/functions.d.ts +52 -0
- package/esm/pg/functions.js +128 -0
- package/esm/pg/indexes.d.ts +192 -0
- package/esm/pg/indexes.js +395 -0
- package/esm/pg/introspect.d.ts +134 -0
- package/esm/pg/introspect.js +274 -0
- package/esm/pg/objects.d.ts +37 -0
- package/esm/pg/objects.js +108 -0
- package/esm/pg/paths.d.ts +114 -0
- package/esm/pg/paths.js +159 -0
- package/esm/pg/proc.d.ts +23 -0
- package/esm/pg/proc.js +55 -0
- package/esm/pg/reach-inputs.d.ts +47 -0
- package/esm/pg/reach-inputs.js +133 -0
- package/esm/pg/roles.d.ts +28 -0
- package/esm/pg/roles.js +50 -0
- package/esm/pg/stats.d.ts +65 -0
- package/esm/pg/stats.js +129 -0
- package/esm/pgpm-test.d.ts +22 -0
- package/esm/pgpm-test.js +39 -0
- package/esm/report/callgraph.d.ts +9 -0
- package/esm/report/callgraph.js +74 -0
- package/esm/report/compare.d.ts +143 -0
- package/esm/report/compare.js +195 -0
- package/esm/report/github.d.ts +74 -0
- package/esm/report/github.js +290 -0
- package/esm/report/json.d.ts +4 -0
- package/esm/report/json.js +3 -0
- package/esm/report/markdown.d.ts +25 -0
- package/esm/report/markdown.js +352 -0
- package/esm/report/pretty.d.ts +14 -0
- package/esm/report/pretty.js +318 -0
- package/esm/report/sarif.d.ts +40 -0
- package/esm/report/sarif.js +189 -0
- package/esm/report/view.d.ts +82 -0
- package/esm/report/view.js +126 -0
- package/esm/rules/registry.d.ts +66 -0
- package/esm/rules/registry.js +562 -0
- package/esm/score/score.d.ts +84 -0
- package/esm/score/score.js +233 -0
- package/esm/score/scorecards.d.ts +68 -0
- package/esm/score/scorecards.js +157 -0
- package/esm/types.d.ts +292 -0
- package/esm/types.js +26 -0
- package/esm/version.d.ts +8 -0
- package/esm/version.js +9 -0
- package/exposure/adapters.d.ts +152 -0
- package/exposure/adapters.js +468 -0
- package/exposure/planes.d.ts +59 -0
- package/exposure/planes.js +151 -0
- package/exposure/reach.d.ts +78 -0
- package/exposure/reach.js +109 -0
- package/exposure/routines.d.ts +36 -0
- package/exposure/routines.js +57 -0
- package/index.d.ts +96 -6
- package/index.js +229 -6
- package/lint/index.d.ts +11 -0
- package/lint/index.js +19 -0
- package/package.json +54 -48
- package/perf/baseline.d.ts +40 -0
- package/perf/baseline.js +92 -0
- package/perf/explain.d.ts +46 -0
- package/perf/explain.js +204 -0
- package/pg/acl.d.ts +72 -0
- package/pg/acl.js +191 -0
- package/pg/behaviors.d.ts +114 -0
- package/pg/behaviors.js +217 -0
- package/pg/exposure.d.ts +74 -0
- package/pg/exposure.js +205 -0
- package/pg/functions.d.ts +52 -0
- package/pg/functions.js +131 -0
- package/pg/indexes.d.ts +192 -0
- package/pg/indexes.js +400 -0
- package/pg/introspect.d.ts +134 -0
- package/pg/introspect.js +280 -0
- package/pg/objects.d.ts +37 -0
- package/pg/objects.js +111 -0
- package/pg/paths.d.ts +114 -0
- package/pg/paths.js +166 -0
- package/pg/proc.d.ts +23 -0
- package/pg/proc.js +58 -0
- package/pg/reach-inputs.d.ts +47 -0
- package/pg/reach-inputs.js +136 -0
- package/pg/roles.d.ts +28 -0
- package/pg/roles.js +54 -0
- package/pg/stats.d.ts +65 -0
- package/pg/stats.js +133 -0
- package/pgpm-test.d.ts +22 -0
- package/pgpm-test.js +42 -0
- package/report/callgraph.d.ts +9 -0
- package/report/callgraph.js +81 -0
- package/report/compare.d.ts +143 -0
- package/report/compare.js +205 -0
- package/report/github.d.ts +74 -0
- package/report/github.js +333 -0
- package/report/json.d.ts +4 -0
- package/report/json.js +6 -0
- package/report/markdown.d.ts +25 -0
- package/report/markdown.js +355 -0
- package/report/pretty.d.ts +14 -0
- package/report/pretty.js +324 -0
- package/report/sarif.d.ts +40 -0
- package/report/sarif.js +193 -0
- package/report/view.d.ts +82 -0
- package/report/view.js +132 -0
- package/rules/registry.d.ts +66 -0
- package/rules/registry.js +569 -0
- package/schema/safegres.schema.json +1119 -0
- package/score/score.d.ts +84 -0
- package/score/score.js +238 -0
- package/score/scorecards.d.ts +68 -0
- package/score/scorecards.js +162 -0
- package/types.d.ts +292 -0
- package/types.js +32 -0
- package/version.d.ts +8 -0
- package/version.js +12 -0
- package/LICENSE +0 -23
- package/admin.d.ts +0 -26
- package/admin.js +0 -182
- package/connect.d.ts +0 -19
- package/connect.js +0 -83
- package/dist/README.md +0 -557
- package/dist/package.json +0 -72
- package/esm/admin.js +0 -178
- package/esm/connect.js +0 -78
- package/esm/manager.js +0 -136
- package/esm/roles.js +0 -32
- package/esm/seed/adapters.js +0 -23
- package/esm/seed/csv.js +0 -44
- package/esm/seed/index.js +0 -16
- package/esm/seed/json.js +0 -18
- package/esm/seed/launchql.js +0 -19
- package/esm/seed/sqitch.js +0 -17
- package/esm/stream.js +0 -43
- package/esm/test-client.js +0 -150
- package/manager.d.ts +0 -25
- package/manager.js +0 -140
- package/roles.d.ts +0 -17
- package/roles.js +0 -38
- package/seed/adapters.d.ts +0 -4
- package/seed/adapters.js +0 -28
- package/seed/csv.d.ts +0 -9
- package/seed/csv.js +0 -49
- package/seed/index.d.ts +0 -16
- package/seed/index.js +0 -33
- package/seed/json.d.ts +0 -6
- package/seed/json.js +0 -21
- package/seed/launchql.d.ts +0 -2
- package/seed/launchql.js +0 -22
- package/seed/sqitch.d.ts +0 -2
- package/seed/sqitch.js +0 -20
- package/seed/types.d.ts +0 -13
- package/stream.d.ts +0 -2
- package/stream.js +0 -46
- package/test-client.d.ts +0 -49
- package/test-client.js +0 -154
- /package/{seed → config}/types.js +0 -0
- /package/esm/{seed → config}/types.js +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy-aware performance checks (X2/X3/X4).
|
|
3
|
+
*
|
|
4
|
+
* These are the checks a generic index linter can't make: they read the RLS
|
|
5
|
+
* policy predicate and ask whether the *security* qual — the one Postgres
|
|
6
|
+
* evaluates first, on every candidate row, for every query against the table —
|
|
7
|
+
* can actually use an index.
|
|
8
|
+
*
|
|
9
|
+
* All three are static (catalog + AST only); no runtime statistics, no EXPLAIN.
|
|
10
|
+
*/
|
|
11
|
+
import type { PgAstNode } from '../ast/parse';
|
|
12
|
+
import type { TableIndexSnapshot } from '../pg/indexes';
|
|
13
|
+
import type { TableSnapshot } from '../pg/introspect';
|
|
14
|
+
import type { ProcVolatility } from '../pg/proc';
|
|
15
|
+
import type { Finding } from '../types';
|
|
16
|
+
export type PolicyClause = 'USING' | 'WITH CHECK';
|
|
17
|
+
/** A column of the policy's own table, as referenced by the policy predicate. */
|
|
18
|
+
export interface PredicateColumn {
|
|
19
|
+
column: string;
|
|
20
|
+
clause: PolicyClause;
|
|
21
|
+
/**
|
|
22
|
+
* The cast type or function name wrapping the column reference, if any.
|
|
23
|
+
* `undefined` means the column is compared directly (index-usable).
|
|
24
|
+
*/
|
|
25
|
+
wrappedBy?: string;
|
|
26
|
+
/** True when `wrappedBy` is a cast rather than a function call. */
|
|
27
|
+
isCast?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Collect the policy's own columns that participate in a comparison, along
|
|
31
|
+
* with any cast/function wrapping them.
|
|
32
|
+
*
|
|
33
|
+
* Only single-part column references (`tenant_id`) and references qualified
|
|
34
|
+
* with the table's own name/alias (`posts.tenant_id`) count — a column from a
|
|
35
|
+
* subquery's table belongs to that table's policies, not this one.
|
|
36
|
+
*/
|
|
37
|
+
export declare function collectPredicateColumns(expr: PgAstNode, clause: PolicyClause, tableName: string): PredicateColumn[];
|
|
38
|
+
/**
|
|
39
|
+
* X2: a policy predicate compares a column that has no index with that column
|
|
40
|
+
* in the leading position, so the security qual degrades to a sequential scan
|
|
41
|
+
* on every query touching the table.
|
|
42
|
+
*/
|
|
43
|
+
export declare function checkUnindexedPolicyColumns(table: TableSnapshot, indexes: TableIndexSnapshot, columns: Map<string, PredicateColumn[]>): Finding[];
|
|
44
|
+
/**
|
|
45
|
+
* X3: the policy wraps its own column in a cast or function call
|
|
46
|
+
* (`tenant_id::text = ...`, `lower(email) = ...`). A plain b-tree index on the
|
|
47
|
+
* bare column can't serve that comparison — it needs a matching expression
|
|
48
|
+
* index, or the cast should move to the other side of the operator.
|
|
49
|
+
*/
|
|
50
|
+
export declare function checkPolicyColumnCasts(table: TableSnapshot, indexes: TableIndexSnapshot, columns: Map<string, PredicateColumn[]>): Finding[];
|
|
51
|
+
/**
|
|
52
|
+
* X4: the policy predicate calls a non-LEAKPROOF function.
|
|
53
|
+
*
|
|
54
|
+
* Postgres assigns security quals a lower security level than user quals and
|
|
55
|
+
* will not push a non-leakproof qual below a join or subquery scan. The
|
|
56
|
+
* practical effect is that the policy filter is applied later and to more rows
|
|
57
|
+
* than necessary, and index scans on the underlying relation are given up.
|
|
58
|
+
*
|
|
59
|
+
* System functions are exempt: their leakproofness is a fixed property of the
|
|
60
|
+
* server, not something the schema author chose.
|
|
61
|
+
*/
|
|
62
|
+
export declare function checkNonLeakproofPolicyFunctions(table: TableSnapshot, expr: PgAstNode, volatility: Map<string, ProcVolatility>, policyName: string): Finding[];
|
|
63
|
+
/**
|
|
64
|
+
* X9: the policy calls a STABLE function whose arguments don't depend on the
|
|
65
|
+
* row, but the call isn't wrapped in a scalar sub-select.
|
|
66
|
+
*
|
|
67
|
+
* `STABLE` promises the result won't change within the statement; it does *not*
|
|
68
|
+
* make the planner evaluate the call once. A bare `current_principal_id()` in a
|
|
69
|
+
* qual is executed for every row the scan considers. Wrapped as
|
|
70
|
+
* `(SELECT current_principal_id())` the expression references no column, so the
|
|
71
|
+
* planner hoists it into an InitPlan: one call per query, and the result is a
|
|
72
|
+
* constant the index can be probed with.
|
|
73
|
+
*
|
|
74
|
+
* VOLATILE calls are deliberately excluded — per-row evaluation is their
|
|
75
|
+
* defined behaviour, and hoisting one would change semantics (P1 covers those).
|
|
76
|
+
* IMMUTABLE calls with constant arguments are folded at plan time already.
|
|
77
|
+
*/
|
|
78
|
+
export declare function checkUnhoistedPolicyFunctions(table: TableSnapshot, expr: PgAstNode, volatility: Map<string, ProcVolatility>, policyName: string, clause: PolicyClause): Finding[];
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Policy-aware performance checks (X2/X3/X4).
|
|
4
|
+
*
|
|
5
|
+
* These are the checks a generic index linter can't make: they read the RLS
|
|
6
|
+
* policy predicate and ask whether the *security* qual — the one Postgres
|
|
7
|
+
* evaluates first, on every candidate row, for every query against the table —
|
|
8
|
+
* can actually use an index.
|
|
9
|
+
*
|
|
10
|
+
* All three are static (catalog + AST only); no runtime statistics, no EXPLAIN.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.collectPredicateColumns = collectPredicateColumns;
|
|
14
|
+
exports.checkUnindexedPolicyColumns = checkUnindexedPolicyColumns;
|
|
15
|
+
exports.checkPolicyColumnCasts = checkPolicyColumnCasts;
|
|
16
|
+
exports.checkNonLeakproofPolicyFunctions = checkNonLeakproofPolicyFunctions;
|
|
17
|
+
exports.checkUnhoistedPolicyFunctions = checkUnhoistedPolicyFunctions;
|
|
18
|
+
const traverse_1 = require("@pgsql/traverse");
|
|
19
|
+
const walk_1 = require("../ast/walk");
|
|
20
|
+
/**
|
|
21
|
+
* Comparison-ish nodes worth indexing for. We deliberately ignore `IS NULL`,
|
|
22
|
+
* boolean tests and the like: an index rarely helps there, so flagging them
|
|
23
|
+
* would be noise.
|
|
24
|
+
*/
|
|
25
|
+
const COMPARISON_TAGS = new Set(['A_Expr', 'SubLink', 'NullTest', 'ScalarArrayOpExpr']);
|
|
26
|
+
/**
|
|
27
|
+
* Collect the policy's own columns that participate in a comparison, along
|
|
28
|
+
* with any cast/function wrapping them.
|
|
29
|
+
*
|
|
30
|
+
* Only single-part column references (`tenant_id`) and references qualified
|
|
31
|
+
* with the table's own name/alias (`posts.tenant_id`) count — a column from a
|
|
32
|
+
* subquery's table belongs to that table's policies, not this one.
|
|
33
|
+
*/
|
|
34
|
+
function collectPredicateColumns(expr, clause, tableName) {
|
|
35
|
+
const out = [];
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
(0, traverse_1.walk)(expr, (path) => {
|
|
38
|
+
if (path.tag !== 'ColumnRef')
|
|
39
|
+
return;
|
|
40
|
+
const parts = (0, walk_1.columnRefPath)(path.node);
|
|
41
|
+
const column = ownColumnName(parts, tableName);
|
|
42
|
+
if (!column)
|
|
43
|
+
return;
|
|
44
|
+
const { wrappedBy, isCast } = wrappingOf(path);
|
|
45
|
+
if (!inComparison(path))
|
|
46
|
+
return;
|
|
47
|
+
const key = `${clause}:${column}:${wrappedBy ?? ''}`;
|
|
48
|
+
if (seen.has(key))
|
|
49
|
+
return;
|
|
50
|
+
seen.add(key);
|
|
51
|
+
out.push({ column, clause, wrappedBy, isCast });
|
|
52
|
+
});
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* X2: a policy predicate compares a column that has no index with that column
|
|
57
|
+
* in the leading position, so the security qual degrades to a sequential scan
|
|
58
|
+
* on every query touching the table.
|
|
59
|
+
*/
|
|
60
|
+
function checkUnindexedPolicyColumns(table, indexes, columns) {
|
|
61
|
+
const out = [];
|
|
62
|
+
const leading = leadingColumns(indexes);
|
|
63
|
+
for (const [policyName, cols] of columns) {
|
|
64
|
+
const reported = new Set();
|
|
65
|
+
for (const col of cols) {
|
|
66
|
+
// A wrapped column is X3's business — the raw column being indexed
|
|
67
|
+
// wouldn't help the wrapped comparison anyway.
|
|
68
|
+
if (col.wrappedBy)
|
|
69
|
+
continue;
|
|
70
|
+
if (leading.has(col.column))
|
|
71
|
+
continue;
|
|
72
|
+
if (reported.has(col.column))
|
|
73
|
+
continue;
|
|
74
|
+
reported.add(col.column);
|
|
75
|
+
out.push({
|
|
76
|
+
code: 'X2',
|
|
77
|
+
severity: 'medium',
|
|
78
|
+
category: 'index',
|
|
79
|
+
schema: table.schema,
|
|
80
|
+
table: table.name,
|
|
81
|
+
policy: policyName,
|
|
82
|
+
message: `Policy "${policyName}" on ${table.schema}.${table.name} filters by ${col.column}, which is not the leading column of any index`,
|
|
83
|
+
hint: `RLS quals run before user quals on every row the planner considers, so an unindexed policy column forces a sequential scan for all callers. CREATE INDEX ON ${table.schema}.${table.name} (${col.column}).`,
|
|
84
|
+
context: { column: col.column, clause: col.clause }
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* X3: the policy wraps its own column in a cast or function call
|
|
92
|
+
* (`tenant_id::text = ...`, `lower(email) = ...`). A plain b-tree index on the
|
|
93
|
+
* bare column can't serve that comparison — it needs a matching expression
|
|
94
|
+
* index, or the cast should move to the other side of the operator.
|
|
95
|
+
*/
|
|
96
|
+
function checkPolicyColumnCasts(table, indexes, columns) {
|
|
97
|
+
const out = [];
|
|
98
|
+
const expressionDefs = indexes.indexes
|
|
99
|
+
.filter((i) => i.expression)
|
|
100
|
+
.map((i) => i.definition.toLowerCase());
|
|
101
|
+
for (const [policyName, cols] of columns) {
|
|
102
|
+
const reported = new Set();
|
|
103
|
+
for (const col of cols) {
|
|
104
|
+
if (!col.wrappedBy)
|
|
105
|
+
continue;
|
|
106
|
+
const key = `${col.column}:${col.wrappedBy}`;
|
|
107
|
+
if (reported.has(key))
|
|
108
|
+
continue;
|
|
109
|
+
reported.add(key);
|
|
110
|
+
if (expressionDefs.some((def) => defCovers(def, col)))
|
|
111
|
+
continue;
|
|
112
|
+
const shape = col.isCast
|
|
113
|
+
? `${col.column}::${col.wrappedBy}`
|
|
114
|
+
: `${col.wrappedBy}(${col.column})`;
|
|
115
|
+
out.push({
|
|
116
|
+
code: 'X3',
|
|
117
|
+
severity: 'medium',
|
|
118
|
+
category: 'index',
|
|
119
|
+
schema: table.schema,
|
|
120
|
+
table: table.name,
|
|
121
|
+
policy: policyName,
|
|
122
|
+
message: `Policy "${policyName}" on ${table.schema}.${table.name} compares ${shape} — a plain index on ${col.column} cannot serve this`,
|
|
123
|
+
hint: col.isCast
|
|
124
|
+
? `Cast the other side of the comparison instead, or add a matching expression index: CREATE INDEX ON ${table.schema}.${table.name} ((${shape})).`
|
|
125
|
+
: `Add a matching expression index: CREATE INDEX ON ${table.schema}.${table.name} ((${shape})).`,
|
|
126
|
+
context: { column: col.column, expression: shape, clause: col.clause }
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* X4: the policy predicate calls a non-LEAKPROOF function.
|
|
134
|
+
*
|
|
135
|
+
* Postgres assigns security quals a lower security level than user quals and
|
|
136
|
+
* will not push a non-leakproof qual below a join or subquery scan. The
|
|
137
|
+
* practical effect is that the policy filter is applied later and to more rows
|
|
138
|
+
* than necessary, and index scans on the underlying relation are given up.
|
|
139
|
+
*
|
|
140
|
+
* System functions are exempt: their leakproofness is a fixed property of the
|
|
141
|
+
* server, not something the schema author chose.
|
|
142
|
+
*/
|
|
143
|
+
function checkNonLeakproofPolicyFunctions(table, expr, volatility, policyName) {
|
|
144
|
+
const out = [];
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
(0, traverse_1.walk)(expr, (path) => {
|
|
147
|
+
if (path.tag !== 'FuncCall')
|
|
148
|
+
return;
|
|
149
|
+
const qualified = (0, walk_1.funcNameQualified)(path.node);
|
|
150
|
+
const bare = qualified.split('.').pop() ?? qualified;
|
|
151
|
+
const info = volatility.get(qualified) ?? volatility.get(bare);
|
|
152
|
+
if (!info)
|
|
153
|
+
return;
|
|
154
|
+
if (info.isSystem)
|
|
155
|
+
return;
|
|
156
|
+
if (info.isLeakproof)
|
|
157
|
+
return;
|
|
158
|
+
if (seen.has(info.name))
|
|
159
|
+
return;
|
|
160
|
+
seen.add(info.name);
|
|
161
|
+
out.push({
|
|
162
|
+
code: 'X4',
|
|
163
|
+
severity: 'low',
|
|
164
|
+
category: 'index',
|
|
165
|
+
schema: table.schema,
|
|
166
|
+
table: table.name,
|
|
167
|
+
policy: policyName,
|
|
168
|
+
message: `Policy "${policyName}" on ${table.schema}.${table.name} calls non-LEAKPROOF function ${info.name}`,
|
|
169
|
+
hint: 'Non-leakproof quals cannot be pushed below joins or subquery scans, so the policy filter is applied late and to more rows than necessary. If the function provably leaks nothing about its arguments (no error messages containing values), mark it LEAKPROOF.',
|
|
170
|
+
context: { function: info.name }
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* System functions worth flagging for X9 despite being built in: reading a GUC
|
|
177
|
+
* per row is the exact pattern the rule exists to catch, whether it goes
|
|
178
|
+
* through a wrapper or not.
|
|
179
|
+
*/
|
|
180
|
+
const HOISTABLE_SYSTEM_FUNCTIONS = new Set(['current_setting', 'pg_catalog.current_setting']);
|
|
181
|
+
/**
|
|
182
|
+
* X9: the policy calls a STABLE function whose arguments don't depend on the
|
|
183
|
+
* row, but the call isn't wrapped in a scalar sub-select.
|
|
184
|
+
*
|
|
185
|
+
* `STABLE` promises the result won't change within the statement; it does *not*
|
|
186
|
+
* make the planner evaluate the call once. A bare `current_principal_id()` in a
|
|
187
|
+
* qual is executed for every row the scan considers. Wrapped as
|
|
188
|
+
* `(SELECT current_principal_id())` the expression references no column, so the
|
|
189
|
+
* planner hoists it into an InitPlan: one call per query, and the result is a
|
|
190
|
+
* constant the index can be probed with.
|
|
191
|
+
*
|
|
192
|
+
* VOLATILE calls are deliberately excluded — per-row evaluation is their
|
|
193
|
+
* defined behaviour, and hoisting one would change semantics (P1 covers those).
|
|
194
|
+
* IMMUTABLE calls with constant arguments are folded at plan time already.
|
|
195
|
+
*/
|
|
196
|
+
function checkUnhoistedPolicyFunctions(table, expr, volatility, policyName, clause) {
|
|
197
|
+
const out = [];
|
|
198
|
+
const seen = new Set();
|
|
199
|
+
(0, traverse_1.walk)(expr, (path) => {
|
|
200
|
+
if (path.tag !== 'FuncCall')
|
|
201
|
+
return;
|
|
202
|
+
const node = path.node;
|
|
203
|
+
const qualified = (0, walk_1.funcNameQualified)(node);
|
|
204
|
+
const bare = qualified.split('.').pop() ?? qualified;
|
|
205
|
+
const info = volatility.get(qualified) ?? volatility.get(bare);
|
|
206
|
+
if (!info)
|
|
207
|
+
return;
|
|
208
|
+
if (info.volatility !== 's')
|
|
209
|
+
return;
|
|
210
|
+
if (info.isSystem && !HOISTABLE_SYSTEM_FUNCTIONS.has(info.name))
|
|
211
|
+
return;
|
|
212
|
+
if (referencesColumn(node))
|
|
213
|
+
return;
|
|
214
|
+
if (isHoisted(path))
|
|
215
|
+
return;
|
|
216
|
+
if (seen.has(info.name))
|
|
217
|
+
return;
|
|
218
|
+
seen.add(info.name);
|
|
219
|
+
out.push({
|
|
220
|
+
code: 'X9',
|
|
221
|
+
severity: 'medium',
|
|
222
|
+
category: 'index',
|
|
223
|
+
schema: table.schema,
|
|
224
|
+
table: table.name,
|
|
225
|
+
policy: policyName,
|
|
226
|
+
message: `Policy "${policyName}" on ${table.schema}.${table.name} calls ${info.name}() per row — the call is not wrapped in a scalar sub-select`,
|
|
227
|
+
hint: `STABLE does not mean "evaluated once". Whenever this qual lands in a Filter rather than an index `
|
|
228
|
+
+ `condition — an unindexed policy column, a join, an OR branch — ${info.name}() is executed for every `
|
|
229
|
+
+ `row the scan considers. Wrap it as (SELECT ${info.name}()) so the expression references no column and `
|
|
230
|
+
+ 'the planner hoists it into an InitPlan: one call per query, whatever plan it picks.',
|
|
231
|
+
context: { function: info.name, clause }
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
/** True when any argument of the call references a column (so it can't be hoisted). */
|
|
237
|
+
function referencesColumn(funcCall) {
|
|
238
|
+
const args = funcCall.args;
|
|
239
|
+
if (!Array.isArray(args) || args.length === 0)
|
|
240
|
+
return false;
|
|
241
|
+
let found = false;
|
|
242
|
+
for (const arg of args) {
|
|
243
|
+
(0, traverse_1.walk)(arg, (path) => {
|
|
244
|
+
if (path.tag === 'ColumnRef')
|
|
245
|
+
found = true;
|
|
246
|
+
});
|
|
247
|
+
if (found)
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* True when the call already sits inside an uncorrelated scalar sub-select —
|
|
254
|
+
* `(SELECT fn())` — which is what makes the planner hoist it.
|
|
255
|
+
*
|
|
256
|
+
* Being inside an `EXISTS` sub-select is *not* enough: that subquery is
|
|
257
|
+
* correlated with the outer row, so it runs per row and takes the function
|
|
258
|
+
* call with it.
|
|
259
|
+
*/
|
|
260
|
+
function isHoisted(path) {
|
|
261
|
+
for (let cursor = path.parent; cursor; cursor = cursor.parent) {
|
|
262
|
+
if (cursor.tag !== 'SubLink')
|
|
263
|
+
continue;
|
|
264
|
+
const sublink = cursor.node;
|
|
265
|
+
if (sublink.subLinkType !== 'EXPR_SUBLINK')
|
|
266
|
+
return false;
|
|
267
|
+
const subselect = sublink.subselect;
|
|
268
|
+
const stmt = (subselect?.SelectStmt ?? subselect);
|
|
269
|
+
const from = stmt?.fromClause;
|
|
270
|
+
return !Array.isArray(from) || from.length === 0;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
/** Columns that sit in the leading position of at least one index. */
|
|
275
|
+
function leadingColumns(indexes) {
|
|
276
|
+
const out = new Set();
|
|
277
|
+
for (const index of indexes.indexes) {
|
|
278
|
+
const first = index.columnNames[0];
|
|
279
|
+
if (first)
|
|
280
|
+
out.add(first);
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Resolve a column reference to a bare column name on `tableName`, or null if
|
|
286
|
+
* it belongs to another relation (or is `*`).
|
|
287
|
+
*/
|
|
288
|
+
function ownColumnName(parts, tableName) {
|
|
289
|
+
if (parts.length === 1)
|
|
290
|
+
return parts[0] === '*' ? null : parts[0];
|
|
291
|
+
if (parts.length === 2 && parts[0] === tableName)
|
|
292
|
+
return parts[1] === '*' ? null : parts[1];
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
/** Nearest enclosing cast or function call, if the column is wrapped by one. */
|
|
296
|
+
function wrappingOf(path) {
|
|
297
|
+
const parent = path.parent;
|
|
298
|
+
if (!parent)
|
|
299
|
+
return {};
|
|
300
|
+
if (parent.tag === 'TypeCast') {
|
|
301
|
+
const typeName = castTypeName(parent.node);
|
|
302
|
+
return typeName ? { wrappedBy: typeName, isCast: true } : {};
|
|
303
|
+
}
|
|
304
|
+
if (parent.tag === 'FuncCall') {
|
|
305
|
+
return { wrappedBy: (0, walk_1.funcNameQualified)(parent.node), isCast: false };
|
|
306
|
+
}
|
|
307
|
+
return {};
|
|
308
|
+
}
|
|
309
|
+
/** True when the column participates in a comparison rather than, say, a projection. */
|
|
310
|
+
function inComparison(path) {
|
|
311
|
+
let cursor = path.parent;
|
|
312
|
+
// Allow one or two wrapping levels (cast, function call) between the column
|
|
313
|
+
// and the comparison it feeds.
|
|
314
|
+
for (let depth = 0; cursor && depth < 3; depth++) {
|
|
315
|
+
if (COMPARISON_TAGS.has(cursor.tag))
|
|
316
|
+
return true;
|
|
317
|
+
cursor = cursor.parent;
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
function castTypeName(typeCast) {
|
|
322
|
+
const typeName = typeCast.typeName;
|
|
323
|
+
if (!typeName)
|
|
324
|
+
return null;
|
|
325
|
+
const names = typeName.names;
|
|
326
|
+
if (!Array.isArray(names) || names.length === 0)
|
|
327
|
+
return null;
|
|
328
|
+
const last = names[names.length - 1];
|
|
329
|
+
const str = (last.String ?? last.string);
|
|
330
|
+
if (!str)
|
|
331
|
+
return null;
|
|
332
|
+
const value = String((str.sval ?? str.str) ?? '');
|
|
333
|
+
return value.length > 0 ? value : null;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Heuristic match of an expression index definition against a wrapped column.
|
|
337
|
+
* `pg_get_indexdef` renders casts as `((col)::text)` and calls as `lower(col)`,
|
|
338
|
+
* so requiring both tokens is enough to avoid the common false positive
|
|
339
|
+
* without parsing the definition back into an AST.
|
|
340
|
+
*/
|
|
341
|
+
function defCovers(definitionLower, col) {
|
|
342
|
+
const wrapper = (col.wrappedBy ?? '').toLowerCase();
|
|
343
|
+
const column = col.column.toLowerCase();
|
|
344
|
+
if (!wrapper)
|
|
345
|
+
return false;
|
|
346
|
+
if (col.isCast)
|
|
347
|
+
return definitionLower.includes(`::${wrapper}`) && definitionLower.includes(column);
|
|
348
|
+
return definitionLower.includes(`${wrapper}(`) && definitionLower.includes(column);
|
|
349
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L21: `granted − reachable`, the revocable-grant rule.
|
|
3
|
+
*
|
|
4
|
+
* For a role R, {@link analyzeRevocableGrants} computes the set of EXECUTE
|
|
5
|
+
* grants R holds that no path can exercise, so a human never has to guess which
|
|
6
|
+
* of R's grants are safe to drop. It is the reachability proof the rest of the
|
|
7
|
+
* L-series only approximates: L1 asks whether an indirect *relation* grant is
|
|
8
|
+
* admitted by current policy coverage, L3 asks whether an object grant is
|
|
9
|
+
* blocked by missing schema USAGE, and L6 suppresses an unaddressable relation
|
|
10
|
+
* finding whenever a policy predicate merely *names* the relation. L21 asks the
|
|
11
|
+
* broader question — "can this grant be exercised at all?" — across object
|
|
12
|
+
* kinds and every execution path a grant can travel, and only claims a grant
|
|
13
|
+
* revocable when it can prove the answer is no.
|
|
14
|
+
*
|
|
15
|
+
* Reach(R) is closed over, at minimum:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Direct call targets** — functions in an exposed schema R can EXECUTE
|
|
18
|
+
* and reach through schema USAGE.
|
|
19
|
+
* 2. **RLS policy predicates.** For every policy on a relation R can address,
|
|
20
|
+
* the `USING`/`WITH CHECK` expressions and the functions and relations
|
|
21
|
+
* they name, closed over transitively. These evaluate as R.
|
|
22
|
+
* 3. **Trigger bodies.** For every ordinary trigger on a relation R can
|
|
23
|
+
* write, the trigger function's body — when it is not SECURITY DEFINER,
|
|
24
|
+
* it runs as R. (EXECUTE on the trigger function itself is *not* reach:
|
|
25
|
+
* Postgres never checks it when firing a trigger, so such a grant is
|
|
26
|
+
* genuinely revocable, which is the whole point of the constructive-db
|
|
27
|
+
* audit.)
|
|
28
|
+
* 4. **Write-time expressions** — column defaults, generated columns and
|
|
29
|
+
* CHECK constraints on relations R can insert into or update. They run as
|
|
30
|
+
* the writing role.
|
|
31
|
+
* 5. **Views and their bodies**, so a grant exercised only through an
|
|
32
|
+
* `security_invoker` view survives.
|
|
33
|
+
*
|
|
34
|
+
* A SECURITY DEFINER boundary **stops** the closure: inside it the owner's
|
|
35
|
+
* privileges apply, not R's, so R needs EXECUTE on the definer function
|
|
36
|
+
* (retained) but not on anything the body goes on to call.
|
|
37
|
+
*
|
|
38
|
+
* **Fail-closed, bounded by USAGE.** Nothing is reported revocable that the
|
|
39
|
+
* analysis cannot prove unused. If any node reached as R runs SQL this analysis
|
|
40
|
+
* cannot read — dynamic `EXECUTE`, a body that fails to parse, a C/internal
|
|
41
|
+
* function — the role's closure is incomplete: that opaque node, running as R,
|
|
42
|
+
* could construct a call to any function R can *actually name and execute*. But
|
|
43
|
+
* Postgres still enforces schema USAGE at call time, so an opaque node running
|
|
44
|
+
* as R can only reach functions in schemas R holds USAGE on. A candidate whose
|
|
45
|
+
* schema R cannot even name is therefore out of an opaque node's reach: it can
|
|
46
|
+
* only be exercised through a reference this analysis *does* follow (a policy
|
|
47
|
+
* predicate, trigger body, write-time expression or view), so if none landed on
|
|
48
|
+
* it, it is revocable. Only unreached candidates in a USAGE-reachable schema are
|
|
49
|
+
* moved from `revocable` to `suppressed`, with the reason recorded. This is what
|
|
50
|
+
* lets the constructive-db audit prove the blanket EXECUTE on private-schema
|
|
51
|
+
* functions revocable — anonymous cannot name those schemas — while never
|
|
52
|
+
* revoking a grant an opaque node could plausibly exercise.
|
|
53
|
+
*/
|
|
54
|
+
import type { SchemaAclInfo } from '../pg/acl';
|
|
55
|
+
import type { FunctionSnapshot } from '../pg/functions';
|
|
56
|
+
import type { ViewSnapshot } from '../pg/indexes';
|
|
57
|
+
import type { TableSnapshot } from '../pg/introspect';
|
|
58
|
+
import type { ReachInputs } from '../pg/reach-inputs';
|
|
59
|
+
import type { Finding } from '../types';
|
|
60
|
+
import { type GrantVia, type RoleGraph } from './lattice';
|
|
61
|
+
/** Which execution path retained a grant — the proof a human reviews. */
|
|
62
|
+
export type RetainReason = 'direct-call' | 'policy-predicate' | 'trigger' | 'default-expression' | 'generated-column' | 'check-constraint' | 'view';
|
|
63
|
+
/** A `(role, object, kind)` grant, the machine-consumable unit of the rule. */
|
|
64
|
+
export interface GrantTuple {
|
|
65
|
+
role: string;
|
|
66
|
+
schema: string;
|
|
67
|
+
object: string;
|
|
68
|
+
/** Argument signature, for the function overload the grant is on. */
|
|
69
|
+
args: string;
|
|
70
|
+
kind: 'function';
|
|
71
|
+
privilege: 'EXECUTE';
|
|
72
|
+
/** How R comes to hold it — `direct` or `member of <role>` (never PUBLIC). */
|
|
73
|
+
via: GrantVia;
|
|
74
|
+
}
|
|
75
|
+
/** A grant the analysis proved reachable, with the path(s) that retained it. */
|
|
76
|
+
export interface RetainedGrant extends GrantTuple {
|
|
77
|
+
reasons: RetainReason[];
|
|
78
|
+
proof: string[];
|
|
79
|
+
}
|
|
80
|
+
/** A grant the analysis could not prove unused, and why. */
|
|
81
|
+
export interface SuppressedGrant extends GrantTuple {
|
|
82
|
+
reason: string;
|
|
83
|
+
}
|
|
84
|
+
/** A node in R's closure that ran SQL the analysis could not read. */
|
|
85
|
+
export interface TaintNode {
|
|
86
|
+
node: string;
|
|
87
|
+
reason: string;
|
|
88
|
+
}
|
|
89
|
+
export interface RoleRevocable {
|
|
90
|
+
role: string;
|
|
91
|
+
summary: {
|
|
92
|
+
candidates: number;
|
|
93
|
+
revocable: number;
|
|
94
|
+
retained: number;
|
|
95
|
+
retainedByPolicy: number;
|
|
96
|
+
retainedByTrigger: number;
|
|
97
|
+
suppressed: number;
|
|
98
|
+
};
|
|
99
|
+
/** Grants that can be dropped without changing what R can do. */
|
|
100
|
+
revocable: GrantTuple[];
|
|
101
|
+
/** Grants that must be kept, each with the path(s) proving it load-bearing. */
|
|
102
|
+
retained: RetainedGrant[];
|
|
103
|
+
/** Grants left retained only because the closure was incomplete. */
|
|
104
|
+
suppressed: SuppressedGrant[];
|
|
105
|
+
/** The opaque nodes that forced the suppressions above. */
|
|
106
|
+
taint: TaintNode[];
|
|
107
|
+
}
|
|
108
|
+
export interface RevocableGrantsReport {
|
|
109
|
+
roles: RoleRevocable[];
|
|
110
|
+
}
|
|
111
|
+
export interface RevocableGrantsInput {
|
|
112
|
+
roles: string[];
|
|
113
|
+
functions: FunctionSnapshot[];
|
|
114
|
+
tables: TableSnapshot[];
|
|
115
|
+
views: ViewSnapshot[];
|
|
116
|
+
graph: RoleGraph;
|
|
117
|
+
schemaAcls: Map<string, SchemaAclInfo>;
|
|
118
|
+
reachInputs: ReachInputs;
|
|
119
|
+
/** Schemas the exposed API surface can reach; empty when the surface is unknown. */
|
|
120
|
+
exposedSchemas: Set<string>;
|
|
121
|
+
/** True when an exposure surface was resolved — otherwise all schemas are reachable. */
|
|
122
|
+
exposureKnown: boolean;
|
|
123
|
+
/** Relations the generated API declares it cannot address (`schema.table`). */
|
|
124
|
+
unaddressable: Set<string>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The revocable, retained and suppressed EXECUTE grants for each configured
|
|
128
|
+
* role. One reachability closure is walked per role; the candidate set is R's
|
|
129
|
+
* own EXECUTE grants (direct or inherited — a grant TO PUBLIC is not R's to
|
|
130
|
+
* revoke on its own, so it is never a candidate).
|
|
131
|
+
*/
|
|
132
|
+
export declare function analyzeRevocableGrants(input: RevocableGrantsInput): Promise<RevocableGrantsReport>;
|
|
133
|
+
/**
|
|
134
|
+
* L21 findings: one per revocable grant. The proof of every *retained* grant
|
|
135
|
+
* lives in `report.revocableGrants`, not in the finding stream — a retained
|
|
136
|
+
* grant is not a problem, and the finding stream is problems. The revocable
|
|
137
|
+
* recommendation carries its own tuple in `context` so a caller can act on it
|
|
138
|
+
* without re-deriving it.
|
|
139
|
+
*/
|
|
140
|
+
export declare function revocableGrantFindings(report: RevocableGrantsReport): Finding[];
|