@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,562 @@
|
|
|
1
|
+
/** The scoring axis a rule belongs to (`security` unless declared otherwise). */
|
|
2
|
+
export function dimensionOf(meta) {
|
|
3
|
+
return meta.dimension ?? 'security';
|
|
4
|
+
}
|
|
5
|
+
/** What a rule's findings are about (`relation` unless declared otherwise). */
|
|
6
|
+
export function subjectOf(meta) {
|
|
7
|
+
return meta.subject ?? 'relation';
|
|
8
|
+
}
|
|
9
|
+
export const RULES = [
|
|
10
|
+
{
|
|
11
|
+
code: 'A1',
|
|
12
|
+
category: 'flags',
|
|
13
|
+
defaultSeverity: 'low',
|
|
14
|
+
direction: 'fail-closed',
|
|
15
|
+
title: 'RLS enabled but no policies (deny-all — confirm the lock is intended)',
|
|
16
|
+
scope: 'table'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
code: 'A2',
|
|
20
|
+
category: 'flags',
|
|
21
|
+
defaultSeverity: 'high',
|
|
22
|
+
direction: 'fail-open',
|
|
23
|
+
title: 'Grants exist on a table with RLS disabled',
|
|
24
|
+
scope: 'table'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
code: 'A3',
|
|
28
|
+
category: 'flags',
|
|
29
|
+
defaultSeverity: 'low',
|
|
30
|
+
direction: 'fail-open',
|
|
31
|
+
title: 'RLS enabled but FORCE ROW LEVEL SECURITY not set (owner bypass)',
|
|
32
|
+
scope: 'table'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
code: 'A4',
|
|
36
|
+
category: 'coverage',
|
|
37
|
+
defaultSeverity: 'low',
|
|
38
|
+
direction: 'fail-closed',
|
|
39
|
+
title: 'INSERT/UPDATE/DELETE grant with no covering policy — writes are denied at runtime',
|
|
40
|
+
scope: 'table'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
code: 'A5',
|
|
44
|
+
category: 'coverage',
|
|
45
|
+
defaultSeverity: 'low',
|
|
46
|
+
direction: 'fail-closed',
|
|
47
|
+
title: 'SELECT grant with no covering policy — queries silently return 0 rows',
|
|
48
|
+
scope: 'table'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
code: 'A6',
|
|
52
|
+
category: 'coverage',
|
|
53
|
+
defaultSeverity: 'info',
|
|
54
|
+
direction: 'fail-closed',
|
|
55
|
+
title: 'UPDATE has USING but no WITH CHECK (row-smuggling surface)',
|
|
56
|
+
scope: 'table'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
code: 'A7',
|
|
60
|
+
category: 'anti-pattern',
|
|
61
|
+
defaultSeverity: 'critical',
|
|
62
|
+
direction: 'fail-open',
|
|
63
|
+
title: 'Trivially-permissive WRITE policy (INSERT/UPDATE/DELETE/ALL with literal true)',
|
|
64
|
+
scope: 'policy-ast'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
code: 'A8',
|
|
68
|
+
category: 'anti-pattern',
|
|
69
|
+
defaultSeverity: 'low',
|
|
70
|
+
direction: 'fail-open',
|
|
71
|
+
title: 'Trivially-permissive SELECT policy (USING (true) — confirm public-read is intended)',
|
|
72
|
+
scope: 'policy-ast'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
code: 'P1',
|
|
76
|
+
category: 'anti-pattern',
|
|
77
|
+
defaultSeverity: 'high',
|
|
78
|
+
direction: 'neutral',
|
|
79
|
+
dimension: 'perf',
|
|
80
|
+
title: 'Policy body calls a VOLATILE function (per-row evaluation)',
|
|
81
|
+
scope: 'policy-ast'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
code: 'P1b',
|
|
85
|
+
category: 'anti-pattern',
|
|
86
|
+
defaultSeverity: 'medium',
|
|
87
|
+
direction: 'neutral',
|
|
88
|
+
dimension: 'perf',
|
|
89
|
+
title: 'Policy body calls a SECURITY DEFINER wrapper (cannot be inlined)',
|
|
90
|
+
scope: 'policy-ast'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
code: 'P5',
|
|
94
|
+
category: 'anti-pattern',
|
|
95
|
+
defaultSeverity: 'high',
|
|
96
|
+
direction: 'fail-open',
|
|
97
|
+
title: 'Policy body references session_user / current_user / pg_has_role',
|
|
98
|
+
scope: 'policy-ast'
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
code: 'R1',
|
|
102
|
+
category: 'anti-pattern',
|
|
103
|
+
defaultSeverity: 'critical',
|
|
104
|
+
direction: 'fail-open',
|
|
105
|
+
title: 'Untrusted role holds a write privilege (options: { roles: [...] })',
|
|
106
|
+
scope: 'table'
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
code: 'R2',
|
|
110
|
+
category: 'anti-pattern',
|
|
111
|
+
defaultSeverity: 'high',
|
|
112
|
+
direction: 'fail-open',
|
|
113
|
+
title: 'Permissive write policy applies to an untrusted role or PUBLIC (options: { roles: [...] })',
|
|
114
|
+
scope: 'table'
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
code: 'R3',
|
|
118
|
+
category: 'anti-pattern',
|
|
119
|
+
defaultSeverity: 'medium',
|
|
120
|
+
direction: 'fail-open',
|
|
121
|
+
title: 'RLS table has grants TO PUBLIC (includes all current and future roles)',
|
|
122
|
+
scope: 'table'
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
code: 'L1',
|
|
126
|
+
category: 'coverage',
|
|
127
|
+
defaultSeverity: 'low',
|
|
128
|
+
direction: 'fail-closed',
|
|
129
|
+
title: 'Dead indirect grant — privilege arrives via PUBLIC or role inheritance but no policy can admit it',
|
|
130
|
+
scope: 'table'
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
code: 'L2',
|
|
134
|
+
category: 'coverage',
|
|
135
|
+
defaultSeverity: 'low',
|
|
136
|
+
direction: 'fail-closed',
|
|
137
|
+
title: 'Dead policy — a permissive policy applies to a role holding no corresponding grant',
|
|
138
|
+
scope: 'table'
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
code: 'L3',
|
|
142
|
+
category: 'coverage',
|
|
143
|
+
defaultSeverity: 'low',
|
|
144
|
+
direction: 'fail-closed',
|
|
145
|
+
title: 'Unreachable grant — object privilege without USAGE on the schema',
|
|
146
|
+
scope: 'table'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
code: 'L4',
|
|
150
|
+
category: 'coverage',
|
|
151
|
+
defaultSeverity: 'info',
|
|
152
|
+
direction: 'neutral',
|
|
153
|
+
title: 'Dead schema USAGE — the role reaches no relation and no function in the schema (advisory)',
|
|
154
|
+
scope: 'table'
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
code: 'L5',
|
|
158
|
+
category: 'anti-pattern',
|
|
159
|
+
defaultSeverity: 'info',
|
|
160
|
+
direction: 'fail-open',
|
|
161
|
+
title: 'Untrusted role reaches an RLS-off table via PUBLIC or inheritance (options: { roles: [...] })',
|
|
162
|
+
scope: 'table'
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
code: 'L6',
|
|
166
|
+
category: 'coverage',
|
|
167
|
+
defaultSeverity: 'info',
|
|
168
|
+
direction: 'neutral',
|
|
169
|
+
title: 'Unaddressable grant — an API role holds privileges on a relation its API cannot name',
|
|
170
|
+
scope: 'table'
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
code: 'L7',
|
|
174
|
+
category: 'anti-pattern',
|
|
175
|
+
// Ships `info` (signal-only, zero score weight) because the rule is new
|
|
176
|
+
// and unproven — NOT because the fact is minor. An unauthenticated role
|
|
177
|
+
// that can SET ROLE to a role with extra grants is a real escalation, and
|
|
178
|
+
// when the assumed role bypasses RLS (context.targetBypassesRls) it should
|
|
179
|
+
// be `high`; escalate via config/preset once the finding proves itself.
|
|
180
|
+
defaultSeverity: 'info',
|
|
181
|
+
direction: 'fail-open',
|
|
182
|
+
title: 'Untrusted role can SET ROLE to a role with more reach (options: { roles: [...] })',
|
|
183
|
+
scope: 'table'
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
code: 'L8',
|
|
187
|
+
category: 'anti-pattern',
|
|
188
|
+
// Ships `info` (signal-only, zero score weight) because the rule is new
|
|
189
|
+
// and, uniquely among the L rules, reads SQL bodies rather than pure
|
|
190
|
+
// catalog. The fact is not informational: a definer view handing an
|
|
191
|
+
// anonymous role a table it holds no grant on is A2/L5 laundered through
|
|
192
|
+
// a view, and it is `medium` on its own merits — `high` when the base
|
|
193
|
+
// table has RLS the view owner is exempt from (`context.rlsBypassed`),
|
|
194
|
+
// because then the view also deletes the row filter. Escalate via
|
|
195
|
+
// config/preset once the finding proves itself in the field.
|
|
196
|
+
defaultSeverity: 'info',
|
|
197
|
+
direction: 'fail-open',
|
|
198
|
+
title: 'DEFINER view bypass — an untrusted role reads a base relation as the view owner (options: { roles: [...] })',
|
|
199
|
+
// Reported against the base relation, and gated on `skipAstChecks` in the
|
|
200
|
+
// audit rather than on `policy-ast`: the body it parses is a view's, not a
|
|
201
|
+
// policy's, so turning the `P*` rules off does not turn this one off.
|
|
202
|
+
scope: 'table'
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
code: 'L9',
|
|
206
|
+
category: 'anti-pattern',
|
|
207
|
+
// Ships `info` for the same reason as L8, and understates the same way: a
|
|
208
|
+
// definer view that is auto-updatable does not just leak rows, it lets an
|
|
209
|
+
// untrusted role *write* a table it holds nothing on, as the owner. On its
|
|
210
|
+
// own merits that is `high` — the write is unconditional, and when the
|
|
211
|
+
// base table has RLS the owner is exempt from (`context.rlsBypassed`) it
|
|
212
|
+
// also writes rows no policy would have admitted. Escalate via
|
|
213
|
+
// config/preset once the finding proves itself in the field.
|
|
214
|
+
defaultSeverity: 'info',
|
|
215
|
+
direction: 'fail-open',
|
|
216
|
+
title: 'DEFINER view write — an untrusted role writes a base relation as the view owner (options: { roles: [...] })',
|
|
217
|
+
scope: 'table'
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
code: 'L10',
|
|
221
|
+
category: 'anti-pattern',
|
|
222
|
+
// Ships `info` on the same new-rule posture. Note this one fires on
|
|
223
|
+
// `security_invoker` views too: `security_invoker` governs the view's own
|
|
224
|
+
// base relations, not the relations a rewrite rule's actions name, which
|
|
225
|
+
// are checked against the rule's table owner either way (verified against
|
|
226
|
+
// PG 18). A rule is also invisible to `pg_get_viewdef`, so this is reach
|
|
227
|
+
// no reading of the view's definition can find.
|
|
228
|
+
defaultSeverity: 'info',
|
|
229
|
+
direction: 'fail-open',
|
|
230
|
+
title: 'Rewrite-rule bypass — a rule on a view writes a relation as the view owner (options: { roles: [...] })',
|
|
231
|
+
scope: 'table'
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
code: 'L11',
|
|
235
|
+
category: 'anti-pattern',
|
|
236
|
+
// Ships `info` on the same new-rule posture, and understates: a matview is
|
|
237
|
+
// a stored copy, so a SELECT grant on it is an unconditional grant on the
|
|
238
|
+
// rows a REFRESH captured — RLS on the base relation never runs, and the
|
|
239
|
+
// matview can carry neither policies nor `security_invoker`. On its own
|
|
240
|
+
// merits that is `high` when the base is RLS-protected.
|
|
241
|
+
defaultSeverity: 'info',
|
|
242
|
+
direction: 'fail-open',
|
|
243
|
+
title: 'Materialized-view snapshot — an untrusted role reads stored rows the base relation would not serve it (options: { roles: [...] })',
|
|
244
|
+
scope: 'table'
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
code: 'L12',
|
|
248
|
+
category: 'anti-pattern',
|
|
249
|
+
// Ships `info`, and is the weakest of the L-series on purpose: the leak
|
|
250
|
+
// needs a leaky operator or a cheap function in the caller's own qual, so
|
|
251
|
+
// it is a capability rather than an unconditional read. `medium` once
|
|
252
|
+
// proven — the rows it exposes are precisely the ones the view was written
|
|
253
|
+
// to withhold.
|
|
254
|
+
defaultSeverity: 'info',
|
|
255
|
+
direction: 'fail-open',
|
|
256
|
+
title: 'Non-barrier filtering view — a view is an untrusted role\'s only path to a relation but its row filter is not a boundary (options: { roles: [...] })',
|
|
257
|
+
scope: 'table'
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
code: 'L13',
|
|
261
|
+
category: 'anti-pattern',
|
|
262
|
+
// Ships `info` on the same new-rule posture. The honest severity depends
|
|
263
|
+
// entirely on the relation: column-level SELECT on an RLS-off table for an
|
|
264
|
+
// anonymous role is the same exposure A2 calls `high`, and until this rule
|
|
265
|
+
// existed nothing in the package saw it at all, because every grant query
|
|
266
|
+
// read `relacl` and a column grant lives in `pg_attribute.attacl`.
|
|
267
|
+
defaultSeverity: 'info',
|
|
268
|
+
direction: 'fail-open',
|
|
269
|
+
title: 'Column-level grant — an untrusted role reaches a relation through column privileges no relation ACL shows (options: { roles: [...] })',
|
|
270
|
+
scope: 'table'
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
code: 'L14',
|
|
274
|
+
// `coverage`, not `anti-pattern`: the finding is that the audit stopped at
|
|
275
|
+
// the schema boundary, not that the view is wrong. Severity stays `info`
|
|
276
|
+
// however the reach is graded later — an unknown is never a proven leak,
|
|
277
|
+
// and scoring one would let an excluded schema move the number.
|
|
278
|
+
category: 'coverage',
|
|
279
|
+
defaultSeverity: 'info',
|
|
280
|
+
direction: 'neutral',
|
|
281
|
+
title: 'Unaudited base relation — a definer view reads a relation in a schema the audit did not introspect (options: { roles: [...] })',
|
|
282
|
+
scope: 'table'
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
code: 'L15',
|
|
286
|
+
// The other half of L14's coverage story: there the far end was out of
|
|
287
|
+
// scope, here the body itself is. Both report that nothing was graded,
|
|
288
|
+
// which is why neither is scored — an unreadable view is not evidence of
|
|
289
|
+
// a leak, and letting one move the number would reward opacity.
|
|
290
|
+
category: 'coverage',
|
|
291
|
+
defaultSeverity: 'info',
|
|
292
|
+
direction: 'neutral',
|
|
293
|
+
title: 'Unreadable body — an untrusted role reaches through a definer view or function whose body could not be followed (options: { roles: [...] })',
|
|
294
|
+
scope: 'table'
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
code: 'L16',
|
|
298
|
+
category: 'anti-pattern',
|
|
299
|
+
defaultSeverity: 'info',
|
|
300
|
+
direction: 'fail-open',
|
|
301
|
+
title: 'Sequence privilege — an untrusted role can advance or read a sequence, which no policy filters (options: { roles: [...] })',
|
|
302
|
+
scope: 'table'
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
code: 'L17',
|
|
306
|
+
// The A2 shape on an object where A2's remedy does not exist: Postgres
|
|
307
|
+
// refuses RLS on a foreign table, so "add a policy" is not available and
|
|
308
|
+
// the grant is unconditionally unfiltered.
|
|
309
|
+
category: 'anti-pattern',
|
|
310
|
+
defaultSeverity: 'info',
|
|
311
|
+
direction: 'fail-open',
|
|
312
|
+
title: 'Foreign-table grant — an untrusted role reaches a relation that cannot carry RLS (options: { roles: [...] })',
|
|
313
|
+
scope: 'table'
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
code: 'L18',
|
|
317
|
+
category: 'anti-pattern',
|
|
318
|
+
// Ships `info` on the same new-rule posture. The honest severity is
|
|
319
|
+
// `medium`: the view's `WHERE` is the only thing standing between an
|
|
320
|
+
// untrusted role and the relation, and without `WITH CHECK OPTION` that
|
|
321
|
+
// condition governs reads alone — the role writes rows outside it, which
|
|
322
|
+
// on a tenant or ownership predicate is a cross-boundary write. It stays
|
|
323
|
+
// below L9 because the write path itself is intended; what is missing is
|
|
324
|
+
// the constraint on it.
|
|
325
|
+
defaultSeverity: 'info',
|
|
326
|
+
direction: 'fail-open',
|
|
327
|
+
title: 'Writable filtering view without WITH CHECK OPTION — an untrusted role writes rows the view excludes (options: { roles: [...] })',
|
|
328
|
+
scope: 'table'
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
code: 'L19',
|
|
332
|
+
category: 'anti-pattern',
|
|
333
|
+
// The largest reach edge in the L-series, shipping on the same new-rule
|
|
334
|
+
// posture as the rest. Proven, the honest severity is A2's: a definer
|
|
335
|
+
// function hands an anonymous role a relation it holds nothing on, and
|
|
336
|
+
// unlike a view there is no `security_invoker` default to fall back on —
|
|
337
|
+
// Postgres grants EXECUTE to PUBLIC unless someone says otherwise.
|
|
338
|
+
defaultSeverity: 'info',
|
|
339
|
+
direction: 'fail-open',
|
|
340
|
+
title: 'Definer-function reach — an untrusted role touches a relation by executing a SECURITY DEFINER function (options: { roles: [...] })',
|
|
341
|
+
scope: 'table',
|
|
342
|
+
// The reach is the function's, not the relation's: a definer function in
|
|
343
|
+
// an unexposed schema still runs as its owner for whoever holds EXECUTE.
|
|
344
|
+
subject: 'routine',
|
|
345
|
+
// One finding per (relation × function) pair, but one revoke per function.
|
|
346
|
+
dedupBy: 'context.function'
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
code: 'L20',
|
|
350
|
+
category: 'anti-pattern',
|
|
351
|
+
// The write half of L19, and the suppression L9 has carried since it
|
|
352
|
+
// shipped: a write against a view with INSTEAD OF triggers becomes the
|
|
353
|
+
// trigger function's body, so it is that function's security attribute —
|
|
354
|
+
// not the view's — that decides who the write runs as.
|
|
355
|
+
defaultSeverity: 'info',
|
|
356
|
+
direction: 'fail-open',
|
|
357
|
+
title: 'INSTEAD OF trigger write — an untrusted role writes a relation through a trigger function that runs as its owner (options: { roles: [...] })',
|
|
358
|
+
scope: 'table',
|
|
359
|
+
subject: 'routine',
|
|
360
|
+
dedupBy: 'context.function'
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
code: 'L21',
|
|
364
|
+
// `granted − reachable`. The user asked for "L10", but L10 has been the
|
|
365
|
+
// rewrite-rule bypass rule since it shipped; this takes the next free
|
|
366
|
+
// L-code and keeps every existing identity intact.
|
|
367
|
+
//
|
|
368
|
+
// `coverage`, not `anti-pattern`: like L1/L3/L6 the finding is about what a
|
|
369
|
+
// grant can and cannot reach, not a leak — and it is `fail-closed`, so it
|
|
370
|
+
// is weightless and never moves the score. It ships `info` because it is
|
|
371
|
+
// new and, uniquely, its verdict is a *revoke recommendation*: the cost of
|
|
372
|
+
// a wrong one is an outage, so it stays advisory until the reachability
|
|
373
|
+
// proof has earned trust in the field. Its honest severity once proven is
|
|
374
|
+
// `low` — a revocable grant is latent surface, not an active leak, and the
|
|
375
|
+
// value is the retained-grant proof list, which is emitted whatever the
|
|
376
|
+
// severity. The rule is fail-closed by construction: it recommends a revoke
|
|
377
|
+
// only for a grant it proved unreachable, and suppresses (retains) every
|
|
378
|
+
// grant whose closure it could not fully read.
|
|
379
|
+
category: 'coverage',
|
|
380
|
+
defaultSeverity: 'info',
|
|
381
|
+
direction: 'fail-closed',
|
|
382
|
+
title: 'Revocable grant — a role holds EXECUTE no reachable path exercises (options: { roles: [...] })',
|
|
383
|
+
scope: 'table',
|
|
384
|
+
subject: 'routine'
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
code: 'W1',
|
|
388
|
+
category: 'meta',
|
|
389
|
+
defaultSeverity: 'medium',
|
|
390
|
+
direction: 'neutral',
|
|
391
|
+
title: 'No exposure surface configured — the whole database is assumed reachable and the score is capped',
|
|
392
|
+
scope: 'table'
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
code: 'X1',
|
|
396
|
+
category: 'index',
|
|
397
|
+
defaultSeverity: 'medium',
|
|
398
|
+
direction: 'neutral',
|
|
399
|
+
dimension: 'perf',
|
|
400
|
+
title: 'Foreign key with no covering index (slow joins and cascading deletes)',
|
|
401
|
+
scope: 'index'
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
code: 'X2',
|
|
405
|
+
category: 'index',
|
|
406
|
+
defaultSeverity: 'medium',
|
|
407
|
+
direction: 'neutral',
|
|
408
|
+
dimension: 'perf',
|
|
409
|
+
title: 'RLS policy filters on a column that is not the leading column of any index',
|
|
410
|
+
scope: 'policy-ast'
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
code: 'X3',
|
|
414
|
+
category: 'index',
|
|
415
|
+
defaultSeverity: 'medium',
|
|
416
|
+
direction: 'neutral',
|
|
417
|
+
dimension: 'perf',
|
|
418
|
+
title: 'RLS policy casts or wraps its own column, defeating a plain index',
|
|
419
|
+
scope: 'policy-ast'
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
code: 'X4',
|
|
423
|
+
category: 'index',
|
|
424
|
+
defaultSeverity: 'low',
|
|
425
|
+
direction: 'neutral',
|
|
426
|
+
dimension: 'perf',
|
|
427
|
+
title: 'RLS policy calls a non-LEAKPROOF function, blocking qual pushdown',
|
|
428
|
+
scope: 'policy-ast'
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
code: 'X5',
|
|
432
|
+
category: 'index',
|
|
433
|
+
defaultSeverity: 'low',
|
|
434
|
+
direction: 'neutral',
|
|
435
|
+
dimension: 'perf',
|
|
436
|
+
title: 'Redundant index — duplicated by, or a leading-column prefix of, another index',
|
|
437
|
+
scope: 'index'
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
code: 'X6',
|
|
441
|
+
category: 'index',
|
|
442
|
+
defaultSeverity: 'low',
|
|
443
|
+
direction: 'neutral',
|
|
444
|
+
dimension: 'perf',
|
|
445
|
+
title: 'Table has no primary key / no usable replica identity',
|
|
446
|
+
scope: 'index'
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
code: 'X7',
|
|
450
|
+
category: 'index',
|
|
451
|
+
defaultSeverity: 'medium',
|
|
452
|
+
direction: 'neutral',
|
|
453
|
+
dimension: 'perf',
|
|
454
|
+
title: 'Search column (tsvector/vector) with no index the search can use',
|
|
455
|
+
scope: 'index'
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
code: 'X8',
|
|
459
|
+
category: 'index',
|
|
460
|
+
defaultSeverity: 'info',
|
|
461
|
+
direction: 'neutral',
|
|
462
|
+
dimension: 'perf',
|
|
463
|
+
title: 'Sort-shaped column (timestamp/date) leads no index — heuristic advisory',
|
|
464
|
+
scope: 'index'
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
code: 'X9',
|
|
468
|
+
category: 'index',
|
|
469
|
+
defaultSeverity: 'medium',
|
|
470
|
+
direction: 'neutral',
|
|
471
|
+
dimension: 'perf',
|
|
472
|
+
title: 'RLS policy calls a STABLE function per row — not wrapped in a scalar sub-select (no InitPlan)',
|
|
473
|
+
scope: 'policy-ast'
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
code: 'S1',
|
|
477
|
+
category: 'index',
|
|
478
|
+
defaultSeverity: 'medium',
|
|
479
|
+
direction: 'neutral',
|
|
480
|
+
dimension: 'perf',
|
|
481
|
+
title: 'Sequential-scan-dominant table (runtime statistics)',
|
|
482
|
+
scope: 'stats'
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
code: 'S2',
|
|
486
|
+
category: 'index',
|
|
487
|
+
defaultSeverity: 'low',
|
|
488
|
+
direction: 'neutral',
|
|
489
|
+
dimension: 'perf',
|
|
490
|
+
title: 'Index the planner has never chosen (runtime statistics)',
|
|
491
|
+
scope: 'stats'
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
code: 'S3',
|
|
495
|
+
category: 'index',
|
|
496
|
+
defaultSeverity: 'low',
|
|
497
|
+
direction: 'neutral',
|
|
498
|
+
dimension: 'perf',
|
|
499
|
+
title: 'Dead-tuple bloat — autovacuum is not keeping up (runtime statistics)',
|
|
500
|
+
scope: 'stats'
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
code: 'S4',
|
|
504
|
+
category: 'index',
|
|
505
|
+
defaultSeverity: 'info',
|
|
506
|
+
direction: 'neutral',
|
|
507
|
+
dimension: 'perf',
|
|
508
|
+
title: 'Statement hotspot on a table in scope (pg_stat_statements)',
|
|
509
|
+
scope: 'stats'
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
code: 'C1',
|
|
513
|
+
category: 'convention',
|
|
514
|
+
defaultSeverity: 'high',
|
|
515
|
+
direction: 'fail-open',
|
|
516
|
+
title: 'Function sets search_path (house rule: never set it — fully-qualify instead)',
|
|
517
|
+
scope: 'function-src',
|
|
518
|
+
subject: 'routine'
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
code: 'C2',
|
|
522
|
+
category: 'convention',
|
|
523
|
+
defaultSeverity: 'medium',
|
|
524
|
+
direction: 'neutral',
|
|
525
|
+
title: 'Function uses a #variable_conflict directive',
|
|
526
|
+
scope: 'function-src',
|
|
527
|
+
subject: 'routine'
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
code: 'C3',
|
|
531
|
+
category: 'convention',
|
|
532
|
+
defaultSeverity: 'low',
|
|
533
|
+
direction: 'neutral',
|
|
534
|
+
title: 'Function has an unqualified relation reference (relies on search_path)',
|
|
535
|
+
scope: 'function-src',
|
|
536
|
+
subject: 'routine',
|
|
537
|
+
// Every unqualified reference in one body is one edit to that body.
|
|
538
|
+
dedupBy: 'context.function'
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
code: 'C4',
|
|
542
|
+
category: 'convention',
|
|
543
|
+
defaultSeverity: 'high',
|
|
544
|
+
direction: 'fail-open',
|
|
545
|
+
title: 'Function uses dynamic SQL (waivable with a categorized reason)',
|
|
546
|
+
scope: 'function-src',
|
|
547
|
+
subject: 'routine',
|
|
548
|
+
dedupBy: 'context.function'
|
|
549
|
+
}
|
|
550
|
+
];
|
|
551
|
+
export const RULES_BY_CODE = new Map(RULES.map((r) => [r.code, r]));
|
|
552
|
+
export function isKnownRule(code) {
|
|
553
|
+
return RULES_BY_CODE.has(code);
|
|
554
|
+
}
|
|
555
|
+
/** Expand a rule selector: an exact code (`P1b`) or a prefix wildcard (`P*`, `*`). */
|
|
556
|
+
export function expandRuleSelector(selector) {
|
|
557
|
+
if (selector.endsWith('*')) {
|
|
558
|
+
const prefix = selector.slice(0, -1);
|
|
559
|
+
return RULES.filter((r) => r.code.startsWith(prefix)).map((r) => r.code);
|
|
560
|
+
}
|
|
561
|
+
return isKnownRule(selector) ? [selector] : [];
|
|
562
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Grade, ScoringConfig } from '../config/types';
|
|
2
|
+
import type { Finding, Severity } from '../types';
|
|
3
|
+
export interface ScoreDeduction {
|
|
4
|
+
code: string;
|
|
5
|
+
count: number;
|
|
6
|
+
points: number;
|
|
7
|
+
/**
|
|
8
|
+
* What the audit would score if this rule were its only problem — the same
|
|
9
|
+
* curve applied to this rule's points alone. Grades the rule rather than
|
|
10
|
+
* the database.
|
|
11
|
+
*/
|
|
12
|
+
score: number;
|
|
13
|
+
grade: Grade;
|
|
14
|
+
/**
|
|
15
|
+
* Score the audit would *gain* by taking this rule to zero. Not
|
|
16
|
+
* interchangeable with `score`: because the curve is exponential a rule's
|
|
17
|
+
* payoff depends on how much other debt exists, so the same fix is worth
|
|
18
|
+
* more once the rest is clean.
|
|
19
|
+
*/
|
|
20
|
+
potential: number;
|
|
21
|
+
/**
|
|
22
|
+
* The rule contributes no points by construction — every finding is
|
|
23
|
+
* `info`-weighted or fail-closed. Reported so a run cannot imply that
|
|
24
|
+
* fixing them would move the score; it cannot.
|
|
25
|
+
*/
|
|
26
|
+
unscored?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The rule's raw points exceeded `maxRuleDensity` and were cut to it. The
|
|
29
|
+
* finding count is unchanged: what is capped is the rule's influence on the
|
|
30
|
+
* score, not the size of the problem it reports.
|
|
31
|
+
*/
|
|
32
|
+
capped?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Distinct units of repair behind `count`, when a rule declares one
|
|
35
|
+
* (`RuleMeta.dedupBy`). Points are charged per unit, so a rule that emits
|
|
36
|
+
* one finding per (relation × function) pair costs what fixing it costs.
|
|
37
|
+
*/
|
|
38
|
+
units?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface Score {
|
|
41
|
+
/** 0-100. */
|
|
42
|
+
value: number;
|
|
43
|
+
grade: Grade;
|
|
44
|
+
/** Scoring model identifier ('density' or 'weighted'). */
|
|
45
|
+
model: string;
|
|
46
|
+
/**
|
|
47
|
+
* Every rule that produced a scorable finding, largest deduction first,
|
|
48
|
+
* with its own score, grade and payoff-to-fix. Rules that contribute no
|
|
49
|
+
* points (info-weighted, fail-closed) are listed last and flagged
|
|
50
|
+
* `unscored` rather than omitted — a report that silently drops them
|
|
51
|
+
* implies that fixing them would help, and it would not.
|
|
52
|
+
*/
|
|
53
|
+
deductions: ScoreDeduction[];
|
|
54
|
+
/** Density model: risk points per exposed table. */
|
|
55
|
+
density?: number;
|
|
56
|
+
/** Density model: the exposed-table denominator. */
|
|
57
|
+
exposedTables?: number;
|
|
58
|
+
/** True when the score was capped because no exposure surface was known. */
|
|
59
|
+
cappedByUnknownExposure?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** Context the audit supplies for exposure-aware scoring. */
|
|
62
|
+
export interface ScoreContext {
|
|
63
|
+
/** Number of tables on the exposed surface (the density denominator). */
|
|
64
|
+
exposedTables?: number;
|
|
65
|
+
/** Whether an exposure surface was configured/resolved. */
|
|
66
|
+
exposureKnown?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The caller has already decided which findings count (a scorecard's
|
|
69
|
+
* selector). Skips the built-in exposure/acknowledgement/meta filtering,
|
|
70
|
+
* which would otherwise silently overrule a selector that asked for
|
|
71
|
+
* unexposed findings.
|
|
72
|
+
*/
|
|
73
|
+
prefiltered?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export declare const DEFAULT_WEIGHTS: Record<Severity, number>;
|
|
76
|
+
export declare const DEFAULT_GRADE_BANDS: Record<Exclude<Grade, 'F'>, number>;
|
|
77
|
+
/**
|
|
78
|
+
* Compute the audit score. The score is config-driven by design: disabling
|
|
79
|
+
* or retuning rules changes the findings and therefore the score, and only
|
|
80
|
+
* findings on the exposed surface count (see `Finding.exposed`).
|
|
81
|
+
*/
|
|
82
|
+
export declare function computeScore(findings: Finding[], config?: ScoringConfig, context?: ScoreContext): Score;
|
|
83
|
+
/** True when `grade` is at least as good as `minimum`. */
|
|
84
|
+
export declare function meetsGrade(grade: Grade, minimum: Grade): boolean;
|