@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,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-query catalog introspection for the safegres audit.
|
|
3
|
+
*
|
|
4
|
+
* Pulls every relation + every policy + (parsed) role ACL per role in one shot,
|
|
5
|
+
* so Script A can process the whole database from one in-memory snapshot.
|
|
6
|
+
*/
|
|
7
|
+
import type { Client, Pool } from 'pg';
|
|
8
|
+
/** Minimal query interface — accepts pg.Client, pg.Pool, or any `{ query }`. */
|
|
9
|
+
export interface QueryExecutor {
|
|
10
|
+
query<T = unknown>(text: string, params?: unknown[]): Promise<{
|
|
11
|
+
rows: T[];
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* `USAGE` never appears on a table — it is the sequence privilege (`nextval`),
|
|
16
|
+
* and `EXECUTE` is the function one. Both are in this union because sequences
|
|
17
|
+
* and functions are graded through the same grant closure as everything else,
|
|
18
|
+
* and a reach path routinely starts on one and ends on a table.
|
|
19
|
+
*/
|
|
20
|
+
export type PgPrivilege = 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'TRUNCATE' | 'REFERENCES' | 'TRIGGER' | 'USAGE' | 'EXECUTE';
|
|
21
|
+
/**
|
|
22
|
+
* Mapping of ACL single-letter codes (from `aclexplode`) to our privilege names.
|
|
23
|
+
* `r` = SELECT, `a` = INSERT, `w` = UPDATE, `d` = DELETE, etc.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ACL_PRIVILEGE_MAP: Record<string, PgPrivilege>;
|
|
26
|
+
export interface GrantInfo {
|
|
27
|
+
role: string;
|
|
28
|
+
privilege: PgPrivilege;
|
|
29
|
+
/** Was the grant delegated (i.e. `WITH GRANT OPTION`). */
|
|
30
|
+
grantable: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* `true` if the grantee role is a superuser or has `BYPASSRLS`. RLS policies
|
|
33
|
+
* do not apply to this role, so coverage findings are suppressed for it.
|
|
34
|
+
* `false` for `PUBLIC` and all other grantees.
|
|
35
|
+
*/
|
|
36
|
+
bypassRls: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A grant on one column (`pg_attribute.attacl`), which Postgres keeps entirely
|
|
40
|
+
* separate from the table's own ACL: `GRANT SELECT (secret) ON t TO anon`
|
|
41
|
+
* leaves `relacl` untouched, so any analysis that reads only `relacl`
|
|
42
|
+
* concludes the role reaches nothing.
|
|
43
|
+
*
|
|
44
|
+
* Only SELECT, INSERT, UPDATE and REFERENCES can be column-scoped.
|
|
45
|
+
*/
|
|
46
|
+
export interface ColumnGrantInfo extends GrantInfo {
|
|
47
|
+
column: string;
|
|
48
|
+
}
|
|
49
|
+
/** `pg_policy.polcmd` uses: `r` SELECT, `a` INSERT, `w` UPDATE, `d` DELETE, `*` ALL. */
|
|
50
|
+
export type PolicyCmd = 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'ALL';
|
|
51
|
+
export declare const POLICY_CMD_MAP: Record<string, PolicyCmd>;
|
|
52
|
+
export interface PolicyInfo {
|
|
53
|
+
name: string;
|
|
54
|
+
cmd: PolicyCmd;
|
|
55
|
+
permissive: boolean;
|
|
56
|
+
/** Role names the policy applies to. `['PUBLIC']` if `polroles = {0}`. */
|
|
57
|
+
roles: string[];
|
|
58
|
+
using: string | null;
|
|
59
|
+
withCheck: string | null;
|
|
60
|
+
}
|
|
61
|
+
export interface TableSnapshot {
|
|
62
|
+
schema: string;
|
|
63
|
+
name: string;
|
|
64
|
+
oid: number;
|
|
65
|
+
rlsEnabled: boolean;
|
|
66
|
+
rlsForced: boolean;
|
|
67
|
+
/** `true` if the table is partitioned or is a view — used to skip irrelevant findings. */
|
|
68
|
+
isPartitioned: boolean;
|
|
69
|
+
owner: string;
|
|
70
|
+
grants: GrantInfo[];
|
|
71
|
+
/**
|
|
72
|
+
* Grants on individual columns. Disjoint from {@link TableSnapshot.grants}:
|
|
73
|
+
* a column grant never appears in `relacl`, and a table grant never appears
|
|
74
|
+
* in `attacl`, so a role can reach a relation through this list alone.
|
|
75
|
+
*/
|
|
76
|
+
columnGrants: ColumnGrantInfo[];
|
|
77
|
+
policies: PolicyInfo[];
|
|
78
|
+
}
|
|
79
|
+
export interface IntrospectOptions {
|
|
80
|
+
/** Schemas to include. If omitted, all non-system schemas. */
|
|
81
|
+
schemas?: string[];
|
|
82
|
+
/**
|
|
83
|
+
* Schemas to exclude. Applied on top of `schemas`.
|
|
84
|
+
* Defaults to `['pg_catalog', 'information_schema', 'pg_toast']`.
|
|
85
|
+
*/
|
|
86
|
+
excludeSchemas?: string[];
|
|
87
|
+
/**
|
|
88
|
+
* Roles to include when reporting grants. If omitted, all roles returned
|
|
89
|
+
* by {@link listAuditableRoles}.
|
|
90
|
+
*/
|
|
91
|
+
roles?: string[];
|
|
92
|
+
/**
|
|
93
|
+
* How to treat relations that belong to an installed extension.
|
|
94
|
+
* Defaults to skipping them: they are the extension author's to secure and
|
|
95
|
+
* tune, not the application's, and they cannot be altered without breaking
|
|
96
|
+
* `pg_dump`/upgrade.
|
|
97
|
+
*/
|
|
98
|
+
extensions?: ExtensionScopeOptions;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Extension relations are the `node_modules` of a database: physically present,
|
|
102
|
+
* scanned like anything else, and not yours to edit.
|
|
103
|
+
*/
|
|
104
|
+
export interface ExtensionScopeOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Skip relations an extension owns (`pg_depend.deptype = 'e'`) and their
|
|
107
|
+
* partitions. Default `true`.
|
|
108
|
+
*/
|
|
109
|
+
skipOwned?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Extension names whose *schemas* are skipped wholesale, for objects the
|
|
112
|
+
* extension creates at runtime and never registers as dependencies —
|
|
113
|
+
* `pg_partman`'s child partitions and templates being the motivating case.
|
|
114
|
+
* Unknown/uninstalled names are ignored.
|
|
115
|
+
*/
|
|
116
|
+
ignore?: string[];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* SQL predicate excluding extension relations, for a query where `c` is the
|
|
120
|
+
* `pg_class` row and `n` its namespace. `paramIndex` is the bind position
|
|
121
|
+
* holding {@link ExtensionScopeOptions.ignore}.
|
|
122
|
+
*/
|
|
123
|
+
export declare function extensionFilter(options: ExtensionScopeOptions | undefined, paramIndex: number): string;
|
|
124
|
+
/**
|
|
125
|
+
* One-query snapshot of every table + its policies + its grants expanded per role.
|
|
126
|
+
*
|
|
127
|
+
* We deliberately avoid views that hide permissive vs restrictive distinctions
|
|
128
|
+
* (like `pg_policies`) — `pg_policy` gives us `polpermissive` directly.
|
|
129
|
+
*/
|
|
130
|
+
export declare function introspectTables(exec: QueryExecutor, options?: IntrospectOptions): Promise<TableSnapshot[]>;
|
|
131
|
+
/**
|
|
132
|
+
* Convenience: accept any of `pg.Client` / `pg.Pool` / a custom executor.
|
|
133
|
+
*/
|
|
134
|
+
export declare function asExecutor(client: Client | Pool | QueryExecutor): QueryExecutor;
|
package/pg/introspect.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Single-query catalog introspection for the safegres audit.
|
|
4
|
+
*
|
|
5
|
+
* Pulls every relation + every policy + (parsed) role ACL per role in one shot,
|
|
6
|
+
* so Script A can process the whole database from one in-memory snapshot.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.POLICY_CMD_MAP = exports.ACL_PRIVILEGE_MAP = void 0;
|
|
10
|
+
exports.extensionFilter = extensionFilter;
|
|
11
|
+
exports.introspectTables = introspectTables;
|
|
12
|
+
exports.asExecutor = asExecutor;
|
|
13
|
+
/**
|
|
14
|
+
* Mapping of ACL single-letter codes (from `aclexplode`) to our privilege names.
|
|
15
|
+
* `r` = SELECT, `a` = INSERT, `w` = UPDATE, `d` = DELETE, etc.
|
|
16
|
+
*/
|
|
17
|
+
exports.ACL_PRIVILEGE_MAP = {
|
|
18
|
+
r: 'SELECT',
|
|
19
|
+
a: 'INSERT',
|
|
20
|
+
w: 'UPDATE',
|
|
21
|
+
d: 'DELETE',
|
|
22
|
+
D: 'TRUNCATE',
|
|
23
|
+
x: 'REFERENCES',
|
|
24
|
+
t: 'TRIGGER'
|
|
25
|
+
};
|
|
26
|
+
exports.POLICY_CMD_MAP = {
|
|
27
|
+
r: 'SELECT',
|
|
28
|
+
a: 'INSERT',
|
|
29
|
+
w: 'UPDATE',
|
|
30
|
+
d: 'DELETE',
|
|
31
|
+
'*': 'ALL'
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* SQL predicate excluding extension relations, for a query where `c` is the
|
|
35
|
+
* `pg_class` row and `n` its namespace. `paramIndex` is the bind position
|
|
36
|
+
* holding {@link ExtensionScopeOptions.ignore}.
|
|
37
|
+
*/
|
|
38
|
+
function extensionFilter(options, paramIndex) {
|
|
39
|
+
const skipOwned = options?.skipOwned ?? true;
|
|
40
|
+
const clauses = [];
|
|
41
|
+
if (skipOwned) {
|
|
42
|
+
// A partition carries no extension dependency of its own, so ownership
|
|
43
|
+
// has to be read off the partition tree it belongs to — at any depth.
|
|
44
|
+
clauses.push(`
|
|
45
|
+
AND NOT EXISTS (
|
|
46
|
+
SELECT 1 FROM pg_depend d
|
|
47
|
+
WHERE d.classid = 'pg_class'::regclass
|
|
48
|
+
AND d.refclassid = 'pg_extension'::regclass
|
|
49
|
+
AND d.deptype = 'e'
|
|
50
|
+
AND (
|
|
51
|
+
d.objid = c.oid
|
|
52
|
+
OR d.objid IN (SELECT relid FROM pg_partition_ancestors(c.oid))
|
|
53
|
+
)
|
|
54
|
+
)`);
|
|
55
|
+
}
|
|
56
|
+
clauses.push(`
|
|
57
|
+
AND NOT (n.oid = ANY (
|
|
58
|
+
SELECT e.extnamespace FROM pg_extension e WHERE e.extname = ANY($${paramIndex}::text[])
|
|
59
|
+
))`);
|
|
60
|
+
return clauses.join('');
|
|
61
|
+
}
|
|
62
|
+
const DEFAULT_EXCLUDES = ['pg_catalog', 'information_schema', 'pg_toast'];
|
|
63
|
+
/**
|
|
64
|
+
* One-query snapshot of every table + its policies + its grants expanded per role.
|
|
65
|
+
*
|
|
66
|
+
* We deliberately avoid views that hide permissive vs restrictive distinctions
|
|
67
|
+
* (like `pg_policies`) — `pg_policy` gives us `polpermissive` directly.
|
|
68
|
+
*/
|
|
69
|
+
async function introspectTables(exec, options = {}) {
|
|
70
|
+
const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
|
|
71
|
+
// PUBLIC (grantee oid 0) always survives the role filter: it is not a role
|
|
72
|
+
// that can be included or excluded — it is every role, including every one
|
|
73
|
+
// in the filter.
|
|
74
|
+
const roleFilter = options.roles && options.roles.length > 0
|
|
75
|
+
? `AND (g.grantee_oid = 0 OR rol.rolname = ANY($3::text[]))`
|
|
76
|
+
: '';
|
|
77
|
+
const schemaFilter = options.schemas && options.schemas.length > 0
|
|
78
|
+
? `AND n.nspname = ANY($1::text[])`
|
|
79
|
+
: `AND NOT (n.nspname = ANY($2::text[]))`;
|
|
80
|
+
const extFilter = extensionFilter(options.extensions, 4);
|
|
81
|
+
// We reference every param at least once (even if trivially) so Postgres
|
|
82
|
+
// can infer types for all of them. Unused parameters otherwise error with
|
|
83
|
+
// "could not determine data type of parameter $N".
|
|
84
|
+
const sql = `
|
|
85
|
+
WITH _params AS (
|
|
86
|
+
SELECT
|
|
87
|
+
$1::text[] AS include_schemas,
|
|
88
|
+
$2::text[] AS exclude_schemas,
|
|
89
|
+
$3::text[] AS role_filter,
|
|
90
|
+
$4::text[] AS ignore_extensions
|
|
91
|
+
),
|
|
92
|
+
rels AS (
|
|
93
|
+
SELECT
|
|
94
|
+
n.nspname AS schema_name,
|
|
95
|
+
c.relname AS table_name,
|
|
96
|
+
c.oid AS oid,
|
|
97
|
+
c.relrowsecurity AS rls_enabled,
|
|
98
|
+
c.relforcerowsecurity AS rls_forced,
|
|
99
|
+
(c.relkind = 'p') AS is_partitioned,
|
|
100
|
+
pg_catalog.pg_get_userbyid(c.relowner) AS owner,
|
|
101
|
+
c.relacl AS relacl
|
|
102
|
+
FROM pg_class c
|
|
103
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
104
|
+
WHERE c.relkind IN ('r', 'p')
|
|
105
|
+
${schemaFilter}${extFilter}
|
|
106
|
+
),
|
|
107
|
+
grants_exploded AS (
|
|
108
|
+
SELECT
|
|
109
|
+
r.oid,
|
|
110
|
+
(aclexplode(r.relacl)).grantee AS grantee_oid,
|
|
111
|
+
(aclexplode(r.relacl)).privilege_type AS privilege_type,
|
|
112
|
+
(aclexplode(r.relacl)).is_grantable AS is_grantable
|
|
113
|
+
FROM rels r
|
|
114
|
+
WHERE r.relacl IS NOT NULL
|
|
115
|
+
),
|
|
116
|
+
grants AS (
|
|
117
|
+
SELECT
|
|
118
|
+
g.oid,
|
|
119
|
+
CASE WHEN g.grantee_oid = 0 THEN 'PUBLIC' ELSE rol.rolname END AS grantee,
|
|
120
|
+
g.privilege_type,
|
|
121
|
+
g.is_grantable,
|
|
122
|
+
CASE
|
|
123
|
+
WHEN g.grantee_oid = 0 THEN false
|
|
124
|
+
ELSE COALESCE(rol.rolsuper OR rol.rolbypassrls, false)
|
|
125
|
+
END AS bypass_rls
|
|
126
|
+
FROM grants_exploded g
|
|
127
|
+
LEFT JOIN pg_roles rol ON rol.oid = g.grantee_oid
|
|
128
|
+
WHERE true
|
|
129
|
+
${roleFilter}
|
|
130
|
+
),
|
|
131
|
+
-- Column ACLs live on pg_attribute, not pg_class: a column grant is
|
|
132
|
+
-- invisible to relacl, and this is the only place it surfaces.
|
|
133
|
+
col_grants_exploded AS (
|
|
134
|
+
SELECT
|
|
135
|
+
r.oid,
|
|
136
|
+
a.attname AS column_name,
|
|
137
|
+
(aclexplode(a.attacl)).grantee AS grantee_oid,
|
|
138
|
+
(aclexplode(a.attacl)).privilege_type AS privilege_type,
|
|
139
|
+
(aclexplode(a.attacl)).is_grantable AS is_grantable
|
|
140
|
+
FROM rels r
|
|
141
|
+
JOIN pg_attribute a ON a.attrelid = r.oid
|
|
142
|
+
WHERE a.attacl IS NOT NULL
|
|
143
|
+
AND a.attnum > 0
|
|
144
|
+
AND NOT a.attisdropped
|
|
145
|
+
),
|
|
146
|
+
col_grants AS (
|
|
147
|
+
SELECT
|
|
148
|
+
g.oid,
|
|
149
|
+
g.column_name,
|
|
150
|
+
CASE WHEN g.grantee_oid = 0 THEN 'PUBLIC' ELSE rol.rolname END AS grantee,
|
|
151
|
+
g.privilege_type,
|
|
152
|
+
g.is_grantable,
|
|
153
|
+
CASE
|
|
154
|
+
WHEN g.grantee_oid = 0 THEN false
|
|
155
|
+
ELSE COALESCE(rol.rolsuper OR rol.rolbypassrls, false)
|
|
156
|
+
END AS bypass_rls
|
|
157
|
+
FROM col_grants_exploded g
|
|
158
|
+
LEFT JOIN pg_roles rol ON rol.oid = g.grantee_oid
|
|
159
|
+
WHERE true
|
|
160
|
+
${roleFilter}
|
|
161
|
+
),
|
|
162
|
+
policies AS (
|
|
163
|
+
SELECT
|
|
164
|
+
p.polrelid AS oid,
|
|
165
|
+
p.polname AS name,
|
|
166
|
+
p.polcmd AS cmd,
|
|
167
|
+
p.polpermissive AS permissive,
|
|
168
|
+
CASE
|
|
169
|
+
WHEN p.polroles = ARRAY[0]::oid[] THEN ARRAY['PUBLIC']
|
|
170
|
+
ELSE COALESCE(
|
|
171
|
+
(SELECT array_agg(rolname) FROM pg_roles WHERE oid = ANY(p.polroles)),
|
|
172
|
+
ARRAY[]::text[]
|
|
173
|
+
)
|
|
174
|
+
END AS roles,
|
|
175
|
+
pg_get_expr(p.polqual, p.polrelid) AS using_expr,
|
|
176
|
+
pg_get_expr(p.polwithcheck, p.polrelid) AS with_check_expr
|
|
177
|
+
FROM pg_policy p
|
|
178
|
+
)
|
|
179
|
+
SELECT
|
|
180
|
+
r.schema_name,
|
|
181
|
+
r.table_name,
|
|
182
|
+
r.oid::int AS oid,
|
|
183
|
+
r.rls_enabled,
|
|
184
|
+
r.rls_forced,
|
|
185
|
+
r.is_partitioned,
|
|
186
|
+
r.owner,
|
|
187
|
+
COALESCE(
|
|
188
|
+
(SELECT jsonb_agg(jsonb_build_object(
|
|
189
|
+
'role', g.grantee,
|
|
190
|
+
'privilege', g.privilege_type,
|
|
191
|
+
'grantable', g.is_grantable,
|
|
192
|
+
'bypassRls', g.bypass_rls
|
|
193
|
+
)) FROM grants g WHERE g.oid = r.oid),
|
|
194
|
+
'[]'::jsonb
|
|
195
|
+
) AS grants,
|
|
196
|
+
COALESCE(
|
|
197
|
+
(SELECT jsonb_agg(jsonb_build_object(
|
|
198
|
+
'role', g.grantee,
|
|
199
|
+
'column', g.column_name,
|
|
200
|
+
'privilege', g.privilege_type,
|
|
201
|
+
'grantable', g.is_grantable,
|
|
202
|
+
'bypassRls', g.bypass_rls
|
|
203
|
+
) ORDER BY g.grantee, g.column_name, g.privilege_type)
|
|
204
|
+
FROM col_grants g WHERE g.oid = r.oid),
|
|
205
|
+
'[]'::jsonb
|
|
206
|
+
) AS column_grants,
|
|
207
|
+
COALESCE(
|
|
208
|
+
(SELECT jsonb_agg(jsonb_build_object(
|
|
209
|
+
'name', p.name,
|
|
210
|
+
'cmd', p.cmd::text,
|
|
211
|
+
'permissive', p.permissive,
|
|
212
|
+
'roles', to_jsonb(p.roles),
|
|
213
|
+
'using', p.using_expr,
|
|
214
|
+
'withCheck', p.with_check_expr
|
|
215
|
+
)) FROM policies p WHERE p.oid = r.oid),
|
|
216
|
+
'[]'::jsonb
|
|
217
|
+
) AS policies
|
|
218
|
+
FROM rels r
|
|
219
|
+
ORDER BY r.schema_name, r.table_name
|
|
220
|
+
`;
|
|
221
|
+
const params = [
|
|
222
|
+
options.schemas ?? [],
|
|
223
|
+
excludes,
|
|
224
|
+
options.roles ?? [],
|
|
225
|
+
options.extensions?.ignore ?? []
|
|
226
|
+
];
|
|
227
|
+
const { rows } = await exec.query(sql, params);
|
|
228
|
+
return rows.map((r) => ({
|
|
229
|
+
schema: r.schema_name,
|
|
230
|
+
name: r.table_name,
|
|
231
|
+
oid: r.oid,
|
|
232
|
+
rlsEnabled: r.rls_enabled,
|
|
233
|
+
rlsForced: r.rls_forced,
|
|
234
|
+
isPartitioned: r.is_partitioned,
|
|
235
|
+
owner: r.owner,
|
|
236
|
+
grants: r.grants.map((g) => ({
|
|
237
|
+
role: g.role,
|
|
238
|
+
privilege: normalizePrivilege(g.privilege),
|
|
239
|
+
grantable: g.grantable,
|
|
240
|
+
bypassRls: g.bypassRls === true
|
|
241
|
+
})),
|
|
242
|
+
columnGrants: r.column_grants.map((g) => ({
|
|
243
|
+
role: g.role,
|
|
244
|
+
column: g.column,
|
|
245
|
+
privilege: normalizePrivilege(g.privilege),
|
|
246
|
+
grantable: g.grantable,
|
|
247
|
+
bypassRls: g.bypassRls === true
|
|
248
|
+
})),
|
|
249
|
+
policies: r.policies.map((p) => ({
|
|
250
|
+
name: p.name,
|
|
251
|
+
cmd: exports.POLICY_CMD_MAP[p.cmd] ?? 'ALL',
|
|
252
|
+
permissive: p.permissive,
|
|
253
|
+
roles: p.roles,
|
|
254
|
+
using: p.using,
|
|
255
|
+
withCheck: p.withCheck
|
|
256
|
+
}))
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
function normalizePrivilege(raw) {
|
|
260
|
+
const upper = raw.toUpperCase();
|
|
261
|
+
switch (upper) {
|
|
262
|
+
case 'SELECT':
|
|
263
|
+
case 'INSERT':
|
|
264
|
+
case 'UPDATE':
|
|
265
|
+
case 'DELETE':
|
|
266
|
+
case 'TRUNCATE':
|
|
267
|
+
case 'REFERENCES':
|
|
268
|
+
case 'TRIGGER':
|
|
269
|
+
return upper;
|
|
270
|
+
default:
|
|
271
|
+
// `r`, `a`, `w`, `d` — ACL single-letter form
|
|
272
|
+
return exports.ACL_PRIVILEGE_MAP[raw] ?? 'SELECT';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Convenience: accept any of `pg.Client` / `pg.Pool` / a custom executor.
|
|
277
|
+
*/
|
|
278
|
+
function asExecutor(client) {
|
|
279
|
+
return client;
|
|
280
|
+
}
|
package/pg/objects.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACLs on the relations that are not tables.
|
|
3
|
+
*
|
|
4
|
+
* `introspectTables` reads `relkind IN ('r','p')` and `introspectViews` reads
|
|
5
|
+
* views and matviews, which leaves two kinds of relation carrying privileges
|
|
6
|
+
* that nothing in this package ever looked at:
|
|
7
|
+
*
|
|
8
|
+
* - **sequences** (`relkind = 'S'`), where `USAGE`/`UPDATE` let a role call
|
|
9
|
+
* `nextval`/`setval` and `SELECT` lets it read `last_value`; and
|
|
10
|
+
* - **foreign tables** (`relkind = 'f'`), which cannot carry RLS at all —
|
|
11
|
+
* `ALTER FOREIGN TABLE ... ENABLE ROW LEVEL SECURITY` is rejected by
|
|
12
|
+
* Postgres (verified on 18), so a grant on one is never row-filtered.
|
|
13
|
+
*
|
|
14
|
+
* Both are read here rather than folded into the table snapshot: neither has
|
|
15
|
+
* RLS state, policies or column ACLs, and giving them a `TableSnapshot` with
|
|
16
|
+
* those fields stubbed out would invite every table rule to grade something it
|
|
17
|
+
* does not understand.
|
|
18
|
+
*/
|
|
19
|
+
import type { GrantInfo, IntrospectOptions, QueryExecutor } from './introspect';
|
|
20
|
+
export type PgObjectKind = 'sequence' | 'foreign table';
|
|
21
|
+
export interface ObjectAclSnapshot {
|
|
22
|
+
schema: string;
|
|
23
|
+
name: string;
|
|
24
|
+
kind: PgObjectKind;
|
|
25
|
+
owner: string;
|
|
26
|
+
grants: GrantInfo[];
|
|
27
|
+
/** The foreign server behind a foreign table; absent for a sequence. */
|
|
28
|
+
server?: string;
|
|
29
|
+
/**
|
|
30
|
+
* For a sequence, the `schema.table.column` it is attached to by an
|
|
31
|
+
* `OWNED BY` dependency (a `serial`/identity column records one). A role
|
|
32
|
+
* that inserts into that table legitimately needs `USAGE`, which is what
|
|
33
|
+
* keeps the sequence rule from recommending a revoke.
|
|
34
|
+
*/
|
|
35
|
+
ownedBy?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function introspectObjectAcls(exec: QueryExecutor, options?: IntrospectOptions): Promise<ObjectAclSnapshot[]>;
|
package/pg/objects.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ACLs on the relations that are not tables.
|
|
4
|
+
*
|
|
5
|
+
* `introspectTables` reads `relkind IN ('r','p')` and `introspectViews` reads
|
|
6
|
+
* views and matviews, which leaves two kinds of relation carrying privileges
|
|
7
|
+
* that nothing in this package ever looked at:
|
|
8
|
+
*
|
|
9
|
+
* - **sequences** (`relkind = 'S'`), where `USAGE`/`UPDATE` let a role call
|
|
10
|
+
* `nextval`/`setval` and `SELECT` lets it read `last_value`; and
|
|
11
|
+
* - **foreign tables** (`relkind = 'f'`), which cannot carry RLS at all —
|
|
12
|
+
* `ALTER FOREIGN TABLE ... ENABLE ROW LEVEL SECURITY` is rejected by
|
|
13
|
+
* Postgres (verified on 18), so a grant on one is never row-filtered.
|
|
14
|
+
*
|
|
15
|
+
* Both are read here rather than folded into the table snapshot: neither has
|
|
16
|
+
* RLS state, policies or column ACLs, and giving them a `TableSnapshot` with
|
|
17
|
+
* those fields stubbed out would invite every table rule to grade something it
|
|
18
|
+
* does not understand.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.introspectObjectAcls = introspectObjectAcls;
|
|
22
|
+
const DEFAULT_EXCLUDES = ['pg_catalog', 'information_schema', 'pg_toast'];
|
|
23
|
+
async function introspectObjectAcls(exec, options = {}) {
|
|
24
|
+
const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
|
|
25
|
+
const schemaFilter = options.schemas && options.schemas.length > 0
|
|
26
|
+
? 'AND n.nspname = ANY($1::text[])'
|
|
27
|
+
: 'AND NOT (n.nspname = ANY($2::text[]))';
|
|
28
|
+
const sql = `
|
|
29
|
+
WITH _params AS (
|
|
30
|
+
SELECT $1::text[] AS include_schemas, $2::text[] AS exclude_schemas
|
|
31
|
+
),
|
|
32
|
+
rels AS (
|
|
33
|
+
SELECT
|
|
34
|
+
n.nspname AS schema_name,
|
|
35
|
+
c.relname AS object_name,
|
|
36
|
+
c.oid AS oid,
|
|
37
|
+
c.relkind AS relkind,
|
|
38
|
+
pg_catalog.pg_get_userbyid(c.relowner) AS owner,
|
|
39
|
+
c.relacl AS relacl
|
|
40
|
+
FROM pg_class c
|
|
41
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
42
|
+
WHERE c.relkind IN ('S', 'f')
|
|
43
|
+
${schemaFilter}
|
|
44
|
+
),
|
|
45
|
+
grants AS (
|
|
46
|
+
SELECT
|
|
47
|
+
r.oid,
|
|
48
|
+
CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE rol.rolname END AS grantee,
|
|
49
|
+
a.privilege_type,
|
|
50
|
+
a.is_grantable,
|
|
51
|
+
CASE
|
|
52
|
+
WHEN a.grantee = 0 THEN false
|
|
53
|
+
ELSE COALESCE(rol.rolsuper OR rol.rolbypassrls, false)
|
|
54
|
+
END AS bypass_rls
|
|
55
|
+
FROM rels r, aclexplode(r.relacl) a
|
|
56
|
+
LEFT JOIN pg_roles rol ON rol.oid = a.grantee
|
|
57
|
+
WHERE r.relacl IS NOT NULL
|
|
58
|
+
),
|
|
59
|
+
-- A serial/identity column records an OWNED BY dependency on its
|
|
60
|
+
-- sequence. The role inserting into that table needs USAGE on it, so the
|
|
61
|
+
-- link is what separates a load-bearing grant from a gratuitous one.
|
|
62
|
+
owners AS (
|
|
63
|
+
SELECT
|
|
64
|
+
d.objid AS seq_oid,
|
|
65
|
+
n.nspname || '.' || c.relname || '.' || a.attname AS owned_by
|
|
66
|
+
FROM pg_depend d
|
|
67
|
+
JOIN pg_class c ON c.oid = d.refobjid
|
|
68
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
69
|
+
JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
|
|
70
|
+
WHERE d.classid = 'pg_class'::regclass
|
|
71
|
+
AND d.refclassid = 'pg_class'::regclass
|
|
72
|
+
AND d.deptype IN ('a', 'i')
|
|
73
|
+
AND d.objid IN (SELECT oid FROM rels WHERE relkind = 'S')
|
|
74
|
+
)
|
|
75
|
+
SELECT
|
|
76
|
+
r.schema_name,
|
|
77
|
+
r.object_name,
|
|
78
|
+
r.relkind,
|
|
79
|
+
r.owner,
|
|
80
|
+
o.owned_by,
|
|
81
|
+
srv.srvname AS server_name,
|
|
82
|
+
COALESCE(
|
|
83
|
+
(
|
|
84
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
85
|
+
'role', g.grantee,
|
|
86
|
+
'privilege', g.privilege_type,
|
|
87
|
+
'grantable', g.is_grantable,
|
|
88
|
+
'bypassRls', g.bypass_rls
|
|
89
|
+
) ORDER BY g.grantee, g.privilege_type)
|
|
90
|
+
FROM grants g
|
|
91
|
+
WHERE g.oid = r.oid
|
|
92
|
+
),
|
|
93
|
+
'[]'::jsonb
|
|
94
|
+
) AS grants
|
|
95
|
+
FROM rels r
|
|
96
|
+
LEFT JOIN owners o ON o.seq_oid = r.oid
|
|
97
|
+
LEFT JOIN pg_foreign_table ft ON ft.ftrelid = r.oid
|
|
98
|
+
LEFT JOIN pg_foreign_server srv ON srv.oid = ft.ftserver
|
|
99
|
+
ORDER BY r.schema_name, r.object_name
|
|
100
|
+
`;
|
|
101
|
+
const { rows } = await exec.query(sql, [options.schemas ?? null, excludes]);
|
|
102
|
+
return rows.map((r) => ({
|
|
103
|
+
schema: r.schema_name,
|
|
104
|
+
name: r.object_name,
|
|
105
|
+
kind: r.relkind === 'S' ? 'sequence' : 'foreign table',
|
|
106
|
+
owner: r.owner,
|
|
107
|
+
grants: r.grants,
|
|
108
|
+
...(r.server_name ? { server: r.server_name } : {}),
|
|
109
|
+
...(r.owned_by ? { ownedBy: r.owned_by } : {})
|
|
110
|
+
}));
|
|
111
|
+
}
|
package/pg/paths.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access-path signals: independent, individually-auditable pieces of evidence
|
|
3
|
+
* about whether anything reads a foreign key.
|
|
4
|
+
*
|
|
5
|
+
* X1 ("foreign key with no covering index") is right about the mechanics — a
|
|
6
|
+
* `DELETE` on the parent really does scan the child — but it assumes the child
|
|
7
|
+
* is a relation somebody traverses. On a provisioning-config table that
|
|
8
|
+
* assumption is false, and acting on the finding buys a write penalty on every
|
|
9
|
+
* insert in exchange for speeding up a scan of one row.
|
|
10
|
+
*
|
|
11
|
+
* The tempting gate, `pg_class.reltuples`, is useless here: safegres grades an
|
|
12
|
+
* ephemeral CI database that has never held data, so every row estimate is 0
|
|
13
|
+
* at exactly the moment we grade. Worse, row count is the wrong question — a
|
|
14
|
+
* huge append-only log nobody joins on wants no FK index, and a tiny lookup
|
|
15
|
+
* table hammered by every request does. The property we need is *reachability*.
|
|
16
|
+
*
|
|
17
|
+
* Reachability is not one boolean, and this module deliberately does not
|
|
18
|
+
* compute one. It collects signals, each of which points in one direction and
|
|
19
|
+
* says why, and leaves the verdict to the caller:
|
|
20
|
+
*
|
|
21
|
+
* - `policy-read` and `view-read` are **reads**. They are decisive: the
|
|
22
|
+
* database itself traverses the column, so no amount of contrary evidence
|
|
23
|
+
* makes the path unreachable.
|
|
24
|
+
* - `write-once-pointer` and `config-record` are **shape**. They are not
|
|
25
|
+
* evidence of unreachability, only of a schema idiom — a `NOT NULL` key
|
|
26
|
+
* defaulting to the nil UUID is a slot a provisioner fills in, and a table
|
|
27
|
+
* with several of them looks like a config record rather than a relation.
|
|
28
|
+
*
|
|
29
|
+
* Shape alone must never suppress a finding. A generated API can expose a
|
|
30
|
+
* reverse relation over any foreign key regardless of how its default is
|
|
31
|
+
* written, and if it does, the path is reachable and the index is wanted.
|
|
32
|
+
*
|
|
33
|
+
* - `behavior-hidden` is the third kind, **declared**: the schema author has
|
|
34
|
+
* said the reverse relation is not in the generated API. It comes from the
|
|
35
|
+
* same behavior tags exposure reach reads, so the two axes agree about what
|
|
36
|
+
* the API contains rather than each guessing separately. It is reported and
|
|
37
|
+
* not acted on — a hidden relation is one missing path, not proof that
|
|
38
|
+
* nothing traverses the key, since the referential-integrity scan on a
|
|
39
|
+
* parent `DELETE` runs whatever the API exposes.
|
|
40
|
+
*/
|
|
41
|
+
import type { TableIndexSnapshot } from './indexes';
|
|
42
|
+
import type { TableSnapshot } from './introspect';
|
|
43
|
+
/**
|
|
44
|
+
* Which way a signal points. `read` means something demonstrably traverses the
|
|
45
|
+
* key; `shape` means the schema resembles an idiom in which nothing does, which
|
|
46
|
+
* is a suspicion rather than a finding.
|
|
47
|
+
*/
|
|
48
|
+
export type SignalDirection = 'read' | 'shape' | 'declared';
|
|
49
|
+
export type SignalName = 'policy-read' | 'view-read' | 'write-once-pointer' | 'config-record' | 'behavior-hidden';
|
|
50
|
+
export interface PathSignal {
|
|
51
|
+
name: SignalName;
|
|
52
|
+
direction: SignalDirection;
|
|
53
|
+
/** Why this signal fired, in one human-readable clause. */
|
|
54
|
+
detail: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The caller-facing summary of a path's signals. Note there is no `unreachable`
|
|
58
|
+
* member: nothing safegres can currently observe proves a path is unreachable,
|
|
59
|
+
* and inventing the state is how a scanner ends up recommending that you drop
|
|
60
|
+
* an index a live API is using.
|
|
61
|
+
*/
|
|
62
|
+
export type PathAssessment =
|
|
63
|
+
/** At least one `read` signal. The key is traversed; X1 applies as written. */
|
|
64
|
+
'read'
|
|
65
|
+
/** Only `shape` signals. Looks like a provisioning pointer; unproven. */
|
|
66
|
+
| 'write-once-shaped'
|
|
67
|
+
/** No signal fired either way. */
|
|
68
|
+
| 'unknown';
|
|
69
|
+
export interface AccessPath {
|
|
70
|
+
schema: string;
|
|
71
|
+
table: string;
|
|
72
|
+
/** Referencing column names, in constraint order. */
|
|
73
|
+
columns: string[];
|
|
74
|
+
/** The foreign-key constraint this path belongs to. */
|
|
75
|
+
constraint: string;
|
|
76
|
+
/** Every signal that fired, so a reviewer can audit the assessment. */
|
|
77
|
+
signals: PathSignal[];
|
|
78
|
+
assessment: PathAssessment;
|
|
79
|
+
}
|
|
80
|
+
export interface ClassifyOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Write-once pointers a table needs before the `config-record` signal fires.
|
|
83
|
+
* Default 2. Lifting the test from the column to the table is what catches
|
|
84
|
+
* the nullable, undefaulted pointer sitting alongside the defaulted ones.
|
|
85
|
+
*/
|
|
86
|
+
minPointers?: number;
|
|
87
|
+
/**
|
|
88
|
+
* `schema.table.constraint` keys whose reverse relation an API declares
|
|
89
|
+
* absent, as computed by exposure reach.
|
|
90
|
+
*/
|
|
91
|
+
hiddenBackwardRelations?: Set<string>;
|
|
92
|
+
}
|
|
93
|
+
export declare const DEFAULT_MIN_POINTERS = 2;
|
|
94
|
+
export declare function isConstantDefault(defaultExpr: string | null): boolean;
|
|
95
|
+
/** Key for {@link AccessPath} lookups: the relation plus the constraint name. */
|
|
96
|
+
export declare function pathKey(schema: string, table: string, constraint: string): string;
|
|
97
|
+
/**
|
|
98
|
+
* One real reader ends the discussion, so a `read` signal always wins. Absent
|
|
99
|
+
* one, the shape has to hold at the *table* level before it means anything: a
|
|
100
|
+
* lone defaulted column is a habit, whereas a table built entirely out of them
|
|
101
|
+
* is an idiom. `write-once-pointer` on its own is therefore reported and not
|
|
102
|
+
* acted on.
|
|
103
|
+
*/
|
|
104
|
+
export declare function assess(signals: PathSignal[]): PathAssessment;
|
|
105
|
+
/**
|
|
106
|
+
* Collect the signals for every foreign key in the snapshot.
|
|
107
|
+
*
|
|
108
|
+
* `tables` supplies the RLS policy predicates (as SQL text) and `viewBodies`
|
|
109
|
+
* the view definitions. Both are matched as whole-word tokens across the
|
|
110
|
+
* *whole* database rather than per relation — deliberately over-eager, because
|
|
111
|
+
* a spurious `read` signal costs one retained finding while a missing one
|
|
112
|
+
* could cost a dropped index.
|
|
113
|
+
*/
|
|
114
|
+
export declare function classifyPaths(indexSnapshot: TableIndexSnapshot[], tables: TableSnapshot[], viewBodies: string[], options?: ClassifyOptions): Map<string, AccessPath>;
|