@safegres/test 0.0.2 → 1.30.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +825 -412
- package/adapters.d.ts +8 -0
- package/adapters.js +18 -0
- package/ast/helpers.d.ts +16 -0
- package/ast/helpers.js +40 -0
- package/ast/parse.d.ts +31 -0
- package/ast/parse.js +87 -0
- package/ast/walk.d.ts +27 -0
- package/ast/walk.js +79 -0
- package/callgraph/baseline.d.ts +28 -0
- package/callgraph/baseline.js +55 -0
- package/callgraph/extract.d.ts +93 -0
- package/callgraph/extract.js +340 -0
- package/callgraph/graph.d.ts +82 -0
- package/callgraph/graph.js +276 -0
- package/checks/anti-patterns.d.ts +53 -0
- package/checks/anti-patterns.js +247 -0
- package/checks/column-grants.d.ts +20 -0
- package/checks/column-grants.js +74 -0
- package/checks/coverage.d.ts +43 -0
- package/checks/coverage.js +110 -0
- package/checks/definer-function.d.ts +91 -0
- package/checks/definer-function.js +458 -0
- package/checks/definer-view.d.ts +167 -0
- package/checks/definer-view.js +392 -0
- package/checks/indexes.d.ts +82 -0
- package/checks/indexes.js +263 -0
- package/checks/lattice.d.ts +179 -0
- package/checks/lattice.js +487 -0
- package/checks/object-acls.d.ts +32 -0
- package/checks/object-acls.js +129 -0
- package/checks/policy-index.d.ts +78 -0
- package/checks/policy-index.js +349 -0
- package/checks/revocable-grants.d.ts +140 -0
- package/checks/revocable-grants.js +377 -0
- package/checks/rls-flags.d.ts +25 -0
- package/checks/rls-flags.js +82 -0
- package/checks/role-reach.d.ts +329 -0
- package/checks/role-reach.js +277 -0
- package/checks/role-trust.d.ts +45 -0
- package/checks/role-trust.js +91 -0
- package/checks/set-role.d.ts +21 -0
- package/checks/set-role.js +66 -0
- package/checks/stats.d.ts +52 -0
- package/checks/stats.js +192 -0
- package/checks/view-exposure.d.ts +68 -0
- package/checks/view-exposure.js +236 -0
- package/checks/view-writes.d.ts +88 -0
- package/checks/view-writes.js +350 -0
- package/ci/baseline.d.ts +104 -0
- package/ci/baseline.js +111 -0
- package/ci/github.d.ts +47 -0
- package/ci/github.js +197 -0
- package/cli/audit.d.ts +3 -0
- package/cli/audit.js +560 -0
- package/cli/baseline.d.ts +3 -0
- package/cli/baseline.js +104 -0
- package/cli/commands.d.ts +2 -0
- package/cli/commands.js +60 -0
- package/cli/doctor.d.ts +3 -0
- package/cli/doctor.js +72 -0
- package/cli/eval.d.ts +3 -0
- package/cli/eval.js +153 -0
- package/cli/print-config.d.ts +3 -0
- package/cli/print-config.js +54 -0
- package/cli/shared.d.ts +43 -0
- package/cli/shared.js +153 -0
- package/cli.d.ts +2 -0
- package/cli.js +69 -0
- package/commands/audit.d.ts +65 -0
- package/commands/audit.js +779 -0
- package/commands/doctor.d.ts +19 -0
- package/commands/doctor.js +181 -0
- package/commands/eval.d.ts +67 -0
- package/commands/eval.js +93 -0
- package/config/fingerprint.d.ts +40 -0
- package/config/fingerprint.js +71 -0
- package/config/loader.d.ts +44 -0
- package/config/loader.js +210 -0
- package/config/presets.d.ts +95 -0
- package/config/presets.js +327 -0
- package/config/resolve.d.ts +47 -0
- package/config/resolve.js +192 -0
- package/config/schema.d.ts +54 -0
- package/config/schema.js +269 -0
- package/config/types.d.ts +562 -0
- package/config/validate.d.ts +14 -0
- package/config/validate.js +125 -0
- package/corpus/README.md +83 -0
- package/corpus/bootstrap.sql +14 -0
- package/corpus/cases/01-anon-write-grant/case.json +26 -0
- package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
- package/corpus/cases/02-grant-to-public/case.json +25 -0
- package/corpus/cases/02-grant-to-public/schema.sql +18 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
- package/corpus/cases/04-permissive-write-policy/case.json +30 -0
- package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
- package/corpus/cases/05-public-read-policy/case.json +25 -0
- package/corpus/cases/05-public-read-policy/schema.sql +16 -0
- package/corpus/cases/06-update-without-with-check/case.json +25 -0
- package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
- package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
- package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
- package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
- package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
- package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
- package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
- package/corpus/cases/10-rls-not-forced/case.json +24 -0
- package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
- package/corpus/cases/11-session-user-in-policy/case.json +24 -0
- package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
- package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
- package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
- package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
- package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
- package/corpus/cases/14-dead-policy/case.json +24 -0
- package/corpus/cases/14-dead-policy/schema.sql +18 -0
- package/corpus/cases/15-unreachable-grant/case.json +24 -0
- package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
- package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
- package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
- package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
- package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
- package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
- package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
- package/corpus/cases/19-policy-column-cast/case.json +24 -0
- package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
- package/corpus/cases/20-stable-function-per-row/case.json +24 -0
- package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
- package/corpus/cases/21-table-without-primary-key/case.json +24 -0
- package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
- package/corpus/cases/22-redundant-index/case.json +24 -0
- package/corpus/cases/22-redundant-index/schema.sql +16 -0
- package/corpus/cases/23-tsvector-without-index/case.json +24 -0
- package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
- package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
- package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
- package/corpus/cases/25-definer-view-bypass/case.json +31 -0
- package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
- package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
- package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
- package/corpus/cases/27-definer-view-write/case.json +36 -0
- package/corpus/cases/27-definer-view-write/schema.sql +40 -0
- package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
- package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
- package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
- package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
- package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
- package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
- package/corpus/cases/31-matview-snapshot/case.json +32 -0
- package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
- package/corpus/cases/33-leaky-filter-view/case.json +36 -0
- package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
- package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
- package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
- package/corpus/cases/35-column-grant-projection/case.json +27 -0
- package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
- package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
- package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
- package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
- package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
- package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
- package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
- package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
- package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
- package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
- package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
- package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
- package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
- package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
- package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
- package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
- package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
- package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
- package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
- package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
- package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
- package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
- package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
- package/corpus/cases/41-unreadable-view-body/case.json +33 -0
- package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
- package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
- package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
- package/corpus/cases/42-search-column-indexed/case.json +21 -0
- package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
- package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
- package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
- package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
- package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
- package/corpus/cases/45-anon-foreign-table/case.json +27 -0
- package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
- package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
- package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
- package/corpus/cases/47-checked-writable-view/case.json +37 -0
- package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
- package/corpus/cases/48-definer-function-reach/case.json +38 -0
- package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
- package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
- package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
- package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
- package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
- package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
- package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
- package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
- package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
- package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
- package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
- package/corpus/index.d.ts +79 -0
- package/corpus/index.js +125 -0
- package/esm/adapters.d.ts +8 -0
- package/esm/adapters.js +6 -0
- package/esm/ast/helpers.d.ts +16 -0
- package/esm/ast/helpers.js +36 -0
- package/esm/ast/parse.d.ts +31 -0
- package/esm/ast/parse.js +82 -0
- package/esm/ast/walk.d.ts +27 -0
- package/esm/ast/walk.js +72 -0
- package/esm/callgraph/baseline.d.ts +28 -0
- package/esm/callgraph/baseline.js +48 -0
- package/esm/callgraph/extract.d.ts +93 -0
- package/esm/callgraph/extract.js +333 -0
- package/esm/callgraph/graph.d.ts +82 -0
- package/esm/callgraph/graph.js +273 -0
- package/esm/checks/anti-patterns.d.ts +53 -0
- package/esm/checks/anti-patterns.js +240 -0
- package/esm/checks/column-grants.d.ts +20 -0
- package/esm/checks/column-grants.js +71 -0
- package/esm/checks/coverage.d.ts +43 -0
- package/esm/checks/coverage.js +104 -0
- package/esm/checks/definer-function.d.ts +91 -0
- package/esm/checks/definer-function.js +452 -0
- package/esm/checks/definer-view.d.ts +167 -0
- package/esm/checks/definer-view.js +382 -0
- package/esm/checks/indexes.d.ts +82 -0
- package/esm/checks/indexes.js +256 -0
- package/esm/checks/lattice.d.ts +179 -0
- package/esm/checks/lattice.js +475 -0
- package/esm/checks/object-acls.d.ts +32 -0
- package/esm/checks/object-acls.js +125 -0
- package/esm/checks/policy-index.d.ts +78 -0
- package/esm/checks/policy-index.js +342 -0
- package/esm/checks/revocable-grants.d.ts +140 -0
- package/esm/checks/revocable-grants.js +373 -0
- package/esm/checks/rls-flags.d.ts +25 -0
- package/esm/checks/rls-flags.js +77 -0
- package/esm/checks/role-reach.d.ts +329 -0
- package/esm/checks/role-reach.js +270 -0
- package/esm/checks/role-trust.d.ts +45 -0
- package/esm/checks/role-trust.js +86 -0
- package/esm/checks/set-role.d.ts +21 -0
- package/esm/checks/set-role.js +63 -0
- package/esm/checks/stats.d.ts +52 -0
- package/esm/checks/stats.js +184 -0
- package/esm/checks/view-exposure.d.ts +68 -0
- package/esm/checks/view-exposure.js +231 -0
- package/esm/checks/view-writes.d.ts +88 -0
- package/esm/checks/view-writes.js +344 -0
- package/esm/ci/baseline.d.ts +104 -0
- package/esm/ci/baseline.js +107 -0
- package/esm/ci/github.d.ts +47 -0
- package/esm/ci/github.js +161 -0
- package/esm/cli/audit.d.ts +3 -0
- package/esm/cli/audit.js +525 -0
- package/esm/cli/baseline.d.ts +3 -0
- package/esm/cli/baseline.js +102 -0
- package/esm/cli/commands.d.ts +2 -0
- package/esm/cli/commands.js +53 -0
- package/esm/cli/doctor.d.ts +3 -0
- package/esm/cli/doctor.js +67 -0
- package/esm/cli/eval.d.ts +3 -0
- package/esm/cli/eval.js +115 -0
- package/esm/cli/print-config.d.ts +3 -0
- package/esm/cli/print-config.js +52 -0
- package/esm/cli/shared.d.ts +43 -0
- package/esm/cli/shared.js +112 -0
- package/esm/cli.d.ts +2 -0
- package/esm/cli.js +67 -0
- package/esm/commands/audit.d.ts +65 -0
- package/esm/commands/audit.js +776 -0
- package/esm/commands/doctor.d.ts +19 -0
- package/esm/commands/doctor.js +178 -0
- package/esm/commands/eval.d.ts +67 -0
- package/esm/commands/eval.js +90 -0
- package/esm/config/fingerprint.d.ts +40 -0
- package/esm/config/fingerprint.js +68 -0
- package/esm/config/loader.d.ts +44 -0
- package/esm/config/loader.js +172 -0
- package/esm/config/presets.d.ts +95 -0
- package/esm/config/presets.js +324 -0
- package/esm/config/resolve.d.ts +47 -0
- package/esm/config/resolve.js +180 -0
- package/esm/config/schema.d.ts +54 -0
- package/esm/config/schema.js +265 -0
- package/esm/config/types.d.ts +562 -0
- package/esm/config/validate.d.ts +14 -0
- package/esm/config/validate.js +122 -0
- package/esm/corpus/index.d.ts +79 -0
- package/esm/corpus/index.js +85 -0
- package/esm/exposure/adapters.d.ts +152 -0
- package/esm/exposure/adapters.js +463 -0
- package/esm/exposure/planes.d.ts +59 -0
- package/esm/exposure/planes.js +144 -0
- package/esm/exposure/reach.d.ts +78 -0
- package/esm/exposure/reach.js +105 -0
- package/esm/exposure/routines.d.ts +36 -0
- package/esm/exposure/routines.js +54 -0
- package/esm/index.d.ts +96 -0
- package/esm/index.js +57 -6
- package/esm/lint/index.d.ts +11 -0
- package/esm/lint/index.js +10 -0
- package/esm/perf/baseline.d.ts +40 -0
- package/esm/perf/baseline.js +83 -0
- package/esm/perf/explain.d.ts +46 -0
- package/esm/perf/explain.js +201 -0
- package/esm/pg/acl.d.ts +72 -0
- package/esm/pg/acl.js +186 -0
- package/esm/pg/behaviors.d.ts +114 -0
- package/esm/pg/behaviors.js +206 -0
- package/esm/pg/exposure.d.ts +74 -0
- package/esm/pg/exposure.js +198 -0
- package/esm/pg/functions.d.ts +52 -0
- package/esm/pg/functions.js +128 -0
- package/esm/pg/indexes.d.ts +192 -0
- package/esm/pg/indexes.js +395 -0
- package/esm/pg/introspect.d.ts +134 -0
- package/esm/pg/introspect.js +274 -0
- package/esm/pg/objects.d.ts +37 -0
- package/esm/pg/objects.js +108 -0
- package/esm/pg/paths.d.ts +114 -0
- package/esm/pg/paths.js +159 -0
- package/esm/pg/proc.d.ts +23 -0
- package/esm/pg/proc.js +55 -0
- package/esm/pg/reach-inputs.d.ts +47 -0
- package/esm/pg/reach-inputs.js +133 -0
- package/esm/pg/roles.d.ts +28 -0
- package/esm/pg/roles.js +50 -0
- package/esm/pg/stats.d.ts +65 -0
- package/esm/pg/stats.js +129 -0
- package/esm/pgpm-test.d.ts +22 -0
- package/esm/pgpm-test.js +39 -0
- package/esm/report/callgraph.d.ts +9 -0
- package/esm/report/callgraph.js +74 -0
- package/esm/report/compare.d.ts +143 -0
- package/esm/report/compare.js +195 -0
- package/esm/report/github.d.ts +74 -0
- package/esm/report/github.js +290 -0
- package/esm/report/json.d.ts +4 -0
- package/esm/report/json.js +3 -0
- package/esm/report/markdown.d.ts +25 -0
- package/esm/report/markdown.js +352 -0
- package/esm/report/pretty.d.ts +14 -0
- package/esm/report/pretty.js +318 -0
- package/esm/report/sarif.d.ts +40 -0
- package/esm/report/sarif.js +189 -0
- package/esm/report/view.d.ts +82 -0
- package/esm/report/view.js +126 -0
- package/esm/rules/registry.d.ts +66 -0
- package/esm/rules/registry.js +562 -0
- package/esm/score/score.d.ts +84 -0
- package/esm/score/score.js +233 -0
- package/esm/score/scorecards.d.ts +68 -0
- package/esm/score/scorecards.js +157 -0
- package/esm/types.d.ts +292 -0
- package/esm/types.js +26 -0
- package/esm/version.d.ts +8 -0
- package/esm/version.js +9 -0
- package/exposure/adapters.d.ts +152 -0
- package/exposure/adapters.js +468 -0
- package/exposure/planes.d.ts +59 -0
- package/exposure/planes.js +151 -0
- package/exposure/reach.d.ts +78 -0
- package/exposure/reach.js +109 -0
- package/exposure/routines.d.ts +36 -0
- package/exposure/routines.js +57 -0
- package/index.d.ts +96 -6
- package/index.js +229 -6
- package/lint/index.d.ts +11 -0
- package/lint/index.js +19 -0
- package/package.json +54 -48
- package/perf/baseline.d.ts +40 -0
- package/perf/baseline.js +92 -0
- package/perf/explain.d.ts +46 -0
- package/perf/explain.js +204 -0
- package/pg/acl.d.ts +72 -0
- package/pg/acl.js +191 -0
- package/pg/behaviors.d.ts +114 -0
- package/pg/behaviors.js +217 -0
- package/pg/exposure.d.ts +74 -0
- package/pg/exposure.js +205 -0
- package/pg/functions.d.ts +52 -0
- package/pg/functions.js +131 -0
- package/pg/indexes.d.ts +192 -0
- package/pg/indexes.js +400 -0
- package/pg/introspect.d.ts +134 -0
- package/pg/introspect.js +280 -0
- package/pg/objects.d.ts +37 -0
- package/pg/objects.js +111 -0
- package/pg/paths.d.ts +114 -0
- package/pg/paths.js +166 -0
- package/pg/proc.d.ts +23 -0
- package/pg/proc.js +58 -0
- package/pg/reach-inputs.d.ts +47 -0
- package/pg/reach-inputs.js +136 -0
- package/pg/roles.d.ts +28 -0
- package/pg/roles.js +54 -0
- package/pg/stats.d.ts +65 -0
- package/pg/stats.js +133 -0
- package/pgpm-test.d.ts +22 -0
- package/pgpm-test.js +42 -0
- package/report/callgraph.d.ts +9 -0
- package/report/callgraph.js +81 -0
- package/report/compare.d.ts +143 -0
- package/report/compare.js +205 -0
- package/report/github.d.ts +74 -0
- package/report/github.js +333 -0
- package/report/json.d.ts +4 -0
- package/report/json.js +6 -0
- package/report/markdown.d.ts +25 -0
- package/report/markdown.js +355 -0
- package/report/pretty.d.ts +14 -0
- package/report/pretty.js +324 -0
- package/report/sarif.d.ts +40 -0
- package/report/sarif.js +193 -0
- package/report/view.d.ts +82 -0
- package/report/view.js +132 -0
- package/rules/registry.d.ts +66 -0
- package/rules/registry.js +569 -0
- package/schema/safegres.schema.json +1119 -0
- package/score/score.d.ts +84 -0
- package/score/score.js +238 -0
- package/score/scorecards.d.ts +68 -0
- package/score/scorecards.js +162 -0
- package/types.d.ts +292 -0
- package/types.js +32 -0
- package/version.d.ts +8 -0
- package/version.js +12 -0
- package/LICENSE +0 -23
- package/admin.d.ts +0 -26
- package/admin.js +0 -182
- package/connect.d.ts +0 -19
- package/connect.js +0 -83
- package/dist/README.md +0 -557
- package/dist/package.json +0 -72
- package/esm/admin.js +0 -178
- package/esm/connect.js +0 -78
- package/esm/manager.js +0 -136
- package/esm/roles.js +0 -32
- package/esm/seed/adapters.js +0 -23
- package/esm/seed/csv.js +0 -44
- package/esm/seed/index.js +0 -16
- package/esm/seed/json.js +0 -18
- package/esm/seed/launchql.js +0 -19
- package/esm/seed/sqitch.js +0 -17
- package/esm/stream.js +0 -43
- package/esm/test-client.js +0 -150
- package/manager.d.ts +0 -25
- package/manager.js +0 -140
- package/roles.d.ts +0 -17
- package/roles.js +0 -38
- package/seed/adapters.d.ts +0 -4
- package/seed/adapters.js +0 -28
- package/seed/csv.d.ts +0 -9
- package/seed/csv.js +0 -49
- package/seed/index.d.ts +0 -16
- package/seed/index.js +0 -33
- package/seed/json.d.ts +0 -6
- package/seed/json.js +0 -21
- package/seed/launchql.d.ts +0 -2
- package/seed/launchql.js +0 -22
- package/seed/sqitch.d.ts +0 -2
- package/seed/sqitch.js +0 -20
- package/seed/types.d.ts +0 -13
- package/stream.d.ts +0 -2
- package/stream.js +0 -46
- package/test-client.d.ts +0 -49
- package/test-client.js +0 -154
- /package/{seed → config}/types.js +0 -0
- /package/esm/{seed → config}/types.js +0 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Script A driver: pure-Postgres RLS audit.
|
|
4
|
+
*
|
|
5
|
+
* Ingests a catalog snapshot, runs every check, and returns a structured report.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.audit = audit;
|
|
9
|
+
const graph_1 = require("../callgraph/graph");
|
|
10
|
+
const anti_patterns_1 = require("../checks/anti-patterns");
|
|
11
|
+
const column_grants_1 = require("../checks/column-grants");
|
|
12
|
+
const coverage_1 = require("../checks/coverage");
|
|
13
|
+
const definer_function_1 = require("../checks/definer-function");
|
|
14
|
+
const definer_view_1 = require("../checks/definer-view");
|
|
15
|
+
const indexes_1 = require("../checks/indexes");
|
|
16
|
+
const lattice_1 = require("../checks/lattice");
|
|
17
|
+
const object_acls_1 = require("../checks/object-acls");
|
|
18
|
+
const policy_index_1 = require("../checks/policy-index");
|
|
19
|
+
const revocable_grants_1 = require("../checks/revocable-grants");
|
|
20
|
+
const rls_flags_1 = require("../checks/rls-flags");
|
|
21
|
+
const role_trust_1 = require("../checks/role-trust");
|
|
22
|
+
const set_role_1 = require("../checks/set-role");
|
|
23
|
+
const stats_1 = require("../checks/stats");
|
|
24
|
+
const view_exposure_1 = require("../checks/view-exposure");
|
|
25
|
+
const view_writes_1 = require("../checks/view-writes");
|
|
26
|
+
const fingerprint_1 = require("../config/fingerprint");
|
|
27
|
+
const resolve_1 = require("../config/resolve");
|
|
28
|
+
const planes_1 = require("../exposure/planes");
|
|
29
|
+
const routines_1 = require("../exposure/routines");
|
|
30
|
+
const lint_1 = require("../lint");
|
|
31
|
+
const explain_1 = require("../perf/explain");
|
|
32
|
+
const acl_1 = require("../pg/acl");
|
|
33
|
+
const exposure_1 = require("../pg/exposure");
|
|
34
|
+
const functions_1 = require("../pg/functions");
|
|
35
|
+
const indexes_2 = require("../pg/indexes");
|
|
36
|
+
const introspect_1 = require("../pg/introspect");
|
|
37
|
+
const objects_1 = require("../pg/objects");
|
|
38
|
+
const paths_1 = require("../pg/paths");
|
|
39
|
+
const proc_1 = require("../pg/proc");
|
|
40
|
+
const reach_inputs_1 = require("../pg/reach-inputs");
|
|
41
|
+
const roles_1 = require("../pg/roles");
|
|
42
|
+
const stats_2 = require("../pg/stats");
|
|
43
|
+
const registry_1 = require("../rules/registry");
|
|
44
|
+
const score_1 = require("../score/score");
|
|
45
|
+
const scorecards_1 = require("../score/scorecards");
|
|
46
|
+
const types_1 = require("../types");
|
|
47
|
+
const version_1 = require("../version");
|
|
48
|
+
async function audit(client, options = {}) {
|
|
49
|
+
const exec = (0, introspect_1.asExecutor)(client);
|
|
50
|
+
const config = options.config ?? {};
|
|
51
|
+
const resolved = (0, resolve_1.resolveRules)(config);
|
|
52
|
+
const extensions = options.extensions ?? config.extensions;
|
|
53
|
+
// Function definitions are read by two independent features (the convention
|
|
54
|
+
// linter and the call graph); introspect them at most once.
|
|
55
|
+
let functionsCache;
|
|
56
|
+
const getFunctions = async () => {
|
|
57
|
+
if (!functionsCache) {
|
|
58
|
+
functionsCache = await (0, functions_1.introspectFunctions)(exec, {
|
|
59
|
+
schemas: options.schemas ?? config.schemas,
|
|
60
|
+
excludeSchemas: options.excludeSchemas ?? config.excludeSchemas,
|
|
61
|
+
extensions
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return functionsCache;
|
|
65
|
+
};
|
|
66
|
+
const statsEnabled = options.stats ?? config.perf?.stats?.enabled ?? false;
|
|
67
|
+
const explainEnabled = options.explain ?? config.perf?.explain?.enabled ?? false;
|
|
68
|
+
// Both tiers are refinements of the perf dimension: asking for either turns
|
|
69
|
+
// it on, but neither is reachable without opting into perf in the first place.
|
|
70
|
+
const perfEnabled = (options.perf ?? config.perf?.enabled ?? false) || statsEnabled || explainEnabled;
|
|
71
|
+
const skipAst = options.skipAstChecks || (0, resolve_1.allAstRulesDisabled)(resolved, { perf: perfEnabled });
|
|
72
|
+
// Resolve role set.
|
|
73
|
+
const allRoles = await (0, roles_1.listAuditableRoles)(exec);
|
|
74
|
+
const resolution = (0, roles_1.resolveRoles)(allRoles, options.includeRoles ?? config.roles, options.excludeRoles ?? config.excludeRoles);
|
|
75
|
+
const exposureConfig = options.exposure ?? config.exposure;
|
|
76
|
+
const exposure = await (0, exposure_1.resolveExposure)(exec, exposureConfig);
|
|
77
|
+
const exposedSchemas = new Set(exposure.schemas);
|
|
78
|
+
const planes = await (0, exposure_1.resolvePlanes)(exec, exposureConfig, exposure);
|
|
79
|
+
// Relation-level precision within those schemas: what the generated API can
|
|
80
|
+
// actually name. Schema membership is the coarse answer; this is the fine
|
|
81
|
+
// one, and only an adapter that can prove it contributes.
|
|
82
|
+
const apiReach = exposure.known
|
|
83
|
+
? await (0, exposure_1.resolveReach)(exec, exposureConfig, {
|
|
84
|
+
schemas: exposure.schemas,
|
|
85
|
+
excludeSchemas: options.excludeSchemas ?? config.excludeSchemas
|
|
86
|
+
})
|
|
87
|
+
: undefined;
|
|
88
|
+
const unaddressable = new Set((apiReach?.unreachable ?? []).map((r) => `${r.schema}.${r.table}`));
|
|
89
|
+
const snapshot = await (0, introspect_1.introspectTables)(exec, {
|
|
90
|
+
schemas: options.schemas ?? config.schemas,
|
|
91
|
+
excludeSchemas: options.excludeSchemas ?? config.excludeSchemas,
|
|
92
|
+
roles: resolution.roles,
|
|
93
|
+
extensions
|
|
94
|
+
});
|
|
95
|
+
const isExposed = (schema, table) => {
|
|
96
|
+
if (!exposedSchemas.has(schema))
|
|
97
|
+
return false;
|
|
98
|
+
return table === undefined || !unaddressable.has(`${schema}.${table}`);
|
|
99
|
+
};
|
|
100
|
+
const exposedTables = exposure.known
|
|
101
|
+
? snapshot.filter((t) => isExposed(t.schema, t.name)).length
|
|
102
|
+
: snapshot.length;
|
|
103
|
+
// Effective-access inputs for the lattice rules: the INHERIT-following
|
|
104
|
+
// role graph and the schema USAGE ACLs. Both are single cheap queries.
|
|
105
|
+
const roleGraph = await (0, acl_1.introspectRoleGraph)(exec);
|
|
106
|
+
const schemaAcls = await (0, acl_1.introspectSchemaAcls)(exec, {
|
|
107
|
+
schemas: options.schemas ?? config.schemas,
|
|
108
|
+
excludeSchemas: options.excludeSchemas ?? config.excludeSchemas
|
|
109
|
+
});
|
|
110
|
+
const schemaAclsByName = new Map(schemaAcls.map((a) => [a.schema, a]));
|
|
111
|
+
// Which schemas the audit actually looked at — the same include/exclude
|
|
112
|
+
// filters the table snapshot used. A view body naming anything outside this
|
|
113
|
+
// set reaches past the audit rather than reaching nothing (L14).
|
|
114
|
+
const auditedSchemas = new Set(schemaAcls.map((a) => a.schema));
|
|
115
|
+
// Roles requests arrive as, and the relations some policy predicate names.
|
|
116
|
+
// L6 needs both: the first is whose grants it is talking about, the second
|
|
117
|
+
// is the veto that stops it recommending a revoke of a load-bearing grant.
|
|
118
|
+
const apiRoles = exposure.roles ?? [];
|
|
119
|
+
const policyReferenced = policyReferencedRelations(snapshot);
|
|
120
|
+
let findings = [];
|
|
121
|
+
// --- Performance dimension (opt-in): index hygiene ---
|
|
122
|
+
const indexSnapshot = perfEnabled
|
|
123
|
+
? await (0, indexes_2.introspectIndexes)(exec, {
|
|
124
|
+
schemas: options.schemas ?? config.schemas,
|
|
125
|
+
excludeSchemas: options.excludeSchemas ?? config.excludeSchemas,
|
|
126
|
+
extensions
|
|
127
|
+
})
|
|
128
|
+
: [];
|
|
129
|
+
const indexesByTable = new Map(indexSnapshot.map((t) => [`${t.schema}.${t.name}`, t]));
|
|
130
|
+
// Views are read for three independent reasons: their bodies refute "nothing
|
|
131
|
+
// queries this column" for the perf paths, their owners carry the L8 reach
|
|
132
|
+
// edge, and their ACLs keep L4 from calling schema USAGE dead when a view is
|
|
133
|
+
// the only thing in the schema the role can reach. One introspection serves
|
|
134
|
+
// all three.
|
|
135
|
+
const definerViewRoles = withExposedRoles(resolved.rules.get('L8')?.options, exposure)?.roles ?? [];
|
|
136
|
+
const viewWriteRoles = withExposedRoles(resolved.rules.get('L9')?.options, exposure)?.roles ?? [];
|
|
137
|
+
const ruleBypassRoles = withExposedRoles(resolved.rules.get('L10')?.options, exposure)?.roles ?? [];
|
|
138
|
+
const uncheckedWriteRoles = withExposedRoles(resolved.rules.get('L18')?.options, exposure)?.roles ?? [];
|
|
139
|
+
const matviewRoles = withExposedRoles(resolved.rules.get('L11')?.options, exposure)?.roles ?? [];
|
|
140
|
+
const leakyViewRoles = withExposedRoles(resolved.rules.get('L12')?.options, exposure)?.roles ?? [];
|
|
141
|
+
const unauditedReachRoles = withExposedRoles(resolved.rules.get('L14')?.options, exposure)?.roles ?? [];
|
|
142
|
+
const unreadableViewRoles = withExposedRoles(resolved.rules.get('L15')?.options, exposure)?.roles ?? [];
|
|
143
|
+
// Function bodies: L19 is the reach a definer function's body confers, L20
|
|
144
|
+
// the write an INSTEAD OF trigger's definer function performs, and L15
|
|
145
|
+
// reports the bodies neither could read.
|
|
146
|
+
const definerFunctionRoles = withExposedRoles(resolved.rules.get('L19')?.options, exposure)?.roles ?? [];
|
|
147
|
+
const triggerWriteRoles = withExposedRoles(resolved.rules.get('L20')?.options, exposure)?.roles ?? [];
|
|
148
|
+
// Sequences and foreign tables: one introspection, two rules, and no reason
|
|
149
|
+
// to run it when neither is configured with a role list.
|
|
150
|
+
const sequenceRoles = withExposedRoles(resolved.rules.get('L16')?.options, exposure)?.roles ?? [];
|
|
151
|
+
const foreignTableRoles = withExposedRoles(resolved.rules.get('L17')?.options, exposure)?.roles ?? [];
|
|
152
|
+
// L21 (`granted − reachable`) runs on the untrusted roles by default — its
|
|
153
|
+
// question is precisely "which of anonymous's grants are load-bearing?" — so
|
|
154
|
+
// it reads its role list from `exposure.anonRoles` unless the config overrides
|
|
155
|
+
// it. It reads function/view bodies, so it is gated on `skipAst` like the
|
|
156
|
+
// other body rules.
|
|
157
|
+
const revocableRoles = withExposedRoles({ rolesFrom: 'anon', ...resolved.rules.get('L21')?.options }, exposure)?.roles ?? [];
|
|
158
|
+
const revocableEnabled = !skipAst && revocableRoles.length > 0 && resolved.rules.get('L21')?.enabled !== false;
|
|
159
|
+
// L8 and L14 are two answers from one body pass: the base relations that
|
|
160
|
+
// were graded, and the ones that could not be because they are out of scope.
|
|
161
|
+
const viewBodiesEnabled = !skipAst
|
|
162
|
+
&& ((definerViewRoles.length > 0 && resolved.rules.get('L8')?.enabled !== false)
|
|
163
|
+
|| (unauditedReachRoles.length > 0 && resolved.rules.get('L14')?.enabled !== false)
|
|
164
|
+
|| (unreadableViewRoles.length > 0 && resolved.rules.get('L15')?.enabled !== false));
|
|
165
|
+
const functionBodiesEnabled = !skipAst
|
|
166
|
+
&& ((definerFunctionRoles.length > 0 && resolved.rules.get('L19')?.enabled !== false)
|
|
167
|
+
|| (triggerWriteRoles.length > 0 && resolved.rules.get('L20')?.enabled !== false)
|
|
168
|
+
|| (unreadableViewRoles.length > 0 && resolved.rules.get('L15')?.enabled !== false));
|
|
169
|
+
const viewExposureEnabled = !skipAst
|
|
170
|
+
&& ((matviewRoles.length > 0 && resolved.rules.get('L11')?.enabled !== false)
|
|
171
|
+
|| (leakyViewRoles.length > 0 && resolved.rules.get('L12')?.enabled !== false));
|
|
172
|
+
const viewWritesEnabled = !skipAst
|
|
173
|
+
&& ((viewWriteRoles.length > 0 && resolved.rules.get('L9')?.enabled !== false)
|
|
174
|
+
|| (ruleBypassRoles.length > 0 && resolved.rules.get('L10')?.enabled !== false)
|
|
175
|
+
|| (uncheckedWriteRoles.length > 0 && resolved.rules.get('L18')?.enabled !== false));
|
|
176
|
+
const needsViews = (perfEnabled && config.perf?.paths?.infer !== false)
|
|
177
|
+
|| resolved.rules.get('L4')?.enabled !== false
|
|
178
|
+
|| viewBodiesEnabled
|
|
179
|
+
|| viewWritesEnabled
|
|
180
|
+
|| viewExposureEnabled
|
|
181
|
+
|| functionBodiesEnabled
|
|
182
|
+
|| revocableEnabled;
|
|
183
|
+
const viewSnapshot = needsViews
|
|
184
|
+
? await (0, indexes_2.introspectViews)(exec, {
|
|
185
|
+
schemas: options.schemas ?? config.schemas,
|
|
186
|
+
excludeSchemas: options.excludeSchemas ?? config.excludeSchemas
|
|
187
|
+
})
|
|
188
|
+
: [];
|
|
189
|
+
// Evidence about which foreign keys anything reads. Reported alongside the
|
|
190
|
+
// findings; by default it changes neither, because no signal available on an
|
|
191
|
+
// empty database proves a path is unreachable.
|
|
192
|
+
const onWriteOncePointer = config.perf?.paths?.onWriteOncePointer ?? 'report';
|
|
193
|
+
const paths = perfEnabled && config.perf?.paths?.infer !== false
|
|
194
|
+
? (0, paths_1.classifyPaths)(indexSnapshot, snapshot, viewSnapshot.map((v) => v.definition), {
|
|
195
|
+
minPointers: config.perf?.paths?.minPointers,
|
|
196
|
+
hiddenBackwardRelations: new Set(apiReach?.hiddenBackwardRelations ?? [])
|
|
197
|
+
})
|
|
198
|
+
: new Map();
|
|
199
|
+
for (const table of snapshot) {
|
|
200
|
+
// --- RLS flags (structural) ---
|
|
201
|
+
const a1 = (0, rls_flags_1.checkRlsEnabledNoPolicies)(table);
|
|
202
|
+
if (a1)
|
|
203
|
+
findings.push(a1);
|
|
204
|
+
const a2 = (0, rls_flags_1.checkGrantsWithoutRls)(table);
|
|
205
|
+
if (a2)
|
|
206
|
+
findings.push(a2);
|
|
207
|
+
const a3 = (0, rls_flags_1.checkRlsNotForced)(table);
|
|
208
|
+
if (a3)
|
|
209
|
+
findings.push(a3);
|
|
210
|
+
// --- Grant-vs-policy coverage ---
|
|
211
|
+
findings.push(...(0, coverage_1.checkCoverageGaps)(table));
|
|
212
|
+
findings.push(...(0, coverage_1.checkUpdateWithCheckCoverage)(table));
|
|
213
|
+
// --- Role-trust (options-driven; per-table overrides can retune roles) ---
|
|
214
|
+
const tableRules = (0, resolve_1.rulesForTable)(resolved, table.schema, table.name);
|
|
215
|
+
findings.push(...(0, role_trust_1.checkUntrustedRoleWrites)(table, withExposedRoles(tableRules.get('R1')?.options, exposure)));
|
|
216
|
+
findings.push(...(0, role_trust_1.checkUntrustedRolePolicies)(table, withExposedRoles(tableRules.get('R2')?.options, exposure)));
|
|
217
|
+
findings.push(...(0, role_trust_1.checkPublicGrants)(table));
|
|
218
|
+
// --- Grant/RLS/policy lattice (effective access: PUBLIC + inheritance) ---
|
|
219
|
+
findings.push(...(0, lattice_1.checkIndirectCoverageGaps)(table, roleGraph));
|
|
220
|
+
findings.push(...(0, lattice_1.checkDeadPolicies)(table, roleGraph));
|
|
221
|
+
findings.push(...(0, lattice_1.checkUnreachableGrants)(table, schemaAclsByName, roleGraph));
|
|
222
|
+
findings.push(...(0, lattice_1.checkUntrustedIndirectAccess)(table, roleGraph, withExposedRoles(tableRules.get('L5')?.options, exposure)));
|
|
223
|
+
if (unaddressable.size > 0 && apiRoles.length > 0) {
|
|
224
|
+
findings.push(...(0, lattice_1.checkUnaddressableGrant)(table, roleGraph, unaddressable, {
|
|
225
|
+
roles: apiRoles,
|
|
226
|
+
policyReferenced
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
findings.push(...(0, set_role_1.checkSetRoleEscalation)(table, roleGraph, withExposedRoles(tableRules.get('L7')?.options, exposure)));
|
|
230
|
+
findings.push(...(0, column_grants_1.checkUntrustedColumnGrants)(table, roleGraph, withExposedRoles(tableRules.get('L13')?.options, exposure)));
|
|
231
|
+
// --- AST-level anti-patterns (and, with perf on, policy-aware index rules) ---
|
|
232
|
+
if (!skipAst) {
|
|
233
|
+
findings.push(...(await auditTableAst(exec, table, perfEnabled ? indexesByTable.get(`${table.schema}.${table.name}`) : undefined)));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
findings.push(...(0, lattice_1.checkDeadSchemaUsage)(schemaAcls, snapshot, roleGraph, viewSnapshot));
|
|
237
|
+
// L16/L17 grade relations that are not tables, so they sit outside the
|
|
238
|
+
// per-table loop entirely.
|
|
239
|
+
const objectAclsEnabled = (sequenceRoles.length > 0 && resolved.rules.get('L16')?.enabled !== false)
|
|
240
|
+
|| (foreignTableRoles.length > 0 && resolved.rules.get('L17')?.enabled !== false);
|
|
241
|
+
if (objectAclsEnabled) {
|
|
242
|
+
const objects = await (0, objects_1.introspectObjectAcls)(exec, {
|
|
243
|
+
schemas: options.schemas ?? config.schemas,
|
|
244
|
+
excludeSchemas: options.excludeSchemas ?? config.excludeSchemas
|
|
245
|
+
});
|
|
246
|
+
if (sequenceRoles.length > 0 && resolved.rules.get('L16')?.enabled !== false) {
|
|
247
|
+
findings.push(...(0, object_acls_1.checkUntrustedSequenceGrants)(objects, roleGraph, { roles: sequenceRoles }));
|
|
248
|
+
}
|
|
249
|
+
if (foreignTableRoles.length > 0 && resolved.rules.get('L17')?.enabled !== false) {
|
|
250
|
+
findings.push(...(0, object_acls_1.checkUntrustedForeignTableGrants)(objects, roleGraph, { roles: foreignTableRoles }));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// L8 is per-view, not per-table: a view body names its own base relations.
|
|
254
|
+
if (viewBodiesEnabled) {
|
|
255
|
+
const viewBodies = await (0, definer_view_1.analyzeViewBodies)(viewSnapshot, snapshot, auditedSchemas);
|
|
256
|
+
if (definerViewRoles.length > 0 && resolved.rules.get('L8')?.enabled !== false) {
|
|
257
|
+
findings.push(...(0, definer_view_1.checkDefinerViewBypass)(viewBodies.views, snapshot, roleGraph, { roles: definerViewRoles }));
|
|
258
|
+
}
|
|
259
|
+
if (unauditedReachRoles.length > 0 && resolved.rules.get('L14')?.enabled !== false) {
|
|
260
|
+
findings.push(...(0, definer_view_1.checkUnauditedViewReach)(viewBodies.views, roleGraph, { roles: unauditedReachRoles }));
|
|
261
|
+
}
|
|
262
|
+
if (unreadableViewRoles.length > 0 && resolved.rules.get('L15')?.enabled !== false) {
|
|
263
|
+
findings.push(...(0, definer_view_1.checkUnreadableViewReach)(viewBodies.views, roleGraph, { roles: unreadableViewRoles }));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// L9/L10/L18 are the write half of the same question, and share one analysis.
|
|
267
|
+
if (viewWritesEnabled) {
|
|
268
|
+
const writes = await (0, view_writes_1.analyzeViewWrites)(viewSnapshot, snapshot);
|
|
269
|
+
if (viewWriteRoles.length > 0 && resolved.rules.get('L9')?.enabled !== false) {
|
|
270
|
+
findings.push(...(0, view_writes_1.checkDefinerViewWrite)(writes.autoUpdatable, snapshot, roleGraph, { roles: viewWriteRoles }));
|
|
271
|
+
}
|
|
272
|
+
if (ruleBypassRoles.length > 0 && resolved.rules.get('L10')?.enabled !== false) {
|
|
273
|
+
findings.push(...(0, view_writes_1.checkViewRuleBypass)(writes.ruleDriven, snapshot, roleGraph, { roles: ruleBypassRoles }));
|
|
274
|
+
}
|
|
275
|
+
if (uncheckedWriteRoles.length > 0 && resolved.rules.get('L18')?.enabled !== false) {
|
|
276
|
+
findings.push(...(0, view_writes_1.checkUncheckedViewWrite)(writes.unchecked, snapshot, roleGraph, {
|
|
277
|
+
roles: uncheckedWriteRoles
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// L19/L20 are the function half of "SQL bodies confer privilege": what a
|
|
282
|
+
// definer function's body reaches, and where an INSTEAD OF trigger's write
|
|
283
|
+
// actually lands. One body pass serves both, plus L15's coverage report.
|
|
284
|
+
if (functionBodiesEnabled) {
|
|
285
|
+
const bodies = await (0, definer_function_1.analyzeFunctionBodies)(await getFunctions(), viewSnapshot, snapshot, auditedSchemas);
|
|
286
|
+
if (definerFunctionRoles.length > 0 && resolved.rules.get('L19')?.enabled !== false) {
|
|
287
|
+
findings.push(...(0, definer_function_1.checkDefinerFunctionReach)(bodies.functions, snapshot, roleGraph, schemaAclsByName, {
|
|
288
|
+
roles: definerFunctionRoles
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
if (triggerWriteRoles.length > 0 && resolved.rules.get('L20')?.enabled !== false) {
|
|
292
|
+
findings.push(...(0, definer_function_1.checkInsteadOfTriggerWrite)(bodies.triggers, snapshot, roleGraph, {
|
|
293
|
+
roles: triggerWriteRoles
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
if (unreadableViewRoles.length > 0 && resolved.rules.get('L15')?.enabled !== false) {
|
|
297
|
+
findings.push(...(0, definer_function_1.checkUnreadableFunctionReach)(bodies.functions, roleGraph, schemaAclsByName, {
|
|
298
|
+
roles: unreadableViewRoles
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// L11/L12 are the other two things a readable view does: store rows, and
|
|
303
|
+
// filter them without being a boundary. One body pass serves both.
|
|
304
|
+
if (viewExposureEnabled) {
|
|
305
|
+
const exposureViews = await (0, view_exposure_1.analyzeViewExposure)(viewSnapshot, snapshot);
|
|
306
|
+
if (matviewRoles.length > 0 && resolved.rules.get('L11')?.enabled !== false) {
|
|
307
|
+
findings.push(...(0, view_exposure_1.checkMatviewSnapshot)(exposureViews.matviews, snapshot, roleGraph, { roles: matviewRoles }));
|
|
308
|
+
}
|
|
309
|
+
if (leakyViewRoles.length > 0 && resolved.rules.get('L12')?.enabled !== false) {
|
|
310
|
+
findings.push(...(0, view_exposure_1.checkLeakyFilterView)(exposureViews.leaky, snapshot, roleGraph, { roles: leakyViewRoles }));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// L21: the EXECUTE grants each untrusted role holds that no reachable path
|
|
314
|
+
// exercises. Its own reachability closure — policy predicates, trigger
|
|
315
|
+
// bodies, write-time expressions and views — needs the trigger/expression
|
|
316
|
+
// inputs no other rule reads. The revocable recommendations enter the finding
|
|
317
|
+
// stream; the retained-and-suppressed proof lists ride on the report.
|
|
318
|
+
let revocableGrants;
|
|
319
|
+
if (revocableEnabled) {
|
|
320
|
+
const reachInputs = await (0, reach_inputs_1.introspectReachInputs)(exec, {
|
|
321
|
+
schemas: options.schemas ?? config.schemas,
|
|
322
|
+
excludeSchemas: options.excludeSchemas ?? config.excludeSchemas
|
|
323
|
+
});
|
|
324
|
+
revocableGrants = await (0, revocable_grants_1.analyzeRevocableGrants)({
|
|
325
|
+
roles: revocableRoles,
|
|
326
|
+
functions: await getFunctions(),
|
|
327
|
+
tables: snapshot,
|
|
328
|
+
views: viewSnapshot,
|
|
329
|
+
graph: roleGraph,
|
|
330
|
+
schemaAcls: schemaAclsByName,
|
|
331
|
+
reachInputs,
|
|
332
|
+
exposedSchemas,
|
|
333
|
+
exposureKnown: exposure.known,
|
|
334
|
+
unaddressable
|
|
335
|
+
});
|
|
336
|
+
findings.push(...(0, revocable_grants_1.revocableGrantFindings)(revocableGrants));
|
|
337
|
+
}
|
|
338
|
+
const statsSnapshot = statsEnabled
|
|
339
|
+
? await (0, stats_2.introspectStats)(exec, {
|
|
340
|
+
schemas: options.schemas ?? config.schemas,
|
|
341
|
+
excludeSchemas: options.excludeSchemas ?? config.excludeSchemas,
|
|
342
|
+
extensions,
|
|
343
|
+
statementLimit: config.perf?.stats?.topStatements
|
|
344
|
+
})
|
|
345
|
+
: null;
|
|
346
|
+
if (perfEnabled) {
|
|
347
|
+
for (const table of indexSnapshot) {
|
|
348
|
+
findings.push(...(0, indexes_1.checkUnindexedForeignKeys)(table, paths, onWriteOncePointer));
|
|
349
|
+
findings.push(...(0, indexes_1.checkRedundantIndexes)(table));
|
|
350
|
+
findings.push(...(0, indexes_1.checkUnindexedSearchColumns)(table));
|
|
351
|
+
findings.push(...(0, indexes_1.checkUnindexedSortColumns)(table));
|
|
352
|
+
const x6 = (0, indexes_1.checkMissingPrimaryKey)(table);
|
|
353
|
+
if (x6)
|
|
354
|
+
findings.push(x6);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (statsSnapshot) {
|
|
358
|
+
findings.push(...(0, stats_1.checkStats)(statsSnapshot, statsThresholds(config)));
|
|
359
|
+
}
|
|
360
|
+
// --- Convention linter (C*): source-level rules over function definitions ---
|
|
361
|
+
const enabledLintRules = lint_1.LINT_RULES.filter((r) => resolved.rules.get(r.code)?.enabled !== false);
|
|
362
|
+
if (enabledLintRules.length > 0) {
|
|
363
|
+
const lintRuleIds = enabledLintRules.map((r) => r.id);
|
|
364
|
+
for (const fn of await getFunctions()) {
|
|
365
|
+
if (!fn.definition)
|
|
366
|
+
continue;
|
|
367
|
+
const subject = `${fn.schema}.${fn.name}(${fn.args})`;
|
|
368
|
+
const { problems, suppressed } = await (0, lint_1.lintDefinition)(fn.definition, fn.language, subject, { rules: lintRuleIds });
|
|
369
|
+
for (const p of problems)
|
|
370
|
+
findings.push(lintFinding(fn, subject, p));
|
|
371
|
+
for (const s of suppressed)
|
|
372
|
+
findings.push(lintFinding(fn, subject, s, true));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
findings = (0, resolve_1.applyRulesToFindings)(resolved, findings);
|
|
376
|
+
// Routine findings are graded by whether an untrusted role can *call the
|
|
377
|
+
// function*, not by whether its schema is on the API surface — see
|
|
378
|
+
// `RuleMeta.subject`. Resolved only when such a finding exists, so an audit
|
|
379
|
+
// with the routine rules off still costs one query less.
|
|
380
|
+
const routineRoles = [...new Set([...apiRoles, ...(exposure.anonRoles ?? [])])];
|
|
381
|
+
let routineReach;
|
|
382
|
+
const isRoutineExposed = async (schema, name) => {
|
|
383
|
+
if (!routineReach) {
|
|
384
|
+
routineReach = (0, routines_1.resolveRoutineReach)(await getFunctions(), {
|
|
385
|
+
roles: routineRoles,
|
|
386
|
+
graph: roleGraph,
|
|
387
|
+
schemaAcls: schemaAclsByName,
|
|
388
|
+
exposedSchemas
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return routineReach.has(`${schema}.${name}`);
|
|
392
|
+
};
|
|
393
|
+
// Stamp direction (from the registry) and exposure on every finding.
|
|
394
|
+
const publicRead = config.public?.read ?? [];
|
|
395
|
+
const perfIgnore = config.perf?.ignore ?? [];
|
|
396
|
+
for (const f of findings) {
|
|
397
|
+
const meta = registry_1.RULES_BY_CODE.get(f.code);
|
|
398
|
+
if (meta && f.direction === undefined)
|
|
399
|
+
f.direction = meta.direction;
|
|
400
|
+
if (meta && f.dimension === undefined)
|
|
401
|
+
f.dimension = (0, registry_1.dimensionOf)(meta);
|
|
402
|
+
if (exposure.known && f.schema) {
|
|
403
|
+
// L14 names a relation outside the audited schemas by construction, so
|
|
404
|
+
// grading it by its own schema would file every finding as an internal
|
|
405
|
+
// advisory. What is exposed is the view an API role reads it through.
|
|
406
|
+
const viewRef = f.code === 'L14'
|
|
407
|
+
? f.context?.view
|
|
408
|
+
: undefined;
|
|
409
|
+
// A routine finding names a function somewhere in the finding — in
|
|
410
|
+
// `context.function` for the reach rules, in `schema`/`table` for the
|
|
411
|
+
// convention linter, which files a function under the fields a relation
|
|
412
|
+
// finding uses.
|
|
413
|
+
const routineRef = meta && (0, registry_1.subjectOf)(meta) === 'routine'
|
|
414
|
+
? f.context?.function?.replace(/\(.*$/, '')
|
|
415
|
+
?? (f.table ? `${f.schema}.${f.table}` : undefined)
|
|
416
|
+
: undefined;
|
|
417
|
+
if (routineRef) {
|
|
418
|
+
const dot = routineRef.lastIndexOf('.');
|
|
419
|
+
f.exposed = await isRoutineExposed(routineRef.slice(0, dot), routineRef.slice(dot + 1));
|
|
420
|
+
}
|
|
421
|
+
else if (viewRef) {
|
|
422
|
+
f.exposed = isExposed(viewRef.slice(0, viewRef.lastIndexOf('.')), viewRef.slice(viewRef.lastIndexOf('.') + 1));
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
f.exposed = isExposed(f.schema, f.table);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// A key that looks like a write-once provisioning pointer, where the
|
|
429
|
+
// reviewer has chosen to read the finding rather than gate on it. Applied
|
|
430
|
+
// here because severities are restamped from the rule registry above.
|
|
431
|
+
if (onWriteOncePointer === 'demote' && f.code === 'X1'
|
|
432
|
+
&& f.context?.pathAssessment === 'write-once-shaped') {
|
|
433
|
+
f.acknowledged = true;
|
|
434
|
+
f.severity = 'info';
|
|
435
|
+
f.message += ' — write-once-shaped (perf.paths.onWriteOncePointer)';
|
|
436
|
+
}
|
|
437
|
+
// Declared-intentional perf debt: cold tables, tiny lookups, anything the
|
|
438
|
+
// planner will seq-scan regardless. Reported, but off the perf score.
|
|
439
|
+
if (f.dimension === 'perf' && f.schema && f.table
|
|
440
|
+
&& perfIgnore.some((p) => (0, resolve_1.matchTablePattern)(p, `${f.schema}.${f.table}`))) {
|
|
441
|
+
f.acknowledged = true;
|
|
442
|
+
f.severity = 'info';
|
|
443
|
+
f.message += ' — acknowledged (perf.ignore)';
|
|
444
|
+
f.hint = 'This table is declared in `perf.ignore`, so the finding is treated as intentional and does not affect the perf score.';
|
|
445
|
+
}
|
|
446
|
+
// Declared-public reads: an open SELECT on a table listed in
|
|
447
|
+
// `public.read` is intent, not a finding — acknowledge it (info,
|
|
448
|
+
// excluded from the score). Undeclared open reads stay scored.
|
|
449
|
+
if (f.code === 'A8' && f.schema && f.table
|
|
450
|
+
&& publicRead.some((p) => (0, resolve_1.matchTablePattern)(p, `${f.schema}.${f.table}`))) {
|
|
451
|
+
f.acknowledged = true;
|
|
452
|
+
f.severity = 'info';
|
|
453
|
+
f.message += ' — declared public read (public.read)';
|
|
454
|
+
f.hint = 'This table is declared in `public.read`, so the open read is treated as intentional and does not affect the score.';
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// W1: no exposure surface — the whole database is assumed reachable.
|
|
458
|
+
if (!exposure.known && resolved.rules.get('W1')?.enabled !== false) {
|
|
459
|
+
findings.push({
|
|
460
|
+
code: 'W1',
|
|
461
|
+
severity: resolved.rules.get('W1')?.severity ?? 'medium',
|
|
462
|
+
category: 'meta',
|
|
463
|
+
direction: 'neutral',
|
|
464
|
+
message: 'No exposure surface configured — the audit assumes the entire database is reachable and the score is capped',
|
|
465
|
+
hint: 'Declare `exposure.schemas` (or use `exposure.resolver: "constructive"` on a Constructive database) so the score reflects what the exposed APIs can actually reach.'
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
// Planner proof runs last: it reads the findings the rules produced and
|
|
469
|
+
// acknowledges the ones the planner disagrees with, before scoring.
|
|
470
|
+
let explainReport;
|
|
471
|
+
if (explainEnabled) {
|
|
472
|
+
explainReport = await (0, explain_1.proveFindings)(exec, findings.filter((f) => f.dimension === 'perf' && !f.acknowledged), indexesByTable, { minRows: config.perf?.explain?.minRows });
|
|
473
|
+
}
|
|
474
|
+
findings.sort(compareFindings);
|
|
475
|
+
const exposureReport = {
|
|
476
|
+
known: exposure.known,
|
|
477
|
+
source: exposure.source,
|
|
478
|
+
plane: planes[0].name,
|
|
479
|
+
schemas: exposure.schemas,
|
|
480
|
+
...(exposure.roles ? { roles: exposure.roles } : {}),
|
|
481
|
+
...(exposure.anonRoles && exposure.anonRoles.length > 0
|
|
482
|
+
? { anonRoles: exposure.anonRoles }
|
|
483
|
+
: {}),
|
|
484
|
+
exposedTables,
|
|
485
|
+
totalTables: snapshot.length,
|
|
486
|
+
...(apiReach && apiReach.unreachable.length > 0
|
|
487
|
+
? { unaddressable: apiReach.unreachable.filter((r) => exposedSchemas.has(r.schema)) }
|
|
488
|
+
: {})
|
|
489
|
+
};
|
|
490
|
+
const securityFindings = findings.filter((f) => f.dimension !== 'perf');
|
|
491
|
+
const perfFindings = findings.filter((f) => f.dimension === 'perf');
|
|
492
|
+
const report = {
|
|
493
|
+
version: version_1.version,
|
|
494
|
+
generatedAt: new Date().toISOString(),
|
|
495
|
+
provenance: {
|
|
496
|
+
version: version_1.version,
|
|
497
|
+
fingerprint: (0, fingerprint_1.configFingerprint)(config, version_1.version),
|
|
498
|
+
sealed: options.sealed === true,
|
|
499
|
+
...(options.preset ? { preset: options.preset } : {})
|
|
500
|
+
},
|
|
501
|
+
summary: (0, types_1.summarize)(findings),
|
|
502
|
+
findings,
|
|
503
|
+
score: (0, score_1.computeScore)(securityFindings, config.scoring, {
|
|
504
|
+
exposedTables,
|
|
505
|
+
exposureKnown: exposure.known
|
|
506
|
+
}),
|
|
507
|
+
exposure: exposureReport
|
|
508
|
+
};
|
|
509
|
+
// Access planes. The primary plane's score is `report.score` — computed
|
|
510
|
+
// above, against the exposure surface — so declaring planes can never move
|
|
511
|
+
// the headline number; the secondaries answer what the headline cannot.
|
|
512
|
+
const reaches = (0, planes_1.resolvePlaneReach)(planes, snapshot, roleGraph, apiReach);
|
|
513
|
+
(0, planes_1.stampPlanes)(findings, reaches);
|
|
514
|
+
if (reaches.length > 1) {
|
|
515
|
+
report.planes = reaches.map((reach) => {
|
|
516
|
+
if (reach.skipped) {
|
|
517
|
+
return {
|
|
518
|
+
...(0, planes_1.scorePlane)(reach, [], config.scoring, exposure.known),
|
|
519
|
+
skipped: reach.skipped
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
if (reach.plane.primary) {
|
|
523
|
+
return {
|
|
524
|
+
...(0, planes_1.scorePlane)(reach, securityFindings, config.scoring, exposure.known),
|
|
525
|
+
exposedTables,
|
|
526
|
+
score: report.score,
|
|
527
|
+
summary: (0, types_1.summarize)(securityFindings.filter((f) => f.exposed !== false))
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return (0, planes_1.scorePlane)(reach, securityFindings, config.scoring, exposure.known);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
// Per-role exposure: computed for the configured untrusted roles (L5/R1
|
|
534
|
+
// options), so the report answers "what can anonymous access?" directly
|
|
535
|
+
// even when no finding fires.
|
|
536
|
+
const untrustedRoles = [...new Set([
|
|
537
|
+
...(withExposedRoles(resolved.rules.get('L5')?.options, exposure)?.roles ?? []),
|
|
538
|
+
...(withExposedRoles(resolved.rules.get('R1')?.options, exposure)?.roles ?? [])
|
|
539
|
+
])].sort();
|
|
540
|
+
if (untrustedRoles.length > 0) {
|
|
541
|
+
const roleAccess = {
|
|
542
|
+
roles: (0, lattice_1.computeRoleAccess)(snapshot, roleGraph, untrustedRoles)
|
|
543
|
+
};
|
|
544
|
+
report.roleAccess = roleAccess;
|
|
545
|
+
}
|
|
546
|
+
if (revocableGrants)
|
|
547
|
+
report.revocableGrants = revocableGrants;
|
|
548
|
+
if (perfEnabled) {
|
|
549
|
+
// `S*` findings are scored by default — opting into `--stats` is the
|
|
550
|
+
// opt-in — but can be demoted to advisories for a deterministic grade.
|
|
551
|
+
const includeStats = config.perf?.scoring?.includeStats ?? true;
|
|
552
|
+
const scorable = includeStats
|
|
553
|
+
? perfFindings
|
|
554
|
+
: perfFindings.filter((f) => !f.code.startsWith('S'));
|
|
555
|
+
const perf = {
|
|
556
|
+
findings: perfFindings,
|
|
557
|
+
summary: (0, types_1.summarize)(perfFindings),
|
|
558
|
+
score: (0, score_1.computeScore)(scorable, config.perf?.scoring, {
|
|
559
|
+
exposedTables,
|
|
560
|
+
exposureKnown: exposure.known
|
|
561
|
+
})
|
|
562
|
+
};
|
|
563
|
+
if (paths.size > 0) {
|
|
564
|
+
const all = [...paths.values()];
|
|
565
|
+
const shaped = all.filter((p) => p.assessment === 'write-once-shaped');
|
|
566
|
+
const declaredHidden = all.filter((p) => p.signals.some((s) => s.name === 'behavior-hidden')).length;
|
|
567
|
+
perf.paths = {
|
|
568
|
+
total: paths.size,
|
|
569
|
+
read: all.filter((p) => p.assessment === 'read').length,
|
|
570
|
+
writeOnceShaped: shaped.length,
|
|
571
|
+
tables: new Set(shaped.map((p) => `${p.schema}.${p.table}`)).size,
|
|
572
|
+
onWriteOncePointer,
|
|
573
|
+
...(declaredHidden > 0 ? { declaredHidden } : {})
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
if (statsSnapshot) {
|
|
577
|
+
const stats = {
|
|
578
|
+
source: 'live',
|
|
579
|
+
tables: statsSnapshot.tables.length,
|
|
580
|
+
statsReset: statsSnapshot.statsReset,
|
|
581
|
+
scored: includeStats
|
|
582
|
+
};
|
|
583
|
+
if (statsSnapshot.statementsUnavailable) {
|
|
584
|
+
stats.notes = [statsSnapshot.statementsUnavailable];
|
|
585
|
+
}
|
|
586
|
+
perf.stats = stats;
|
|
587
|
+
}
|
|
588
|
+
if (explainReport)
|
|
589
|
+
perf.explain = explainReport;
|
|
590
|
+
report.perf = perf;
|
|
591
|
+
}
|
|
592
|
+
// Scorecards last: they are named questions asked of the finished finding
|
|
593
|
+
// set, so every stamp a scorecard can select on (exposure, planes, perf
|
|
594
|
+
// dimension) is already on the findings by now.
|
|
595
|
+
report.scorecards = (0, scorecards_1.computeScorecards)(findings, report.score, { ...scorecards_1.RESERVED_SCORECARDS, ...(config.scorecards ?? {}) }, { exposedTables, totalTables: snapshot.length, exposureKnown: exposure.known });
|
|
596
|
+
if (options.callGraph) {
|
|
597
|
+
const functions = await getFunctions();
|
|
598
|
+
report.callGraph = await (0, graph_1.buildCallGraph)({
|
|
599
|
+
functions,
|
|
600
|
+
tables: snapshot,
|
|
601
|
+
exposedSchemas: exposure.known ? exposure.schemas : undefined,
|
|
602
|
+
apiRoles: exposure.roles && exposure.roles.length > 0
|
|
603
|
+
? exposure.roles
|
|
604
|
+
: ['anonymous', 'authenticated']
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return report;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Materializes `rolesFrom: 'exposure'` into concrete role names. An adapter
|
|
611
|
+
* resolves the API-edge roles from the catalog (a Constructive API's
|
|
612
|
+
* `anon_role`, PostgREST's `pgrst.db_anon_role`, the role a graphile
|
|
613
|
+
* authenticator can `SET ROLE` to); those roles are exactly the untrusted
|
|
614
|
+
* ones, and their names are per-deployment, so a preset names the *source*
|
|
615
|
+
* instead of the roles. Explicit `roles` still apply — the two union.
|
|
616
|
+
*/
|
|
617
|
+
function withExposedRoles(options, exposure) {
|
|
618
|
+
if (!options?.rolesFrom)
|
|
619
|
+
return options;
|
|
620
|
+
const resolved = options.rolesFrom === 'anon' ? exposure.anonRoles : exposure.roles;
|
|
621
|
+
const roles = [...new Set([...(options.roles ?? []), ...(resolved ?? [])])].sort();
|
|
622
|
+
return { ...options, roles };
|
|
623
|
+
}
|
|
624
|
+
/** Stats floors, config over defaults. */
|
|
625
|
+
function statsThresholds(config) {
|
|
626
|
+
const stats = config.perf?.stats ?? {};
|
|
627
|
+
return {
|
|
628
|
+
minRows: stats.minRows ?? stats_1.DEFAULT_STATS_THRESHOLDS.minRows,
|
|
629
|
+
seqScanRatio: stats.seqScanRatio ?? stats_1.DEFAULT_STATS_THRESHOLDS.seqScanRatio,
|
|
630
|
+
minIndexBytes: stats.minIndexBytes ?? stats_1.DEFAULT_STATS_THRESHOLDS.minIndexBytes,
|
|
631
|
+
deadTupleRatio: stats.deadTupleRatio ?? stats_1.DEFAULT_STATS_THRESHOLDS.deadTupleRatio,
|
|
632
|
+
minTimeShare: stats.minTimeShare ?? stats_1.DEFAULT_STATS_THRESHOLDS.minTimeShare,
|
|
633
|
+
topStatements: stats.topStatements ?? stats_1.DEFAULT_STATS_THRESHOLDS.topStatements
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
async function auditTableAst(exec, table, indexes) {
|
|
637
|
+
if (table.policies.length === 0)
|
|
638
|
+
return [];
|
|
639
|
+
// Collect all function names referenced across this table's policies so we
|
|
640
|
+
// can resolve volatility in one round-trip.
|
|
641
|
+
const funcNames = [];
|
|
642
|
+
const parsed = [];
|
|
643
|
+
for (const p of table.policies) {
|
|
644
|
+
const using = await (0, anti_patterns_1.parseOrNull)(p.using, `${table.schema}.${table.name}.${p.name} USING`);
|
|
645
|
+
const withCheck = await (0, anti_patterns_1.parseOrNull)(p.withCheck, `${table.schema}.${table.name}.${p.name} WITH CHECK`);
|
|
646
|
+
parsed.push({ policy: p, using, withCheck });
|
|
647
|
+
if (using)
|
|
648
|
+
funcNames.push(...(0, anti_patterns_1.collectFunctionNames)(using));
|
|
649
|
+
if (withCheck)
|
|
650
|
+
funcNames.push(...(0, anti_patterns_1.collectFunctionNames)(withCheck));
|
|
651
|
+
}
|
|
652
|
+
let volatility;
|
|
653
|
+
try {
|
|
654
|
+
volatility = await (0, proc_1.lookupVolatility)(exec, funcNames);
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
volatility = new Map();
|
|
658
|
+
}
|
|
659
|
+
const findings = [];
|
|
660
|
+
const predicateColumns = new Map();
|
|
661
|
+
for (const { policy, using, withCheck } of parsed) {
|
|
662
|
+
const trivial = (0, anti_patterns_1.checkTriviallyPermissive)(table, policy, using, withCheck);
|
|
663
|
+
if (trivial)
|
|
664
|
+
findings.push(trivial);
|
|
665
|
+
if (using) {
|
|
666
|
+
findings.push(...(0, anti_patterns_1.checkVolatileFunctions)(table, using, volatility, policy.name));
|
|
667
|
+
findings.push(...(0, anti_patterns_1.checkSessionUserGating)(table, using, policy.name));
|
|
668
|
+
}
|
|
669
|
+
if (withCheck) {
|
|
670
|
+
findings.push(...(0, anti_patterns_1.checkVolatileFunctions)(table, withCheck, volatility, policy.name));
|
|
671
|
+
findings.push(...(0, anti_patterns_1.checkSessionUserGating)(table, withCheck, policy.name));
|
|
672
|
+
}
|
|
673
|
+
if (!indexes)
|
|
674
|
+
continue;
|
|
675
|
+
// --- Policy-aware perf rules (X2/X3/X4/X9) ---
|
|
676
|
+
const cols = [];
|
|
677
|
+
if (using) {
|
|
678
|
+
cols.push(...(0, policy_index_1.collectPredicateColumns)(using, 'USING', table.name));
|
|
679
|
+
findings.push(...(0, policy_index_1.checkNonLeakproofPolicyFunctions)(table, using, volatility, policy.name));
|
|
680
|
+
findings.push(...(0, policy_index_1.checkUnhoistedPolicyFunctions)(table, using, volatility, policy.name, 'USING'));
|
|
681
|
+
}
|
|
682
|
+
if (withCheck) {
|
|
683
|
+
cols.push(...(0, policy_index_1.collectPredicateColumns)(withCheck, 'WITH CHECK', table.name));
|
|
684
|
+
findings.push(...(0, policy_index_1.checkNonLeakproofPolicyFunctions)(table, withCheck, volatility, policy.name));
|
|
685
|
+
findings.push(...(0, policy_index_1.checkUnhoistedPolicyFunctions)(table, withCheck, volatility, policy.name, 'WITH CHECK'));
|
|
686
|
+
}
|
|
687
|
+
if (cols.length > 0)
|
|
688
|
+
predicateColumns.set(policy.name, cols);
|
|
689
|
+
}
|
|
690
|
+
if (indexes && predicateColumns.size > 0) {
|
|
691
|
+
findings.push(...(0, policy_index_1.checkUnindexedPolicyColumns)(table, indexes, predicateColumns));
|
|
692
|
+
findings.push(...(0, policy_index_1.checkPolicyColumnCasts)(table, indexes, predicateColumns));
|
|
693
|
+
}
|
|
694
|
+
return dedupe(findings);
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Relations named by any RLS policy predicate in the snapshot.
|
|
698
|
+
*
|
|
699
|
+
* Whole-word token matching over the predicate text, matching both the bare
|
|
700
|
+
* and the schema-qualified name. Deliberately over-eager: a spurious match
|
|
701
|
+
* costs one unreported L6, a missed one costs a recommendation to revoke a
|
|
702
|
+
* grant that authorization depends on.
|
|
703
|
+
*/
|
|
704
|
+
function policyReferencedRelations(tables) {
|
|
705
|
+
const tokens = new Set();
|
|
706
|
+
for (const table of tables) {
|
|
707
|
+
for (const policy of table.policies) {
|
|
708
|
+
for (const clause of [policy.using, policy.withCheck]) {
|
|
709
|
+
if (!clause)
|
|
710
|
+
continue;
|
|
711
|
+
for (const match of clause.matchAll(/[A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)?/g)) {
|
|
712
|
+
tokens.add(match[0].toLowerCase());
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const referenced = new Set();
|
|
718
|
+
for (const table of tables) {
|
|
719
|
+
const key = `${table.schema}.${table.name}`;
|
|
720
|
+
if (tokens.has(key.toLowerCase()) || tokens.has(table.name.toLowerCase())) {
|
|
721
|
+
referenced.add(key);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return referenced;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Map a source-level lint problem onto a `Finding`. The function's name rides
|
|
728
|
+
* in the `table` slot so overrides, exposure and sorting key on it the same
|
|
729
|
+
* way they do for table findings. Suppressed problems are emitted as
|
|
730
|
+
* acknowledged findings — visible as accepted risk, off the score — carrying
|
|
731
|
+
* their waiver reason and scope in `context`.
|
|
732
|
+
*/
|
|
733
|
+
function lintFinding(fn, subject, problem, suppressed = false) {
|
|
734
|
+
const meta = lint_1.LINT_RULES_BY_ID.get(problem.ruleId);
|
|
735
|
+
return {
|
|
736
|
+
code: meta.code,
|
|
737
|
+
severity: registry_1.RULES_BY_CODE.get(meta.code).defaultSeverity,
|
|
738
|
+
category: 'convention',
|
|
739
|
+
schema: fn.schema,
|
|
740
|
+
table: fn.name,
|
|
741
|
+
message: `${problem.message} in ${subject}`,
|
|
742
|
+
...(problem.hint ? { hint: problem.hint } : {}),
|
|
743
|
+
...(suppressed ? { acknowledged: true } : {}),
|
|
744
|
+
context: {
|
|
745
|
+
...problem.context,
|
|
746
|
+
function: subject,
|
|
747
|
+
line: problem.line,
|
|
748
|
+
...(suppressed
|
|
749
|
+
? {
|
|
750
|
+
suppressed: true,
|
|
751
|
+
reason: problem.reason,
|
|
752
|
+
suppressionScope: problem.scope
|
|
753
|
+
}
|
|
754
|
+
: {})
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function compareFindings(a, b) {
|
|
759
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
760
|
+
if (order[a.severity] !== order[b.severity])
|
|
761
|
+
return order[a.severity] - order[b.severity];
|
|
762
|
+
if (a.schema !== b.schema)
|
|
763
|
+
return (a.schema ?? '').localeCompare(b.schema ?? '');
|
|
764
|
+
if (a.table !== b.table)
|
|
765
|
+
return (a.table ?? '').localeCompare(b.table ?? '');
|
|
766
|
+
return a.code.localeCompare(b.code);
|
|
767
|
+
}
|
|
768
|
+
function dedupe(findings) {
|
|
769
|
+
const seen = new Set();
|
|
770
|
+
const out = [];
|
|
771
|
+
for (const f of findings) {
|
|
772
|
+
const key = [f.code, f.schema, f.table, f.policy, f.message].join('::');
|
|
773
|
+
if (seen.has(key))
|
|
774
|
+
continue;
|
|
775
|
+
seen.add(key);
|
|
776
|
+
out.push(f);
|
|
777
|
+
}
|
|
778
|
+
return out;
|
|
779
|
+
}
|