@safegres/test 0.0.1 → 1.30.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +825 -412
- package/adapters.d.ts +8 -0
- package/adapters.js +18 -0
- package/ast/helpers.d.ts +16 -0
- package/ast/helpers.js +40 -0
- package/ast/parse.d.ts +31 -0
- package/ast/parse.js +87 -0
- package/ast/walk.d.ts +27 -0
- package/ast/walk.js +79 -0
- package/callgraph/baseline.d.ts +28 -0
- package/callgraph/baseline.js +55 -0
- package/callgraph/extract.d.ts +93 -0
- package/callgraph/extract.js +340 -0
- package/callgraph/graph.d.ts +82 -0
- package/callgraph/graph.js +276 -0
- package/checks/anti-patterns.d.ts +53 -0
- package/checks/anti-patterns.js +247 -0
- package/checks/column-grants.d.ts +20 -0
- package/checks/column-grants.js +74 -0
- package/checks/coverage.d.ts +43 -0
- package/checks/coverage.js +110 -0
- package/checks/definer-function.d.ts +91 -0
- package/checks/definer-function.js +458 -0
- package/checks/definer-view.d.ts +167 -0
- package/checks/definer-view.js +392 -0
- package/checks/indexes.d.ts +82 -0
- package/checks/indexes.js +263 -0
- package/checks/lattice.d.ts +179 -0
- package/checks/lattice.js +487 -0
- package/checks/object-acls.d.ts +32 -0
- package/checks/object-acls.js +129 -0
- package/checks/policy-index.d.ts +78 -0
- package/checks/policy-index.js +349 -0
- package/checks/revocable-grants.d.ts +140 -0
- package/checks/revocable-grants.js +377 -0
- package/checks/rls-flags.d.ts +25 -0
- package/checks/rls-flags.js +82 -0
- package/checks/role-reach.d.ts +329 -0
- package/checks/role-reach.js +277 -0
- package/checks/role-trust.d.ts +45 -0
- package/checks/role-trust.js +91 -0
- package/checks/set-role.d.ts +21 -0
- package/checks/set-role.js +66 -0
- package/checks/stats.d.ts +52 -0
- package/checks/stats.js +192 -0
- package/checks/view-exposure.d.ts +68 -0
- package/checks/view-exposure.js +236 -0
- package/checks/view-writes.d.ts +88 -0
- package/checks/view-writes.js +350 -0
- package/ci/baseline.d.ts +104 -0
- package/ci/baseline.js +111 -0
- package/ci/github.d.ts +47 -0
- package/ci/github.js +197 -0
- package/cli/audit.d.ts +3 -0
- package/cli/audit.js +560 -0
- package/cli/baseline.d.ts +3 -0
- package/cli/baseline.js +104 -0
- package/cli/commands.d.ts +2 -0
- package/cli/commands.js +60 -0
- package/cli/doctor.d.ts +3 -0
- package/cli/doctor.js +72 -0
- package/cli/eval.d.ts +3 -0
- package/cli/eval.js +153 -0
- package/cli/print-config.d.ts +3 -0
- package/cli/print-config.js +54 -0
- package/cli/shared.d.ts +43 -0
- package/cli/shared.js +153 -0
- package/cli.d.ts +2 -0
- package/cli.js +69 -0
- package/commands/audit.d.ts +65 -0
- package/commands/audit.js +779 -0
- package/commands/doctor.d.ts +19 -0
- package/commands/doctor.js +181 -0
- package/commands/eval.d.ts +67 -0
- package/commands/eval.js +93 -0
- package/config/fingerprint.d.ts +40 -0
- package/config/fingerprint.js +71 -0
- package/config/loader.d.ts +44 -0
- package/config/loader.js +210 -0
- package/config/presets.d.ts +95 -0
- package/config/presets.js +327 -0
- package/config/resolve.d.ts +47 -0
- package/config/resolve.js +192 -0
- package/config/schema.d.ts +54 -0
- package/config/schema.js +269 -0
- package/config/types.d.ts +562 -0
- package/config/validate.d.ts +14 -0
- package/config/validate.js +125 -0
- package/corpus/README.md +83 -0
- package/corpus/bootstrap.sql +14 -0
- package/corpus/cases/01-anon-write-grant/case.json +26 -0
- package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
- package/corpus/cases/02-grant-to-public/case.json +25 -0
- package/corpus/cases/02-grant-to-public/schema.sql +18 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
- package/corpus/cases/04-permissive-write-policy/case.json +30 -0
- package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
- package/corpus/cases/05-public-read-policy/case.json +25 -0
- package/corpus/cases/05-public-read-policy/schema.sql +16 -0
- package/corpus/cases/06-update-without-with-check/case.json +25 -0
- package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
- package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
- package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
- package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
- package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
- package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
- package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
- package/corpus/cases/10-rls-not-forced/case.json +24 -0
- package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
- package/corpus/cases/11-session-user-in-policy/case.json +24 -0
- package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
- package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
- package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
- package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
- package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
- package/corpus/cases/14-dead-policy/case.json +24 -0
- package/corpus/cases/14-dead-policy/schema.sql +18 -0
- package/corpus/cases/15-unreachable-grant/case.json +24 -0
- package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
- package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
- package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
- package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
- package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
- package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
- package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
- package/corpus/cases/19-policy-column-cast/case.json +24 -0
- package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
- package/corpus/cases/20-stable-function-per-row/case.json +24 -0
- package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
- package/corpus/cases/21-table-without-primary-key/case.json +24 -0
- package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
- package/corpus/cases/22-redundant-index/case.json +24 -0
- package/corpus/cases/22-redundant-index/schema.sql +16 -0
- package/corpus/cases/23-tsvector-without-index/case.json +24 -0
- package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
- package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
- package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
- package/corpus/cases/25-definer-view-bypass/case.json +31 -0
- package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
- package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
- package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
- package/corpus/cases/27-definer-view-write/case.json +36 -0
- package/corpus/cases/27-definer-view-write/schema.sql +40 -0
- package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
- package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
- package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
- package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
- package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
- package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
- package/corpus/cases/31-matview-snapshot/case.json +32 -0
- package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
- package/corpus/cases/33-leaky-filter-view/case.json +36 -0
- package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
- package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
- package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
- package/corpus/cases/35-column-grant-projection/case.json +27 -0
- package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
- package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
- package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
- package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
- package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
- package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
- package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
- package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
- package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
- package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
- package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
- package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
- package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
- package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
- package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
- package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
- package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
- package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
- package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
- package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
- package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
- package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
- package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
- package/corpus/cases/41-unreadable-view-body/case.json +33 -0
- package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
- package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
- package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
- package/corpus/cases/42-search-column-indexed/case.json +21 -0
- package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
- package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
- package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
- package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
- package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
- package/corpus/cases/45-anon-foreign-table/case.json +27 -0
- package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
- package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
- package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
- package/corpus/cases/47-checked-writable-view/case.json +37 -0
- package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
- package/corpus/cases/48-definer-function-reach/case.json +38 -0
- package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
- package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
- package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
- package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
- package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
- package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
- package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
- package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
- package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
- package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
- package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
- package/corpus/index.d.ts +79 -0
- package/corpus/index.js +125 -0
- package/esm/adapters.d.ts +8 -0
- package/esm/adapters.js +6 -0
- package/esm/ast/helpers.d.ts +16 -0
- package/esm/ast/helpers.js +36 -0
- package/esm/ast/parse.d.ts +31 -0
- package/esm/ast/parse.js +82 -0
- package/esm/ast/walk.d.ts +27 -0
- package/esm/ast/walk.js +72 -0
- package/esm/callgraph/baseline.d.ts +28 -0
- package/esm/callgraph/baseline.js +48 -0
- package/esm/callgraph/extract.d.ts +93 -0
- package/esm/callgraph/extract.js +333 -0
- package/esm/callgraph/graph.d.ts +82 -0
- package/esm/callgraph/graph.js +273 -0
- package/esm/checks/anti-patterns.d.ts +53 -0
- package/esm/checks/anti-patterns.js +240 -0
- package/esm/checks/column-grants.d.ts +20 -0
- package/esm/checks/column-grants.js +71 -0
- package/esm/checks/coverage.d.ts +43 -0
- package/esm/checks/coverage.js +104 -0
- package/esm/checks/definer-function.d.ts +91 -0
- package/esm/checks/definer-function.js +452 -0
- package/esm/checks/definer-view.d.ts +167 -0
- package/esm/checks/definer-view.js +382 -0
- package/esm/checks/indexes.d.ts +82 -0
- package/esm/checks/indexes.js +256 -0
- package/esm/checks/lattice.d.ts +179 -0
- package/esm/checks/lattice.js +475 -0
- package/esm/checks/object-acls.d.ts +32 -0
- package/esm/checks/object-acls.js +125 -0
- package/esm/checks/policy-index.d.ts +78 -0
- package/esm/checks/policy-index.js +342 -0
- package/esm/checks/revocable-grants.d.ts +140 -0
- package/esm/checks/revocable-grants.js +373 -0
- package/esm/checks/rls-flags.d.ts +25 -0
- package/esm/checks/rls-flags.js +77 -0
- package/esm/checks/role-reach.d.ts +329 -0
- package/esm/checks/role-reach.js +270 -0
- package/esm/checks/role-trust.d.ts +45 -0
- package/esm/checks/role-trust.js +86 -0
- package/esm/checks/set-role.d.ts +21 -0
- package/esm/checks/set-role.js +63 -0
- package/esm/checks/stats.d.ts +52 -0
- package/esm/checks/stats.js +184 -0
- package/esm/checks/view-exposure.d.ts +68 -0
- package/esm/checks/view-exposure.js +231 -0
- package/esm/checks/view-writes.d.ts +88 -0
- package/esm/checks/view-writes.js +344 -0
- package/esm/ci/baseline.d.ts +104 -0
- package/esm/ci/baseline.js +107 -0
- package/esm/ci/github.d.ts +47 -0
- package/esm/ci/github.js +161 -0
- package/esm/cli/audit.d.ts +3 -0
- package/esm/cli/audit.js +525 -0
- package/esm/cli/baseline.d.ts +3 -0
- package/esm/cli/baseline.js +102 -0
- package/esm/cli/commands.d.ts +2 -0
- package/esm/cli/commands.js +53 -0
- package/esm/cli/doctor.d.ts +3 -0
- package/esm/cli/doctor.js +67 -0
- package/esm/cli/eval.d.ts +3 -0
- package/esm/cli/eval.js +115 -0
- package/esm/cli/print-config.d.ts +3 -0
- package/esm/cli/print-config.js +52 -0
- package/esm/cli/shared.d.ts +43 -0
- package/esm/cli/shared.js +112 -0
- package/esm/cli.d.ts +2 -0
- package/esm/cli.js +67 -0
- package/esm/commands/audit.d.ts +65 -0
- package/esm/commands/audit.js +776 -0
- package/esm/commands/doctor.d.ts +19 -0
- package/esm/commands/doctor.js +178 -0
- package/esm/commands/eval.d.ts +67 -0
- package/esm/commands/eval.js +90 -0
- package/esm/config/fingerprint.d.ts +40 -0
- package/esm/config/fingerprint.js +68 -0
- package/esm/config/loader.d.ts +44 -0
- package/esm/config/loader.js +172 -0
- package/esm/config/presets.d.ts +95 -0
- package/esm/config/presets.js +324 -0
- package/esm/config/resolve.d.ts +47 -0
- package/esm/config/resolve.js +180 -0
- package/esm/config/schema.d.ts +54 -0
- package/esm/config/schema.js +265 -0
- package/esm/config/types.d.ts +562 -0
- package/esm/config/validate.d.ts +14 -0
- package/esm/config/validate.js +122 -0
- package/esm/corpus/index.d.ts +79 -0
- package/esm/corpus/index.js +85 -0
- package/esm/exposure/adapters.d.ts +152 -0
- package/esm/exposure/adapters.js +463 -0
- package/esm/exposure/planes.d.ts +59 -0
- package/esm/exposure/planes.js +144 -0
- package/esm/exposure/reach.d.ts +78 -0
- package/esm/exposure/reach.js +105 -0
- package/esm/exposure/routines.d.ts +36 -0
- package/esm/exposure/routines.js +54 -0
- package/esm/index.d.ts +96 -0
- package/esm/index.js +57 -6
- package/esm/lint/index.d.ts +11 -0
- package/esm/lint/index.js +10 -0
- package/esm/perf/baseline.d.ts +40 -0
- package/esm/perf/baseline.js +83 -0
- package/esm/perf/explain.d.ts +46 -0
- package/esm/perf/explain.js +201 -0
- package/esm/pg/acl.d.ts +72 -0
- package/esm/pg/acl.js +186 -0
- package/esm/pg/behaviors.d.ts +114 -0
- package/esm/pg/behaviors.js +206 -0
- package/esm/pg/exposure.d.ts +74 -0
- package/esm/pg/exposure.js +198 -0
- package/esm/pg/functions.d.ts +52 -0
- package/esm/pg/functions.js +128 -0
- package/esm/pg/indexes.d.ts +192 -0
- package/esm/pg/indexes.js +395 -0
- package/esm/pg/introspect.d.ts +134 -0
- package/esm/pg/introspect.js +274 -0
- package/esm/pg/objects.d.ts +37 -0
- package/esm/pg/objects.js +108 -0
- package/esm/pg/paths.d.ts +114 -0
- package/esm/pg/paths.js +159 -0
- package/esm/pg/proc.d.ts +23 -0
- package/esm/pg/proc.js +55 -0
- package/esm/pg/reach-inputs.d.ts +47 -0
- package/esm/pg/reach-inputs.js +133 -0
- package/esm/pg/roles.d.ts +28 -0
- package/esm/pg/roles.js +50 -0
- package/esm/pg/stats.d.ts +65 -0
- package/esm/pg/stats.js +129 -0
- package/esm/pgpm-test.d.ts +22 -0
- package/esm/pgpm-test.js +39 -0
- package/esm/report/callgraph.d.ts +9 -0
- package/esm/report/callgraph.js +74 -0
- package/esm/report/compare.d.ts +143 -0
- package/esm/report/compare.js +195 -0
- package/esm/report/github.d.ts +74 -0
- package/esm/report/github.js +290 -0
- package/esm/report/json.d.ts +4 -0
- package/esm/report/json.js +3 -0
- package/esm/report/markdown.d.ts +25 -0
- package/esm/report/markdown.js +352 -0
- package/esm/report/pretty.d.ts +14 -0
- package/esm/report/pretty.js +318 -0
- package/esm/report/sarif.d.ts +40 -0
- package/esm/report/sarif.js +189 -0
- package/esm/report/view.d.ts +82 -0
- package/esm/report/view.js +126 -0
- package/esm/rules/registry.d.ts +66 -0
- package/esm/rules/registry.js +562 -0
- package/esm/score/score.d.ts +84 -0
- package/esm/score/score.js +233 -0
- package/esm/score/scorecards.d.ts +68 -0
- package/esm/score/scorecards.js +157 -0
- package/esm/types.d.ts +292 -0
- package/esm/types.js +26 -0
- package/esm/version.d.ts +8 -0
- package/esm/version.js +9 -0
- package/exposure/adapters.d.ts +152 -0
- package/exposure/adapters.js +468 -0
- package/exposure/planes.d.ts +59 -0
- package/exposure/planes.js +151 -0
- package/exposure/reach.d.ts +78 -0
- package/exposure/reach.js +109 -0
- package/exposure/routines.d.ts +36 -0
- package/exposure/routines.js +57 -0
- package/index.d.ts +96 -6
- package/index.js +229 -6
- package/lint/index.d.ts +11 -0
- package/lint/index.js +19 -0
- package/package.json +54 -48
- package/perf/baseline.d.ts +40 -0
- package/perf/baseline.js +92 -0
- package/perf/explain.d.ts +46 -0
- package/perf/explain.js +204 -0
- package/pg/acl.d.ts +72 -0
- package/pg/acl.js +191 -0
- package/pg/behaviors.d.ts +114 -0
- package/pg/behaviors.js +217 -0
- package/pg/exposure.d.ts +74 -0
- package/pg/exposure.js +205 -0
- package/pg/functions.d.ts +52 -0
- package/pg/functions.js +131 -0
- package/pg/indexes.d.ts +192 -0
- package/pg/indexes.js +400 -0
- package/pg/introspect.d.ts +134 -0
- package/pg/introspect.js +280 -0
- package/pg/objects.d.ts +37 -0
- package/pg/objects.js +111 -0
- package/pg/paths.d.ts +114 -0
- package/pg/paths.js +166 -0
- package/pg/proc.d.ts +23 -0
- package/pg/proc.js +58 -0
- package/pg/reach-inputs.d.ts +47 -0
- package/pg/reach-inputs.js +136 -0
- package/pg/roles.d.ts +28 -0
- package/pg/roles.js +54 -0
- package/pg/stats.d.ts +65 -0
- package/pg/stats.js +133 -0
- package/pgpm-test.d.ts +22 -0
- package/pgpm-test.js +42 -0
- package/report/callgraph.d.ts +9 -0
- package/report/callgraph.js +81 -0
- package/report/compare.d.ts +143 -0
- package/report/compare.js +205 -0
- package/report/github.d.ts +74 -0
- package/report/github.js +333 -0
- package/report/json.d.ts +4 -0
- package/report/json.js +6 -0
- package/report/markdown.d.ts +25 -0
- package/report/markdown.js +355 -0
- package/report/pretty.d.ts +14 -0
- package/report/pretty.js +324 -0
- package/report/sarif.d.ts +40 -0
- package/report/sarif.js +193 -0
- package/report/view.d.ts +82 -0
- package/report/view.js +132 -0
- package/rules/registry.d.ts +66 -0
- package/rules/registry.js +569 -0
- package/schema/safegres.schema.json +1119 -0
- package/score/score.d.ts +84 -0
- package/score/score.js +238 -0
- package/score/scorecards.d.ts +68 -0
- package/score/scorecards.js +162 -0
- package/types.d.ts +292 -0
- package/types.js +32 -0
- package/version.d.ts +8 -0
- package/version.js +12 -0
- package/LICENSE +0 -23
- package/admin.d.ts +0 -26
- package/admin.js +0 -182
- package/connect.d.ts +0 -19
- package/connect.js +0 -83
- package/dist/README.md +0 -557
- package/dist/package.json +0 -72
- package/esm/admin.js +0 -178
- package/esm/connect.js +0 -78
- package/esm/manager.js +0 -136
- package/esm/roles.js +0 -32
- package/esm/seed/adapters.js +0 -23
- package/esm/seed/csv.js +0 -44
- package/esm/seed/index.js +0 -16
- package/esm/seed/json.js +0 -18
- package/esm/seed/launchql.js +0 -19
- package/esm/seed/sqitch.js +0 -17
- package/esm/stream.js +0 -43
- package/esm/test-client.js +0 -150
- package/manager.d.ts +0 -25
- package/manager.js +0 -140
- package/roles.d.ts +0 -17
- package/roles.js +0 -38
- package/seed/adapters.d.ts +0 -4
- package/seed/adapters.js +0 -28
- package/seed/csv.d.ts +0 -9
- package/seed/csv.js +0 -49
- package/seed/index.d.ts +0 -16
- package/seed/index.js +0 -33
- package/seed/json.d.ts +0 -6
- package/seed/json.js +0 -21
- package/seed/launchql.d.ts +0 -2
- package/seed/launchql.js +0 -22
- package/seed/sqitch.d.ts +0 -2
- package/seed/sqitch.js +0 -20
- package/seed/types.d.ts +0 -13
- package/stream.d.ts +0 -2
- package/stream.js +0 -46
- package/test-client.d.ts +0 -49
- package/test-client.js +0 -154
- /package/{seed → config}/types.js +0 -0
- /package/esm/{seed → config}/types.js +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Structural index-hygiene checks (perf dimension, `X*`).
|
|
4
|
+
*
|
|
5
|
+
* Everything here is pure catalog analysis — deterministic, workload-free,
|
|
6
|
+
* and safe to run in CI against an empty database.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.checkUnindexedForeignKeys = checkUnindexedForeignKeys;
|
|
10
|
+
exports.checkRedundantIndexes = checkRedundantIndexes;
|
|
11
|
+
exports.checkMissingPrimaryKey = checkMissingPrimaryKey;
|
|
12
|
+
exports.checkUnindexedSearchColumns = checkUnindexedSearchColumns;
|
|
13
|
+
exports.checkUnindexedSortColumns = checkUnindexedSortColumns;
|
|
14
|
+
const paths_1 = require("../pg/paths");
|
|
15
|
+
/**
|
|
16
|
+
* X1: a foreign key with no index that can serve it.
|
|
17
|
+
*
|
|
18
|
+
* An index covers a FK when its *leading* columns are exactly the FK's
|
|
19
|
+
* columns (order among them is irrelevant for equality lookups) and it
|
|
20
|
+
* covers every row. Without one, `ON DELETE`/`ON UPDATE` checks on the
|
|
21
|
+
* referenced side and joins on the referencing side degrade to sequential
|
|
22
|
+
* scans.
|
|
23
|
+
*
|
|
24
|
+
* Partial and expression indexes do not count: the planner cannot rely on
|
|
25
|
+
* them for the referential-integrity lookup.
|
|
26
|
+
*
|
|
27
|
+
* `paths` supplies the access-path signals. They never remove a finding on
|
|
28
|
+
* their own — see {@link WriteOncePointerPolicy} and `classifyPaths`.
|
|
29
|
+
*/
|
|
30
|
+
function checkUnindexedForeignKeys(table, paths, onWriteOncePointer = 'report') {
|
|
31
|
+
// Partitions inherit their parent's indexes; the parent carries the finding.
|
|
32
|
+
if (table.isPartition)
|
|
33
|
+
return [];
|
|
34
|
+
const findings = [];
|
|
35
|
+
for (const fk of table.foreignKeys) {
|
|
36
|
+
if (fk.columns.length === 0)
|
|
37
|
+
continue;
|
|
38
|
+
if (table.indexes.some((idx) => indexCoversColumns(idx, fk.columns)))
|
|
39
|
+
continue;
|
|
40
|
+
const path = paths?.get((0, paths_1.pathKey)(table.schema, table.name, fk.name));
|
|
41
|
+
const writeOnceShaped = path?.assessment === 'write-once-shaped';
|
|
42
|
+
if (writeOnceShaped && onWriteOncePointer === 'suppress')
|
|
43
|
+
continue;
|
|
44
|
+
const cols = fk.columnNames.join(', ');
|
|
45
|
+
findings.push({
|
|
46
|
+
code: 'X1',
|
|
47
|
+
severity: 'medium',
|
|
48
|
+
category: 'index',
|
|
49
|
+
schema: table.schema,
|
|
50
|
+
table: table.name,
|
|
51
|
+
message: `Foreign key ${fk.name} on ${table.schema}.${table.name} (${cols}) has no covering index`,
|
|
52
|
+
hint: writeOnceShaped
|
|
53
|
+
? `CREATE INDEX ON ${table.schema}.${table.name} (${cols}) — or, if nothing queries this relation, stop exposing it. ${path.signals.map((s) => s.detail).join('; ')}.`
|
|
54
|
+
: `CREATE INDEX ON ${table.schema}.${table.name} (${cols}); without it, deletes/updates on ${fk.references} and joins across this key scan the whole table.`,
|
|
55
|
+
context: {
|
|
56
|
+
constraint: fk.name,
|
|
57
|
+
columns: fk.columnNames,
|
|
58
|
+
references: fk.references,
|
|
59
|
+
...(path ? { pathSignals: path.signals.map((s) => s.name), pathAssessment: path.assessment } : {})
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return findings;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* X5: an index that another index already serves.
|
|
67
|
+
*
|
|
68
|
+
* Two shapes: an exact duplicate, or a non-unique index whose columns are a
|
|
69
|
+
* leading-column prefix of a wider index. Both cost write throughput and
|
|
70
|
+
* disk for nothing. Constraint-backed indexes (PK/UNIQUE/EXCLUDE) are never
|
|
71
|
+
* reported — dropping them would drop the constraint.
|
|
72
|
+
*
|
|
73
|
+
* An index attached to a parent's partitioned index is never reported either:
|
|
74
|
+
* it can only be dropped with the parent's index, and the parent carries that
|
|
75
|
+
* finding once instead of every partition repeating it. An index created on
|
|
76
|
+
* the partition alone is the partition's own, and is reported there.
|
|
77
|
+
*/
|
|
78
|
+
function checkRedundantIndexes(table) {
|
|
79
|
+
const findings = [];
|
|
80
|
+
const candidates = new Set(table.indexes.filter((i) => !i.primary && !i.constraint && !i.partial && !i.expression && !i.unique && !i.attached));
|
|
81
|
+
for (const idx of candidates) {
|
|
82
|
+
const covering = table.indexes.find((other) => {
|
|
83
|
+
if (other.name === idx.name)
|
|
84
|
+
return false;
|
|
85
|
+
if (other.partial || other.expression)
|
|
86
|
+
return false;
|
|
87
|
+
if (other.method !== idx.method)
|
|
88
|
+
return false;
|
|
89
|
+
if (!isPrefix(idx.columns, other.columns))
|
|
90
|
+
return false;
|
|
91
|
+
// Exact duplicates: when both are droppable, report only one of the
|
|
92
|
+
// pair, deterministically.
|
|
93
|
+
if (idx.columns.length === other.columns.length
|
|
94
|
+
&& candidates.has(other)
|
|
95
|
+
&& idx.name < other.name)
|
|
96
|
+
return false;
|
|
97
|
+
return true;
|
|
98
|
+
});
|
|
99
|
+
if (!covering)
|
|
100
|
+
continue;
|
|
101
|
+
const duplicate = idx.columns.length === covering.columns.length;
|
|
102
|
+
findings.push({
|
|
103
|
+
code: 'X5',
|
|
104
|
+
severity: 'low',
|
|
105
|
+
category: 'index',
|
|
106
|
+
schema: table.schema,
|
|
107
|
+
table: table.name,
|
|
108
|
+
message: `Index ${idx.name} (${idx.columnNames.join(', ')}) is ${duplicate ? 'a duplicate of' : 'a leading-column prefix of'} `
|
|
109
|
+
+ `${covering.name} (${covering.columnNames.join(', ')})`,
|
|
110
|
+
hint: `DROP INDEX ${table.schema}.${idx.name}; every query it serves is served by ${covering.name}.`,
|
|
111
|
+
context: { index: idx.name, coveredBy: covering.name, duplicate }
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return findings;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* X6: no primary key and no usable replica identity.
|
|
118
|
+
*
|
|
119
|
+
* Rows cannot be addressed individually: updates and deletes have no cheap
|
|
120
|
+
* lookup path, logical replication and CDC cannot identify old rows, and
|
|
121
|
+
* realtime/change-feed features break.
|
|
122
|
+
*/
|
|
123
|
+
function checkMissingPrimaryKey(table) {
|
|
124
|
+
if (table.isPartition)
|
|
125
|
+
return null;
|
|
126
|
+
if (table.hasPrimaryKey)
|
|
127
|
+
return null;
|
|
128
|
+
// `f` (FULL) and `i` (index) still give logical decoding an identity.
|
|
129
|
+
if (table.replicaIdentity === 'f' || table.replicaIdentity === 'i')
|
|
130
|
+
return null;
|
|
131
|
+
return {
|
|
132
|
+
code: 'X6',
|
|
133
|
+
severity: 'low',
|
|
134
|
+
category: 'index',
|
|
135
|
+
schema: table.schema,
|
|
136
|
+
table: table.name,
|
|
137
|
+
message: `${table.schema}.${table.name} has no primary key`,
|
|
138
|
+
hint: 'Add a primary key, or set REPLICA IDENTITY FULL / USING INDEX, so rows can be addressed by updates, deletes, and logical replication.',
|
|
139
|
+
context: { replicaIdentity: table.replicaIdentity }
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Search column types that only perform with a specialised index, mapped to
|
|
144
|
+
* the access methods that can serve them.
|
|
145
|
+
*
|
|
146
|
+
* The column type *is* the declaration: `graphile-search` detects searchable
|
|
147
|
+
* columns by codec (tsvector → full-text filter, vector → similarity search),
|
|
148
|
+
* so a bare `tsvector` column is already an API promise. BM25 and pg_trgm are
|
|
149
|
+
* absent on purpose — they are discovered *from* their indexes, so a missing
|
|
150
|
+
* index means the feature was never exposed rather than exposed and slow.
|
|
151
|
+
*/
|
|
152
|
+
const SEARCH_TYPES = {
|
|
153
|
+
tsvector: {
|
|
154
|
+
methods: ['gin', 'gist'],
|
|
155
|
+
feature: 'full-text search',
|
|
156
|
+
example: 'USING gin (%s)'
|
|
157
|
+
},
|
|
158
|
+
vector: {
|
|
159
|
+
methods: ['hnsw', 'ivfflat'],
|
|
160
|
+
feature: 'vector similarity search',
|
|
161
|
+
example: 'USING hnsw (%s vector_cosine_ops)'
|
|
162
|
+
},
|
|
163
|
+
halfvec: {
|
|
164
|
+
methods: ['hnsw', 'ivfflat'],
|
|
165
|
+
feature: 'vector similarity search',
|
|
166
|
+
example: 'USING hnsw (%s halfvec_cosine_ops)'
|
|
167
|
+
},
|
|
168
|
+
sparsevec: {
|
|
169
|
+
methods: ['hnsw'],
|
|
170
|
+
feature: 'vector similarity search',
|
|
171
|
+
example: 'USING hnsw (%s sparsevec_cosine_ops)'
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* X7: a search column with no index the search can use.
|
|
176
|
+
*
|
|
177
|
+
* A `tsvector` or `vector` column is exposed as a search field by
|
|
178
|
+
* `graphile-search` purely from its type — the schema promises a fast path.
|
|
179
|
+
* Without a GIN/GiST (full-text) or HNSW/IVFFlat (vector) index the promise
|
|
180
|
+
* is served by a sequential scan plus a per-row distance or match computation,
|
|
181
|
+
* which is the single worst plan the API can produce.
|
|
182
|
+
*/
|
|
183
|
+
function checkUnindexedSearchColumns(table) {
|
|
184
|
+
if (table.isPartition)
|
|
185
|
+
return [];
|
|
186
|
+
const findings = [];
|
|
187
|
+
for (const column of table.columns) {
|
|
188
|
+
const spec = SEARCH_TYPES[column.baseType];
|
|
189
|
+
if (!spec)
|
|
190
|
+
continue;
|
|
191
|
+
const served = table.indexes.some((idx) => spec.methods.includes(idx.method) && idx.columns.includes(column.attnum));
|
|
192
|
+
if (served)
|
|
193
|
+
continue;
|
|
194
|
+
findings.push({
|
|
195
|
+
code: 'X7',
|
|
196
|
+
severity: 'medium',
|
|
197
|
+
category: 'index',
|
|
198
|
+
schema: table.schema,
|
|
199
|
+
table: table.name,
|
|
200
|
+
message: `${table.schema}.${table.name}.${column.name} is a ${column.type} column with no ${spec.methods.join('/')} index — ${spec.feature} scans the whole table`,
|
|
201
|
+
hint: `CREATE INDEX ON ${table.schema}.${table.name} ${spec.example.replace('%s', column.name)};`,
|
|
202
|
+
context: { column: column.name, type: column.type, methods: spec.methods }
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return findings;
|
|
206
|
+
}
|
|
207
|
+
/** Types whose columns are, in practice, what a connection is sorted by. */
|
|
208
|
+
const SORT_TYPES = new Set(['timestamptz', 'timestamp', 'date']);
|
|
209
|
+
/**
|
|
210
|
+
* X8: a sort-shaped column that leads no index.
|
|
211
|
+
*
|
|
212
|
+
* Every PostGraphile connection can be ordered by any column and paginated
|
|
213
|
+
* with a cursor over that order. Sorting on an unindexed column forces a full
|
|
214
|
+
* sort of the (RLS-filtered) result on every page, and keyset pagination
|
|
215
|
+
* degenerates into a scan-and-discard. Timestamp columns are singled out
|
|
216
|
+
* because they are what feeds are actually ordered by — a heuristic, hence
|
|
217
|
+
* `info` by default; turn it off with `perf.rules: { "X8": "off" }`.
|
|
218
|
+
*/
|
|
219
|
+
function checkUnindexedSortColumns(table) {
|
|
220
|
+
if (table.isPartition)
|
|
221
|
+
return [];
|
|
222
|
+
// A leading column can serve both ASC and DESC ordering; a trailing one
|
|
223
|
+
// cannot serve the sort on its own.
|
|
224
|
+
const leading = new Set(table.indexes.filter((i) => !i.partial).map((i) => i.columns[0]).filter((c) => c !== undefined));
|
|
225
|
+
const findings = [];
|
|
226
|
+
for (const column of table.columns) {
|
|
227
|
+
if (!SORT_TYPES.has(column.baseType))
|
|
228
|
+
continue;
|
|
229
|
+
if (leading.has(column.attnum))
|
|
230
|
+
continue;
|
|
231
|
+
findings.push({
|
|
232
|
+
code: 'X8',
|
|
233
|
+
severity: 'info',
|
|
234
|
+
category: 'index',
|
|
235
|
+
schema: table.schema,
|
|
236
|
+
table: table.name,
|
|
237
|
+
message: `${table.schema}.${table.name}.${column.name} (${column.type}) leads no index — ordering or paginating a connection by it sorts the whole table`,
|
|
238
|
+
hint: `If the API orders by this column, CREATE INDEX ON ${table.schema}.${table.name} (${column.name} DESC); otherwise disable X8 or add the table to perf.ignore.`,
|
|
239
|
+
context: { column: column.name, type: column.type }
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return findings;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* True when `index` can serve an equality lookup on exactly `columns`:
|
|
246
|
+
* its leading columns are that same set, and it covers every row.
|
|
247
|
+
*/
|
|
248
|
+
function indexCoversColumns(index, columns) {
|
|
249
|
+
if (index.partial || index.expression)
|
|
250
|
+
return false;
|
|
251
|
+
if (index.method !== 'btree')
|
|
252
|
+
return false;
|
|
253
|
+
if (index.columns.length < columns.length)
|
|
254
|
+
return false;
|
|
255
|
+
const leading = new Set(index.columns.slice(0, columns.length));
|
|
256
|
+
return columns.every((c) => leading.has(c));
|
|
257
|
+
}
|
|
258
|
+
/** True when `a` is a leading-column prefix of `b` (`a` may equal `b`). */
|
|
259
|
+
function isPrefix(a, b) {
|
|
260
|
+
if (a.length > b.length)
|
|
261
|
+
return false;
|
|
262
|
+
return a.every((col, i) => b[i] === col);
|
|
263
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The grant/RLS/policy lattice (L-series).
|
|
3
|
+
*
|
|
4
|
+
* The A/R rules reason from grants exactly as the catalog stores them, which
|
|
5
|
+
* misses two ways access actually arrives: `GRANT TO PUBLIC` (one ACL row
|
|
6
|
+
* that applies to every role) and role inheritance (zero ACL rows on the
|
|
7
|
+
* member). The lattice rules evaluate the *effective* cell each
|
|
8
|
+
* (relation, role, privilege) lands in:
|
|
9
|
+
*
|
|
10
|
+
* | grant | RLS | policy | verdict |
|
|
11
|
+
* |-------|-----|--------|-------------------------------------------|
|
|
12
|
+
* | yes | on | yes | normal — policy-mediated access |
|
|
13
|
+
* | yes | on | no | dead grant (L1, indirect; A4/A5, direct) |
|
|
14
|
+
* | yes | off | — | unmediated (L5 for untrusted, indirect) |
|
|
15
|
+
* | no | — | yes | dead policy (L2) |
|
|
16
|
+
*
|
|
17
|
+
* plus schema-USAGE composition: an object grant the grantee cannot reach
|
|
18
|
+
* because USAGE is missing (L3), and USAGE that reaches no object (L4).
|
|
19
|
+
*
|
|
20
|
+
* Every L rule ships signal-only: L1–L3 are fail-closed (zero score weight by
|
|
21
|
+
* default), L4/L5 default to `info`. Escalate via config once the findings
|
|
22
|
+
* prove themselves.
|
|
23
|
+
*/
|
|
24
|
+
import type { RoleAttributes, SchemaAclInfo } from '../pg/acl';
|
|
25
|
+
import type { GrantInfo, PgPrivilege, TableSnapshot } from '../pg/introspect';
|
|
26
|
+
import type { Finding } from '../types';
|
|
27
|
+
export type RoleGraph = Map<string, RoleAttributes>;
|
|
28
|
+
/** How an effective privilege arrived at a role. */
|
|
29
|
+
export type GrantVia = 'direct' | 'PUBLIC' | `member of ${string}`;
|
|
30
|
+
export interface EffectiveGrant {
|
|
31
|
+
privilege: PgPrivilege;
|
|
32
|
+
via: GrantVia;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Every privilege `role` effectively holds on `table`: direct grants, grants
|
|
36
|
+
* TO PUBLIC, and grants to roles it inherits from. Deduplicated per privilege
|
|
37
|
+
* with the most direct provenance winning (direct > PUBLIC > inherited).
|
|
38
|
+
*
|
|
39
|
+
* Takes anything carrying an ACL, not just a table: a view's own grants
|
|
40
|
+
* compose the same way, and the view-ownership reach edge asks this same
|
|
41
|
+
* question of them.
|
|
42
|
+
*/
|
|
43
|
+
export declare function effectiveGrants(table: Pick<TableSnapshot, 'grants'>, role: string, graph: RoleGraph): EffectiveGrant[];
|
|
44
|
+
/**
|
|
45
|
+
* How `role` comes to hold EXECUTE on a function, or `null` when it does not.
|
|
46
|
+
*
|
|
47
|
+
* A function ACL carries one privilege, so the closure {@link effectiveGrants}
|
|
48
|
+
* performs per privilege collapses to a single provenance — but it is the same
|
|
49
|
+
* closure: a grant to PUBLIC and a grant to a role the caller INHERITs from
|
|
50
|
+
* both confer EXECUTE without naming the caller anywhere.
|
|
51
|
+
*/
|
|
52
|
+
export declare function effectiveExecute(fn: {
|
|
53
|
+
grants: Array<{
|
|
54
|
+
role: string;
|
|
55
|
+
}>;
|
|
56
|
+
}, role: string, graph: RoleGraph): GrantVia | null;
|
|
57
|
+
/** A privilege `role` holds on some, but not all, of a relation's columns. */
|
|
58
|
+
export interface EffectiveColumnGrant extends EffectiveGrant {
|
|
59
|
+
/** The columns it covers, sorted. */
|
|
60
|
+
columns: string[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Every privilege `role` effectively holds on individual *columns* of `table`,
|
|
64
|
+
* composed exactly like {@link effectiveGrants} — direct, PUBLIC, inherited.
|
|
65
|
+
*
|
|
66
|
+
* A column grant lives in `pg_attribute.attacl` and never appears in the
|
|
67
|
+
* relation's ACL, so a role can hold nothing at all by `effectiveGrants` and
|
|
68
|
+
* still read the columns that matter. Privileges the role already holds on the
|
|
69
|
+
* whole relation are dropped: the column grant then adds no reach, and
|
|
70
|
+
* reporting it would double-count the same access.
|
|
71
|
+
*/
|
|
72
|
+
export declare function effectiveColumnGrants(table: Pick<TableSnapshot, 'grants' | 'columnGrants'>, role: string, graph: RoleGraph): EffectiveColumnGrant[];
|
|
73
|
+
/**
|
|
74
|
+
* L1: an *indirect* grant (TO PUBLIC, or inherited from a granted role) on an
|
|
75
|
+
* RLS-enabled table with no applicable permissive policy — a dead grant that
|
|
76
|
+
* A4/A5 cannot see because no direct ACL row names the role. Default-deny
|
|
77
|
+
* admits nothing today; the risk is the grant silently activating the day a
|
|
78
|
+
* broad policy is added.
|
|
79
|
+
*/
|
|
80
|
+
export declare function checkIndirectCoverageGaps(table: TableSnapshot, graph: RoleGraph): Finding[];
|
|
81
|
+
/**
|
|
82
|
+
* L2: a permissive policy names a role that holds no grant the policy could
|
|
83
|
+
* mediate — authorization written for a role that cannot reach the table.
|
|
84
|
+
* Either the grant was forgotten (a broken feature) or the policy is stale.
|
|
85
|
+
*/
|
|
86
|
+
export declare function checkDeadPolicies(table: TableSnapshot, graph: RoleGraph): Finding[];
|
|
87
|
+
/**
|
|
88
|
+
* L3: an object grant the grantee cannot use because it lacks USAGE on the
|
|
89
|
+
* schema — direct, via PUBLIC, or inherited. The grant is unreachable today
|
|
90
|
+
* and activates silently the day USAGE is granted.
|
|
91
|
+
*/
|
|
92
|
+
export declare function checkUnreachableGrants(table: TableSnapshot, schemaAcls: Map<string, SchemaAclInfo>, graph: RoleGraph): Finding[];
|
|
93
|
+
/**
|
|
94
|
+
* L4: schema USAGE that opens the door to nothing — the role reaches no
|
|
95
|
+
* relation (directly, via PUBLIC, or by inheritance) and can EXECUTE no
|
|
96
|
+
* function in the schema. Advisory only: sequences, types and future objects
|
|
97
|
+
* are not modeled, so this is a review candidate, not a proof.
|
|
98
|
+
*
|
|
99
|
+
* `views` matters more than its optionality suggests: a role whose only
|
|
100
|
+
* reachable object in the schema is a view holds USAGE that is load-bearing,
|
|
101
|
+
* and L8 exists precisely because that view can be the role's entire read
|
|
102
|
+
* path. Called without them, L4 sees a table-only schema and reports a grant
|
|
103
|
+
* the API depends on as revokable — so pass them wherever they are available.
|
|
104
|
+
*/
|
|
105
|
+
export declare function checkDeadSchemaUsage(schemaAcls: SchemaAclInfo[], tables: TableSnapshot[], graph: RoleGraph, views?: Array<{
|
|
106
|
+
schema: string;
|
|
107
|
+
grants: GrantInfo[];
|
|
108
|
+
}>): Finding[];
|
|
109
|
+
export interface LatticeRoleOptions {
|
|
110
|
+
/** Role names considered untrusted (exact match). */
|
|
111
|
+
roles?: string[];
|
|
112
|
+
/** Take the untrusted roles from the resolved exposure surface. See `RoleTrustOptions`. */
|
|
113
|
+
rolesFrom?: 'exposure' | 'anon';
|
|
114
|
+
}
|
|
115
|
+
export interface UnaddressableGrantOptions {
|
|
116
|
+
/** API-edge roles: the roles requests actually arrive as. */
|
|
117
|
+
roles: string[];
|
|
118
|
+
/**
|
|
119
|
+
* Relations named by some RLS policy predicate anywhere in the database.
|
|
120
|
+
*
|
|
121
|
+
* The reason this parameter exists: a grant on a relation the API cannot
|
|
122
|
+
* address is *not* automatically dead, because a policy on a different
|
|
123
|
+
* relation may subquery it under the querying role. Revoking that grant
|
|
124
|
+
* breaks authorization at runtime, silently, everywhere. A relation any
|
|
125
|
+
* predicate mentions is therefore never reported, whatever the API says.
|
|
126
|
+
*/
|
|
127
|
+
policyReferenced: Set<string>;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* L6: an API-edge role holds privileges on a relation its own API cannot
|
|
131
|
+
* address.
|
|
132
|
+
*
|
|
133
|
+
* The composition the two halves of safegres were built for: the lattice knows
|
|
134
|
+
* what a role effectively holds, exposure reach knows what the generated API
|
|
135
|
+
* can name, and the difference is grant surface that exists for nothing. It is
|
|
136
|
+
* not a leak — the role would have to connect directly to use it — which is
|
|
137
|
+
* exactly why it is the kind of finding that survives for years.
|
|
138
|
+
*/
|
|
139
|
+
export declare function checkUnaddressableGrant(table: TableSnapshot, graph: RoleGraph, unaddressable: Set<string>, options: UnaddressableGrantOptions): Finding[];
|
|
140
|
+
/**
|
|
141
|
+
* L5: an untrusted role reaches an RLS-disabled table *indirectly* — through
|
|
142
|
+
* a grant TO PUBLIC or by inheriting from a granted role. Direct grants are
|
|
143
|
+
* A2/R1 territory; the indirect paths are exactly the ones that slip past
|
|
144
|
+
* exact-name matching (e.g. a world-readable table reached by `anonymous`
|
|
145
|
+
* through PUBLIC).
|
|
146
|
+
*/
|
|
147
|
+
export declare function checkUntrustedIndirectAccess(table: TableSnapshot, graph: RoleGraph, options?: LatticeRoleOptions): Finding[];
|
|
148
|
+
/** One role's effective reach into one relation. */
|
|
149
|
+
export interface RoleAccessRelation {
|
|
150
|
+
schema: string;
|
|
151
|
+
table: string;
|
|
152
|
+
privileges: PgPrivilege[];
|
|
153
|
+
/** Most direct provenance across the privileges. */
|
|
154
|
+
via: GrantVia;
|
|
155
|
+
/**
|
|
156
|
+
* Present when the reach is column-scoped: the columns reachable, and the
|
|
157
|
+
* privileges above are the column-level ones. Absent for whole-relation
|
|
158
|
+
* access.
|
|
159
|
+
*/
|
|
160
|
+
columns?: string[];
|
|
161
|
+
/** `unmediated` = RLS off or bypassed; `mediated` = at least one applicable policy; `dead` = RLS default-deny. */
|
|
162
|
+
access: 'unmediated' | 'mediated' | 'dead';
|
|
163
|
+
}
|
|
164
|
+
export interface RoleAccessEntry {
|
|
165
|
+
role: string;
|
|
166
|
+
/** Relations where at least one privilege is usable (mediated or unmediated). */
|
|
167
|
+
accessibleTables: number;
|
|
168
|
+
/** Relations readable/writable with no RLS mediation at all. */
|
|
169
|
+
unmediated: RoleAccessRelation[];
|
|
170
|
+
/** Relations reachable only through policies. */
|
|
171
|
+
mediated: number;
|
|
172
|
+
/** Relations where every effective privilege is dead (RLS default-deny). */
|
|
173
|
+
dead: number;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The direct answer to "what can role X see?": every relation the role
|
|
177
|
+
* effectively reaches, classified by whether RLS mediates the access.
|
|
178
|
+
*/
|
|
179
|
+
export declare function computeRoleAccess(tables: TableSnapshot[], graph: RoleGraph, roles: string[]): RoleAccessEntry[];
|