@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,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role reachability: what a role can *actually* reach, not just what the ACL
|
|
3
|
+
* rows name.
|
|
4
|
+
*
|
|
5
|
+
* `effectiveGrants` (see `lattice.ts`) already folds the three ways a
|
|
6
|
+
* privilege arrives passively — a direct grant, a grant TO PUBLIC, and a grant
|
|
7
|
+
* to a role the caller INHERITs from. That closure answers "what privileges
|
|
8
|
+
* does this role hold?" but not "what can this role make Postgres do?", and on
|
|
9
|
+
* PG16+ the two diverge: a membership can confer `SET ROLE` (`set_option`)
|
|
10
|
+
* *without* inheritance, so a role that passively holds nothing can still
|
|
11
|
+
* execute with a target role's full privileges by assuming it.
|
|
12
|
+
*
|
|
13
|
+
* This module is the first step of the reachability lattice (planning
|
|
14
|
+
* #1358/#1361): it projects a role's reach into each relation as a set of
|
|
15
|
+
* cells, each carrying the role the access actually *executes as*
|
|
16
|
+
* (`effectiveRole`) and how that reach arrived (`path`). Stage 1 models two
|
|
17
|
+
* edge kinds — passive grants and `SET ROLE` — both catalog-proven
|
|
18
|
+
* (`proof: 'catalog'`). Stage 2 adds the view-ownership edge: a read through a
|
|
19
|
+
* non-`security_invoker` view executes as the view's owner, and the base
|
|
20
|
+
* relations it touches are read out of the view body, so those cells carry
|
|
21
|
+
* `proof: 'ast'`. SECURITY DEFINER function edges come later.
|
|
22
|
+
*/
|
|
23
|
+
import type { FunctionGrant } from '../pg/functions';
|
|
24
|
+
import type { GrantInfo, PgPrivilege, TableSnapshot } from '../pg/introspect';
|
|
25
|
+
import { type GrantVia, type RoleGraph } from './lattice';
|
|
26
|
+
/** One hop in the path by which a role reaches a relation. */
|
|
27
|
+
export type RoleReachEdge =
|
|
28
|
+
/** A privilege held passively: directly, via PUBLIC, or via INHERIT. */
|
|
29
|
+
{
|
|
30
|
+
kind: 'grant';
|
|
31
|
+
via: GrantVia;
|
|
32
|
+
privilege: PgPrivilege;
|
|
33
|
+
}
|
|
34
|
+
/** The caller assumed `to` with `SET ROLE` (`pg_auth_members.set_option`). */
|
|
35
|
+
| {
|
|
36
|
+
kind: 'setrole';
|
|
37
|
+
to: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The caller read through a view that executes as `owner` — every relation
|
|
41
|
+
* the body names is read under the owner's privileges, not the caller's.
|
|
42
|
+
*/
|
|
43
|
+
| {
|
|
44
|
+
kind: 'view';
|
|
45
|
+
view: string;
|
|
46
|
+
owner: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The caller read a materialized view whose rows were computed as `owner`
|
|
50
|
+
* at REFRESH time. Unlike the view edge this holds whatever the reader's
|
|
51
|
+
* privileges are: the rows are stored, so the bases are never consulted and
|
|
52
|
+
* their policies never run.
|
|
53
|
+
*/
|
|
54
|
+
| {
|
|
55
|
+
kind: 'matview';
|
|
56
|
+
view: string;
|
|
57
|
+
owner: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The caller's command fired a rewrite rule on `view`. The rule's actions
|
|
61
|
+
* are permission-checked against the rule's table owner, and unlike the
|
|
62
|
+
* view edge this is *not* governed by `security_invoker`.
|
|
63
|
+
*/
|
|
64
|
+
| {
|
|
65
|
+
kind: 'rule';
|
|
66
|
+
view: string;
|
|
67
|
+
rule: string;
|
|
68
|
+
owner: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The caller executed a function that runs as `owner` — a SECURITY DEFINER,
|
|
72
|
+
* or anything called from one. Every relation the body touches is touched
|
|
73
|
+
* with the owner's privileges, which is the function twin of the view edge.
|
|
74
|
+
*/
|
|
75
|
+
| {
|
|
76
|
+
kind: 'function';
|
|
77
|
+
fn: string;
|
|
78
|
+
owner: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The caller's write against `view` fired an `INSTEAD OF` trigger. The
|
|
82
|
+
* write itself never reaches a base relation: it becomes the trigger
|
|
83
|
+
* function's body, checked against *that function's* effective user — the
|
|
84
|
+
* caller, unless the function is SECURITY DEFINER. The view's own owner and
|
|
85
|
+
* `security_invoker` do not enter into it.
|
|
86
|
+
*/
|
|
87
|
+
| {
|
|
88
|
+
kind: 'trigger';
|
|
89
|
+
view: string;
|
|
90
|
+
trigger: string;
|
|
91
|
+
fn: string;
|
|
92
|
+
owner: string;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* How well-founded a reach cell is. Stage 1 is entirely `catalog` — every
|
|
96
|
+
* edge is a row in `pg_auth_members` or an ACL entry. Later edges derived from
|
|
97
|
+
* parsing function/view bodies are `ast`; a body safegres cannot follow
|
|
98
|
+
* (dynamic SQL) taints everything downstream as `opaque-tainted`, which callers
|
|
99
|
+
* must treat as *unknown*, never as *safe*.
|
|
100
|
+
*/
|
|
101
|
+
export type ReachProof = 'catalog' | 'ast' | 'opaque-tainted';
|
|
102
|
+
/** One relation a role reaches, under one effective role, by one path. */
|
|
103
|
+
export interface RoleReachCell {
|
|
104
|
+
schema: string;
|
|
105
|
+
table: string;
|
|
106
|
+
privileges: PgPrivilege[];
|
|
107
|
+
/**
|
|
108
|
+
* The role the access executes as. Equal to the querying role for passive
|
|
109
|
+
* grants; the assumed role once a `setrole` edge is on the path. Postgres
|
|
110
|
+
* checks the relation's ACLs and RLS policies against *this* role, which is
|
|
111
|
+
* why it has to be retained rather than collapsed back onto the caller.
|
|
112
|
+
*/
|
|
113
|
+
effectiveRole: string;
|
|
114
|
+
/** The edges, caller-first, by which the reach arrived. */
|
|
115
|
+
path: RoleReachEdge[];
|
|
116
|
+
proof: ReachProof;
|
|
117
|
+
/**
|
|
118
|
+
* The relation was not introspected: it is outside the audited schemas.
|
|
119
|
+
* `privileges` is what the path exercises, not what the relation permits.
|
|
120
|
+
*/
|
|
121
|
+
external?: boolean;
|
|
122
|
+
/** The columns of the relation the path reaches, where they are known. */
|
|
123
|
+
columns?: string[];
|
|
124
|
+
/**
|
|
125
|
+
* Why the path is only a lower bound, on a `opaque-tainted` cell: the body
|
|
126
|
+
* that produced it executes SQL this analysis cannot follow, so the relation
|
|
127
|
+
* it names is the *view itself* and what lies beyond it is unknown.
|
|
128
|
+
*/
|
|
129
|
+
taint?: string;
|
|
130
|
+
}
|
|
131
|
+
/** Everything one role reaches, across the relations examined. */
|
|
132
|
+
export interface RoleReach {
|
|
133
|
+
role: string;
|
|
134
|
+
cells: RoleReachCell[];
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Project each role's reach into `tables`. Stage 1 emits, per (role, table):
|
|
138
|
+
*
|
|
139
|
+
* - one `effectiveRole === role` cell for the passive `effectiveGrants`
|
|
140
|
+
* closure, when non-empty; and
|
|
141
|
+
* - one cell per role in the caller's transitive `SET ROLE` closure that
|
|
142
|
+
* itself holds privileges on the table — the access it gains by assuming
|
|
143
|
+
* that role.
|
|
144
|
+
*
|
|
145
|
+
* A relation the role cannot reach at all produces no cell.
|
|
146
|
+
*/
|
|
147
|
+
export declare function computeRoleReach(tables: TableSnapshot[], graph: RoleGraph, roles: string[]): RoleReach[];
|
|
148
|
+
/** One relation a view body reads, with the roles the hops execute as. */
|
|
149
|
+
export interface ViewBaseRelation {
|
|
150
|
+
schema: string;
|
|
151
|
+
table: string;
|
|
152
|
+
/**
|
|
153
|
+
* The view hops the read passes through, outermost first. The last hop's
|
|
154
|
+
* owner is the role the base relation is actually read as.
|
|
155
|
+
*/
|
|
156
|
+
hops: Array<{
|
|
157
|
+
view: string;
|
|
158
|
+
owner: string;
|
|
159
|
+
}>;
|
|
160
|
+
/**
|
|
161
|
+
* The relation lives outside the audited schemas, so nothing is known about
|
|
162
|
+
* its ACL, its RLS or its owner. The read is proven; its consequences are
|
|
163
|
+
* not, and a consumer must not grade it as if the relation were in scope.
|
|
164
|
+
*/
|
|
165
|
+
external?: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* The columns of the relation that actually escape through the view, from
|
|
168
|
+
* the catalog's own dependency rows. Absent means unknown — never "none".
|
|
169
|
+
*/
|
|
170
|
+
columns?: string[];
|
|
171
|
+
}
|
|
172
|
+
/** A view, its own ACL, and the base relations its body was found to read. */
|
|
173
|
+
export interface ViewReachInput {
|
|
174
|
+
schema: string;
|
|
175
|
+
name: string;
|
|
176
|
+
owner: string;
|
|
177
|
+
/** ACL rows on the view itself — who can SELECT the view at all. */
|
|
178
|
+
grants: GrantInfo[];
|
|
179
|
+
baseRelations: ViewBaseRelation[];
|
|
180
|
+
/**
|
|
181
|
+
* The outermost relation is a materialized view, so its first hop is a
|
|
182
|
+
* `matview` edge: the rows were computed at REFRESH time rather than read
|
|
183
|
+
* through on demand.
|
|
184
|
+
*/
|
|
185
|
+
materialized?: boolean;
|
|
186
|
+
/**
|
|
187
|
+
* The body was wholly or partly unreadable, and why. `baseRelations` is
|
|
188
|
+
* then a *lower bound*: everything in it is proven, but the body reaches
|
|
189
|
+
* relations this analysis cannot name.
|
|
190
|
+
*/
|
|
191
|
+
unreadable?: string;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Project the view-ownership edge into the same reach model: for every role
|
|
195
|
+
* that can SELECT a view, one cell per base relation the view body reads,
|
|
196
|
+
* under the owner the hop executes as.
|
|
197
|
+
*
|
|
198
|
+
* Only SELECT is modelled here; the write half is
|
|
199
|
+
* {@link computeViewWriteReach}, which needs the catalog's updatability
|
|
200
|
+
* answer on top of the body's relation set.
|
|
201
|
+
*
|
|
202
|
+
* A view whose body could not be read (dynamic SQL, an unparseable body)
|
|
203
|
+
* still belongs in `views`, carrying {@link ViewReachInput.unreadable}: an
|
|
204
|
+
* unreadable body is unknown, not empty, and it yields one extra cell naming
|
|
205
|
+
* the *view* with `proof: 'opaque-tainted'` — the reach is real, its far end
|
|
206
|
+
* is not knowable here. Grading rules must skip those cells; only a coverage
|
|
207
|
+
* rule should report them.
|
|
208
|
+
*/
|
|
209
|
+
export declare function computeViewReach(views: ViewReachInput[], graph: RoleGraph, roles: string[]): RoleReach[];
|
|
210
|
+
/** One hop of a call chain, and the role in force once it is taken. */
|
|
211
|
+
export interface FunctionHop {
|
|
212
|
+
/** `schema.name` — overloads collapse, as they do in the call graph. */
|
|
213
|
+
fn: string;
|
|
214
|
+
owner: string;
|
|
215
|
+
}
|
|
216
|
+
/** One relation a function body touches, under the role in force there. */
|
|
217
|
+
export interface FunctionAccess {
|
|
218
|
+
schema: string;
|
|
219
|
+
table: string;
|
|
220
|
+
privilege: PgPrivilege;
|
|
221
|
+
/** Call hops, entry first; the last hop's owner executes the access. */
|
|
222
|
+
hops: FunctionHop[];
|
|
223
|
+
/** Views the access passed through after the call chain, outermost first. */
|
|
224
|
+
viewHops?: Array<{
|
|
225
|
+
view: string;
|
|
226
|
+
owner: string;
|
|
227
|
+
}>;
|
|
228
|
+
/** The relation is outside the audited schemas: nothing about it is known. */
|
|
229
|
+
external?: boolean;
|
|
230
|
+
}
|
|
231
|
+
/** A function, its EXECUTE ACL, and the relations its body reaches. */
|
|
232
|
+
export interface FunctionReachInput {
|
|
233
|
+
schema: string;
|
|
234
|
+
name: string;
|
|
235
|
+
/** Identity arguments — what distinguishes one overload from another. */
|
|
236
|
+
args: string;
|
|
237
|
+
owner: string;
|
|
238
|
+
grants: FunctionGrant[];
|
|
239
|
+
/** The EXECUTE came from Postgres's default ACL, not a deliberate grant. */
|
|
240
|
+
defaultAcl: boolean;
|
|
241
|
+
accesses: FunctionAccess[];
|
|
242
|
+
/**
|
|
243
|
+
* The body was wholly or partly unreadable, and why. `accesses` is then a
|
|
244
|
+
* lower bound: what is there is proven, what is missing is unknowable.
|
|
245
|
+
*/
|
|
246
|
+
unreadable?: string;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Project the function-execution edge into the reach model: for every role
|
|
250
|
+
* that can EXECUTE a function running as someone else, one cell per relation
|
|
251
|
+
* its body touches, under the role in force at that point in the call chain.
|
|
252
|
+
*
|
|
253
|
+
* The shape mirrors {@link computeViewReach} deliberately — a definer function
|
|
254
|
+
* and a definer view are the same escalation with different syntax — and, as
|
|
255
|
+
* there, a body that could not be read yields one `opaque-tainted` cell naming
|
|
256
|
+
* the *function*: the execution is proven, its far end is not.
|
|
257
|
+
*/
|
|
258
|
+
export declare function computeFunctionReach(functions: FunctionReachInput[], graph: RoleGraph, roles: string[]): RoleReach[];
|
|
259
|
+
/** A view, its ACL, and the relations an `INSTEAD OF` trigger's body reaches. */
|
|
260
|
+
export interface TriggerWriteInput {
|
|
261
|
+
schema: string;
|
|
262
|
+
name: string;
|
|
263
|
+
/** ACL rows on the view — what the caller must hold to fire the trigger. */
|
|
264
|
+
grants: GrantInfo[];
|
|
265
|
+
trigger: string;
|
|
266
|
+
/** The commands on the view that fire it. */
|
|
267
|
+
events: PgPrivilege[];
|
|
268
|
+
/** `schema.name` of the trigger function. */
|
|
269
|
+
fn: string;
|
|
270
|
+
/** The role the trigger function's body executes as. */
|
|
271
|
+
fnOwner: string;
|
|
272
|
+
/** Relations the body touches, under `fnOwner`. */
|
|
273
|
+
accesses: Array<{
|
|
274
|
+
schema: string;
|
|
275
|
+
table: string;
|
|
276
|
+
privilege: PgPrivilege;
|
|
277
|
+
external?: boolean;
|
|
278
|
+
}>;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Project the `INSTEAD OF` trigger edge: for every role holding a command the
|
|
282
|
+
* trigger fires on, one cell per relation the trigger function's body touches.
|
|
283
|
+
*
|
|
284
|
+
* Callers must only pass triggers whose function actually re-owns the write —
|
|
285
|
+
* an invoker trigger function runs the body as the caller, which is no edge at
|
|
286
|
+
* all, and Postgres denies it exactly as it would deny the direct write.
|
|
287
|
+
*/
|
|
288
|
+
export declare function computeTriggerWriteReach(triggers: TriggerWriteInput[], graph: RoleGraph, roles: string[]): RoleReach[];
|
|
289
|
+
/**
|
|
290
|
+
* One relation a *write* against a view lands on.
|
|
291
|
+
*
|
|
292
|
+
* The two privileges are distinct on purpose. `via` is what the caller must
|
|
293
|
+
* hold on the view to issue the command; `privilege` is what the rewritten
|
|
294
|
+
* command exercises on the target. They coincide for an auto-updatable view
|
|
295
|
+
* (an INSERT on the view is an INSERT on its base relation) and routinely
|
|
296
|
+
* differ for a rewrite rule, where `ON INSERT ... DO INSTEAD UPDATE other`
|
|
297
|
+
* turns one into the other.
|
|
298
|
+
*/
|
|
299
|
+
export interface ViewWriteEdge {
|
|
300
|
+
schema: string;
|
|
301
|
+
table: string;
|
|
302
|
+
via: PgPrivilege;
|
|
303
|
+
privilege: PgPrivilege;
|
|
304
|
+
/** View hops, outermost first; the last hop's owner executes the write. */
|
|
305
|
+
hops: Array<{
|
|
306
|
+
view: string;
|
|
307
|
+
owner: string;
|
|
308
|
+
}>;
|
|
309
|
+
/** Set when the edge exists because of a rewrite rule, not auto-update. */
|
|
310
|
+
rule?: string;
|
|
311
|
+
}
|
|
312
|
+
/** A view, its own ACL, and the relations writes against it reach. */
|
|
313
|
+
export interface ViewWriteInput {
|
|
314
|
+
schema: string;
|
|
315
|
+
name: string;
|
|
316
|
+
owner: string;
|
|
317
|
+
grants: GrantInfo[];
|
|
318
|
+
writeEdges: ViewWriteEdge[];
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Project write edges through views into the reach model: for every role that
|
|
322
|
+
* holds the triggering command on the view, one cell per relation that write
|
|
323
|
+
* lands on, under the role the landing executes as.
|
|
324
|
+
*
|
|
325
|
+
* As with {@link computeViewReach}, a view whose write target could not be
|
|
326
|
+
* proven must not appear in `views` — an `INSTEAD OF` trigger's body, a
|
|
327
|
+
* multi-relation body, or an unreadable rule action is unknown, not empty.
|
|
328
|
+
*/
|
|
329
|
+
export declare function computeViewWriteReach(views: ViewWriteInput[], graph: RoleGraph, roles: string[]): RoleReach[];
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Role reachability: what a role can *actually* reach, not just what the ACL
|
|
4
|
+
* rows name.
|
|
5
|
+
*
|
|
6
|
+
* `effectiveGrants` (see `lattice.ts`) already folds the three ways a
|
|
7
|
+
* privilege arrives passively — a direct grant, a grant TO PUBLIC, and a grant
|
|
8
|
+
* to a role the caller INHERITs from. That closure answers "what privileges
|
|
9
|
+
* does this role hold?" but not "what can this role make Postgres do?", and on
|
|
10
|
+
* PG16+ the two diverge: a membership can confer `SET ROLE` (`set_option`)
|
|
11
|
+
* *without* inheritance, so a role that passively holds nothing can still
|
|
12
|
+
* execute with a target role's full privileges by assuming it.
|
|
13
|
+
*
|
|
14
|
+
* This module is the first step of the reachability lattice (planning
|
|
15
|
+
* #1358/#1361): it projects a role's reach into each relation as a set of
|
|
16
|
+
* cells, each carrying the role the access actually *executes as*
|
|
17
|
+
* (`effectiveRole`) and how that reach arrived (`path`). Stage 1 models two
|
|
18
|
+
* edge kinds — passive grants and `SET ROLE` — both catalog-proven
|
|
19
|
+
* (`proof: 'catalog'`). Stage 2 adds the view-ownership edge: a read through a
|
|
20
|
+
* non-`security_invoker` view executes as the view's owner, and the base
|
|
21
|
+
* relations it touches are read out of the view body, so those cells carry
|
|
22
|
+
* `proof: 'ast'`. SECURITY DEFINER function edges come later.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.computeRoleReach = computeRoleReach;
|
|
26
|
+
exports.computeViewReach = computeViewReach;
|
|
27
|
+
exports.computeFunctionReach = computeFunctionReach;
|
|
28
|
+
exports.computeTriggerWriteReach = computeTriggerWriteReach;
|
|
29
|
+
exports.computeViewWriteReach = computeViewWriteReach;
|
|
30
|
+
const lattice_1 = require("./lattice");
|
|
31
|
+
const RLS_PRIVILEGES = ['SELECT', 'INSERT', 'UPDATE', 'DELETE'];
|
|
32
|
+
/**
|
|
33
|
+
* Project each role's reach into `tables`. Stage 1 emits, per (role, table):
|
|
34
|
+
*
|
|
35
|
+
* - one `effectiveRole === role` cell for the passive `effectiveGrants`
|
|
36
|
+
* closure, when non-empty; and
|
|
37
|
+
* - one cell per role in the caller's transitive `SET ROLE` closure that
|
|
38
|
+
* itself holds privileges on the table — the access it gains by assuming
|
|
39
|
+
* that role.
|
|
40
|
+
*
|
|
41
|
+
* A relation the role cannot reach at all produces no cell.
|
|
42
|
+
*/
|
|
43
|
+
function computeRoleReach(tables, graph, roles) {
|
|
44
|
+
return roles.map((role) => {
|
|
45
|
+
const attrs = graph.get(role);
|
|
46
|
+
const cells = [];
|
|
47
|
+
for (const table of tables) {
|
|
48
|
+
const direct = (0, lattice_1.effectiveGrants)(table, role, graph).filter((g) => RLS_PRIVILEGES.includes(g.privilege));
|
|
49
|
+
if (direct.length > 0) {
|
|
50
|
+
cells.push({
|
|
51
|
+
schema: table.schema,
|
|
52
|
+
table: table.name,
|
|
53
|
+
privileges: direct.map((g) => g.privilege),
|
|
54
|
+
effectiveRole: role,
|
|
55
|
+
path: direct.map((g) => ({ kind: 'grant', via: g.via, privilege: g.privilege })),
|
|
56
|
+
proof: 'catalog'
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
for (const target of attrs?.canSetRole ?? []) {
|
|
60
|
+
const assumed = (0, lattice_1.effectiveGrants)(table, target, graph).filter((g) => RLS_PRIVILEGES.includes(g.privilege));
|
|
61
|
+
if (assumed.length === 0)
|
|
62
|
+
continue;
|
|
63
|
+
cells.push({
|
|
64
|
+
schema: table.schema,
|
|
65
|
+
table: table.name,
|
|
66
|
+
privileges: assumed.map((g) => g.privilege),
|
|
67
|
+
effectiveRole: target,
|
|
68
|
+
path: [
|
|
69
|
+
{ kind: 'setrole', to: target },
|
|
70
|
+
...assumed.map((g) => ({ kind: 'grant', via: g.via, privilege: g.privilege }))
|
|
71
|
+
],
|
|
72
|
+
proof: 'catalog'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { role, cells };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Project the view-ownership edge into the same reach model: for every role
|
|
81
|
+
* that can SELECT a view, one cell per base relation the view body reads,
|
|
82
|
+
* under the owner the hop executes as.
|
|
83
|
+
*
|
|
84
|
+
* Only SELECT is modelled here; the write half is
|
|
85
|
+
* {@link computeViewWriteReach}, which needs the catalog's updatability
|
|
86
|
+
* answer on top of the body's relation set.
|
|
87
|
+
*
|
|
88
|
+
* A view whose body could not be read (dynamic SQL, an unparseable body)
|
|
89
|
+
* still belongs in `views`, carrying {@link ViewReachInput.unreadable}: an
|
|
90
|
+
* unreadable body is unknown, not empty, and it yields one extra cell naming
|
|
91
|
+
* the *view* with `proof: 'opaque-tainted'` — the reach is real, its far end
|
|
92
|
+
* is not knowable here. Grading rules must skip those cells; only a coverage
|
|
93
|
+
* rule should report them.
|
|
94
|
+
*/
|
|
95
|
+
function computeViewReach(views, graph, roles) {
|
|
96
|
+
return roles.map((role) => {
|
|
97
|
+
const cells = [];
|
|
98
|
+
for (const view of views) {
|
|
99
|
+
const select = (0, lattice_1.effectiveGrants)(view, role, graph).find((g) => g.privilege === 'SELECT');
|
|
100
|
+
if (!select)
|
|
101
|
+
continue;
|
|
102
|
+
for (const base of view.baseRelations) {
|
|
103
|
+
if (base.hops.length === 0)
|
|
104
|
+
continue;
|
|
105
|
+
cells.push({
|
|
106
|
+
schema: base.schema,
|
|
107
|
+
table: base.table,
|
|
108
|
+
privileges: ['SELECT'],
|
|
109
|
+
effectiveRole: base.hops[base.hops.length - 1].owner,
|
|
110
|
+
path: [
|
|
111
|
+
{ kind: 'grant', via: select.via, privilege: 'SELECT' },
|
|
112
|
+
...base.hops.map((h, i) => ({
|
|
113
|
+
kind: view.materialized && i === 0 ? 'matview' : 'view',
|
|
114
|
+
view: h.view,
|
|
115
|
+
owner: h.owner
|
|
116
|
+
}))
|
|
117
|
+
],
|
|
118
|
+
proof: 'ast',
|
|
119
|
+
...(base.external ? { external: true } : {}),
|
|
120
|
+
...(base.columns ? { columns: base.columns } : {})
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (view.unreadable) {
|
|
124
|
+
cells.push({
|
|
125
|
+
schema: view.schema,
|
|
126
|
+
table: view.name,
|
|
127
|
+
privileges: ['SELECT'],
|
|
128
|
+
effectiveRole: view.owner,
|
|
129
|
+
path: [
|
|
130
|
+
{ kind: 'grant', via: select.via, privilege: 'SELECT' },
|
|
131
|
+
{ kind: 'view', view: `${view.schema}.${view.name}`, owner: view.owner }
|
|
132
|
+
],
|
|
133
|
+
proof: 'opaque-tainted',
|
|
134
|
+
taint: view.unreadable
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { role, cells };
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Project the function-execution edge into the reach model: for every role
|
|
143
|
+
* that can EXECUTE a function running as someone else, one cell per relation
|
|
144
|
+
* its body touches, under the role in force at that point in the call chain.
|
|
145
|
+
*
|
|
146
|
+
* The shape mirrors {@link computeViewReach} deliberately — a definer function
|
|
147
|
+
* and a definer view are the same escalation with different syntax — and, as
|
|
148
|
+
* there, a body that could not be read yields one `opaque-tainted` cell naming
|
|
149
|
+
* the *function*: the execution is proven, its far end is not.
|
|
150
|
+
*/
|
|
151
|
+
function computeFunctionReach(functions, graph, roles) {
|
|
152
|
+
return roles.map((role) => {
|
|
153
|
+
const cells = [];
|
|
154
|
+
for (const fn of functions) {
|
|
155
|
+
const via = (0, lattice_1.effectiveExecute)(fn, role, graph);
|
|
156
|
+
if (!via)
|
|
157
|
+
continue;
|
|
158
|
+
for (const access of fn.accesses) {
|
|
159
|
+
if (access.hops.length === 0)
|
|
160
|
+
continue;
|
|
161
|
+
const viewHops = access.viewHops ?? [];
|
|
162
|
+
const last = viewHops.length > 0
|
|
163
|
+
? viewHops[viewHops.length - 1].owner
|
|
164
|
+
: access.hops[access.hops.length - 1].owner;
|
|
165
|
+
cells.push({
|
|
166
|
+
schema: access.schema,
|
|
167
|
+
table: access.table,
|
|
168
|
+
privileges: [access.privilege],
|
|
169
|
+
effectiveRole: last,
|
|
170
|
+
path: [
|
|
171
|
+
{ kind: 'grant', via, privilege: 'EXECUTE' },
|
|
172
|
+
...access.hops.map((h) => ({ kind: 'function', fn: h.fn, owner: h.owner })),
|
|
173
|
+
...viewHops.map((h) => ({ kind: 'view', view: h.view, owner: h.owner }))
|
|
174
|
+
],
|
|
175
|
+
proof: 'ast',
|
|
176
|
+
...(access.external ? { external: true } : {})
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (fn.unreadable) {
|
|
180
|
+
cells.push({
|
|
181
|
+
schema: fn.schema,
|
|
182
|
+
table: `${fn.name}(${fn.args})`,
|
|
183
|
+
privileges: ['EXECUTE'],
|
|
184
|
+
effectiveRole: fn.owner,
|
|
185
|
+
path: [
|
|
186
|
+
{ kind: 'grant', via, privilege: 'EXECUTE' },
|
|
187
|
+
{ kind: 'function', fn: `${fn.schema}.${fn.name}`, owner: fn.owner }
|
|
188
|
+
],
|
|
189
|
+
proof: 'opaque-tainted',
|
|
190
|
+
taint: fn.unreadable
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { role, cells };
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Project the `INSTEAD OF` trigger edge: for every role holding a command the
|
|
199
|
+
* trigger fires on, one cell per relation the trigger function's body touches.
|
|
200
|
+
*
|
|
201
|
+
* Callers must only pass triggers whose function actually re-owns the write —
|
|
202
|
+
* an invoker trigger function runs the body as the caller, which is no edge at
|
|
203
|
+
* all, and Postgres denies it exactly as it would deny the direct write.
|
|
204
|
+
*/
|
|
205
|
+
function computeTriggerWriteReach(triggers, graph, roles) {
|
|
206
|
+
return roles.map((role) => {
|
|
207
|
+
const cells = [];
|
|
208
|
+
for (const trigger of triggers) {
|
|
209
|
+
const held = (0, lattice_1.effectiveGrants)(trigger, role, graph);
|
|
210
|
+
const fired = trigger.events.filter((e) => held.some((g) => g.privilege === e));
|
|
211
|
+
if (fired.length === 0)
|
|
212
|
+
continue;
|
|
213
|
+
const grant = held.find((g) => g.privilege === fired[0]);
|
|
214
|
+
for (const access of trigger.accesses) {
|
|
215
|
+
cells.push({
|
|
216
|
+
schema: access.schema,
|
|
217
|
+
table: access.table,
|
|
218
|
+
privileges: [access.privilege],
|
|
219
|
+
effectiveRole: trigger.fnOwner,
|
|
220
|
+
path: [
|
|
221
|
+
{ kind: 'grant', via: grant.via, privilege: fired[0] },
|
|
222
|
+
{
|
|
223
|
+
kind: 'trigger',
|
|
224
|
+
view: `${trigger.schema}.${trigger.name}`,
|
|
225
|
+
trigger: trigger.trigger,
|
|
226
|
+
fn: trigger.fn,
|
|
227
|
+
owner: trigger.fnOwner
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
proof: 'ast',
|
|
231
|
+
...(access.external ? { external: true } : {})
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { role, cells };
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Project write edges through views into the reach model: for every role that
|
|
240
|
+
* holds the triggering command on the view, one cell per relation that write
|
|
241
|
+
* lands on, under the role the landing executes as.
|
|
242
|
+
*
|
|
243
|
+
* As with {@link computeViewReach}, a view whose write target could not be
|
|
244
|
+
* proven must not appear in `views` — an `INSTEAD OF` trigger's body, a
|
|
245
|
+
* multi-relation body, or an unreadable rule action is unknown, not empty.
|
|
246
|
+
*/
|
|
247
|
+
function computeViewWriteReach(views, graph, roles) {
|
|
248
|
+
return roles.map((role) => {
|
|
249
|
+
const cells = [];
|
|
250
|
+
for (const view of views) {
|
|
251
|
+
const held = (0, lattice_1.effectiveGrants)(view, role, graph);
|
|
252
|
+
for (const edge of view.writeEdges) {
|
|
253
|
+
if (edge.hops.length === 0)
|
|
254
|
+
continue;
|
|
255
|
+
const grant = held.find((g) => g.privilege === edge.via);
|
|
256
|
+
if (!grant)
|
|
257
|
+
continue;
|
|
258
|
+
const last = edge.hops[edge.hops.length - 1];
|
|
259
|
+
cells.push({
|
|
260
|
+
schema: edge.schema,
|
|
261
|
+
table: edge.table,
|
|
262
|
+
privileges: [edge.privilege],
|
|
263
|
+
effectiveRole: last.owner,
|
|
264
|
+
path: [
|
|
265
|
+
{ kind: 'grant', via: grant.via, privilege: edge.via },
|
|
266
|
+
...edge.hops.map((h) => ({ kind: 'view', view: h.view, owner: h.owner })),
|
|
267
|
+
...(edge.rule
|
|
268
|
+
? [{ kind: 'rule', view: last.view, rule: edge.rule, owner: last.owner }]
|
|
269
|
+
: [])
|
|
270
|
+
],
|
|
271
|
+
proof: 'ast'
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return { role, cells };
|
|
276
|
+
});
|
|
277
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TableSnapshot } from '../pg/introspect';
|
|
2
|
+
import type { Finding } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Role-trust rules (R-series): findings driven by *who* is granted access,
|
|
5
|
+
* not just whether coverage exists. R1/R2 take a configurable list of
|
|
6
|
+
* untrusted roles (e.g. `anonymous`) via rule options; with no roles
|
|
7
|
+
* configured they are no-ops, so they cost nothing on databases without
|
|
8
|
+
* such a role model.
|
|
9
|
+
*/
|
|
10
|
+
export interface RoleTrustOptions {
|
|
11
|
+
/** Role names considered untrusted (exact match). */
|
|
12
|
+
roles?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Take the untrusted roles from the resolved exposure surface instead of
|
|
15
|
+
* naming them — their names are per-deployment (`myapp_visitor`, a
|
|
16
|
+
* Constructive API's `anon_role`), so a preset cannot hardcode them.
|
|
17
|
+
*
|
|
18
|
+
* - `anon`: only the roles an *unauthenticated* caller arrives as. The right
|
|
19
|
+
* default for a stack whose signed-in role legitimately writes: flagging
|
|
20
|
+
* `authenticated` for holding an INSERT grant would flag the product.
|
|
21
|
+
* - `exposure`: every role at the API edge, signed-in ones included. The
|
|
22
|
+
* stricter reading, for a surface where no role should write directly.
|
|
23
|
+
*
|
|
24
|
+
* Unions with `roles`.
|
|
25
|
+
*/
|
|
26
|
+
rolesFrom?: 'exposure' | 'anon';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* R1: an untrusted role holds a write privilege on a table. Even with
|
|
30
|
+
* airtight policies, write access for e.g. `anonymous` is almost always a
|
|
31
|
+
* grant mistake — unauthenticated actors can INSERT/UPDATE/DELETE.
|
|
32
|
+
*/
|
|
33
|
+
export declare function checkUntrustedRoleWrites(table: TableSnapshot, options?: RoleTrustOptions): Finding[];
|
|
34
|
+
/**
|
|
35
|
+
* R2: a permissive policy makes write operations pass RLS for an untrusted
|
|
36
|
+
* role (directly or via PUBLIC). Pairs with R1: the grant is the door, the
|
|
37
|
+
* policy is the unlocked latch.
|
|
38
|
+
*/
|
|
39
|
+
export declare function checkUntrustedRolePolicies(table: TableSnapshot, options?: RoleTrustOptions): Finding[];
|
|
40
|
+
/**
|
|
41
|
+
* R3: a table with RLS enabled has grants TO PUBLIC. PUBLIC includes every
|
|
42
|
+
* present and future role, which silently widens access as roles are added
|
|
43
|
+
* and defeats role-scoped policy reasoning.
|
|
44
|
+
*/
|
|
45
|
+
export declare function checkPublicGrants(table: TableSnapshot): Finding[];
|