@safegres/test 0.0.2 → 1.30.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +825 -412
- package/adapters.d.ts +8 -0
- package/adapters.js +18 -0
- package/ast/helpers.d.ts +16 -0
- package/ast/helpers.js +40 -0
- package/ast/parse.d.ts +31 -0
- package/ast/parse.js +87 -0
- package/ast/walk.d.ts +27 -0
- package/ast/walk.js +79 -0
- package/callgraph/baseline.d.ts +28 -0
- package/callgraph/baseline.js +55 -0
- package/callgraph/extract.d.ts +93 -0
- package/callgraph/extract.js +340 -0
- package/callgraph/graph.d.ts +82 -0
- package/callgraph/graph.js +276 -0
- package/checks/anti-patterns.d.ts +53 -0
- package/checks/anti-patterns.js +247 -0
- package/checks/column-grants.d.ts +20 -0
- package/checks/column-grants.js +74 -0
- package/checks/coverage.d.ts +43 -0
- package/checks/coverage.js +110 -0
- package/checks/definer-function.d.ts +91 -0
- package/checks/definer-function.js +458 -0
- package/checks/definer-view.d.ts +167 -0
- package/checks/definer-view.js +392 -0
- package/checks/indexes.d.ts +82 -0
- package/checks/indexes.js +263 -0
- package/checks/lattice.d.ts +179 -0
- package/checks/lattice.js +487 -0
- package/checks/object-acls.d.ts +32 -0
- package/checks/object-acls.js +129 -0
- package/checks/policy-index.d.ts +78 -0
- package/checks/policy-index.js +349 -0
- package/checks/revocable-grants.d.ts +140 -0
- package/checks/revocable-grants.js +377 -0
- package/checks/rls-flags.d.ts +25 -0
- package/checks/rls-flags.js +82 -0
- package/checks/role-reach.d.ts +329 -0
- package/checks/role-reach.js +277 -0
- package/checks/role-trust.d.ts +45 -0
- package/checks/role-trust.js +91 -0
- package/checks/set-role.d.ts +21 -0
- package/checks/set-role.js +66 -0
- package/checks/stats.d.ts +52 -0
- package/checks/stats.js +192 -0
- package/checks/view-exposure.d.ts +68 -0
- package/checks/view-exposure.js +236 -0
- package/checks/view-writes.d.ts +88 -0
- package/checks/view-writes.js +350 -0
- package/ci/baseline.d.ts +104 -0
- package/ci/baseline.js +111 -0
- package/ci/github.d.ts +47 -0
- package/ci/github.js +197 -0
- package/cli/audit.d.ts +3 -0
- package/cli/audit.js +560 -0
- package/cli/baseline.d.ts +3 -0
- package/cli/baseline.js +104 -0
- package/cli/commands.d.ts +2 -0
- package/cli/commands.js +60 -0
- package/cli/doctor.d.ts +3 -0
- package/cli/doctor.js +72 -0
- package/cli/eval.d.ts +3 -0
- package/cli/eval.js +153 -0
- package/cli/print-config.d.ts +3 -0
- package/cli/print-config.js +54 -0
- package/cli/shared.d.ts +43 -0
- package/cli/shared.js +153 -0
- package/cli.d.ts +2 -0
- package/cli.js +69 -0
- package/commands/audit.d.ts +65 -0
- package/commands/audit.js +779 -0
- package/commands/doctor.d.ts +19 -0
- package/commands/doctor.js +181 -0
- package/commands/eval.d.ts +67 -0
- package/commands/eval.js +93 -0
- package/config/fingerprint.d.ts +40 -0
- package/config/fingerprint.js +71 -0
- package/config/loader.d.ts +44 -0
- package/config/loader.js +210 -0
- package/config/presets.d.ts +95 -0
- package/config/presets.js +327 -0
- package/config/resolve.d.ts +47 -0
- package/config/resolve.js +192 -0
- package/config/schema.d.ts +54 -0
- package/config/schema.js +269 -0
- package/config/types.d.ts +562 -0
- package/config/validate.d.ts +14 -0
- package/config/validate.js +125 -0
- package/corpus/README.md +83 -0
- package/corpus/bootstrap.sql +14 -0
- package/corpus/cases/01-anon-write-grant/case.json +26 -0
- package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
- package/corpus/cases/02-grant-to-public/case.json +25 -0
- package/corpus/cases/02-grant-to-public/schema.sql +18 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
- package/corpus/cases/04-permissive-write-policy/case.json +30 -0
- package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
- package/corpus/cases/05-public-read-policy/case.json +25 -0
- package/corpus/cases/05-public-read-policy/schema.sql +16 -0
- package/corpus/cases/06-update-without-with-check/case.json +25 -0
- package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
- package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
- package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
- package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
- package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
- package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
- package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
- package/corpus/cases/10-rls-not-forced/case.json +24 -0
- package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
- package/corpus/cases/11-session-user-in-policy/case.json +24 -0
- package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
- package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
- package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
- package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
- package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
- package/corpus/cases/14-dead-policy/case.json +24 -0
- package/corpus/cases/14-dead-policy/schema.sql +18 -0
- package/corpus/cases/15-unreachable-grant/case.json +24 -0
- package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
- package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
- package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
- package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
- package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
- package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
- package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
- package/corpus/cases/19-policy-column-cast/case.json +24 -0
- package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
- package/corpus/cases/20-stable-function-per-row/case.json +24 -0
- package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
- package/corpus/cases/21-table-without-primary-key/case.json +24 -0
- package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
- package/corpus/cases/22-redundant-index/case.json +24 -0
- package/corpus/cases/22-redundant-index/schema.sql +16 -0
- package/corpus/cases/23-tsvector-without-index/case.json +24 -0
- package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
- package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
- package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
- package/corpus/cases/25-definer-view-bypass/case.json +31 -0
- package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
- package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
- package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
- package/corpus/cases/27-definer-view-write/case.json +36 -0
- package/corpus/cases/27-definer-view-write/schema.sql +40 -0
- package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
- package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
- package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
- package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
- package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
- package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
- package/corpus/cases/31-matview-snapshot/case.json +32 -0
- package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
- package/corpus/cases/33-leaky-filter-view/case.json +36 -0
- package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
- package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
- package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
- package/corpus/cases/35-column-grant-projection/case.json +27 -0
- package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
- package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
- package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
- package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
- package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
- package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
- package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
- package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
- package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
- package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
- package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
- package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
- package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
- package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
- package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
- package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
- package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
- package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
- package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
- package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
- package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
- package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
- package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
- package/corpus/cases/41-unreadable-view-body/case.json +33 -0
- package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
- package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
- package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
- package/corpus/cases/42-search-column-indexed/case.json +21 -0
- package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
- package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
- package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
- package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
- package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
- package/corpus/cases/45-anon-foreign-table/case.json +27 -0
- package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
- package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
- package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
- package/corpus/cases/47-checked-writable-view/case.json +37 -0
- package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
- package/corpus/cases/48-definer-function-reach/case.json +38 -0
- package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
- package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
- package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
- package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
- package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
- package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
- package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
- package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
- package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
- package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
- package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
- package/corpus/index.d.ts +79 -0
- package/corpus/index.js +125 -0
- package/esm/adapters.d.ts +8 -0
- package/esm/adapters.js +6 -0
- package/esm/ast/helpers.d.ts +16 -0
- package/esm/ast/helpers.js +36 -0
- package/esm/ast/parse.d.ts +31 -0
- package/esm/ast/parse.js +82 -0
- package/esm/ast/walk.d.ts +27 -0
- package/esm/ast/walk.js +72 -0
- package/esm/callgraph/baseline.d.ts +28 -0
- package/esm/callgraph/baseline.js +48 -0
- package/esm/callgraph/extract.d.ts +93 -0
- package/esm/callgraph/extract.js +333 -0
- package/esm/callgraph/graph.d.ts +82 -0
- package/esm/callgraph/graph.js +273 -0
- package/esm/checks/anti-patterns.d.ts +53 -0
- package/esm/checks/anti-patterns.js +240 -0
- package/esm/checks/column-grants.d.ts +20 -0
- package/esm/checks/column-grants.js +71 -0
- package/esm/checks/coverage.d.ts +43 -0
- package/esm/checks/coverage.js +104 -0
- package/esm/checks/definer-function.d.ts +91 -0
- package/esm/checks/definer-function.js +452 -0
- package/esm/checks/definer-view.d.ts +167 -0
- package/esm/checks/definer-view.js +382 -0
- package/esm/checks/indexes.d.ts +82 -0
- package/esm/checks/indexes.js +256 -0
- package/esm/checks/lattice.d.ts +179 -0
- package/esm/checks/lattice.js +475 -0
- package/esm/checks/object-acls.d.ts +32 -0
- package/esm/checks/object-acls.js +125 -0
- package/esm/checks/policy-index.d.ts +78 -0
- package/esm/checks/policy-index.js +342 -0
- package/esm/checks/revocable-grants.d.ts +140 -0
- package/esm/checks/revocable-grants.js +373 -0
- package/esm/checks/rls-flags.d.ts +25 -0
- package/esm/checks/rls-flags.js +77 -0
- package/esm/checks/role-reach.d.ts +329 -0
- package/esm/checks/role-reach.js +270 -0
- package/esm/checks/role-trust.d.ts +45 -0
- package/esm/checks/role-trust.js +86 -0
- package/esm/checks/set-role.d.ts +21 -0
- package/esm/checks/set-role.js +63 -0
- package/esm/checks/stats.d.ts +52 -0
- package/esm/checks/stats.js +184 -0
- package/esm/checks/view-exposure.d.ts +68 -0
- package/esm/checks/view-exposure.js +231 -0
- package/esm/checks/view-writes.d.ts +88 -0
- package/esm/checks/view-writes.js +344 -0
- package/esm/ci/baseline.d.ts +104 -0
- package/esm/ci/baseline.js +107 -0
- package/esm/ci/github.d.ts +47 -0
- package/esm/ci/github.js +161 -0
- package/esm/cli/audit.d.ts +3 -0
- package/esm/cli/audit.js +525 -0
- package/esm/cli/baseline.d.ts +3 -0
- package/esm/cli/baseline.js +102 -0
- package/esm/cli/commands.d.ts +2 -0
- package/esm/cli/commands.js +53 -0
- package/esm/cli/doctor.d.ts +3 -0
- package/esm/cli/doctor.js +67 -0
- package/esm/cli/eval.d.ts +3 -0
- package/esm/cli/eval.js +115 -0
- package/esm/cli/print-config.d.ts +3 -0
- package/esm/cli/print-config.js +52 -0
- package/esm/cli/shared.d.ts +43 -0
- package/esm/cli/shared.js +112 -0
- package/esm/cli.d.ts +2 -0
- package/esm/cli.js +67 -0
- package/esm/commands/audit.d.ts +65 -0
- package/esm/commands/audit.js +776 -0
- package/esm/commands/doctor.d.ts +19 -0
- package/esm/commands/doctor.js +178 -0
- package/esm/commands/eval.d.ts +67 -0
- package/esm/commands/eval.js +90 -0
- package/esm/config/fingerprint.d.ts +40 -0
- package/esm/config/fingerprint.js +68 -0
- package/esm/config/loader.d.ts +44 -0
- package/esm/config/loader.js +172 -0
- package/esm/config/presets.d.ts +95 -0
- package/esm/config/presets.js +324 -0
- package/esm/config/resolve.d.ts +47 -0
- package/esm/config/resolve.js +180 -0
- package/esm/config/schema.d.ts +54 -0
- package/esm/config/schema.js +265 -0
- package/esm/config/types.d.ts +562 -0
- package/esm/config/validate.d.ts +14 -0
- package/esm/config/validate.js +122 -0
- package/esm/corpus/index.d.ts +79 -0
- package/esm/corpus/index.js +85 -0
- package/esm/exposure/adapters.d.ts +152 -0
- package/esm/exposure/adapters.js +463 -0
- package/esm/exposure/planes.d.ts +59 -0
- package/esm/exposure/planes.js +144 -0
- package/esm/exposure/reach.d.ts +78 -0
- package/esm/exposure/reach.js +105 -0
- package/esm/exposure/routines.d.ts +36 -0
- package/esm/exposure/routines.js +54 -0
- package/esm/index.d.ts +96 -0
- package/esm/index.js +57 -6
- package/esm/lint/index.d.ts +11 -0
- package/esm/lint/index.js +10 -0
- package/esm/perf/baseline.d.ts +40 -0
- package/esm/perf/baseline.js +83 -0
- package/esm/perf/explain.d.ts +46 -0
- package/esm/perf/explain.js +201 -0
- package/esm/pg/acl.d.ts +72 -0
- package/esm/pg/acl.js +186 -0
- package/esm/pg/behaviors.d.ts +114 -0
- package/esm/pg/behaviors.js +206 -0
- package/esm/pg/exposure.d.ts +74 -0
- package/esm/pg/exposure.js +198 -0
- package/esm/pg/functions.d.ts +52 -0
- package/esm/pg/functions.js +128 -0
- package/esm/pg/indexes.d.ts +192 -0
- package/esm/pg/indexes.js +395 -0
- package/esm/pg/introspect.d.ts +134 -0
- package/esm/pg/introspect.js +274 -0
- package/esm/pg/objects.d.ts +37 -0
- package/esm/pg/objects.js +108 -0
- package/esm/pg/paths.d.ts +114 -0
- package/esm/pg/paths.js +159 -0
- package/esm/pg/proc.d.ts +23 -0
- package/esm/pg/proc.js +55 -0
- package/esm/pg/reach-inputs.d.ts +47 -0
- package/esm/pg/reach-inputs.js +133 -0
- package/esm/pg/roles.d.ts +28 -0
- package/esm/pg/roles.js +50 -0
- package/esm/pg/stats.d.ts +65 -0
- package/esm/pg/stats.js +129 -0
- package/esm/pgpm-test.d.ts +22 -0
- package/esm/pgpm-test.js +39 -0
- package/esm/report/callgraph.d.ts +9 -0
- package/esm/report/callgraph.js +74 -0
- package/esm/report/compare.d.ts +143 -0
- package/esm/report/compare.js +195 -0
- package/esm/report/github.d.ts +74 -0
- package/esm/report/github.js +290 -0
- package/esm/report/json.d.ts +4 -0
- package/esm/report/json.js +3 -0
- package/esm/report/markdown.d.ts +25 -0
- package/esm/report/markdown.js +352 -0
- package/esm/report/pretty.d.ts +14 -0
- package/esm/report/pretty.js +318 -0
- package/esm/report/sarif.d.ts +40 -0
- package/esm/report/sarif.js +189 -0
- package/esm/report/view.d.ts +82 -0
- package/esm/report/view.js +126 -0
- package/esm/rules/registry.d.ts +66 -0
- package/esm/rules/registry.js +562 -0
- package/esm/score/score.d.ts +84 -0
- package/esm/score/score.js +233 -0
- package/esm/score/scorecards.d.ts +68 -0
- package/esm/score/scorecards.js +157 -0
- package/esm/types.d.ts +292 -0
- package/esm/types.js +26 -0
- package/esm/version.d.ts +8 -0
- package/esm/version.js +9 -0
- package/exposure/adapters.d.ts +152 -0
- package/exposure/adapters.js +468 -0
- package/exposure/planes.d.ts +59 -0
- package/exposure/planes.js +151 -0
- package/exposure/reach.d.ts +78 -0
- package/exposure/reach.js +109 -0
- package/exposure/routines.d.ts +36 -0
- package/exposure/routines.js +57 -0
- package/index.d.ts +96 -6
- package/index.js +229 -6
- package/lint/index.d.ts +11 -0
- package/lint/index.js +19 -0
- package/package.json +54 -48
- package/perf/baseline.d.ts +40 -0
- package/perf/baseline.js +92 -0
- package/perf/explain.d.ts +46 -0
- package/perf/explain.js +204 -0
- package/pg/acl.d.ts +72 -0
- package/pg/acl.js +191 -0
- package/pg/behaviors.d.ts +114 -0
- package/pg/behaviors.js +217 -0
- package/pg/exposure.d.ts +74 -0
- package/pg/exposure.js +205 -0
- package/pg/functions.d.ts +52 -0
- package/pg/functions.js +131 -0
- package/pg/indexes.d.ts +192 -0
- package/pg/indexes.js +400 -0
- package/pg/introspect.d.ts +134 -0
- package/pg/introspect.js +280 -0
- package/pg/objects.d.ts +37 -0
- package/pg/objects.js +111 -0
- package/pg/paths.d.ts +114 -0
- package/pg/paths.js +166 -0
- package/pg/proc.d.ts +23 -0
- package/pg/proc.js +58 -0
- package/pg/reach-inputs.d.ts +47 -0
- package/pg/reach-inputs.js +136 -0
- package/pg/roles.d.ts +28 -0
- package/pg/roles.js +54 -0
- package/pg/stats.d.ts +65 -0
- package/pg/stats.js +133 -0
- package/pgpm-test.d.ts +22 -0
- package/pgpm-test.js +42 -0
- package/report/callgraph.d.ts +9 -0
- package/report/callgraph.js +81 -0
- package/report/compare.d.ts +143 -0
- package/report/compare.js +205 -0
- package/report/github.d.ts +74 -0
- package/report/github.js +333 -0
- package/report/json.d.ts +4 -0
- package/report/json.js +6 -0
- package/report/markdown.d.ts +25 -0
- package/report/markdown.js +355 -0
- package/report/pretty.d.ts +14 -0
- package/report/pretty.js +324 -0
- package/report/sarif.d.ts +40 -0
- package/report/sarif.js +193 -0
- package/report/view.d.ts +82 -0
- package/report/view.js +132 -0
- package/rules/registry.d.ts +66 -0
- package/rules/registry.js +569 -0
- package/schema/safegres.schema.json +1119 -0
- package/score/score.d.ts +84 -0
- package/score/score.js +238 -0
- package/score/scorecards.d.ts +68 -0
- package/score/scorecards.js +162 -0
- package/types.d.ts +292 -0
- package/types.js +32 -0
- package/version.d.ts +8 -0
- package/version.js +12 -0
- package/LICENSE +0 -23
- package/admin.d.ts +0 -26
- package/admin.js +0 -182
- package/connect.d.ts +0 -19
- package/connect.js +0 -83
- package/dist/README.md +0 -557
- package/dist/package.json +0 -72
- package/esm/admin.js +0 -178
- package/esm/connect.js +0 -78
- package/esm/manager.js +0 -136
- package/esm/roles.js +0 -32
- package/esm/seed/adapters.js +0 -23
- package/esm/seed/csv.js +0 -44
- package/esm/seed/index.js +0 -16
- package/esm/seed/json.js +0 -18
- package/esm/seed/launchql.js +0 -19
- package/esm/seed/sqitch.js +0 -17
- package/esm/stream.js +0 -43
- package/esm/test-client.js +0 -150
- package/manager.d.ts +0 -25
- package/manager.js +0 -140
- package/roles.d.ts +0 -17
- package/roles.js +0 -38
- package/seed/adapters.d.ts +0 -4
- package/seed/adapters.js +0 -28
- package/seed/csv.d.ts +0 -9
- package/seed/csv.js +0 -49
- package/seed/index.d.ts +0 -16
- package/seed/index.js +0 -33
- package/seed/json.d.ts +0 -6
- package/seed/json.js +0 -21
- package/seed/launchql.d.ts +0 -2
- package/seed/launchql.js +0 -22
- package/seed/sqitch.d.ts +0 -2
- package/seed/sqitch.js +0 -20
- package/seed/types.d.ts +0 -13
- package/stream.d.ts +0 -2
- package/stream.js +0 -46
- package/test-client.d.ts +0 -49
- package/test-client.js +0 -154
- /package/{seed → config}/types.js +0 -0
- /package/esm/{seed → config}/types.js +0 -0
package/config/loader.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.safegresConfigLoader = safegresConfigLoader;
|
|
37
|
+
exports.loadConfig = loadConfig;
|
|
38
|
+
exports.configPathBase = configPathBase;
|
|
39
|
+
const confstash_1 = require("confstash");
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const presets_1 = require("./presets");
|
|
42
|
+
const resolve_1 = require("./resolve");
|
|
43
|
+
const validate_1 = require("./validate");
|
|
44
|
+
/**
|
|
45
|
+
* The safegres config loader. Discovers `safegres.config.{ts,js,mjs,cjs}`,
|
|
46
|
+
* `.safegresrc{,.json,.yaml,.yml,.js}`, `safegres.json`, or the `"safegres"`
|
|
47
|
+
* package.json key by walking up from cwd; resolves `extends` against the
|
|
48
|
+
* built-in presets (or files / npm packages).
|
|
49
|
+
*/
|
|
50
|
+
function safegresConfigLoader() {
|
|
51
|
+
return (0, confstash_1.createConfigLoader)({
|
|
52
|
+
tool: 'safegres',
|
|
53
|
+
presets: presets_1.PRESETS,
|
|
54
|
+
defaults: {},
|
|
55
|
+
validate: (config) => {
|
|
56
|
+
(0, validate_1.validateConfigShape)(config); // unknown keys, wrong value kinds
|
|
57
|
+
(0, resolve_1.resolveRules)(config); // throws ConfigValidationError on bad rules/overrides
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** Load and merge the effective safegres config. */
|
|
62
|
+
function loadConfig(params = {}) {
|
|
63
|
+
if (params.sealed)
|
|
64
|
+
return loadSealedConfig(params.preset);
|
|
65
|
+
const loader = safegresConfigLoader();
|
|
66
|
+
const overrides = { ...(params.overrides ?? {}) };
|
|
67
|
+
if (params.preset) {
|
|
68
|
+
const name = params.preset.includes(':') ? params.preset : `safegres:${params.preset}`;
|
|
69
|
+
if (!(name in presets_1.PRESETS)) {
|
|
70
|
+
throw new Error(`Unknown preset "${params.preset}". Available: ${Object.keys(presets_1.PRESETS).join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
overrides.extends = name;
|
|
73
|
+
}
|
|
74
|
+
// `extends` in overrides isn't expanded by confstash (it only expands file
|
|
75
|
+
// layers), so a CLI --preset is merged in as a layer *below* the file config.
|
|
76
|
+
const result = describeExtendsFailure(() => loader.loadSync({
|
|
77
|
+
cwd: params.cwd,
|
|
78
|
+
configFile: params.configFile,
|
|
79
|
+
overrides: stripExtends(overrides)
|
|
80
|
+
}));
|
|
81
|
+
const config = withUnionedOverrides(result);
|
|
82
|
+
if (params.preset) {
|
|
83
|
+
const presetConfig = presets_1.PRESETS[overrides.extends];
|
|
84
|
+
return { ...result, config: mergePreset(presetConfig, config) };
|
|
85
|
+
}
|
|
86
|
+
return { ...result, config };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A missing `extends` target surfaces from the loader as a bare `ENOENT` on a
|
|
90
|
+
* path nobody typed — the resolved one. Say what it was reached from, and that
|
|
91
|
+
* a name is a preset while a path is a file, which is the actual mistake.
|
|
92
|
+
*/
|
|
93
|
+
function describeExtendsFailure(load) {
|
|
94
|
+
try {
|
|
95
|
+
return load();
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
if (!message.includes('ENOENT'))
|
|
100
|
+
throw error;
|
|
101
|
+
throw new Error(`${message}\nsafegres: an "extends" target could not be read. A built-in preset is a name `
|
|
102
|
+
+ `(${Object.keys(presets_1.PRESETS).join(', ')}); anything else is resolved as a path relative to `
|
|
103
|
+
+ 'the file that declared it, or as an npm package.');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `overrides` is a list of scoped exceptions, so inheriting one file from
|
|
108
|
+
* another has to *add* to it. Array-typed keys otherwise replace — a child
|
|
109
|
+
* that narrows `exposure.schemas` means the narrower list — but replacing here
|
|
110
|
+
* would silently drop the base file's exceptions, which is how a shared config
|
|
111
|
+
* turns into two divergent copies. Preset chains have always unioned it; this
|
|
112
|
+
* makes a file chain agree.
|
|
113
|
+
*/
|
|
114
|
+
function withUnionedOverrides(result) {
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
const unioned = [];
|
|
117
|
+
for (const layer of result.layers) {
|
|
118
|
+
for (const override of layer.config.overrides ?? []) {
|
|
119
|
+
const key = JSON.stringify(override);
|
|
120
|
+
if (seen.has(key))
|
|
121
|
+
continue;
|
|
122
|
+
seen.add(key);
|
|
123
|
+
unioned.push(override);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (unioned.length === 0)
|
|
127
|
+
return result.config;
|
|
128
|
+
return { ...result.config, overrides: unioned };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resolve each configured path against the file that declared it, so an
|
|
132
|
+
* inherited `perf.baseline` keeps pointing at the base file's directory.
|
|
133
|
+
* Values that came from a preset or from CLI overrides have no directory of
|
|
134
|
+
* their own and fall back to the discovered config file, then to `cwd`.
|
|
135
|
+
*/
|
|
136
|
+
function configPathBase(result, cwd = process.cwd()) {
|
|
137
|
+
const fallback = result.filepath ? path.dirname(result.filepath) : cwd;
|
|
138
|
+
return {
|
|
139
|
+
dirFor(key) {
|
|
140
|
+
// Highest precedence first: the layer that won the merge is the layer
|
|
141
|
+
// whose directory the value is written relative to.
|
|
142
|
+
for (let i = result.layers.length - 1; i >= 0; i--) {
|
|
143
|
+
const layer = result.layers[i];
|
|
144
|
+
if (valueAt(layer.config, key) === undefined)
|
|
145
|
+
continue;
|
|
146
|
+
return layer.source === 'file' && layer.origin ? path.dirname(layer.origin) : fallback;
|
|
147
|
+
}
|
|
148
|
+
return fallback;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function valueAt(config, key) {
|
|
153
|
+
let node = config;
|
|
154
|
+
for (const part of key.split('.')) {
|
|
155
|
+
if (node == null || typeof node !== 'object')
|
|
156
|
+
return undefined;
|
|
157
|
+
node = node[part];
|
|
158
|
+
}
|
|
159
|
+
return node;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The sealed config: one named preset, expanded, and nothing else. No
|
|
163
|
+
* discovery, no overrides, no `extends` to a local file — so the only thing
|
|
164
|
+
* that can change the score is the database.
|
|
165
|
+
*/
|
|
166
|
+
function loadSealedConfig(preset = 'recommended') {
|
|
167
|
+
const name = preset.includes(':') ? preset : `safegres:${preset}`;
|
|
168
|
+
if (!(name in presets_1.PRESETS)) {
|
|
169
|
+
throw new Error(`Unknown preset "${preset}". Available: ${Object.keys(presets_1.PRESETS).join(', ')}`);
|
|
170
|
+
}
|
|
171
|
+
return { config: expandPreset(presets_1.PRESETS[name]), layers: [], isEmpty: true };
|
|
172
|
+
}
|
|
173
|
+
function stripExtends(config) {
|
|
174
|
+
const { extends: _extends, ...rest } = config;
|
|
175
|
+
return rest;
|
|
176
|
+
}
|
|
177
|
+
function mergePreset(preset, over) {
|
|
178
|
+
const base = expandPreset(preset);
|
|
179
|
+
return {
|
|
180
|
+
...base,
|
|
181
|
+
...over,
|
|
182
|
+
rules: { ...(base.rules ?? {}), ...(over.rules ?? {}) },
|
|
183
|
+
scoring: { ...(base.scoring ?? {}), ...(over.scoring ?? {}) },
|
|
184
|
+
failOn: { ...(base.failOn ?? {}), ...(over.failOn ?? {}) },
|
|
185
|
+
// Per name: a project adds its own card without dropping the preset's,
|
|
186
|
+
// and redefines one by naming it.
|
|
187
|
+
...(base.scorecards || over.scorecards
|
|
188
|
+
? { scorecards: { ...(base.scorecards ?? {}), ...(over.scorecards ?? {}) } }
|
|
189
|
+
: {}),
|
|
190
|
+
overrides: [...(base.overrides ?? []), ...(over.overrides ?? [])],
|
|
191
|
+
// Merged per-key so retuning `skipOwned` doesn't silently drop the
|
|
192
|
+
// preset's `ignore` list. Omitted entirely when neither side sets it.
|
|
193
|
+
...(base.extensions || over.extensions
|
|
194
|
+
? { extensions: { ...(base.extensions ?? {}), ...(over.extensions ?? {}) } }
|
|
195
|
+
: {})
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function expandPreset(preset) {
|
|
199
|
+
if (!preset.extends)
|
|
200
|
+
return preset;
|
|
201
|
+
const parents = Array.isArray(preset.extends) ? preset.extends : [preset.extends];
|
|
202
|
+
let acc = {};
|
|
203
|
+
for (const p of parents) {
|
|
204
|
+
const parent = presets_1.PRESETS[p];
|
|
205
|
+
if (parent)
|
|
206
|
+
acc = mergePreset(expandPreset(parent), acc);
|
|
207
|
+
}
|
|
208
|
+
const { extends: _extends, ...rest } = preset;
|
|
209
|
+
return mergePreset(acc, rest);
|
|
210
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { SafegresConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Built-in presets, resolvable via `extends: 'safegres:<name>'`.
|
|
4
|
+
* A preset is just a partial config; users can publish their own as files
|
|
5
|
+
* or npm packages.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Today's default behavior: every rule at its registry default severity.
|
|
9
|
+
*
|
|
10
|
+
* The untrusted-role rules take whichever roles the surface says are reachable
|
|
11
|
+
* without credentials, so a declared `exposure.anonRoles` (or an adapter that
|
|
12
|
+
* resolves one) is enough to switch them on. With no such surface `anonRoles`
|
|
13
|
+
* is empty and they stay inert, exactly as before.
|
|
14
|
+
*/
|
|
15
|
+
export declare const recommended: SafegresConfig;
|
|
16
|
+
/**
|
|
17
|
+
* Everything on and escalated. Fail-closed hygiene findings (dead grants,
|
|
18
|
+
* locked tables) are re-tuned upward and even contribute a fraction of
|
|
19
|
+
* their weight to the score.
|
|
20
|
+
*/
|
|
21
|
+
export declare const strict: SafegresConfig;
|
|
22
|
+
/**
|
|
23
|
+
* Tuned for Constructive's architecture:
|
|
24
|
+
* - the exposure surface auto-resolves from the routing plane
|
|
25
|
+
* (`routing_public.apis` → `api_schemas` → `metaschema_public.schema`),
|
|
26
|
+
* so only what the exposed APIs can reach drives the score;
|
|
27
|
+
* - untrusted-role rules watch `anonymous`; anything that can leak rows
|
|
28
|
+
* across the role boundary is critical;
|
|
29
|
+
* - A3 is demoted to `info` rather than switched off — API roles never own
|
|
30
|
+
* tables in the Constructive model, so non-FORCEd RLS is not an exposure
|
|
31
|
+
* here, but the finding stays visible (info carries zero weight, so the
|
|
32
|
+
* score is identical to switching it off) and re-tunable;
|
|
33
|
+
* - `pg_partman`'s schema is skipped: it creates child partitions and
|
|
34
|
+
* templates at runtime with no dependency on the extension, so ownership
|
|
35
|
+
* alone leaves them looking like unsecured application tables.
|
|
36
|
+
*/
|
|
37
|
+
export declare const constructive: SafegresConfig;
|
|
38
|
+
/** Structural flags only — a fast CI smoke check. */
|
|
39
|
+
export declare const minimal: SafegresConfig;
|
|
40
|
+
/**
|
|
41
|
+
* PostgREST: the exposed schemas come from `pgrst.db_schemas` in the catalog,
|
|
42
|
+
* and `anon` is the role an unauthenticated request runs as — so anything it
|
|
43
|
+
* can write, or any policy that lets it through, is the whole threat model.
|
|
44
|
+
*
|
|
45
|
+
* The connecting role (`authenticator`) is graded as a secondary plane rather
|
|
46
|
+
* than folded into the headline: it can `SET ROLE`, so its own grants are a
|
|
47
|
+
* separate question from what the API serves.
|
|
48
|
+
*/
|
|
49
|
+
export declare const postgrest: SafegresConfig;
|
|
50
|
+
/**
|
|
51
|
+
* Supabase is PostgREST with a fixed role vocabulary: `anon` (unauthenticated)
|
|
52
|
+
* and `authenticated` (any signed-up user — a much weaker boundary than it
|
|
53
|
+
* reads, since anyone can sign up) are both untrusted; `service_role` bypasses
|
|
54
|
+
* RLS by design, so its plane reports as skipped rather than F.
|
|
55
|
+
*
|
|
56
|
+
* `auth`, `storage`, `realtime`, `vault` and friends are Supabase's own
|
|
57
|
+
* schemas: they ship with their own policies and are not the developer's to
|
|
58
|
+
* fix, so they are scoped out of the score while staying in the report.
|
|
59
|
+
*/
|
|
60
|
+
export declare const supabase: SafegresConfig;
|
|
61
|
+
/**
|
|
62
|
+
* PostGraphile / graphile-starter: `app_public` is served, `app_hidden` is
|
|
63
|
+
* reachable through it, `app_private` is not exposed.
|
|
64
|
+
*
|
|
65
|
+
* The request role is `<app>_visitor` — project-specific, so it cannot be
|
|
66
|
+
* named here. The adapter resolves it from role membership and the rules take
|
|
67
|
+
* it from there. Every request runs as that role, authenticated or not (the
|
|
68
|
+
* caller is a JWT claim, not a role), which is exactly what makes it
|
|
69
|
+
* untrusted.
|
|
70
|
+
*
|
|
71
|
+
* A3 stays at its default: in this layout the API role is *not* the table
|
|
72
|
+
* owner, so a missing FORCE genuinely is an owner-bypass hole.
|
|
73
|
+
*/
|
|
74
|
+
export declare const graphile: SafegresConfig;
|
|
75
|
+
/** Hasura: the surface is whatever is *tracked* in `hdb_catalog`. */
|
|
76
|
+
export declare const hasura: SafegresConfig;
|
|
77
|
+
/**
|
|
78
|
+
* Tenancy posture, independent of stack: in a shared-table multi-tenant
|
|
79
|
+
* database every RLS gap is a cross-tenant read, not a self-service leak, so
|
|
80
|
+
* the row-visibility rules are escalated and a single critical caps the grade
|
|
81
|
+
* at D. Owner-bypass hygiene is *not* escalated — it is a deployment property,
|
|
82
|
+
* and drowning the report in it hides the boundary findings.
|
|
83
|
+
*
|
|
84
|
+
* Compose it: `"extends": ["safegres:supabase", "safegres:multi-tenant"]`.
|
|
85
|
+
*/
|
|
86
|
+
export declare const multiTenant: SafegresConfig;
|
|
87
|
+
/**
|
|
88
|
+
* Perf posture for a read-heavy OLTP database, where a sequential scan behind
|
|
89
|
+
* an RLS policy is a production incident rather than a slow report. Security
|
|
90
|
+
* stays at whatever the composed preset says; this only retunes the P/X axis
|
|
91
|
+
* and gates on the perf grade, which is only meaningful once a baseline
|
|
92
|
+
* exists — see `--write-baseline`.
|
|
93
|
+
*/
|
|
94
|
+
export declare const oltp: SafegresConfig;
|
|
95
|
+
export declare const PRESETS: Record<string, SafegresConfig>;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PRESETS = exports.oltp = exports.multiTenant = exports.hasura = exports.graphile = exports.supabase = exports.postgrest = exports.minimal = exports.constructive = exports.strict = exports.recommended = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Built-in presets, resolvable via `extends: 'safegres:<name>'`.
|
|
6
|
+
* A preset is just a partial config; users can publish their own as files
|
|
7
|
+
* or npm packages.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Today's default behavior: every rule at its registry default severity.
|
|
11
|
+
*
|
|
12
|
+
* The untrusted-role rules take whichever roles the surface says are reachable
|
|
13
|
+
* without credentials, so a declared `exposure.anonRoles` (or an adapter that
|
|
14
|
+
* resolves one) is enough to switch them on. With no such surface `anonRoles`
|
|
15
|
+
* is empty and they stay inert, exactly as before.
|
|
16
|
+
*/
|
|
17
|
+
exports.recommended = {
|
|
18
|
+
rules: {
|
|
19
|
+
// The convention linter (`C*`) enforces Constructive house style rather
|
|
20
|
+
// than a universal security fact, so the big-tent preset leaves it off;
|
|
21
|
+
// the `constructive` preset turns it on.
|
|
22
|
+
'C*': 'off',
|
|
23
|
+
R1: ['critical', { rolesFrom: 'anon' }],
|
|
24
|
+
R2: ['high', { rolesFrom: 'anon' }],
|
|
25
|
+
L5: ['info', { rolesFrom: 'anon' }],
|
|
26
|
+
// Signal-only for now: an unauthenticated role that can SET ROLE to a role
|
|
27
|
+
// with more reach is a real escalation, but the rule is new and unproven,
|
|
28
|
+
// so it reports at zero weight until validated. See the registry entry.
|
|
29
|
+
L7: ['info', { rolesFrom: 'anon' }],
|
|
30
|
+
// Same posture, same reason: a view that executes as its owner hands its
|
|
31
|
+
// readers that owner's reach, which is a real bypass, but the rule is new
|
|
32
|
+
// and body-derived, so it reports at zero weight until validated.
|
|
33
|
+
L8: ['info', { rolesFrom: 'anon' }],
|
|
34
|
+
// The write half of the same story: an auto-updatable definer view, and a
|
|
35
|
+
// rewrite rule whose action runs as the view owner. Same posture again —
|
|
36
|
+
// new, body-derived, zero weight until validated.
|
|
37
|
+
L9: ['info', { rolesFrom: 'anon' }],
|
|
38
|
+
L10: ['info', { rolesFrom: 'anon' }],
|
|
39
|
+
// The other two ways a readable view is not what its definition says: a
|
|
40
|
+
// materialized view serves rows RLS never filtered, and a filtering view
|
|
41
|
+
// without `security_barrier` is not a boundary. Same posture again.
|
|
42
|
+
L11: ['info', { rolesFrom: 'anon' }],
|
|
43
|
+
L12: ['info', { rolesFrom: 'anon' }],
|
|
44
|
+
// Reach that only `pg_attribute.attacl` shows. New, so zero weight — but
|
|
45
|
+
// note this one is catalog-derived, not body-derived: what was missing was
|
|
46
|
+
// the introspection, not the proof.
|
|
47
|
+
L13: ['info', { rolesFrom: 'anon' }],
|
|
48
|
+
// The coverage half of L8: a body reference the audit could not follow
|
|
49
|
+
// because the schema was out of scope. Reports an unknown, never a leak.
|
|
50
|
+
L14: ['info', { rolesFrom: 'anon' }],
|
|
51
|
+
// And the coverage half of the body itself: a view an untrusted role reads
|
|
52
|
+
// whose definition this analysis could not follow. Also an unknown.
|
|
53
|
+
L15: ['info', { rolesFrom: 'anon' }],
|
|
54
|
+
// Privileges on the objects that are not tables. Neither is row-filterable
|
|
55
|
+
// — RLS does not apply to a sequence and cannot be enabled on a foreign
|
|
56
|
+
// table at all — so both are reach the table rules never saw.
|
|
57
|
+
L16: ['info', { rolesFrom: 'anon' }],
|
|
58
|
+
L17: ['info', { rolesFrom: 'anon' }],
|
|
59
|
+
// The write-side twin of L12: a writable view whose `WHERE` is a read
|
|
60
|
+
// filter only, because `WITH CHECK OPTION` is not the default. Same
|
|
61
|
+
// posture again.
|
|
62
|
+
L18: ['info', { rolesFrom: 'anon' }],
|
|
63
|
+
// Privilege that arrives through a function body: a definer function's
|
|
64
|
+
// reach, and the write an INSTEAD OF trigger's definer function performs.
|
|
65
|
+
// Both are the L8 escalation with different syntax, and ship the same way.
|
|
66
|
+
L19: ['info', { rolesFrom: 'anon' }],
|
|
67
|
+
L20: ['info', { rolesFrom: 'anon' }]
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Everything on and escalated. Fail-closed hygiene findings (dead grants,
|
|
72
|
+
* locked tables) are re-tuned upward and even contribute a fraction of
|
|
73
|
+
* their weight to the score.
|
|
74
|
+
*/
|
|
75
|
+
exports.strict = {
|
|
76
|
+
extends: 'safegres:recommended',
|
|
77
|
+
rules: {
|
|
78
|
+
A1: 'medium',
|
|
79
|
+
A3: 'medium',
|
|
80
|
+
A4: 'high',
|
|
81
|
+
A5: 'medium',
|
|
82
|
+
A6: 'medium',
|
|
83
|
+
A8: 'medium',
|
|
84
|
+
P1b: 'high'
|
|
85
|
+
},
|
|
86
|
+
scoring: { failClosedWeight: 0.25 },
|
|
87
|
+
failOn: { severity: 'high' }
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Tuned for Constructive's architecture:
|
|
91
|
+
* - the exposure surface auto-resolves from the routing plane
|
|
92
|
+
* (`routing_public.apis` → `api_schemas` → `metaschema_public.schema`),
|
|
93
|
+
* so only what the exposed APIs can reach drives the score;
|
|
94
|
+
* - untrusted-role rules watch `anonymous`; anything that can leak rows
|
|
95
|
+
* across the role boundary is critical;
|
|
96
|
+
* - A3 is demoted to `info` rather than switched off — API roles never own
|
|
97
|
+
* tables in the Constructive model, so non-FORCEd RLS is not an exposure
|
|
98
|
+
* here, but the finding stays visible (info carries zero weight, so the
|
|
99
|
+
* score is identical to switching it off) and re-tunable;
|
|
100
|
+
* - `pg_partman`'s schema is skipped: it creates child partitions and
|
|
101
|
+
* templates at runtime with no dependency on the extension, so ownership
|
|
102
|
+
* alone leaves them looking like unsecured application tables.
|
|
103
|
+
*/
|
|
104
|
+
exports.constructive = {
|
|
105
|
+
extends: 'safegres:recommended',
|
|
106
|
+
exposure: { resolver: 'constructive' },
|
|
107
|
+
extensions: { ignore: ['pg_partman'] },
|
|
108
|
+
rules: {
|
|
109
|
+
A2: 'critical',
|
|
110
|
+
A3: 'info',
|
|
111
|
+
P5: 'critical',
|
|
112
|
+
// Each API declares its own `anon_role`; the adapter reads it, so a
|
|
113
|
+
// custom one is picked up instead of being missed, with the platform
|
|
114
|
+
// default kept explicitly so an unresolved surface still checks it.
|
|
115
|
+
// `role_name` (`authenticated`) is deliberately *not* included: a
|
|
116
|
+
// signed-in user holding a write grant is the product, not a finding.
|
|
117
|
+
R1: ['critical', { roles: ['anonymous'], rolesFrom: 'anon' }],
|
|
118
|
+
R2: ['high', { roles: ['anonymous'], rolesFrom: 'anon' }],
|
|
119
|
+
R3: 'medium',
|
|
120
|
+
L5: ['info', { roles: ['anonymous'], rolesFrom: 'anon' }],
|
|
121
|
+
// House-style convention rules, enforced here for the first time:
|
|
122
|
+
// never set search_path (C1), never use #variable_conflict (C2),
|
|
123
|
+
// schema-qualify every relation (C3, adoption severity — ratchet to
|
|
124
|
+
// error once clean), and no dynamic SQL (C4) unless waived inline with a
|
|
125
|
+
// categorized reason (`-- safegres-disable-next-line no-dynamic-sql --
|
|
126
|
+
// lookup-only: …`).
|
|
127
|
+
C1: 'high',
|
|
128
|
+
C2: 'medium',
|
|
129
|
+
C3: 'low',
|
|
130
|
+
C4: 'high'
|
|
131
|
+
},
|
|
132
|
+
scoring: { floorOnCritical: 'C' },
|
|
133
|
+
// The headline grades the API surface, which is the product's own question.
|
|
134
|
+
// These are the two other questions this codebase actually gates on, and
|
|
135
|
+
// neither is well served by being averaged into the first.
|
|
136
|
+
scorecards: {
|
|
137
|
+
'anon-surface': {
|
|
138
|
+
title: 'Anonymous surface',
|
|
139
|
+
description: 'What an unauthenticated caller reaches — exposure ignored, leaks only.',
|
|
140
|
+
select: {
|
|
141
|
+
roles: ['anonymous'],
|
|
142
|
+
direction: 'fail-open',
|
|
143
|
+
exposure: 'all',
|
|
144
|
+
denominator: 'all',
|
|
145
|
+
severities: 'declared'
|
|
146
|
+
},
|
|
147
|
+
// Nothing about the anonymous surface is informational. The L-series
|
|
148
|
+
// ships at `info` because each rule is new, not because "an
|
|
149
|
+
// unauthenticated caller reaches this" is a matter of interest — so on
|
|
150
|
+
// the card that asks exactly that question, a finding costs a point.
|
|
151
|
+
weights: { info: 1 },
|
|
152
|
+
floorOnCritical: 'C'
|
|
153
|
+
},
|
|
154
|
+
'sql-conventions': {
|
|
155
|
+
title: 'SQL conventions',
|
|
156
|
+
description: 'House style in function bodies — reachable or not, it is still the codebase.',
|
|
157
|
+
select: { rules: ['C*'], exposure: 'all', denominator: 'all' }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
/** Structural flags only — a fast CI smoke check. */
|
|
162
|
+
exports.minimal = {
|
|
163
|
+
rules: {
|
|
164
|
+
'*': 'off',
|
|
165
|
+
A1: 'critical',
|
|
166
|
+
A2: 'high',
|
|
167
|
+
A3: 'medium'
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* PostgREST: the exposed schemas come from `pgrst.db_schemas` in the catalog,
|
|
172
|
+
* and `anon` is the role an unauthenticated request runs as — so anything it
|
|
173
|
+
* can write, or any policy that lets it through, is the whole threat model.
|
|
174
|
+
*
|
|
175
|
+
* The connecting role (`authenticator`) is graded as a secondary plane rather
|
|
176
|
+
* than folded into the headline: it can `SET ROLE`, so its own grants are a
|
|
177
|
+
* separate question from what the API serves.
|
|
178
|
+
*/
|
|
179
|
+
exports.postgrest = {
|
|
180
|
+
extends: 'safegres:recommended',
|
|
181
|
+
exposure: { adapters: ['postgrest'] },
|
|
182
|
+
rules: {
|
|
183
|
+
// The anon role is `pgrst.db_anon_role`, so the adapter knows its name and
|
|
184
|
+
// the rules take it from the resolved surface rather than assuming `anon`.
|
|
185
|
+
R1: ['critical', { rolesFrom: 'anon' }],
|
|
186
|
+
R2: ['critical', { rolesFrom: 'anon' }],
|
|
187
|
+
R3: 'high',
|
|
188
|
+
L5: ['high', { rolesFrom: 'anon' }]
|
|
189
|
+
},
|
|
190
|
+
scoring: { floorOnCritical: 'C' }
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Supabase is PostgREST with a fixed role vocabulary: `anon` (unauthenticated)
|
|
194
|
+
* and `authenticated` (any signed-up user — a much weaker boundary than it
|
|
195
|
+
* reads, since anyone can sign up) are both untrusted; `service_role` bypasses
|
|
196
|
+
* RLS by design, so its plane reports as skipped rather than F.
|
|
197
|
+
*
|
|
198
|
+
* `auth`, `storage`, `realtime`, `vault` and friends are Supabase's own
|
|
199
|
+
* schemas: they ship with their own policies and are not the developer's to
|
|
200
|
+
* fix, so they are scoped out of the score while staying in the report.
|
|
201
|
+
*/
|
|
202
|
+
exports.supabase = {
|
|
203
|
+
extends: 'safegres:postgrest',
|
|
204
|
+
exposure: { adapters: ['supabase'] },
|
|
205
|
+
rules: {
|
|
206
|
+
// Both are named outright, not just the anon one: on Supabase anyone can
|
|
207
|
+
// sign up, so `authenticated` is a boundary the internet crosses at will.
|
|
208
|
+
// Fixed by the platform, so unlike PostgREST's anon role they are safe to
|
|
209
|
+
// name — and the adapter's anon set unions in on a self-hosted project.
|
|
210
|
+
R1: ['critical', { roles: ['anon', 'authenticated'], rolesFrom: 'anon' }],
|
|
211
|
+
R2: ['critical', { roles: ['anon', 'authenticated'], rolesFrom: 'anon' }],
|
|
212
|
+
L5: ['high', { roles: ['anon', 'authenticated'], rolesFrom: 'anon' }]
|
|
213
|
+
},
|
|
214
|
+
overrides: [
|
|
215
|
+
// Supabase-managed schemas: their policies ship with the platform, not
|
|
216
|
+
// with your migrations. Demoted, not excluded — still in the report.
|
|
217
|
+
{
|
|
218
|
+
tables: [
|
|
219
|
+
'auth.*',
|
|
220
|
+
'storage.*',
|
|
221
|
+
'realtime.*',
|
|
222
|
+
'vault.*',
|
|
223
|
+
'extensions.*',
|
|
224
|
+
'graphql.*',
|
|
225
|
+
'supabase_migrations.*'
|
|
226
|
+
],
|
|
227
|
+
rules: { '*': 'info' }
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* PostGraphile / graphile-starter: `app_public` is served, `app_hidden` is
|
|
233
|
+
* reachable through it, `app_private` is not exposed.
|
|
234
|
+
*
|
|
235
|
+
* The request role is `<app>_visitor` — project-specific, so it cannot be
|
|
236
|
+
* named here. The adapter resolves it from role membership and the rules take
|
|
237
|
+
* it from there. Every request runs as that role, authenticated or not (the
|
|
238
|
+
* caller is a JWT claim, not a role), which is exactly what makes it
|
|
239
|
+
* untrusted.
|
|
240
|
+
*
|
|
241
|
+
* A3 stays at its default: in this layout the API role is *not* the table
|
|
242
|
+
* owner, so a missing FORCE genuinely is an owner-bypass hole.
|
|
243
|
+
*/
|
|
244
|
+
exports.graphile = {
|
|
245
|
+
extends: 'safegres:recommended',
|
|
246
|
+
exposure: { adapters: ['graphile'] },
|
|
247
|
+
rules: {
|
|
248
|
+
// Every request is the visitor role, signed in or not, so for graphile the
|
|
249
|
+
// anon set and the API-edge set are the same thing.
|
|
250
|
+
R1: ['high', { rolesFrom: 'anon' }],
|
|
251
|
+
R2: ['high', { rolesFrom: 'anon' }],
|
|
252
|
+
R3: 'high',
|
|
253
|
+
L5: ['medium', { rolesFrom: 'anon' }]
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
/** Hasura: the surface is whatever is *tracked* in `hdb_catalog`. */
|
|
257
|
+
exports.hasura = {
|
|
258
|
+
extends: 'safegres:recommended',
|
|
259
|
+
exposure: { adapters: ['hasura'] },
|
|
260
|
+
rules: {
|
|
261
|
+
R1: ['critical', { roles: ['anonymous', 'public'] }],
|
|
262
|
+
R2: ['critical', { roles: ['anonymous', 'public'] }],
|
|
263
|
+
R3: 'high'
|
|
264
|
+
},
|
|
265
|
+
overrides: [
|
|
266
|
+
// Hasura's own metadata catalog, managed by the engine.
|
|
267
|
+
{ tables: ['hdb_catalog.*'], rules: { '*': 'info' } }
|
|
268
|
+
]
|
|
269
|
+
};
|
|
270
|
+
/**
|
|
271
|
+
* Tenancy posture, independent of stack: in a shared-table multi-tenant
|
|
272
|
+
* database every RLS gap is a cross-tenant read, not a self-service leak, so
|
|
273
|
+
* the row-visibility rules are escalated and a single critical caps the grade
|
|
274
|
+
* at D. Owner-bypass hygiene is *not* escalated — it is a deployment property,
|
|
275
|
+
* and drowning the report in it hides the boundary findings.
|
|
276
|
+
*
|
|
277
|
+
* Compose it: `"extends": ["safegres:supabase", "safegres:multi-tenant"]`.
|
|
278
|
+
*/
|
|
279
|
+
exports.multiTenant = {
|
|
280
|
+
rules: {
|
|
281
|
+
A1: 'critical',
|
|
282
|
+
A2: 'critical',
|
|
283
|
+
A4: 'high',
|
|
284
|
+
L1: 'critical',
|
|
285
|
+
L2: 'critical',
|
|
286
|
+
L3: 'high',
|
|
287
|
+
L5: 'high',
|
|
288
|
+
P5: 'high'
|
|
289
|
+
},
|
|
290
|
+
scoring: { floorOnCritical: 'D' },
|
|
291
|
+
failOn: { severity: 'critical' }
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* Perf posture for a read-heavy OLTP database, where a sequential scan behind
|
|
295
|
+
* an RLS policy is a production incident rather than a slow report. Security
|
|
296
|
+
* stays at whatever the composed preset says; this only retunes the P/X axis
|
|
297
|
+
* and gates on the perf grade, which is only meaningful once a baseline
|
|
298
|
+
* exists — see `--write-baseline`.
|
|
299
|
+
*/
|
|
300
|
+
exports.oltp = {
|
|
301
|
+
rules: {
|
|
302
|
+
// The four that turn a policy into a per-row function call or defeat the
|
|
303
|
+
// index it should be using — the difference between an index scan and a
|
|
304
|
+
// seq scan per request.
|
|
305
|
+
X2: 'critical',
|
|
306
|
+
X3: 'critical',
|
|
307
|
+
X4: 'high',
|
|
308
|
+
X9: 'high',
|
|
309
|
+
X1: 'high',
|
|
310
|
+
X6: 'high',
|
|
311
|
+
P1b: 'high',
|
|
312
|
+
P5: 'high'
|
|
313
|
+
},
|
|
314
|
+
failOn: { perfGrade: 'C' }
|
|
315
|
+
};
|
|
316
|
+
exports.PRESETS = {
|
|
317
|
+
'safegres:recommended': exports.recommended,
|
|
318
|
+
'safegres:strict': exports.strict,
|
|
319
|
+
'safegres:constructive': exports.constructive,
|
|
320
|
+
'safegres:minimal': exports.minimal,
|
|
321
|
+
'safegres:postgrest': exports.postgrest,
|
|
322
|
+
'safegres:supabase': exports.supabase,
|
|
323
|
+
'safegres:graphile': exports.graphile,
|
|
324
|
+
'safegres:hasura': exports.hasura,
|
|
325
|
+
'safegres:multi-tenant': exports.multiTenant,
|
|
326
|
+
'safegres:oltp': exports.oltp
|
|
327
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Dimension, Finding, Severity } from '../types';
|
|
2
|
+
import type { OverrideEntry, RulesConfig, SafegresConfig } from './types';
|
|
3
|
+
export interface ResolvedRule {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
severity: Severity;
|
|
6
|
+
options?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface ResolvedRules {
|
|
9
|
+
rules: Map<string, ResolvedRule>;
|
|
10
|
+
overrides: OverrideEntry[];
|
|
11
|
+
}
|
|
12
|
+
export declare class ConfigValidationError extends Error {
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Apply a RulesConfig on top of a base rule map. Wildcard selectors apply
|
|
17
|
+
* first, exact codes second — so `{ 'P*': 'off', P1: 'high' }` disables all
|
|
18
|
+
* P-rules except P1.
|
|
19
|
+
*/
|
|
20
|
+
export declare function applyRulesConfig(base: Map<string, ResolvedRule>, rulesConfig: RulesConfig, strictUnknown?: boolean): Map<string, ResolvedRule>;
|
|
21
|
+
/** Base rule map: every registered rule enabled at its default severity. */
|
|
22
|
+
export declare function defaultRuleMap(): Map<string, ResolvedRule>;
|
|
23
|
+
/** Resolve the rule + override portion of a merged SafegresConfig. */
|
|
24
|
+
export declare function resolveRules(config: SafegresConfig): ResolvedRules;
|
|
25
|
+
/** Rule codes belonging to a scoring dimension. */
|
|
26
|
+
export declare function ruleCodesForDimension(dimension: Dimension): string[];
|
|
27
|
+
/** Simple `*` glob match against a qualified `schema.table` name. */
|
|
28
|
+
export declare function matchTablePattern(pattern: string, qualified: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The effective rule settings for one table: base rules plus any matching
|
|
31
|
+
* override entries (later entries win).
|
|
32
|
+
*/
|
|
33
|
+
export declare function rulesForTable(resolved: ResolvedRules, schema: string, table: string): Map<string, ResolvedRule>;
|
|
34
|
+
/**
|
|
35
|
+
* Apply resolved rules to raw findings: drop findings for disabled rules and
|
|
36
|
+
* restamp severities from config (respecting per-table overrides).
|
|
37
|
+
*/
|
|
38
|
+
export declare function applyRulesToFindings(resolved: ResolvedRules, findings: Finding[]): Finding[];
|
|
39
|
+
/**
|
|
40
|
+
* True when every AST-scoped rule is disabled (lets the audit skip parsing).
|
|
41
|
+
*
|
|
42
|
+
* The policy-aware index rules (X2–X4) are AST-scoped but only run when the
|
|
43
|
+
* perf dimension is on, so they hold parsing open only in that mode.
|
|
44
|
+
*/
|
|
45
|
+
export declare function allAstRulesDisabled(resolved: ResolvedRules, options?: {
|
|
46
|
+
perf?: boolean;
|
|
47
|
+
}): boolean;
|