@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
package/pg/behaviors.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PostGraphile *behaviors*, read from the object comments they are declared in.
|
|
4
|
+
*
|
|
5
|
+
* A behavior string is the schema author stating which parts of the generated
|
|
6
|
+
* API an object participates in — `@behavior -list -connection` on a foreign
|
|
7
|
+
* key constraint says the reverse relation is not exposed. That is a different
|
|
8
|
+
* kind of evidence from anything else safegres collects: `pg_class.reltuples`
|
|
9
|
+
* and `pg_stat_user_indexes.idx_scan` are *measurements*, and safegres grades
|
|
10
|
+
* an ephemeral CI database that has never held data, so both read zero at
|
|
11
|
+
* exactly the moment they would have to mean something. A declaration reads the
|
|
12
|
+
* same in CI as in production.
|
|
13
|
+
*
|
|
14
|
+
* Read from the comment rather than from a running Graphile instance, or from
|
|
15
|
+
* whatever tables the author generated the comment out of:
|
|
16
|
+
*
|
|
17
|
+
* - `@behavior` in a comment is the PostGraphile v5 convention, so this works
|
|
18
|
+
* on any Graphile database rather than one project's metadata schema.
|
|
19
|
+
* - The comment is the composed value — one tag per object — so it is already
|
|
20
|
+
* the author's resolved intent.
|
|
21
|
+
* - A live instance would additionally resolve preset defaults, but requires an
|
|
22
|
+
* API to be running. safegres grades a database.
|
|
23
|
+
*
|
|
24
|
+
* The consequence of that last point is the rule this module exists to enforce:
|
|
25
|
+
* **only an explicit negative fragment is evidence.** Presets grant most
|
|
26
|
+
* behaviors by default, so the *absence* of `+list` says nothing whatsoever. A
|
|
27
|
+
* scanner that reads silence as denial is a scanner that tells you to drop an
|
|
28
|
+
* index a live API is using.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.SYSTEM_SCHEMAS = void 0;
|
|
32
|
+
exports.emptyBehaviorSnapshot = emptyBehaviorSnapshot;
|
|
33
|
+
exports.directionalBehavior = directionalBehavior;
|
|
34
|
+
exports.parseBehaviorTag = parseBehaviorTag;
|
|
35
|
+
exports.parseBehaviorTags = parseBehaviorTags;
|
|
36
|
+
exports.parseFragments = parseFragments;
|
|
37
|
+
exports.resolveAbility = resolveAbility;
|
|
38
|
+
exports.deniesAll = deniesAll;
|
|
39
|
+
exports.introspectBehaviors = introspectBehaviors;
|
|
40
|
+
function emptyBehaviorSnapshot() {
|
|
41
|
+
return {
|
|
42
|
+
tables: new Map(),
|
|
43
|
+
columns: new Map(),
|
|
44
|
+
constraints: new Map(),
|
|
45
|
+
constraintDirections: new Map()
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The behavior governing one direction of a relation: the directional tag when
|
|
50
|
+
* present, else the undirected one. They do not merge — Graphile resolves the
|
|
51
|
+
* specific tag against the preset, and treating `@behavior -list` as an extra
|
|
52
|
+
* fragment on top of `@backwardBehavior +list` would invert the author's
|
|
53
|
+
* override.
|
|
54
|
+
*/
|
|
55
|
+
function directionalBehavior(behaviors, direction) {
|
|
56
|
+
if (!behaviors)
|
|
57
|
+
return undefined;
|
|
58
|
+
return behaviors[direction] ?? behaviors.both;
|
|
59
|
+
}
|
|
60
|
+
/** Catalog schemas no API surface ever contains. */
|
|
61
|
+
exports.SYSTEM_SCHEMAS = ['pg_catalog', 'information_schema', 'pg_toast'];
|
|
62
|
+
const DEFAULT_EXCLUDES = exports.SYSTEM_SCHEMAS;
|
|
63
|
+
/**
|
|
64
|
+
* Extract the `@behavior` smart tag from an object comment.
|
|
65
|
+
*
|
|
66
|
+
* Smart tags occupy the leading lines of the comment, `@tag [value]`, and stop
|
|
67
|
+
* at the first line that is not one; everything after is the description. Only
|
|
68
|
+
* `@behavior` is returned — `@omit` is deliberately not translated here, since
|
|
69
|
+
* its v4 semantics ("remove from the schema") are not the same question as a
|
|
70
|
+
* behavior fragment and conflating them would put a guess in the evidence.
|
|
71
|
+
*/
|
|
72
|
+
function parseBehaviorTag(comment) {
|
|
73
|
+
return parseBehaviorTags(comment).both ?? null;
|
|
74
|
+
}
|
|
75
|
+
/** Every behavior tag on a comment, by direction. */
|
|
76
|
+
function parseBehaviorTags(comment) {
|
|
77
|
+
const out = {};
|
|
78
|
+
if (!comment)
|
|
79
|
+
return out;
|
|
80
|
+
for (const line of comment.split('\n')) {
|
|
81
|
+
const trimmed = line.trim();
|
|
82
|
+
if (!trimmed.startsWith('@'))
|
|
83
|
+
break;
|
|
84
|
+
const match = /^@(behavior|forwardBehavior|backwardBehavior)\s+(.+)$/.exec(trimmed);
|
|
85
|
+
if (!match)
|
|
86
|
+
continue;
|
|
87
|
+
const value = match[2].trim();
|
|
88
|
+
if (match[1] === 'behavior')
|
|
89
|
+
out.both = value;
|
|
90
|
+
else if (match[1] === 'forwardBehavior')
|
|
91
|
+
out.forward = value;
|
|
92
|
+
else
|
|
93
|
+
out.backward = value;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/** Split a behavior string into fragments. A bare scope is a grant, as in PostGraphile. */
|
|
98
|
+
function parseFragments(behavior) {
|
|
99
|
+
if (!behavior)
|
|
100
|
+
return [];
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const term of behavior.trim().split(/\s+/)) {
|
|
103
|
+
if (!term)
|
|
104
|
+
continue;
|
|
105
|
+
const modifier = term[0] === '-' ? '-' : '+';
|
|
106
|
+
const scope = term[0] === '+' || term[0] === '-' ? term.slice(1) : term;
|
|
107
|
+
if (scope)
|
|
108
|
+
out.push({ modifier, scope });
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Does this fragment's scope speak to `ability`?
|
|
114
|
+
*
|
|
115
|
+
* Scope paths are `:`-separated and get more specific left to right
|
|
116
|
+
* (`resource:connection`), so the ability is the final segment. `*` matches
|
|
117
|
+
* anything, which is how `-*` denies wholesale.
|
|
118
|
+
*/
|
|
119
|
+
function scopeMatches(scope, ability) {
|
|
120
|
+
const segments = scope.split(':');
|
|
121
|
+
const last = segments[segments.length - 1];
|
|
122
|
+
return last === ability || last === '*';
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolve one ability against a behavior string.
|
|
126
|
+
*
|
|
127
|
+
* `undefined` means *undeclared*, and is the answer that matters: it is not
|
|
128
|
+
* `false`. Later fragments win, matching PostGraphile — `-* +list` grants
|
|
129
|
+
* `list` and denies the rest.
|
|
130
|
+
*/
|
|
131
|
+
function resolveAbility(behavior, ability) {
|
|
132
|
+
let verdict;
|
|
133
|
+
for (const fragment of parseFragments(behavior)) {
|
|
134
|
+
if (scopeMatches(fragment.scope, ability))
|
|
135
|
+
verdict = fragment.modifier === '+';
|
|
136
|
+
}
|
|
137
|
+
return verdict;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* True when *every* one of `abilities` is explicitly denied.
|
|
141
|
+
*
|
|
142
|
+
* Every, not some: a relation reachable as a single record is still reachable,
|
|
143
|
+
* and one undeclared ability is enough to leave the question open.
|
|
144
|
+
*/
|
|
145
|
+
function deniesAll(behavior, abilities) {
|
|
146
|
+
if (abilities.length === 0)
|
|
147
|
+
return false;
|
|
148
|
+
return abilities.every((ability) => resolveAbility(behavior, ability) === false);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Behavior tags on every table, column and constraint in scope.
|
|
152
|
+
*
|
|
153
|
+
* Extension-owned objects are not filtered here: the maps are only ever
|
|
154
|
+
* consulted for relations that survived the scan's own filtering, so an extra
|
|
155
|
+
* entry costs a map slot and changes nothing.
|
|
156
|
+
*/
|
|
157
|
+
async function introspectBehaviors(exec, options = {}) {
|
|
158
|
+
const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
|
|
159
|
+
const schemaFilter = options.schemas && options.schemas.length > 0
|
|
160
|
+
? `n.nspname = ANY($1::text[])`
|
|
161
|
+
: `NOT (n.nspname = ANY($2::text[]))`;
|
|
162
|
+
// Both parameters are referenced (even when only one filters) so Postgres can
|
|
163
|
+
// infer their types — an unused $N errors out at bind time.
|
|
164
|
+
const sql = `
|
|
165
|
+
WITH _params AS (
|
|
166
|
+
SELECT $1::text[] AS include_schemas, $2::text[] AS exclude_schemas
|
|
167
|
+
),
|
|
168
|
+
rels AS (
|
|
169
|
+
SELECT c.oid, n.nspname AS schema_name, c.relname AS table_name
|
|
170
|
+
FROM pg_class c
|
|
171
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
172
|
+
WHERE c.relkind IN ('r', 'p', 'v', 'm')
|
|
173
|
+
AND ${schemaFilter}
|
|
174
|
+
)
|
|
175
|
+
SELECT 'table'::text AS kind, r.schema_name, r.table_name, NULL::text AS member, d.description
|
|
176
|
+
FROM rels r
|
|
177
|
+
JOIN pg_description d
|
|
178
|
+
ON d.objoid = r.oid AND d.classoid = 'pg_class'::regclass AND d.objsubid = 0
|
|
179
|
+
UNION ALL
|
|
180
|
+
SELECT 'column', r.schema_name, r.table_name, a.attname, d.description
|
|
181
|
+
FROM rels r
|
|
182
|
+
JOIN pg_attribute a ON a.attrelid = r.oid AND a.attnum > 0 AND NOT a.attisdropped
|
|
183
|
+
JOIN pg_description d
|
|
184
|
+
ON d.objoid = r.oid AND d.classoid = 'pg_class'::regclass AND d.objsubid = a.attnum
|
|
185
|
+
UNION ALL
|
|
186
|
+
SELECT 'constraint', r.schema_name, r.table_name, co.conname, d.description
|
|
187
|
+
FROM rels r
|
|
188
|
+
JOIN pg_constraint co ON co.conrelid = r.oid
|
|
189
|
+
JOIN pg_description d
|
|
190
|
+
ON d.objoid = co.oid AND d.classoid = 'pg_constraint'::regclass
|
|
191
|
+
`;
|
|
192
|
+
const { rows } = await exec.query(sql, [options.schemas ?? [], excludes]);
|
|
193
|
+
const snapshot = emptyBehaviorSnapshot();
|
|
194
|
+
for (const row of rows) {
|
|
195
|
+
const tags = parseBehaviorTags(row.description);
|
|
196
|
+
const relation = `${row.schema_name}.${row.table_name}`;
|
|
197
|
+
if (row.kind === 'table') {
|
|
198
|
+
if (tags.both)
|
|
199
|
+
snapshot.tables.set(relation, tags.both);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!row.member)
|
|
203
|
+
continue;
|
|
204
|
+
const key = `${relation}.${row.member}`;
|
|
205
|
+
if (row.kind === 'column') {
|
|
206
|
+
if (tags.both)
|
|
207
|
+
snapshot.columns.set(key, tags.both);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (tags.both)
|
|
211
|
+
snapshot.constraints.set(key, tags.both);
|
|
212
|
+
if (tags.both || tags.forward || tags.backward) {
|
|
213
|
+
snapshot.constraintDirections.set(key, tags);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return snapshot;
|
|
217
|
+
}
|
package/pg/exposure.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exposure-surface resolution: what is actually reachable, and by whom.
|
|
3
|
+
*
|
|
4
|
+
* A database has more than one way in. The declared API is the one the score
|
|
5
|
+
* is *about* — `resolveExposure` returns it, unchanged, and it stays the
|
|
6
|
+
* headline. `resolvePlanes` returns it alongside every other access path the
|
|
7
|
+
* config declares or an adapter discovers, so a direct connection as some
|
|
8
|
+
* role can be graded too, without moving the number that describes the API.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExposureConfig, PlaneKind } from '../config/types';
|
|
11
|
+
import type { ApiReach } from '../exposure/reach';
|
|
12
|
+
import type { QueryExecutor } from './introspect';
|
|
13
|
+
export interface ResolvedExposure {
|
|
14
|
+
/** True when a surface was configured or auto-resolved. */
|
|
15
|
+
known: boolean;
|
|
16
|
+
source: string;
|
|
17
|
+
schemas: string[];
|
|
18
|
+
roles?: string[];
|
|
19
|
+
/** The subset of `roles` reachable without authenticating. */
|
|
20
|
+
anonRoles?: string[];
|
|
21
|
+
}
|
|
22
|
+
/** A plane before its reach is computed against the table snapshot. */
|
|
23
|
+
export interface ResolvedPlane {
|
|
24
|
+
name: string;
|
|
25
|
+
kind: PlaneKind;
|
|
26
|
+
/** The headline plane: its score is `report.score`. Exactly one is primary. */
|
|
27
|
+
primary: boolean;
|
|
28
|
+
/** Where the plane came from: an adapter name, `config`, or `none`. */
|
|
29
|
+
source: string;
|
|
30
|
+
schemas: string[];
|
|
31
|
+
roles: string[];
|
|
32
|
+
anonRoles: string[];
|
|
33
|
+
}
|
|
34
|
+
export declare const UNKNOWN_EXPOSURE: ResolvedExposure;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the primary exposure surface: the declared API. Adapters run first
|
|
37
|
+
* (static `schemas`/`roles` extend, never replace, what they find), then the
|
|
38
|
+
* static surface, then `UNKNOWN_EXPOSURE`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveExposure(exec: QueryExecutor, config?: ExposureConfig): Promise<ResolvedExposure>;
|
|
41
|
+
/**
|
|
42
|
+
* Every plane to grade, primary first.
|
|
43
|
+
*
|
|
44
|
+
* The primary plane is the resolved exposure surface — the same set of
|
|
45
|
+
* schemas the score has always been computed against — so adding planes
|
|
46
|
+
* cannot move the headline number. Secondary planes come from adapters (one
|
|
47
|
+
* per API, where a stack has several) and from `exposure.planes`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolvePlanes(exec: QueryExecutor, config: ExposureConfig | undefined, primary: ResolvedExposure): Promise<ResolvedPlane[]>;
|
|
50
|
+
/**
|
|
51
|
+
* Relation-level reach for the exposed schemas, from whichever adapters can
|
|
52
|
+
* compute it.
|
|
53
|
+
*
|
|
54
|
+
* Adapters *intersect*: a relation is subtracted from a plane only when every
|
|
55
|
+
* adapter that has an opinion agrees it is unaddressable. One adapter's
|
|
56
|
+
* silence is not agreement, so an adapter without `reach` is not consulted,
|
|
57
|
+
* but an adapter that ran and did not name a relation is a positive vote to
|
|
58
|
+
* keep it.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveReach(exec: QueryExecutor, config: ExposureConfig | undefined, context: {
|
|
61
|
+
schemas: string[];
|
|
62
|
+
excludeSchemas?: string[];
|
|
63
|
+
}): Promise<ApiReach | undefined>;
|
|
64
|
+
/**
|
|
65
|
+
* Constructive routing-plane introspection.
|
|
66
|
+
*
|
|
67
|
+
* @deprecated Use `constructiveAdapter` from `safegres/adapters`; this wrapper
|
|
68
|
+
* preserves the pre-adapter shape (one union of every API) for callers that
|
|
69
|
+
* still import it.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveConstructiveExposure(exec: QueryExecutor): Promise<{
|
|
72
|
+
schemas: string[];
|
|
73
|
+
roles: string[];
|
|
74
|
+
} | null>;
|
package/pg/exposure.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Exposure-surface resolution: what is actually reachable, and by whom.
|
|
4
|
+
*
|
|
5
|
+
* A database has more than one way in. The declared API is the one the score
|
|
6
|
+
* is *about* — `resolveExposure` returns it, unchanged, and it stays the
|
|
7
|
+
* headline. `resolvePlanes` returns it alongside every other access path the
|
|
8
|
+
* config declares or an adapter discovers, so a direct connection as some
|
|
9
|
+
* role can be graded too, without moving the number that describes the API.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.UNKNOWN_EXPOSURE = void 0;
|
|
13
|
+
exports.resolveExposure = resolveExposure;
|
|
14
|
+
exports.resolvePlanes = resolvePlanes;
|
|
15
|
+
exports.resolveReach = resolveReach;
|
|
16
|
+
exports.resolveConstructiveExposure = resolveConstructiveExposure;
|
|
17
|
+
const adapters_1 = require("../exposure/adapters");
|
|
18
|
+
exports.UNKNOWN_EXPOSURE = {
|
|
19
|
+
known: false,
|
|
20
|
+
source: 'none',
|
|
21
|
+
schemas: []
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the primary exposure surface: the declared API. Adapters run first
|
|
25
|
+
* (static `schemas`/`roles` extend, never replace, what they find), then the
|
|
26
|
+
* static surface, then `UNKNOWN_EXPOSURE`.
|
|
27
|
+
*/
|
|
28
|
+
async function resolveExposure(exec, config) {
|
|
29
|
+
if (!config)
|
|
30
|
+
return exports.UNKNOWN_EXPOSURE;
|
|
31
|
+
for (const adapter of adaptersFor(config)) {
|
|
32
|
+
const planes = await runAdapter(exec, adapter);
|
|
33
|
+
if (planes.length === 0)
|
|
34
|
+
continue;
|
|
35
|
+
const primary = planes.find((p) => p.primary) ?? planes[0];
|
|
36
|
+
return {
|
|
37
|
+
known: true,
|
|
38
|
+
source: adapter.name,
|
|
39
|
+
schemas: union(primary.schemas ?? [], config.schemas),
|
|
40
|
+
roles: union(primary.roles ?? [], config.roles),
|
|
41
|
+
anonRoles: union(primary.anonRoles ?? [], config.anonRoles)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (config.schemas && config.schemas.length > 0) {
|
|
45
|
+
return {
|
|
46
|
+
known: true,
|
|
47
|
+
source: 'config',
|
|
48
|
+
schemas: [...config.schemas].sort(),
|
|
49
|
+
roles: config.roles,
|
|
50
|
+
anonRoles: config.anonRoles
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return exports.UNKNOWN_EXPOSURE;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Every plane to grade, primary first.
|
|
57
|
+
*
|
|
58
|
+
* The primary plane is the resolved exposure surface — the same set of
|
|
59
|
+
* schemas the score has always been computed against — so adding planes
|
|
60
|
+
* cannot move the headline number. Secondary planes come from adapters (one
|
|
61
|
+
* per API, where a stack has several) and from `exposure.planes`.
|
|
62
|
+
*/
|
|
63
|
+
async function resolvePlanes(exec, config, primary) {
|
|
64
|
+
const primaryName = config?.name ?? 'api';
|
|
65
|
+
const planes = [
|
|
66
|
+
{
|
|
67
|
+
name: primaryName,
|
|
68
|
+
kind: 'api',
|
|
69
|
+
primary: true,
|
|
70
|
+
source: primary.source,
|
|
71
|
+
schemas: primary.schemas,
|
|
72
|
+
roles: primary.roles ?? [],
|
|
73
|
+
anonRoles: primary.anonRoles ?? []
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
const seen = new Set([primaryName]);
|
|
77
|
+
const push = (input, source) => {
|
|
78
|
+
if (input.primary || seen.has(input.name))
|
|
79
|
+
return;
|
|
80
|
+
seen.add(input.name);
|
|
81
|
+
planes.push({
|
|
82
|
+
name: input.name,
|
|
83
|
+
kind: input.kind ?? defaultKind(input),
|
|
84
|
+
primary: false,
|
|
85
|
+
source,
|
|
86
|
+
schemas: [...(input.schemas ?? [])].sort(),
|
|
87
|
+
roles: [...(input.roles ?? [])].sort(),
|
|
88
|
+
anonRoles: [...(input.anonRoles ?? [])].sort()
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
if (config && primary.known) {
|
|
92
|
+
for (const adapter of adaptersFor(config)) {
|
|
93
|
+
for (const plane of await runAdapter(exec, adapter))
|
|
94
|
+
push(plane, adapter.name);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const declared = config?.planes ?? [];
|
|
98
|
+
const claimingPrimary = declared.filter((p) => p.primary);
|
|
99
|
+
if (claimingPrimary.length > 1) {
|
|
100
|
+
throw new Error('more than one exposure plane declares `primary: true` '
|
|
101
|
+
+ `(${claimingPrimary.map((p) => p.name).join(', ')}) — exactly one plane is the headline score`);
|
|
102
|
+
}
|
|
103
|
+
for (const plane of declared)
|
|
104
|
+
push(plane, 'config');
|
|
105
|
+
// A declared plane may take the headline over from the API surface: a
|
|
106
|
+
// database whose product *is* direct SQL access has no API to grade.
|
|
107
|
+
const override = claimingPrimary[0];
|
|
108
|
+
if (override) {
|
|
109
|
+
planes[0] = {
|
|
110
|
+
name: override.name,
|
|
111
|
+
kind: override.kind ?? defaultKind(override),
|
|
112
|
+
primary: true,
|
|
113
|
+
source: 'config',
|
|
114
|
+
schemas: [...(override.schemas ?? [])].sort(),
|
|
115
|
+
roles: [...(override.roles ?? [])].sort(),
|
|
116
|
+
anonRoles: [...(override.anonRoles ?? [])].sort()
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return planes;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Relation-level reach for the exposed schemas, from whichever adapters can
|
|
123
|
+
* compute it.
|
|
124
|
+
*
|
|
125
|
+
* Adapters *intersect*: a relation is subtracted from a plane only when every
|
|
126
|
+
* adapter that has an opinion agrees it is unaddressable. One adapter's
|
|
127
|
+
* silence is not agreement, so an adapter without `reach` is not consulted,
|
|
128
|
+
* but an adapter that ran and did not name a relation is a positive vote to
|
|
129
|
+
* keep it.
|
|
130
|
+
*/
|
|
131
|
+
async function resolveReach(exec, config, context) {
|
|
132
|
+
if (!config || config.reach === false)
|
|
133
|
+
return undefined;
|
|
134
|
+
let agreed;
|
|
135
|
+
const hidden = new Set();
|
|
136
|
+
let ran = false;
|
|
137
|
+
for (const adapter of adaptersFor(config)) {
|
|
138
|
+
if (!adapter.reach)
|
|
139
|
+
continue;
|
|
140
|
+
if (!(await adapter.detect(exec)))
|
|
141
|
+
continue;
|
|
142
|
+
const reach = await adapter.reach(exec, context);
|
|
143
|
+
ran = true;
|
|
144
|
+
for (const key of reach.hiddenBackwardRelations)
|
|
145
|
+
hidden.add(key);
|
|
146
|
+
const named = new Map(reach.unreachable.map((r) => [`${r.schema}.${r.table}`, r]));
|
|
147
|
+
if (agreed === undefined)
|
|
148
|
+
agreed = named;
|
|
149
|
+
else
|
|
150
|
+
for (const key of [...agreed.keys()])
|
|
151
|
+
if (!named.has(key))
|
|
152
|
+
agreed.delete(key);
|
|
153
|
+
}
|
|
154
|
+
if (!ran)
|
|
155
|
+
return undefined;
|
|
156
|
+
return {
|
|
157
|
+
unreachable: [...(agreed?.values() ?? [])].sort((a, b) => `${a.schema}.${a.table}`.localeCompare(`${b.schema}.${b.table}`)),
|
|
158
|
+
hiddenBackwardRelations: [...hidden].sort()
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function defaultKind(plane) {
|
|
162
|
+
if (plane.kind)
|
|
163
|
+
return plane.kind;
|
|
164
|
+
return plane.roles && plane.roles.length > 0 && !(plane.schemas && plane.schemas.length > 0)
|
|
165
|
+
? 'role'
|
|
166
|
+
: 'schema';
|
|
167
|
+
}
|
|
168
|
+
/** Adapters named by the config, plus the `resolver` alias for the built-in. */
|
|
169
|
+
function adaptersFor(config) {
|
|
170
|
+
const adapters = (0, adapters_1.resolveAdapters)(config.adapters);
|
|
171
|
+
if (config.resolver && config.resolver !== 'static') {
|
|
172
|
+
const builtin = adapters_1.BUILTIN_ADAPTERS[config.resolver];
|
|
173
|
+
if (builtin && !adapters.some((a) => a.name === builtin.name))
|
|
174
|
+
adapters.unshift(builtin);
|
|
175
|
+
}
|
|
176
|
+
return adapters;
|
|
177
|
+
}
|
|
178
|
+
/** An adapter that isn't present in this database contributes nothing. */
|
|
179
|
+
async function runAdapter(exec, adapter) {
|
|
180
|
+
if (!(await adapter.detect(exec)))
|
|
181
|
+
return [];
|
|
182
|
+
return adapter.resolve(exec);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Constructive routing-plane introspection.
|
|
186
|
+
*
|
|
187
|
+
* @deprecated Use `constructiveAdapter` from `safegres/adapters`; this wrapper
|
|
188
|
+
* preserves the pre-adapter shape (one union of every API) for callers that
|
|
189
|
+
* still import it.
|
|
190
|
+
*/
|
|
191
|
+
async function resolveConstructiveExposure(exec) {
|
|
192
|
+
const adapter = adapters_1.BUILTIN_ADAPTERS.constructive;
|
|
193
|
+
if (!(await adapter.detect(exec)))
|
|
194
|
+
return null;
|
|
195
|
+
const planes = await adapter.resolve(exec);
|
|
196
|
+
const primary = planes.find((p) => p.primary) ?? planes[0];
|
|
197
|
+
if (!primary || (primary.schemas ?? []).length === 0)
|
|
198
|
+
return null;
|
|
199
|
+
return { schemas: primary.schemas ?? [], roles: primary.roles ?? [] };
|
|
200
|
+
}
|
|
201
|
+
function union(base, extra) {
|
|
202
|
+
if (!extra || extra.length === 0)
|
|
203
|
+
return base;
|
|
204
|
+
return [...new Set([...base, ...extra])].sort();
|
|
205
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function catalog introspection for the call-graph audit.
|
|
3
|
+
*
|
|
4
|
+
* One query over `pg_proc` returns every user-defined function with the
|
|
5
|
+
* attributes the trust-boundary analysis needs: SECURITY DEFINER vs INVOKER,
|
|
6
|
+
* the owner role (what a DEFINER actually runs as), whether `search_path`
|
|
7
|
+
* is pinned, the language, the body source, and EXECUTE grants per role.
|
|
8
|
+
*/
|
|
9
|
+
import type { ExtensionScopeOptions, QueryExecutor } from './introspect';
|
|
10
|
+
export interface FunctionGrant {
|
|
11
|
+
role: string;
|
|
12
|
+
/** Was the grant delegated (`WITH GRANT OPTION`). */
|
|
13
|
+
grantable: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface FunctionSnapshot {
|
|
16
|
+
oid: number;
|
|
17
|
+
schema: string;
|
|
18
|
+
name: string;
|
|
19
|
+
/** Argument type signature, e.g. `text, uuid` — disambiguates overloads. */
|
|
20
|
+
args: string;
|
|
21
|
+
owner: string;
|
|
22
|
+
/** Owner is a superuser or has BYPASSRLS — RLS never applies to it. */
|
|
23
|
+
ownerBypassesRls: boolean;
|
|
24
|
+
isSecurityDefiner: boolean;
|
|
25
|
+
/** True when `proconfig` pins `search_path` (CWE-426 mitigation for DEFINERs). */
|
|
26
|
+
searchPathPinned: boolean;
|
|
27
|
+
language: string;
|
|
28
|
+
/** Raw body (`prosrc`); null for C/internal functions. */
|
|
29
|
+
source: string | null;
|
|
30
|
+
/** Full `CREATE FUNCTION` statement — what `parsePlPgSQL` consumes. */
|
|
31
|
+
definition: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* EXECUTE grants. `PUBLIC` appears as the role name `PUBLIC`.
|
|
34
|
+
* When `proacl` is NULL Postgres applies the default function ACL, which
|
|
35
|
+
* includes EXECUTE to PUBLIC — represented here as a synthetic PUBLIC grant.
|
|
36
|
+
*/
|
|
37
|
+
grants: FunctionGrant[];
|
|
38
|
+
/** True when the grants came from the default ACL (no explicit GRANT/REVOKE). */
|
|
39
|
+
defaultAcl: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The function returns `trigger`, so Postgres refuses to call it directly
|
|
42
|
+
* however wide its EXECUTE ACL is. EXECUTE on one confers nothing on its
|
|
43
|
+
* own: the body is only reachable by firing the trigger it is attached to.
|
|
44
|
+
*/
|
|
45
|
+
returnsTrigger: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface IntrospectFunctionOptions {
|
|
48
|
+
schemas?: string[];
|
|
49
|
+
excludeSchemas?: string[];
|
|
50
|
+
extensions?: ExtensionScopeOptions;
|
|
51
|
+
}
|
|
52
|
+
export declare function introspectFunctions(exec: QueryExecutor, options?: IntrospectFunctionOptions): Promise<FunctionSnapshot[]>;
|
package/pg/functions.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Function catalog introspection for the call-graph audit.
|
|
4
|
+
*
|
|
5
|
+
* One query over `pg_proc` returns every user-defined function with the
|
|
6
|
+
* attributes the trust-boundary analysis needs: SECURITY DEFINER vs INVOKER,
|
|
7
|
+
* the owner role (what a DEFINER actually runs as), whether `search_path`
|
|
8
|
+
* is pinned, the language, the body source, and EXECUTE grants per role.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.introspectFunctions = introspectFunctions;
|
|
12
|
+
const DEFAULT_EXCLUDES = ['pg_catalog', 'information_schema', 'pg_toast'];
|
|
13
|
+
/**
|
|
14
|
+
* Extension scoping for routines. The table-side filter reads ownership off
|
|
15
|
+
* `pg_class`; a function's is on `pg_proc`, and nothing about a routine is
|
|
16
|
+
* inherited the way a partition inherits its parent's dependency — so this is
|
|
17
|
+
* the same rule expressed against the other catalog, not a shared helper.
|
|
18
|
+
*
|
|
19
|
+
* Without it `extensions.ignore` narrowed the relation rules and left the
|
|
20
|
+
* routine rules reading the extension's own bodies: `pg_partman` alone
|
|
21
|
+
* accounted for every C1 finding and 60% of C4 on a real audit.
|
|
22
|
+
*/
|
|
23
|
+
function routineExtensionFilter(options, paramIndex) {
|
|
24
|
+
const clauses = [];
|
|
25
|
+
if (options?.skipOwned ?? true) {
|
|
26
|
+
clauses.push(`
|
|
27
|
+
AND NOT EXISTS (
|
|
28
|
+
SELECT 1 FROM pg_depend d
|
|
29
|
+
WHERE d.classid = 'pg_proc'::regclass
|
|
30
|
+
AND d.refclassid = 'pg_extension'::regclass
|
|
31
|
+
AND d.deptype = 'e'
|
|
32
|
+
AND d.objid = p.oid
|
|
33
|
+
)`);
|
|
34
|
+
}
|
|
35
|
+
clauses.push(`
|
|
36
|
+
AND NOT (n.oid = ANY (
|
|
37
|
+
SELECT e.extnamespace FROM pg_extension e WHERE e.extname = ANY($${paramIndex}::text[])
|
|
38
|
+
))`);
|
|
39
|
+
return clauses.join('');
|
|
40
|
+
}
|
|
41
|
+
async function introspectFunctions(exec, options = {}) {
|
|
42
|
+
const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
|
|
43
|
+
const schemaFilter = options.schemas && options.schemas.length > 0
|
|
44
|
+
? `AND n.nspname = ANY($1::text[])`
|
|
45
|
+
: `AND NOT (n.nspname = ANY($2::text[]))`;
|
|
46
|
+
const extFilter = routineExtensionFilter(options.extensions, 3);
|
|
47
|
+
const sql = `
|
|
48
|
+
WITH _params AS (
|
|
49
|
+
SELECT $1::text[] AS include_schemas, $2::text[] AS exclude_schemas,
|
|
50
|
+
$3::text[] AS ignore_extensions
|
|
51
|
+
),
|
|
52
|
+
procs AS (
|
|
53
|
+
SELECT
|
|
54
|
+
p.oid,
|
|
55
|
+
n.nspname AS schema_name,
|
|
56
|
+
p.proname AS name,
|
|
57
|
+
pg_get_function_identity_arguments(p.oid) AS args,
|
|
58
|
+
pg_get_userbyid(p.proowner) AS owner,
|
|
59
|
+
COALESCE(o.rolsuper OR o.rolbypassrls, false) AS owner_bypasses_rls,
|
|
60
|
+
p.prosecdef AS security_definer,
|
|
61
|
+
EXISTS (
|
|
62
|
+
SELECT 1 FROM unnest(COALESCE(p.proconfig, ARRAY[]::text[])) cfg
|
|
63
|
+
WHERE cfg LIKE 'search_path=%'
|
|
64
|
+
) AS search_path_pinned,
|
|
65
|
+
l.lanname AS language,
|
|
66
|
+
p.prorettype = 'pg_catalog.trigger'::regtype AS returns_trigger,
|
|
67
|
+
p.prosrc AS source,
|
|
68
|
+
CASE WHEN l.lanname IN ('sql', 'plpgsql')
|
|
69
|
+
THEN pg_get_functiondef(p.oid)
|
|
70
|
+
ELSE NULL END AS definition,
|
|
71
|
+
p.proacl AS proacl
|
|
72
|
+
FROM pg_proc p
|
|
73
|
+
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
74
|
+
JOIN pg_language l ON l.oid = p.prolang
|
|
75
|
+
LEFT JOIN pg_roles o ON o.oid = p.proowner
|
|
76
|
+
WHERE p.prokind IN ('f', 'p')
|
|
77
|
+
${schemaFilter}${extFilter}
|
|
78
|
+
),
|
|
79
|
+
grants AS (
|
|
80
|
+
SELECT
|
|
81
|
+
pr.oid,
|
|
82
|
+
CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE rol.rolname END AS grantee,
|
|
83
|
+
a.is_grantable
|
|
84
|
+
FROM procs pr,
|
|
85
|
+
LATERAL aclexplode(pr.proacl) a
|
|
86
|
+
LEFT JOIN pg_roles rol ON rol.oid = a.grantee
|
|
87
|
+
WHERE pr.proacl IS NOT NULL
|
|
88
|
+
AND a.privilege_type = 'EXECUTE'
|
|
89
|
+
)
|
|
90
|
+
SELECT
|
|
91
|
+
p.oid::int AS oid,
|
|
92
|
+
p.schema_name,
|
|
93
|
+
p.name,
|
|
94
|
+
p.args,
|
|
95
|
+
p.owner,
|
|
96
|
+
p.owner_bypasses_rls,
|
|
97
|
+
p.security_definer,
|
|
98
|
+
p.search_path_pinned,
|
|
99
|
+
p.returns_trigger,
|
|
100
|
+
p.language,
|
|
101
|
+
p.source,
|
|
102
|
+
p.definition,
|
|
103
|
+
COALESCE(
|
|
104
|
+
(SELECT jsonb_agg(jsonb_build_object('role', g.grantee, 'grantable', g.is_grantable))
|
|
105
|
+
FROM grants g WHERE g.oid = p.oid),
|
|
106
|
+
'[]'::jsonb
|
|
107
|
+
) AS grants,
|
|
108
|
+
(p.proacl IS NULL) AS default_acl
|
|
109
|
+
FROM procs p
|
|
110
|
+
ORDER BY p.schema_name, p.name, p.args
|
|
111
|
+
`;
|
|
112
|
+
const { rows } = await exec.query(sql, [options.schemas ?? [], excludes, options.extensions?.ignore ?? []]);
|
|
113
|
+
return rows.map((r) => ({
|
|
114
|
+
oid: r.oid,
|
|
115
|
+
schema: r.schema_name,
|
|
116
|
+
name: r.name,
|
|
117
|
+
args: r.args,
|
|
118
|
+
owner: r.owner,
|
|
119
|
+
ownerBypassesRls: r.owner_bypasses_rls,
|
|
120
|
+
isSecurityDefiner: r.security_definer,
|
|
121
|
+
searchPathPinned: r.search_path_pinned,
|
|
122
|
+
returnsTrigger: r.returns_trigger,
|
|
123
|
+
language: r.language,
|
|
124
|
+
source: r.source,
|
|
125
|
+
definition: r.definition,
|
|
126
|
+
grants: r.default_acl
|
|
127
|
+
? [{ role: 'PUBLIC', grantable: false }]
|
|
128
|
+
: r.grants.map((g) => ({ role: g.role, grantable: g.grantable })),
|
|
129
|
+
defaultAcl: r.default_acl
|
|
130
|
+
}));
|
|
131
|
+
}
|