@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,392 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* L8: a view that is not `security_invoker` hands its readers the owner's
|
|
4
|
+
* privileges on every relation its body touches.
|
|
5
|
+
*
|
|
6
|
+
* A view executes as its owner unless it was created `WITH (security_invoker
|
|
7
|
+
* = true)`. So a role holding nothing but SELECT on the view reads the base
|
|
8
|
+
* relations under the *owner's* rights — including tables it has no grant on,
|
|
9
|
+
* and, when the owner owns the base table or bypasses RLS, without the row
|
|
10
|
+
* filter the base table's policies would have applied.
|
|
11
|
+
*
|
|
12
|
+
* Nothing in the catalog-only effective-access layer can see this:
|
|
13
|
+
* `effectiveGrants` never names the calling role on the base relation, and
|
|
14
|
+
* the base relation's ACL never names it either. The edge only exists in the
|
|
15
|
+
* view's *body*, which makes this the first rule that reads SQL rather than
|
|
16
|
+
* pure catalog — the `proof: 'ast'` edge of the reach model in `role-reach.ts`.
|
|
17
|
+
*
|
|
18
|
+
* Two conservatism rules the rest of the analysis depends on:
|
|
19
|
+
*
|
|
20
|
+
* - **A body we cannot read is unknown, not empty.** Dynamic SQL or an
|
|
21
|
+
* unparseable body suppresses the view entirely rather than producing a
|
|
22
|
+
* partial (and therefore misleading) base-relation set.
|
|
23
|
+
* - **The fix is never a revoke.** The grant on the view is what the API
|
|
24
|
+
* serves; the defect is the view executing as its owner. L8 recommends
|
|
25
|
+
* `security_invoker = true` or a different owner, and nothing else.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.analyzeViewBodies = analyzeViewBodies;
|
|
29
|
+
exports.readBodies = readBodies;
|
|
30
|
+
exports.resolveViewBases = resolveViewBases;
|
|
31
|
+
exports.buildRelationIndex = buildRelationIndex;
|
|
32
|
+
exports.resolveRelation = resolveRelation;
|
|
33
|
+
exports.checkDefinerViewBypass = checkDefinerViewBypass;
|
|
34
|
+
exports.checkUnauditedViewReach = checkUnauditedViewReach;
|
|
35
|
+
exports.checkUnreadableViewReach = checkUnreadableViewReach;
|
|
36
|
+
const extract_1 = require("../callgraph/extract");
|
|
37
|
+
const lattice_1 = require("./lattice");
|
|
38
|
+
const role_reach_1 = require("./role-reach");
|
|
39
|
+
/** How deep a chain of views on views is followed before giving up. */
|
|
40
|
+
const MAX_VIEW_DEPTH = 8;
|
|
41
|
+
/**
|
|
42
|
+
* Read every definer view's body and resolve the base relations it reaches.
|
|
43
|
+
*
|
|
44
|
+
* Nested views are followed: a definer view over an invoker view still
|
|
45
|
+
* executes the inner body as the *outer* owner, while an inner definer view
|
|
46
|
+
* switches the executing role again. Each resolved relation therefore carries
|
|
47
|
+
* the hops it passed through and the owner of the last one.
|
|
48
|
+
*/
|
|
49
|
+
async function analyzeViewBodies(views, tables, auditedSchemas) {
|
|
50
|
+
// A materialized view stores its rows: reading it touches no base relation,
|
|
51
|
+
// so it is a leaf here, never an edge.
|
|
52
|
+
const queryable = views.filter((v) => !v.materialized);
|
|
53
|
+
const index = buildRelationIndex(queryable, tables, auditedSchemas);
|
|
54
|
+
const bodies = await readBodies(queryable);
|
|
55
|
+
const out = [];
|
|
56
|
+
const suppressed = [];
|
|
57
|
+
for (const view of queryable) {
|
|
58
|
+
if (view.securityInvoker)
|
|
59
|
+
continue;
|
|
60
|
+
const { bases, opaque, tainted } = resolveViewBases(view, index, bodies);
|
|
61
|
+
// An unreadable body is unknown, not empty: its fragmentary relation list
|
|
62
|
+
// is discarded, but the view stays in the model so the gap is reportable
|
|
63
|
+
// rather than a silent clean bill (L15).
|
|
64
|
+
if (opaque)
|
|
65
|
+
suppressed.push({ view: `${view.schema}.${view.name}`, reason: opaque });
|
|
66
|
+
const unreadable = opaque ?? tainted;
|
|
67
|
+
if (!unreadable && bases.length === 0)
|
|
68
|
+
continue;
|
|
69
|
+
out.push({
|
|
70
|
+
schema: view.schema,
|
|
71
|
+
name: view.name,
|
|
72
|
+
owner: view.owner,
|
|
73
|
+
grants: view.grants,
|
|
74
|
+
baseRelations: opaque ? [] : bases,
|
|
75
|
+
...(unreadable ? { unreadable } : {})
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return { views: out, suppressed };
|
|
79
|
+
}
|
|
80
|
+
async function readBodies(views) {
|
|
81
|
+
const bodies = new Map();
|
|
82
|
+
for (const v of views)
|
|
83
|
+
bodies.set(`${v.schema}.${v.name}`, await (0, extract_1.extractQuery)(v.definition));
|
|
84
|
+
return bodies;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The base relations `root` reads, following nested views, with the owner in
|
|
88
|
+
* force at each hop.
|
|
89
|
+
*
|
|
90
|
+
* `opaque` is set to the first reason the walk had to stop, and when it is set
|
|
91
|
+
* the relation list is a fragment: the caller must discard it, because a body
|
|
92
|
+
* we could only partly read under-reports what the view reaches.
|
|
93
|
+
*
|
|
94
|
+
* `root` itself need not be in `index` — a materialized view is not a
|
|
95
|
+
* queryable relation for the purposes of resolving *other* bodies, but its own
|
|
96
|
+
* body reads the same way.
|
|
97
|
+
*/
|
|
98
|
+
function resolveViewBases(root, index, bodies) {
|
|
99
|
+
const bases = [];
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
let opaque;
|
|
102
|
+
let tainted;
|
|
103
|
+
const walk = (current, hops) => {
|
|
104
|
+
const key = `${current.schema}.${current.name}`;
|
|
105
|
+
if (hops.length > MAX_VIEW_DEPTH) {
|
|
106
|
+
opaque ??= `view chain deeper than ${MAX_VIEW_DEPTH} hops`;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const body = bodies.get(key);
|
|
110
|
+
if (!body)
|
|
111
|
+
return;
|
|
112
|
+
if (body.opaque) {
|
|
113
|
+
opaque ??= body.opaqueReason ?? 'body could not be read';
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Readable, but it runs SQL of its own: the relations below are real and
|
|
117
|
+
// the list of them is a lower bound.
|
|
118
|
+
if (body.tainted)
|
|
119
|
+
tainted ??= `${key}: ${body.tainted}`;
|
|
120
|
+
for (const ref of body.tables) {
|
|
121
|
+
const relation = resolveRelation(ref, current.schema, index);
|
|
122
|
+
if (!relation)
|
|
123
|
+
continue; // a CTE, an alias, or a name we cannot pin down
|
|
124
|
+
if (relation.kind === 'view') {
|
|
125
|
+
const nested = relation.view;
|
|
126
|
+
if (`${nested.schema}.${nested.name}` === key)
|
|
127
|
+
continue;
|
|
128
|
+
// An inner definer view re-owns the read; an inner invoker view runs
|
|
129
|
+
// under whichever owner is already in force.
|
|
130
|
+
const owner = nested.securityInvoker ? hops[hops.length - 1].owner : nested.owner;
|
|
131
|
+
walk(nested, [...hops, { view: `${nested.schema}.${nested.name}`, owner }]);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const relKey = `${relation.schema}.${relation.name}`;
|
|
135
|
+
const dedupe = `${relKey}::${hops[hops.length - 1].owner}`;
|
|
136
|
+
if (seen.has(dedupe))
|
|
137
|
+
continue;
|
|
138
|
+
seen.add(dedupe);
|
|
139
|
+
// Which columns escape is the catalog's answer, not the body's: the
|
|
140
|
+
// rewriter recorded one dependency row per column the view reads.
|
|
141
|
+
const columns = current.columnDeps?.find((d) => d.schema === relation.schema && d.table === relation.name)?.columns;
|
|
142
|
+
bases.push({
|
|
143
|
+
schema: relation.schema,
|
|
144
|
+
table: relation.name,
|
|
145
|
+
hops: [...hops],
|
|
146
|
+
...(relation.kind === 'external' ? { external: true } : {}),
|
|
147
|
+
...(columns ? { columns } : {})
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
walk(root, [{ view: `${root.schema}.${root.name}`, owner: root.owner }]);
|
|
152
|
+
return { bases, ...(opaque ? { opaque } : {}), ...(tainted ? { tainted } : {}) };
|
|
153
|
+
}
|
|
154
|
+
function buildRelationIndex(views, tables, auditedSchemas) {
|
|
155
|
+
const viewsByName = new Map();
|
|
156
|
+
for (const v of views)
|
|
157
|
+
viewsByName.set(v.name, [...(viewsByName.get(v.name) ?? []), v]);
|
|
158
|
+
return {
|
|
159
|
+
tableKeys: new Set(tables.map((t) => `${t.schema}.${t.name}`)),
|
|
160
|
+
viewKeys: new Map(views.map((v) => [`${v.schema}.${v.name}`, v])),
|
|
161
|
+
viewsByName,
|
|
162
|
+
auditedSchemas: new Set(auditedSchemas ?? [...tables.map((t) => t.schema), ...views.map((v) => v.schema)])
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Pin a body reference to a relation in the snapshot.
|
|
167
|
+
*
|
|
168
|
+
* `pg_get_viewdef` qualifies anything outside the creating `search_path` but
|
|
169
|
+
* leaves the rest bare, so an unqualified name is resolved against the view's
|
|
170
|
+
* own schema first and then against the snapshot as a whole — and only when
|
|
171
|
+
* exactly one relation answers to it. An ambiguous or unknown name (a CTE, a
|
|
172
|
+
* table alias, a relation outside the audited schemas) resolves to nothing:
|
|
173
|
+
* over-approximating here would attribute a read to a relation nobody named.
|
|
174
|
+
*/
|
|
175
|
+
function resolveRelation(ref, viewSchema, { tableKeys, viewKeys, viewsByName, auditedSchemas }) {
|
|
176
|
+
const candidates = ref.schema ? [ref.schema] : [viewSchema];
|
|
177
|
+
for (const schema of candidates) {
|
|
178
|
+
const key = `${schema}.${ref.name}`;
|
|
179
|
+
const view = viewKeys.get(key);
|
|
180
|
+
if (view)
|
|
181
|
+
return { kind: 'view', view };
|
|
182
|
+
if (tableKeys.has(key))
|
|
183
|
+
return { kind: 'table', schema, name: ref.name };
|
|
184
|
+
}
|
|
185
|
+
// Qualified and unknown: either a relation the audit skipped (external, and
|
|
186
|
+
// the reach is still real) or a genuine miss inside a schema it did read.
|
|
187
|
+
if (ref.schema) {
|
|
188
|
+
return auditedSchemas.has(ref.schema)
|
|
189
|
+
? null
|
|
190
|
+
: { kind: 'external', schema: ref.schema, name: ref.name };
|
|
191
|
+
}
|
|
192
|
+
const matches = [
|
|
193
|
+
...[...tableKeys]
|
|
194
|
+
.filter((k) => k.endsWith(`.${ref.name}`))
|
|
195
|
+
.map((k) => ({ kind: 'table', schema: k.slice(0, -(ref.name.length + 1)), name: ref.name })),
|
|
196
|
+
...(viewsByName.get(ref.name) ?? []).map((v) => ({ kind: 'view', view: v }))
|
|
197
|
+
];
|
|
198
|
+
return matches.length === 1 ? matches[0] : null;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* L8: an untrusted role reads a base relation through a definer view.
|
|
202
|
+
*
|
|
203
|
+
* Fires once per (role, view, base relation) where the role can SELECT the
|
|
204
|
+
* view, the view executes as someone else, and the role holds no SELECT of
|
|
205
|
+
* its own on the relation the view reads. A `security_invoker` view over the
|
|
206
|
+
* same shape produces nothing — it executes as the caller, so the base
|
|
207
|
+
* relation's own ACL and policies apply and there is no edge to report.
|
|
208
|
+
*/
|
|
209
|
+
function checkDefinerViewBypass(views, tables, graph, options = {}) {
|
|
210
|
+
const untrusted = options.roles ?? [];
|
|
211
|
+
if (untrusted.length === 0 || views.length === 0)
|
|
212
|
+
return [];
|
|
213
|
+
const byKey = new Map(tables.map((t) => [`${t.schema}.${t.name}`, t]));
|
|
214
|
+
const out = [];
|
|
215
|
+
for (const { role, cells } of (0, role_reach_1.computeViewReach)(views, graph, untrusted)) {
|
|
216
|
+
for (const cell of cells) {
|
|
217
|
+
if (cell.effectiveRole === role)
|
|
218
|
+
continue;
|
|
219
|
+
// A tainted cell names the view, not a base relation: there is nothing
|
|
220
|
+
// here to grade, and L15 reports it instead.
|
|
221
|
+
if (cell.proof === 'opaque-tainted')
|
|
222
|
+
continue;
|
|
223
|
+
const base = byKey.get(`${cell.schema}.${cell.table}`);
|
|
224
|
+
if (!base)
|
|
225
|
+
continue;
|
|
226
|
+
// Already reachable in its own right: the view launders nothing.
|
|
227
|
+
if ((0, lattice_1.effectiveGrants)(base, role, graph).some((g) => g.privilege === 'SELECT'))
|
|
228
|
+
continue;
|
|
229
|
+
// Nor does it when the columns that escape are ones the role could
|
|
230
|
+
// already read column-by-column (L13's closure) and the base has no RLS
|
|
231
|
+
// for the owner's read to skip. With RLS on, the projection matching is
|
|
232
|
+
// beside the point: the owner sees rows the caller's policies hide.
|
|
233
|
+
const granted = (0, lattice_1.effectiveColumnGrants)(base, role, graph)
|
|
234
|
+
.find((g) => g.privilege === 'SELECT');
|
|
235
|
+
if (cell.columns && granted && !base.rlsEnabled
|
|
236
|
+
&& cell.columns.every((c) => granted.columns.includes(c)))
|
|
237
|
+
continue;
|
|
238
|
+
const hops = cell.path.filter((e) => e.kind === 'view');
|
|
239
|
+
const outermost = hops[0];
|
|
240
|
+
const owner = cell.effectiveRole;
|
|
241
|
+
const ownerAttrs = graph.get(owner);
|
|
242
|
+
// The owner is exempt from the base table's policies — either because
|
|
243
|
+
// it owns the table and FORCE is off, or because it bypasses RLS
|
|
244
|
+
// outright — so the rows the policies would have filtered come back.
|
|
245
|
+
const rlsBypassed = base.rlsEnabled
|
|
246
|
+
&& (!!ownerAttrs?.bypassRls || (base.owner === owner && !base.rlsForced));
|
|
247
|
+
out.push({
|
|
248
|
+
code: 'L8',
|
|
249
|
+
severity: 'info',
|
|
250
|
+
category: 'anti-pattern',
|
|
251
|
+
schema: base.schema,
|
|
252
|
+
table: base.name,
|
|
253
|
+
role,
|
|
254
|
+
privilege: 'SELECT',
|
|
255
|
+
message: `Untrusted role ${role} reads ${base.schema}.${base.name}`
|
|
256
|
+
+ (cell.columns ? ` (${cell.columns.join(', ')})` : '')
|
|
257
|
+
+ ` through view ${outermost.view}, `
|
|
258
|
+
+ `which executes as its owner ${owner} — ${role} holds no SELECT on the base relation`
|
|
259
|
+
+ (rlsBypassed ? `, and ${owner} is not subject to its RLS policies` : ''),
|
|
260
|
+
hint: `The view is not \`security_invoker\`, so Postgres checks ${owner}'s privileges on the base `
|
|
261
|
+
+ `relation, not ${role}'s. Recreate it \`WITH (security_invoker = true)\` so the caller's `
|
|
262
|
+
+ `own grants and policies apply, or give it an owner whose reach matches what the view is `
|
|
263
|
+
+ `meant to expose. Do not revoke the SELECT on the view — that grant is what the API serves.`,
|
|
264
|
+
context: {
|
|
265
|
+
view: outermost.view,
|
|
266
|
+
effectiveRole: owner,
|
|
267
|
+
viaViews: hops.map((h) => h.view),
|
|
268
|
+
baseRlsEnabled: base.rlsEnabled,
|
|
269
|
+
rlsBypassed,
|
|
270
|
+
...(cell.columns ? { columns: cell.columns } : {}),
|
|
271
|
+
...(granted ? { grantedColumns: granted.columns } : {}),
|
|
272
|
+
proof: cell.proof
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* L14: a definer view hands an untrusted role a relation the audit never
|
|
281
|
+
* looked at.
|
|
282
|
+
*
|
|
283
|
+
* Excluding a schema is a statement about what is *graded*, not about what is
|
|
284
|
+
* *reachable*. A view in an audited schema can read `information_schema`,
|
|
285
|
+
* `pg_catalog`, an extension's tables, or any schema left out of
|
|
286
|
+
* `exposure.schemas` — and because that relation is not in the snapshot,
|
|
287
|
+
* every rule that grades a base relation silently drops the edge. The view
|
|
288
|
+
* looked clean because the audit could not see the far end.
|
|
289
|
+
*
|
|
290
|
+
* This is the one L-rule that reports an *absence of knowledge*. It says the
|
|
291
|
+
* read happens and that nothing was graded on the other side; it does not
|
|
292
|
+
* claim the relation is sensitive, and it recommends no revoke — the remedy
|
|
293
|
+
* is to bring the schema into scope, or to satisfy yourself that the
|
|
294
|
+
* projection is safe and leave it.
|
|
295
|
+
*/
|
|
296
|
+
function checkUnauditedViewReach(views, graph, options = {}) {
|
|
297
|
+
const untrusted = options.roles ?? [];
|
|
298
|
+
if (untrusted.length === 0 || views.length === 0)
|
|
299
|
+
return [];
|
|
300
|
+
const out = [];
|
|
301
|
+
for (const { role, cells } of (0, role_reach_1.computeViewReach)(views, graph, untrusted)) {
|
|
302
|
+
for (const cell of cells) {
|
|
303
|
+
if (!cell.external || cell.proof === 'opaque-tainted')
|
|
304
|
+
continue;
|
|
305
|
+
const hops = cell.path.filter((e) => e.kind === 'view');
|
|
306
|
+
const outermost = hops[0];
|
|
307
|
+
if (!outermost)
|
|
308
|
+
continue;
|
|
309
|
+
const owner = cell.effectiveRole;
|
|
310
|
+
out.push({
|
|
311
|
+
code: 'L14',
|
|
312
|
+
severity: 'info',
|
|
313
|
+
category: 'coverage',
|
|
314
|
+
schema: cell.schema,
|
|
315
|
+
table: cell.table,
|
|
316
|
+
role,
|
|
317
|
+
privilege: 'SELECT',
|
|
318
|
+
message: `Untrusted role ${role} reads ${cell.schema}.${cell.table} through view ${outermost.view} `
|
|
319
|
+
+ `as its owner ${owner}, and ${cell.schema} is outside the audited schemas — nothing `
|
|
320
|
+
+ 'graded what that relation exposes',
|
|
321
|
+
hint: `The read is proven from the view body; its consequences are not, because ${cell.schema} `
|
|
322
|
+
+ 'was never introspected. Add the schema to the audit (`schemas`, or remove it from '
|
|
323
|
+
+ '`excludeSchemas`) to grade the far end, or confirm the projection is safe to serve. '
|
|
324
|
+
+ 'This is a gap in coverage, not a proven leak: do not revoke anything on its strength.',
|
|
325
|
+
context: {
|
|
326
|
+
view: outermost.view,
|
|
327
|
+
effectiveRole: owner,
|
|
328
|
+
viaViews: hops.map((h) => h.view),
|
|
329
|
+
unauditedSchema: cell.schema,
|
|
330
|
+
proof: cell.proof
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* **L15 — an untrusted role reads a view whose body this analysis cannot
|
|
339
|
+
* read.** The coverage sibling of L14: there, the far end was out of scope;
|
|
340
|
+
* here, the body itself is.
|
|
341
|
+
*
|
|
342
|
+
* Two shapes reach this rule, and the distinction is the point of the
|
|
343
|
+
* `tainted` bit in {@link ExtractedBody}. A body that could not be read at all
|
|
344
|
+
* — an unparseable definition, a chain deeper than the hop limit — yields no
|
|
345
|
+
* relations, and every rule that grades a base relation had nothing to grade.
|
|
346
|
+
* A body that *was* read but calls something executing SQL of its own
|
|
347
|
+
* (`dblink`, `query_to_xml`) yields relations that are all real and not all of
|
|
348
|
+
* them: the graded ones still grade, and the gap is what is reported here.
|
|
349
|
+
*
|
|
350
|
+
* Either way the view executes as its owner and an untrusted role can read it,
|
|
351
|
+
* so the reach is proven and only its far end is unknown — which is exactly
|
|
352
|
+
* `proof: 'opaque-tainted'`, the state the reach model has carried since it
|
|
353
|
+
* was written and nothing could produce until now.
|
|
354
|
+
*
|
|
355
|
+
* Like L14 it is `info`/`neutral` and recommends no revoke: an unknown is not
|
|
356
|
+
* a leak, and scoring one would let an unreadable body move the number.
|
|
357
|
+
*/
|
|
358
|
+
function checkUnreadableViewReach(views, graph, options = {}) {
|
|
359
|
+
const untrusted = options.roles ?? [];
|
|
360
|
+
if (untrusted.length === 0 || views.length === 0)
|
|
361
|
+
return [];
|
|
362
|
+
const out = [];
|
|
363
|
+
for (const { role, cells } of (0, role_reach_1.computeViewReach)(views, graph, untrusted)) {
|
|
364
|
+
for (const cell of cells) {
|
|
365
|
+
if (cell.proof !== 'opaque-tainted')
|
|
366
|
+
continue;
|
|
367
|
+
out.push({
|
|
368
|
+
code: 'L15',
|
|
369
|
+
severity: 'info',
|
|
370
|
+
category: 'coverage',
|
|
371
|
+
schema: cell.schema,
|
|
372
|
+
table: cell.table,
|
|
373
|
+
role,
|
|
374
|
+
privilege: 'SELECT',
|
|
375
|
+
message: `Untrusted role ${role} reads view ${cell.schema}.${cell.table}, which executes as its `
|
|
376
|
+
+ `owner ${cell.effectiveRole} and whose body this analysis could not fully read `
|
|
377
|
+
+ `(${cell.taint}) — what it reaches under that owner was never graded`,
|
|
378
|
+
hint: 'The read is proven; what it reaches is not, so no view rule graded this path. Rewrite '
|
|
379
|
+
+ 'the body so the relations it reads are visible statically, or satisfy yourself that '
|
|
380
|
+
+ 'what it returns is safe to serve this role. Reporting stayed silent rather than '
|
|
381
|
+
+ 'guessing, and nothing here justifies a revoke.',
|
|
382
|
+
context: {
|
|
383
|
+
view: `${cell.schema}.${cell.table}`,
|
|
384
|
+
effectiveRole: cell.effectiveRole,
|
|
385
|
+
taint: cell.taint,
|
|
386
|
+
proof: cell.proof
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural index-hygiene checks (perf dimension, `X*`).
|
|
3
|
+
*
|
|
4
|
+
* Everything here is pure catalog analysis — deterministic, workload-free,
|
|
5
|
+
* and safe to run in CI against an empty database.
|
|
6
|
+
*/
|
|
7
|
+
import type { TableIndexSnapshot } from '../pg/indexes';
|
|
8
|
+
import { type AccessPath } from '../pg/paths';
|
|
9
|
+
import type { Finding } from '../types';
|
|
10
|
+
/**
|
|
11
|
+
* What X1 does with a foreign key whose only evidence is shape — it looks like
|
|
12
|
+
* a write-once provisioning pointer, but nothing has proven the path is
|
|
13
|
+
* unreachable.
|
|
14
|
+
*
|
|
15
|
+
* `report` is the default and the only honest one until a signal exists that
|
|
16
|
+
* can observe the generated API surface: the finding stands, and the shape is
|
|
17
|
+
* attached to it as context so a reviewer can act on it. The other two exist
|
|
18
|
+
* so the decision is a line of config rather than a fork of the scanner.
|
|
19
|
+
*
|
|
20
|
+
* `demote` is applied in `audit()` rather than here, because severities are
|
|
21
|
+
* restamped from the rule registry after the checks run.
|
|
22
|
+
*/
|
|
23
|
+
export type WriteOncePointerPolicy = 'report' | 'demote' | 'suppress';
|
|
24
|
+
/**
|
|
25
|
+
* X1: a foreign key with no index that can serve it.
|
|
26
|
+
*
|
|
27
|
+
* An index covers a FK when its *leading* columns are exactly the FK's
|
|
28
|
+
* columns (order among them is irrelevant for equality lookups) and it
|
|
29
|
+
* covers every row. Without one, `ON DELETE`/`ON UPDATE` checks on the
|
|
30
|
+
* referenced side and joins on the referencing side degrade to sequential
|
|
31
|
+
* scans.
|
|
32
|
+
*
|
|
33
|
+
* Partial and expression indexes do not count: the planner cannot rely on
|
|
34
|
+
* them for the referential-integrity lookup.
|
|
35
|
+
*
|
|
36
|
+
* `paths` supplies the access-path signals. They never remove a finding on
|
|
37
|
+
* their own — see {@link WriteOncePointerPolicy} and `classifyPaths`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function checkUnindexedForeignKeys(table: TableIndexSnapshot, paths?: Map<string, AccessPath>, onWriteOncePointer?: WriteOncePointerPolicy): Finding[];
|
|
40
|
+
/**
|
|
41
|
+
* X5: an index that another index already serves.
|
|
42
|
+
*
|
|
43
|
+
* Two shapes: an exact duplicate, or a non-unique index whose columns are a
|
|
44
|
+
* leading-column prefix of a wider index. Both cost write throughput and
|
|
45
|
+
* disk for nothing. Constraint-backed indexes (PK/UNIQUE/EXCLUDE) are never
|
|
46
|
+
* reported — dropping them would drop the constraint.
|
|
47
|
+
*
|
|
48
|
+
* An index attached to a parent's partitioned index is never reported either:
|
|
49
|
+
* it can only be dropped with the parent's index, and the parent carries that
|
|
50
|
+
* finding once instead of every partition repeating it. An index created on
|
|
51
|
+
* the partition alone is the partition's own, and is reported there.
|
|
52
|
+
*/
|
|
53
|
+
export declare function checkRedundantIndexes(table: TableIndexSnapshot): Finding[];
|
|
54
|
+
/**
|
|
55
|
+
* X6: no primary key and no usable replica identity.
|
|
56
|
+
*
|
|
57
|
+
* Rows cannot be addressed individually: updates and deletes have no cheap
|
|
58
|
+
* lookup path, logical replication and CDC cannot identify old rows, and
|
|
59
|
+
* realtime/change-feed features break.
|
|
60
|
+
*/
|
|
61
|
+
export declare function checkMissingPrimaryKey(table: TableIndexSnapshot): Finding | null;
|
|
62
|
+
/**
|
|
63
|
+
* X7: a search column with no index the search can use.
|
|
64
|
+
*
|
|
65
|
+
* A `tsvector` or `vector` column is exposed as a search field by
|
|
66
|
+
* `graphile-search` purely from its type — the schema promises a fast path.
|
|
67
|
+
* Without a GIN/GiST (full-text) or HNSW/IVFFlat (vector) index the promise
|
|
68
|
+
* is served by a sequential scan plus a per-row distance or match computation,
|
|
69
|
+
* which is the single worst plan the API can produce.
|
|
70
|
+
*/
|
|
71
|
+
export declare function checkUnindexedSearchColumns(table: TableIndexSnapshot): Finding[];
|
|
72
|
+
/**
|
|
73
|
+
* X8: a sort-shaped column that leads no index.
|
|
74
|
+
*
|
|
75
|
+
* Every PostGraphile connection can be ordered by any column and paginated
|
|
76
|
+
* with a cursor over that order. Sorting on an unindexed column forces a full
|
|
77
|
+
* sort of the (RLS-filtered) result on every page, and keyset pagination
|
|
78
|
+
* degenerates into a scan-and-discard. Timestamp columns are singled out
|
|
79
|
+
* because they are what feeds are actually ordered by — a heuristic, hence
|
|
80
|
+
* `info` by default; turn it off with `perf.rules: { "X8": "off" }`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function checkUnindexedSortColumns(table: TableIndexSnapshot): Finding[];
|