@safegres/test 0.0.1 → 1.30.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +825 -412
- package/adapters.d.ts +8 -0
- package/adapters.js +18 -0
- package/ast/helpers.d.ts +16 -0
- package/ast/helpers.js +40 -0
- package/ast/parse.d.ts +31 -0
- package/ast/parse.js +87 -0
- package/ast/walk.d.ts +27 -0
- package/ast/walk.js +79 -0
- package/callgraph/baseline.d.ts +28 -0
- package/callgraph/baseline.js +55 -0
- package/callgraph/extract.d.ts +93 -0
- package/callgraph/extract.js +340 -0
- package/callgraph/graph.d.ts +82 -0
- package/callgraph/graph.js +276 -0
- package/checks/anti-patterns.d.ts +53 -0
- package/checks/anti-patterns.js +247 -0
- package/checks/column-grants.d.ts +20 -0
- package/checks/column-grants.js +74 -0
- package/checks/coverage.d.ts +43 -0
- package/checks/coverage.js +110 -0
- package/checks/definer-function.d.ts +91 -0
- package/checks/definer-function.js +458 -0
- package/checks/definer-view.d.ts +167 -0
- package/checks/definer-view.js +392 -0
- package/checks/indexes.d.ts +82 -0
- package/checks/indexes.js +263 -0
- package/checks/lattice.d.ts +179 -0
- package/checks/lattice.js +487 -0
- package/checks/object-acls.d.ts +32 -0
- package/checks/object-acls.js +129 -0
- package/checks/policy-index.d.ts +78 -0
- package/checks/policy-index.js +349 -0
- package/checks/revocable-grants.d.ts +140 -0
- package/checks/revocable-grants.js +377 -0
- package/checks/rls-flags.d.ts +25 -0
- package/checks/rls-flags.js +82 -0
- package/checks/role-reach.d.ts +329 -0
- package/checks/role-reach.js +277 -0
- package/checks/role-trust.d.ts +45 -0
- package/checks/role-trust.js +91 -0
- package/checks/set-role.d.ts +21 -0
- package/checks/set-role.js +66 -0
- package/checks/stats.d.ts +52 -0
- package/checks/stats.js +192 -0
- package/checks/view-exposure.d.ts +68 -0
- package/checks/view-exposure.js +236 -0
- package/checks/view-writes.d.ts +88 -0
- package/checks/view-writes.js +350 -0
- package/ci/baseline.d.ts +104 -0
- package/ci/baseline.js +111 -0
- package/ci/github.d.ts +47 -0
- package/ci/github.js +197 -0
- package/cli/audit.d.ts +3 -0
- package/cli/audit.js +560 -0
- package/cli/baseline.d.ts +3 -0
- package/cli/baseline.js +104 -0
- package/cli/commands.d.ts +2 -0
- package/cli/commands.js +60 -0
- package/cli/doctor.d.ts +3 -0
- package/cli/doctor.js +72 -0
- package/cli/eval.d.ts +3 -0
- package/cli/eval.js +153 -0
- package/cli/print-config.d.ts +3 -0
- package/cli/print-config.js +54 -0
- package/cli/shared.d.ts +43 -0
- package/cli/shared.js +153 -0
- package/cli.d.ts +2 -0
- package/cli.js +69 -0
- package/commands/audit.d.ts +65 -0
- package/commands/audit.js +779 -0
- package/commands/doctor.d.ts +19 -0
- package/commands/doctor.js +181 -0
- package/commands/eval.d.ts +67 -0
- package/commands/eval.js +93 -0
- package/config/fingerprint.d.ts +40 -0
- package/config/fingerprint.js +71 -0
- package/config/loader.d.ts +44 -0
- package/config/loader.js +210 -0
- package/config/presets.d.ts +95 -0
- package/config/presets.js +327 -0
- package/config/resolve.d.ts +47 -0
- package/config/resolve.js +192 -0
- package/config/schema.d.ts +54 -0
- package/config/schema.js +269 -0
- package/config/types.d.ts +562 -0
- package/config/validate.d.ts +14 -0
- package/config/validate.js +125 -0
- package/corpus/README.md +83 -0
- package/corpus/bootstrap.sql +14 -0
- package/corpus/cases/01-anon-write-grant/case.json +26 -0
- package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
- package/corpus/cases/02-grant-to-public/case.json +25 -0
- package/corpus/cases/02-grant-to-public/schema.sql +18 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
- package/corpus/cases/04-permissive-write-policy/case.json +30 -0
- package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
- package/corpus/cases/05-public-read-policy/case.json +25 -0
- package/corpus/cases/05-public-read-policy/schema.sql +16 -0
- package/corpus/cases/06-update-without-with-check/case.json +25 -0
- package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
- package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
- package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
- package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
- package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
- package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
- package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
- package/corpus/cases/10-rls-not-forced/case.json +24 -0
- package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
- package/corpus/cases/11-session-user-in-policy/case.json +24 -0
- package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
- package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
- package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
- package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
- package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
- package/corpus/cases/14-dead-policy/case.json +24 -0
- package/corpus/cases/14-dead-policy/schema.sql +18 -0
- package/corpus/cases/15-unreachable-grant/case.json +24 -0
- package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
- package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
- package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
- package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
- package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
- package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
- package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
- package/corpus/cases/19-policy-column-cast/case.json +24 -0
- package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
- package/corpus/cases/20-stable-function-per-row/case.json +24 -0
- package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
- package/corpus/cases/21-table-without-primary-key/case.json +24 -0
- package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
- package/corpus/cases/22-redundant-index/case.json +24 -0
- package/corpus/cases/22-redundant-index/schema.sql +16 -0
- package/corpus/cases/23-tsvector-without-index/case.json +24 -0
- package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
- package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
- package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
- package/corpus/cases/25-definer-view-bypass/case.json +31 -0
- package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
- package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
- package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
- package/corpus/cases/27-definer-view-write/case.json +36 -0
- package/corpus/cases/27-definer-view-write/schema.sql +40 -0
- package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
- package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
- package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
- package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
- package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
- package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
- package/corpus/cases/31-matview-snapshot/case.json +32 -0
- package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
- package/corpus/cases/33-leaky-filter-view/case.json +36 -0
- package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
- package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
- package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
- package/corpus/cases/35-column-grant-projection/case.json +27 -0
- package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
- package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
- package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
- package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
- package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
- package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
- package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
- package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
- package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
- package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
- package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
- package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
- package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
- package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
- package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
- package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
- package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
- package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
- package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
- package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
- package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
- package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
- package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
- package/corpus/cases/41-unreadable-view-body/case.json +33 -0
- package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
- package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
- package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
- package/corpus/cases/42-search-column-indexed/case.json +21 -0
- package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
- package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
- package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
- package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
- package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
- package/corpus/cases/45-anon-foreign-table/case.json +27 -0
- package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
- package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
- package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
- package/corpus/cases/47-checked-writable-view/case.json +37 -0
- package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
- package/corpus/cases/48-definer-function-reach/case.json +38 -0
- package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
- package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
- package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
- package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
- package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
- package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
- package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
- package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
- package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
- package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
- package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
- package/corpus/index.d.ts +79 -0
- package/corpus/index.js +125 -0
- package/esm/adapters.d.ts +8 -0
- package/esm/adapters.js +6 -0
- package/esm/ast/helpers.d.ts +16 -0
- package/esm/ast/helpers.js +36 -0
- package/esm/ast/parse.d.ts +31 -0
- package/esm/ast/parse.js +82 -0
- package/esm/ast/walk.d.ts +27 -0
- package/esm/ast/walk.js +72 -0
- package/esm/callgraph/baseline.d.ts +28 -0
- package/esm/callgraph/baseline.js +48 -0
- package/esm/callgraph/extract.d.ts +93 -0
- package/esm/callgraph/extract.js +333 -0
- package/esm/callgraph/graph.d.ts +82 -0
- package/esm/callgraph/graph.js +273 -0
- package/esm/checks/anti-patterns.d.ts +53 -0
- package/esm/checks/anti-patterns.js +240 -0
- package/esm/checks/column-grants.d.ts +20 -0
- package/esm/checks/column-grants.js +71 -0
- package/esm/checks/coverage.d.ts +43 -0
- package/esm/checks/coverage.js +104 -0
- package/esm/checks/definer-function.d.ts +91 -0
- package/esm/checks/definer-function.js +452 -0
- package/esm/checks/definer-view.d.ts +167 -0
- package/esm/checks/definer-view.js +382 -0
- package/esm/checks/indexes.d.ts +82 -0
- package/esm/checks/indexes.js +256 -0
- package/esm/checks/lattice.d.ts +179 -0
- package/esm/checks/lattice.js +475 -0
- package/esm/checks/object-acls.d.ts +32 -0
- package/esm/checks/object-acls.js +125 -0
- package/esm/checks/policy-index.d.ts +78 -0
- package/esm/checks/policy-index.js +342 -0
- package/esm/checks/revocable-grants.d.ts +140 -0
- package/esm/checks/revocable-grants.js +373 -0
- package/esm/checks/rls-flags.d.ts +25 -0
- package/esm/checks/rls-flags.js +77 -0
- package/esm/checks/role-reach.d.ts +329 -0
- package/esm/checks/role-reach.js +270 -0
- package/esm/checks/role-trust.d.ts +45 -0
- package/esm/checks/role-trust.js +86 -0
- package/esm/checks/set-role.d.ts +21 -0
- package/esm/checks/set-role.js +63 -0
- package/esm/checks/stats.d.ts +52 -0
- package/esm/checks/stats.js +184 -0
- package/esm/checks/view-exposure.d.ts +68 -0
- package/esm/checks/view-exposure.js +231 -0
- package/esm/checks/view-writes.d.ts +88 -0
- package/esm/checks/view-writes.js +344 -0
- package/esm/ci/baseline.d.ts +104 -0
- package/esm/ci/baseline.js +107 -0
- package/esm/ci/github.d.ts +47 -0
- package/esm/ci/github.js +161 -0
- package/esm/cli/audit.d.ts +3 -0
- package/esm/cli/audit.js +525 -0
- package/esm/cli/baseline.d.ts +3 -0
- package/esm/cli/baseline.js +102 -0
- package/esm/cli/commands.d.ts +2 -0
- package/esm/cli/commands.js +53 -0
- package/esm/cli/doctor.d.ts +3 -0
- package/esm/cli/doctor.js +67 -0
- package/esm/cli/eval.d.ts +3 -0
- package/esm/cli/eval.js +115 -0
- package/esm/cli/print-config.d.ts +3 -0
- package/esm/cli/print-config.js +52 -0
- package/esm/cli/shared.d.ts +43 -0
- package/esm/cli/shared.js +112 -0
- package/esm/cli.d.ts +2 -0
- package/esm/cli.js +67 -0
- package/esm/commands/audit.d.ts +65 -0
- package/esm/commands/audit.js +776 -0
- package/esm/commands/doctor.d.ts +19 -0
- package/esm/commands/doctor.js +178 -0
- package/esm/commands/eval.d.ts +67 -0
- package/esm/commands/eval.js +90 -0
- package/esm/config/fingerprint.d.ts +40 -0
- package/esm/config/fingerprint.js +68 -0
- package/esm/config/loader.d.ts +44 -0
- package/esm/config/loader.js +172 -0
- package/esm/config/presets.d.ts +95 -0
- package/esm/config/presets.js +324 -0
- package/esm/config/resolve.d.ts +47 -0
- package/esm/config/resolve.js +180 -0
- package/esm/config/schema.d.ts +54 -0
- package/esm/config/schema.js +265 -0
- package/esm/config/types.d.ts +562 -0
- package/esm/config/validate.d.ts +14 -0
- package/esm/config/validate.js +122 -0
- package/esm/corpus/index.d.ts +79 -0
- package/esm/corpus/index.js +85 -0
- package/esm/exposure/adapters.d.ts +152 -0
- package/esm/exposure/adapters.js +463 -0
- package/esm/exposure/planes.d.ts +59 -0
- package/esm/exposure/planes.js +144 -0
- package/esm/exposure/reach.d.ts +78 -0
- package/esm/exposure/reach.js +105 -0
- package/esm/exposure/routines.d.ts +36 -0
- package/esm/exposure/routines.js +54 -0
- package/esm/index.d.ts +96 -0
- package/esm/index.js +57 -6
- package/esm/lint/index.d.ts +11 -0
- package/esm/lint/index.js +10 -0
- package/esm/perf/baseline.d.ts +40 -0
- package/esm/perf/baseline.js +83 -0
- package/esm/perf/explain.d.ts +46 -0
- package/esm/perf/explain.js +201 -0
- package/esm/pg/acl.d.ts +72 -0
- package/esm/pg/acl.js +186 -0
- package/esm/pg/behaviors.d.ts +114 -0
- package/esm/pg/behaviors.js +206 -0
- package/esm/pg/exposure.d.ts +74 -0
- package/esm/pg/exposure.js +198 -0
- package/esm/pg/functions.d.ts +52 -0
- package/esm/pg/functions.js +128 -0
- package/esm/pg/indexes.d.ts +192 -0
- package/esm/pg/indexes.js +395 -0
- package/esm/pg/introspect.d.ts +134 -0
- package/esm/pg/introspect.js +274 -0
- package/esm/pg/objects.d.ts +37 -0
- package/esm/pg/objects.js +108 -0
- package/esm/pg/paths.d.ts +114 -0
- package/esm/pg/paths.js +159 -0
- package/esm/pg/proc.d.ts +23 -0
- package/esm/pg/proc.js +55 -0
- package/esm/pg/reach-inputs.d.ts +47 -0
- package/esm/pg/reach-inputs.js +133 -0
- package/esm/pg/roles.d.ts +28 -0
- package/esm/pg/roles.js +50 -0
- package/esm/pg/stats.d.ts +65 -0
- package/esm/pg/stats.js +129 -0
- package/esm/pgpm-test.d.ts +22 -0
- package/esm/pgpm-test.js +39 -0
- package/esm/report/callgraph.d.ts +9 -0
- package/esm/report/callgraph.js +74 -0
- package/esm/report/compare.d.ts +143 -0
- package/esm/report/compare.js +195 -0
- package/esm/report/github.d.ts +74 -0
- package/esm/report/github.js +290 -0
- package/esm/report/json.d.ts +4 -0
- package/esm/report/json.js +3 -0
- package/esm/report/markdown.d.ts +25 -0
- package/esm/report/markdown.js +352 -0
- package/esm/report/pretty.d.ts +14 -0
- package/esm/report/pretty.js +318 -0
- package/esm/report/sarif.d.ts +40 -0
- package/esm/report/sarif.js +189 -0
- package/esm/report/view.d.ts +82 -0
- package/esm/report/view.js +126 -0
- package/esm/rules/registry.d.ts +66 -0
- package/esm/rules/registry.js +562 -0
- package/esm/score/score.d.ts +84 -0
- package/esm/score/score.js +233 -0
- package/esm/score/scorecards.d.ts +68 -0
- package/esm/score/scorecards.js +157 -0
- package/esm/types.d.ts +292 -0
- package/esm/types.js +26 -0
- package/esm/version.d.ts +8 -0
- package/esm/version.js +9 -0
- package/exposure/adapters.d.ts +152 -0
- package/exposure/adapters.js +468 -0
- package/exposure/planes.d.ts +59 -0
- package/exposure/planes.js +151 -0
- package/exposure/reach.d.ts +78 -0
- package/exposure/reach.js +109 -0
- package/exposure/routines.d.ts +36 -0
- package/exposure/routines.js +57 -0
- package/index.d.ts +96 -6
- package/index.js +229 -6
- package/lint/index.d.ts +11 -0
- package/lint/index.js +19 -0
- package/package.json +54 -48
- package/perf/baseline.d.ts +40 -0
- package/perf/baseline.js +92 -0
- package/perf/explain.d.ts +46 -0
- package/perf/explain.js +204 -0
- package/pg/acl.d.ts +72 -0
- package/pg/acl.js +191 -0
- package/pg/behaviors.d.ts +114 -0
- package/pg/behaviors.js +217 -0
- package/pg/exposure.d.ts +74 -0
- package/pg/exposure.js +205 -0
- package/pg/functions.d.ts +52 -0
- package/pg/functions.js +131 -0
- package/pg/indexes.d.ts +192 -0
- package/pg/indexes.js +400 -0
- package/pg/introspect.d.ts +134 -0
- package/pg/introspect.js +280 -0
- package/pg/objects.d.ts +37 -0
- package/pg/objects.js +111 -0
- package/pg/paths.d.ts +114 -0
- package/pg/paths.js +166 -0
- package/pg/proc.d.ts +23 -0
- package/pg/proc.js +58 -0
- package/pg/reach-inputs.d.ts +47 -0
- package/pg/reach-inputs.js +136 -0
- package/pg/roles.d.ts +28 -0
- package/pg/roles.js +54 -0
- package/pg/stats.d.ts +65 -0
- package/pg/stats.js +133 -0
- package/pgpm-test.d.ts +22 -0
- package/pgpm-test.js +42 -0
- package/report/callgraph.d.ts +9 -0
- package/report/callgraph.js +81 -0
- package/report/compare.d.ts +143 -0
- package/report/compare.js +205 -0
- package/report/github.d.ts +74 -0
- package/report/github.js +333 -0
- package/report/json.d.ts +4 -0
- package/report/json.js +6 -0
- package/report/markdown.d.ts +25 -0
- package/report/markdown.js +355 -0
- package/report/pretty.d.ts +14 -0
- package/report/pretty.js +324 -0
- package/report/sarif.d.ts +40 -0
- package/report/sarif.js +193 -0
- package/report/view.d.ts +82 -0
- package/report/view.js +132 -0
- package/rules/registry.d.ts +66 -0
- package/rules/registry.js +569 -0
- package/schema/safegres.schema.json +1119 -0
- package/score/score.d.ts +84 -0
- package/score/score.js +238 -0
- package/score/scorecards.d.ts +68 -0
- package/score/scorecards.js +162 -0
- package/types.d.ts +292 -0
- package/types.js +32 -0
- package/version.d.ts +8 -0
- package/version.js +12 -0
- package/LICENSE +0 -23
- package/admin.d.ts +0 -26
- package/admin.js +0 -182
- package/connect.d.ts +0 -19
- package/connect.js +0 -83
- package/dist/README.md +0 -557
- package/dist/package.json +0 -72
- package/esm/admin.js +0 -178
- package/esm/connect.js +0 -78
- package/esm/manager.js +0 -136
- package/esm/roles.js +0 -32
- package/esm/seed/adapters.js +0 -23
- package/esm/seed/csv.js +0 -44
- package/esm/seed/index.js +0 -16
- package/esm/seed/json.js +0 -18
- package/esm/seed/launchql.js +0 -19
- package/esm/seed/sqitch.js +0 -17
- package/esm/stream.js +0 -43
- package/esm/test-client.js +0 -150
- package/manager.d.ts +0 -25
- package/manager.js +0 -140
- package/roles.d.ts +0 -17
- package/roles.js +0 -38
- package/seed/adapters.d.ts +0 -4
- package/seed/adapters.js +0 -28
- package/seed/csv.d.ts +0 -9
- package/seed/csv.js +0 -49
- package/seed/index.d.ts +0 -16
- package/seed/index.js +0 -33
- package/seed/json.d.ts +0 -6
- package/seed/json.js +0 -21
- package/seed/launchql.d.ts +0 -2
- package/seed/launchql.js +0 -22
- package/seed/sqitch.d.ts +0 -2
- package/seed/sqitch.js +0 -20
- package/seed/types.d.ts +0 -13
- package/stream.d.ts +0 -2
- package/stream.js +0 -46
- package/test-client.d.ts +0 -49
- package/test-client.js +0 -154
- /package/{seed → config}/types.js +0 -0
- /package/esm/{seed → config}/types.js +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `safegres doctor` — diagnose the environment before/alongside an audit:
|
|
3
|
+
* connection health, catalog visibility, audit blind spots, parser
|
|
4
|
+
* availability, and configuration problems.
|
|
5
|
+
*/
|
|
6
|
+
import { type LoadConfigParams } from '../config/loader';
|
|
7
|
+
import { type QueryExecutor } from '../pg/introspect';
|
|
8
|
+
export type DoctorStatus = 'ok' | 'warn' | 'fail';
|
|
9
|
+
export interface DoctorCheck {
|
|
10
|
+
name: string;
|
|
11
|
+
status: DoctorStatus;
|
|
12
|
+
detail: string;
|
|
13
|
+
}
|
|
14
|
+
export interface DoctorReport {
|
|
15
|
+
checks: DoctorCheck[];
|
|
16
|
+
ok: boolean;
|
|
17
|
+
}
|
|
18
|
+
export type DoctorOptions = LoadConfigParams;
|
|
19
|
+
export declare function doctor(client: QueryExecutor | null, options?: DoctorOptions): Promise<DoctorReport>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `safegres doctor` — diagnose the environment before/alongside an audit:
|
|
3
|
+
* connection health, catalog visibility, audit blind spots, parser
|
|
4
|
+
* availability, and configuration problems.
|
|
5
|
+
*/
|
|
6
|
+
import { parsePolicyExpression } from '../ast/parse';
|
|
7
|
+
import { loadConfig } from '../config/loader';
|
|
8
|
+
import { matchTablePattern, resolveRules } from '../config/resolve';
|
|
9
|
+
import { resolveExposure } from '../pg/exposure';
|
|
10
|
+
import { asExecutor } from '../pg/introspect';
|
|
11
|
+
export async function doctor(client, options = {}) {
|
|
12
|
+
const checks = [];
|
|
13
|
+
let exposureConfig;
|
|
14
|
+
let publicRead = [];
|
|
15
|
+
// --- configuration ---
|
|
16
|
+
try {
|
|
17
|
+
const loaded = loadConfig(options);
|
|
18
|
+
exposureConfig = loaded.config.exposure;
|
|
19
|
+
publicRead = loaded.config.public?.read ?? [];
|
|
20
|
+
if (loaded.isEmpty) {
|
|
21
|
+
checks.push({
|
|
22
|
+
name: 'config',
|
|
23
|
+
status: 'ok',
|
|
24
|
+
detail: 'no config file found — using built-in defaults (safegres:recommended behavior)'
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
checks.push({ name: 'config', status: 'ok', detail: `loaded ${loaded.filepath}` });
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const resolved = resolveRules(loaded.config);
|
|
32
|
+
const disabled = [...resolved.rules.entries()].filter(([, r]) => !r.enabled).map(([c]) => c);
|
|
33
|
+
checks.push({
|
|
34
|
+
name: 'rules',
|
|
35
|
+
status: 'ok',
|
|
36
|
+
detail: disabled.length > 0
|
|
37
|
+
? `${resolved.rules.size - disabled.length} enabled, ${disabled.length} disabled (${disabled.join(', ')})`
|
|
38
|
+
: `all ${resolved.rules.size} rules enabled`
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
checks.push({ name: 'rules', status: 'fail', detail: err.message });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
checks.push({ name: 'config', status: 'fail', detail: err.message });
|
|
47
|
+
}
|
|
48
|
+
// --- SQL parser ---
|
|
49
|
+
try {
|
|
50
|
+
await parsePolicyExpression('true');
|
|
51
|
+
checks.push({ name: 'parser', status: 'ok', detail: 'pgsql-parser is available (AST rules can run)' });
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
checks.push({
|
|
55
|
+
name: 'parser',
|
|
56
|
+
status: 'warn',
|
|
57
|
+
detail: `pgsql-parser unavailable — AST rules (A7, P1, P1b, P5) will be skipped: ${err.message}`
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// --- database ---
|
|
61
|
+
if (!client) {
|
|
62
|
+
checks.push({
|
|
63
|
+
name: 'connection',
|
|
64
|
+
status: 'fail',
|
|
65
|
+
detail: 'could not connect — check PG* env vars or --connection'
|
|
66
|
+
});
|
|
67
|
+
return finish(checks);
|
|
68
|
+
}
|
|
69
|
+
const exec = asExecutor(client);
|
|
70
|
+
try {
|
|
71
|
+
const { rows } = await exec.query('SELECT version() AS version, current_user AS usr, current_database() AS db');
|
|
72
|
+
checks.push({
|
|
73
|
+
name: 'connection',
|
|
74
|
+
status: 'ok',
|
|
75
|
+
detail: `${rows[0].db} as ${rows[0].usr} — ${rows[0].version.split(' on ')[0]}`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
checks.push({ name: 'connection', status: 'fail', detail: err.message });
|
|
80
|
+
return finish(checks);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const { rows } = await exec.query('SELECT count(*)::text AS n FROM pg_policy');
|
|
84
|
+
checks.push({ name: 'catalog', status: 'ok', detail: `pg_policy readable (${rows[0].n} policies)` });
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
checks.push({
|
|
88
|
+
name: 'catalog',
|
|
89
|
+
status: 'fail',
|
|
90
|
+
detail: `cannot read pg_policy — the audit needs catalog access: ${err.message}`
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const { rows } = await exec.query('SELECT rolsuper AS super, rolbypassrls AS bypass FROM pg_roles WHERE rolname = current_user');
|
|
95
|
+
const { super: isSuper, bypass } = rows[0] ?? { super: false, bypass: false };
|
|
96
|
+
if (isSuper || bypass) {
|
|
97
|
+
checks.push({
|
|
98
|
+
name: 'privileges',
|
|
99
|
+
status: 'ok',
|
|
100
|
+
detail: `current role ${isSuper ? 'is superuser' : 'has BYPASSRLS'} — full catalog visibility`
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
checks.push({
|
|
105
|
+
name: 'privileges',
|
|
106
|
+
status: 'warn',
|
|
107
|
+
detail: 'current role is not superuser and lacks BYPASSRLS — policies/grants on tables it cannot see will be missing from the audit'
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
checks.push({ name: 'privileges', status: 'warn', detail: err.message });
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const { rows } = await exec.query(`SELECT count(*)::text AS n FROM pg_class c
|
|
116
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
117
|
+
WHERE c.relkind IN ('r','p') AND c.relrowsecurity
|
|
118
|
+
AND n.nspname NOT IN ('pg_catalog','information_schema')`);
|
|
119
|
+
const n = Number(rows[0].n);
|
|
120
|
+
checks.push({
|
|
121
|
+
name: 'rls',
|
|
122
|
+
status: n > 0 ? 'ok' : 'warn',
|
|
123
|
+
detail: n > 0
|
|
124
|
+
? `${n} table(s) have RLS enabled`
|
|
125
|
+
: 'no tables with RLS enabled — nothing for coverage/anti-pattern rules to audit'
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
checks.push({ name: 'rls', status: 'warn', detail: err.message });
|
|
130
|
+
}
|
|
131
|
+
// --- exposure surface ---
|
|
132
|
+
try {
|
|
133
|
+
const exposure = await resolveExposure(exec, exposureConfig);
|
|
134
|
+
if (exposure.known) {
|
|
135
|
+
checks.push({
|
|
136
|
+
name: 'exposure',
|
|
137
|
+
status: 'ok',
|
|
138
|
+
detail: `${exposure.schemas.length} exposed schema(s) via ${exposure.source}${exposure.roles && exposure.roles.length > 0 ? ` (api roles: ${exposure.roles.join(', ')})` : ''}`
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
checks.push({
|
|
143
|
+
name: 'exposure',
|
|
144
|
+
status: 'warn',
|
|
145
|
+
detail: 'no exposure surface configured — the audit assumes the whole database is reachable and caps the score. Declare `exposure.schemas` or use `exposure.resolver: "constructive"`.'
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
checks.push({ name: 'exposure', status: 'warn', detail: err.message });
|
|
151
|
+
}
|
|
152
|
+
// --- declared public surface ---
|
|
153
|
+
if (publicRead.length > 0) {
|
|
154
|
+
try {
|
|
155
|
+
const { rows } = await exec.query(`SELECT n.nspname || '.' || c.relname AS qualified
|
|
156
|
+
FROM pg_class c
|
|
157
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
158
|
+
WHERE c.relkind IN ('r','p')
|
|
159
|
+
AND n.nspname NOT IN ('pg_catalog','information_schema')`);
|
|
160
|
+
const qualified = rows.map((r) => r.qualified);
|
|
161
|
+
const stale = publicRead.filter((p) => !qualified.some((q) => matchTablePattern(p, q)));
|
|
162
|
+
checks.push({
|
|
163
|
+
name: 'public',
|
|
164
|
+
status: stale.length > 0 ? 'warn' : 'ok',
|
|
165
|
+
detail: stale.length > 0
|
|
166
|
+
? `stale \`public.read\` pattern(s) match no table: ${stale.join(', ')}`
|
|
167
|
+
: `${publicRead.length} \`public.read\` pattern(s), all match at least one table`
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
checks.push({ name: 'public', status: 'warn', detail: err.message });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return finish(checks);
|
|
175
|
+
}
|
|
176
|
+
function finish(checks) {
|
|
177
|
+
return { checks, ok: checks.every((c) => c.status !== 'fail') };
|
|
178
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `safegres eval` — grade the auditor against a corpus with known answers.
|
|
3
|
+
*
|
|
4
|
+
* The audit answers "what is wrong with this database". Eval answers the
|
|
5
|
+
* question one level up: *is the auditor right?* It deploys each corpus case
|
|
6
|
+
* into a live database, audits it under a sealed preset, and grades the report
|
|
7
|
+
* against the case's answer key — recall (every expected finding present) and
|
|
8
|
+
* precision (nothing the case forbids).
|
|
9
|
+
*
|
|
10
|
+
* That makes it three things with one implementation: safegres's own
|
|
11
|
+
* calibration check, a harness an agent can be scored against (fix the case,
|
|
12
|
+
* re-run, require the findings to disappear), and a demonstration that the
|
|
13
|
+
* rules mean what the docs say.
|
|
14
|
+
*/
|
|
15
|
+
import type { Grade, SafegresConfig } from '../config/types';
|
|
16
|
+
import { type CaseResult, type CorpusCase } from '../corpus';
|
|
17
|
+
import { type QueryExecutor } from '../pg/introspect';
|
|
18
|
+
export interface EvalOptions {
|
|
19
|
+
/**
|
|
20
|
+
* The configuration cases are graded under. Normally a sealed preset: the
|
|
21
|
+
* corpus measures the rules, so a local config file editing them would be
|
|
22
|
+
* measuring the wrong thing.
|
|
23
|
+
*/
|
|
24
|
+
config?: SafegresConfig;
|
|
25
|
+
/** The preset name, recorded in each report's provenance. */
|
|
26
|
+
preset?: string;
|
|
27
|
+
/** Corpus directory. Defaults to the corpus shipped with the package. */
|
|
28
|
+
corpus?: string;
|
|
29
|
+
/** Case ids (or id prefixes) to run. Defaults to all of them. */
|
|
30
|
+
cases?: string[];
|
|
31
|
+
/** Skip the role bootstrap — for a database that already has the roles. */
|
|
32
|
+
skipBootstrap?: boolean;
|
|
33
|
+
/** Leave each case's schemas in the database instead of dropping them. */
|
|
34
|
+
keep?: boolean;
|
|
35
|
+
/** Called as each case finishes, for streaming progress. */
|
|
36
|
+
onResult?: (result: EvalCaseResult) => void;
|
|
37
|
+
}
|
|
38
|
+
export interface EvalCaseResult extends CaseResult {
|
|
39
|
+
title: string;
|
|
40
|
+
dimension: CorpusCase['dimension'];
|
|
41
|
+
/** The score on the case's own dimension, after its flaw is accounted for. */
|
|
42
|
+
score: number;
|
|
43
|
+
grade: Grade;
|
|
44
|
+
/** The answer key, so a failing case reads as an instruction. */
|
|
45
|
+
fix: string;
|
|
46
|
+
}
|
|
47
|
+
export interface EvalReport {
|
|
48
|
+
generatedAt: string;
|
|
49
|
+
/** The preset every case was graded under. */
|
|
50
|
+
preset?: string;
|
|
51
|
+
/** Configuration fingerprint shared by every case's report. */
|
|
52
|
+
fingerprint?: string;
|
|
53
|
+
sealed: boolean;
|
|
54
|
+
total: number;
|
|
55
|
+
passed: number;
|
|
56
|
+
/** Expected findings produced / expected findings total. */
|
|
57
|
+
recall: number;
|
|
58
|
+
/** Cases that produced no forbidden finding / cases total. */
|
|
59
|
+
precision: number;
|
|
60
|
+
results: EvalCaseResult[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Deploy, audit and grade every selected case. Each case owns its schemas —
|
|
64
|
+
* its SQL drops them first — so cases neither see nor disturb each other, and
|
|
65
|
+
* the database is left as it was found unless `keep` says otherwise.
|
|
66
|
+
*/
|
|
67
|
+
export declare function runEval(client: QueryExecutor, options?: EvalOptions): Promise<EvalReport>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `safegres eval` — grade the auditor against a corpus with known answers.
|
|
3
|
+
*
|
|
4
|
+
* The audit answers "what is wrong with this database". Eval answers the
|
|
5
|
+
* question one level up: *is the auditor right?* It deploys each corpus case
|
|
6
|
+
* into a live database, audits it under a sealed preset, and grades the report
|
|
7
|
+
* against the case's answer key — recall (every expected finding present) and
|
|
8
|
+
* precision (nothing the case forbids).
|
|
9
|
+
*
|
|
10
|
+
* That makes it three things with one implementation: safegres's own
|
|
11
|
+
* calibration check, a harness an agent can be scored against (fix the case,
|
|
12
|
+
* re-run, require the findings to disappear), and a demonstration that the
|
|
13
|
+
* rules mean what the docs say.
|
|
14
|
+
*/
|
|
15
|
+
import { corpusBootstrap, gradeCase, loadCorpus } from '../corpus';
|
|
16
|
+
import { asExecutor } from '../pg/introspect';
|
|
17
|
+
import { audit } from './audit';
|
|
18
|
+
function selectCases(all, ids) {
|
|
19
|
+
if (!ids || ids.length === 0)
|
|
20
|
+
return all;
|
|
21
|
+
const selected = all.filter((c) => ids.some((id) => c.id === id || c.id.startsWith(id)));
|
|
22
|
+
if (selected.length === 0) {
|
|
23
|
+
throw new Error(`No corpus case matches ${ids.join(', ')}. Available: ${all.map((c) => c.id).join(', ')}`);
|
|
24
|
+
}
|
|
25
|
+
return selected;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Deploy, audit and grade every selected case. Each case owns its schemas —
|
|
29
|
+
* its SQL drops them first — so cases neither see nor disturb each other, and
|
|
30
|
+
* the database is left as it was found unless `keep` says otherwise.
|
|
31
|
+
*/
|
|
32
|
+
export async function runEval(client, options = {}) {
|
|
33
|
+
const exec = asExecutor(client);
|
|
34
|
+
const cases = selectCases(loadCorpus(options.corpus), options.cases);
|
|
35
|
+
if (!options.skipBootstrap)
|
|
36
|
+
await exec.query(corpusBootstrap());
|
|
37
|
+
const results = [];
|
|
38
|
+
let fingerprint;
|
|
39
|
+
let sealed = false;
|
|
40
|
+
for (const c of cases) {
|
|
41
|
+
await exec.query(c.sql);
|
|
42
|
+
try {
|
|
43
|
+
const report = await audit(exec, {
|
|
44
|
+
config: options.config,
|
|
45
|
+
exposure: c.exposure,
|
|
46
|
+
schemas: c.exposure.schemas,
|
|
47
|
+
perf: true,
|
|
48
|
+
sealed: true,
|
|
49
|
+
...(options.preset ? { preset: options.preset } : {})
|
|
50
|
+
});
|
|
51
|
+
fingerprint ??= report.provenance?.fingerprint;
|
|
52
|
+
sealed = report.provenance?.sealed ?? false;
|
|
53
|
+
const score = c.dimension === 'perf' ? report.perf?.score : report.score;
|
|
54
|
+
const result = {
|
|
55
|
+
...gradeCase(report, c),
|
|
56
|
+
title: c.title,
|
|
57
|
+
dimension: c.dimension,
|
|
58
|
+
score: score?.value ?? 100,
|
|
59
|
+
grade: score?.grade ?? 'A+',
|
|
60
|
+
fix: c.fix
|
|
61
|
+
};
|
|
62
|
+
results.push(result);
|
|
63
|
+
options.onResult?.(result);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
if (!options.keep) {
|
|
67
|
+
for (const schema of c.exposure.schemas ?? []) {
|
|
68
|
+
await exec.query(`DROP SCHEMA IF EXISTS ${quoteIdent(schema)} CASCADE`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const expected = results.reduce((n, r) => n + r.found.length + r.missed.length, 0);
|
|
74
|
+
const found = results.reduce((n, r) => n + r.found.length, 0);
|
|
75
|
+
const clean = results.filter((r) => r.falsePositives.length === 0).length;
|
|
76
|
+
return {
|
|
77
|
+
generatedAt: new Date().toISOString(),
|
|
78
|
+
...(options.preset ? { preset: options.preset } : {}),
|
|
79
|
+
...(fingerprint ? { fingerprint } : {}),
|
|
80
|
+
sealed,
|
|
81
|
+
total: results.length,
|
|
82
|
+
passed: results.filter((r) => r.passed).length,
|
|
83
|
+
recall: expected === 0 ? 1 : found / expected,
|
|
84
|
+
precision: results.length === 0 ? 1 : clean / results.length,
|
|
85
|
+
results
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function quoteIdent(name) {
|
|
89
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
90
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provenance: proving *which rules* produced a score.
|
|
3
|
+
*
|
|
4
|
+
* Every knob that moves the number — a rule's severity, an override, a perf
|
|
5
|
+
* ignore, the exposed surface, the scoring weights — is deliberate and useful
|
|
6
|
+
* to a developer declaring intent in CI. To an evaluation harness scoring an
|
|
7
|
+
* agent, the same knobs are the cheapest way to win: turn off the rule, or
|
|
8
|
+
* re-baseline the debt, and the score goes up without the database changing.
|
|
9
|
+
*
|
|
10
|
+
* So a report carries a fingerprint of the configuration it was produced
|
|
11
|
+
* under. Two runs with the same fingerprint were graded by the same rules and
|
|
12
|
+
* are comparable; a different fingerprint means the ruler changed, and a
|
|
13
|
+
* harness can reject the run instead of believing the number.
|
|
14
|
+
*
|
|
15
|
+
* The fingerprint covers the *resolved* rules rather than the config text, so
|
|
16
|
+
* it is invariant to how the same posture was expressed (preset vs. explicit
|
|
17
|
+
* rules, key order, whitespace) and sensitive to anything that changes it.
|
|
18
|
+
*/
|
|
19
|
+
import type { SafegresConfig } from './types';
|
|
20
|
+
/** Everything about a run that a comparison must be able to trust. */
|
|
21
|
+
export interface Provenance {
|
|
22
|
+
/** The safegres version that produced the report. */
|
|
23
|
+
version: string;
|
|
24
|
+
/** `sha256:<hex>` over the resolved, score-relevant configuration. */
|
|
25
|
+
fingerprint: string;
|
|
26
|
+
/**
|
|
27
|
+
* True when the run refused local configuration: no discovered config file,
|
|
28
|
+
* no rule overrides, no baselines. An evaluation harness should require it.
|
|
29
|
+
*/
|
|
30
|
+
sealed: boolean;
|
|
31
|
+
/** The preset the sealed run was graded under, when one was named. */
|
|
32
|
+
preset?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* `sha256:<hex>` over the resolved scoring surface and the safegres version.
|
|
36
|
+
* The version participates because a rule's *meaning* can change without its
|
|
37
|
+
* configuration changing, and two scores from different analyzers are no more
|
|
38
|
+
* comparable than two scores from different rule sets.
|
|
39
|
+
*/
|
|
40
|
+
export declare function configFingerprint(config: SafegresConfig, version: string): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provenance: proving *which rules* produced a score.
|
|
3
|
+
*
|
|
4
|
+
* Every knob that moves the number — a rule's severity, an override, a perf
|
|
5
|
+
* ignore, the exposed surface, the scoring weights — is deliberate and useful
|
|
6
|
+
* to a developer declaring intent in CI. To an evaluation harness scoring an
|
|
7
|
+
* agent, the same knobs are the cheapest way to win: turn off the rule, or
|
|
8
|
+
* re-baseline the debt, and the score goes up without the database changing.
|
|
9
|
+
*
|
|
10
|
+
* So a report carries a fingerprint of the configuration it was produced
|
|
11
|
+
* under. Two runs with the same fingerprint were graded by the same rules and
|
|
12
|
+
* are comparable; a different fingerprint means the ruler changed, and a
|
|
13
|
+
* harness can reject the run instead of believing the number.
|
|
14
|
+
*
|
|
15
|
+
* The fingerprint covers the *resolved* rules rather than the config text, so
|
|
16
|
+
* it is invariant to how the same posture was expressed (preset vs. explicit
|
|
17
|
+
* rules, key order, whitespace) and sensitive to anything that changes it.
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from 'crypto';
|
|
20
|
+
import { resolveRules } from './resolve';
|
|
21
|
+
/**
|
|
22
|
+
* A canonical JSON encoding: object keys sorted at every depth, so two configs
|
|
23
|
+
* that mean the same thing hash the same regardless of how they were written.
|
|
24
|
+
*/
|
|
25
|
+
function canonical(value) {
|
|
26
|
+
if (Array.isArray(value))
|
|
27
|
+
return value.map(canonical);
|
|
28
|
+
if (value && typeof value === 'object') {
|
|
29
|
+
const entries = Object.entries(value)
|
|
30
|
+
.filter(([, v]) => v !== undefined)
|
|
31
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
32
|
+
return Object.fromEntries(entries.map(([k, v]) => [k, canonical(v)]));
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The score-relevant projection of a config. Deliberately *not* the whole
|
|
38
|
+
* config: connection parameters, output formats and renderer options cannot
|
|
39
|
+
* move the number, and hashing them would make every run look incomparable.
|
|
40
|
+
*/
|
|
41
|
+
function scoringSurface(config) {
|
|
42
|
+
const { rules } = resolveRules(config);
|
|
43
|
+
return {
|
|
44
|
+
rules: [...rules.entries()]
|
|
45
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
46
|
+
.map(([code, rule]) => [code, rule.enabled, rule.severity, canonical(rule.options ?? null)]),
|
|
47
|
+
overrides: (config.overrides ?? []).map((o) => canonical(o)),
|
|
48
|
+
scoring: canonical(config.scoring ?? {}),
|
|
49
|
+
publicRead: [...(config.public?.read ?? [])].sort(),
|
|
50
|
+
perfIgnore: [...(config.perf?.ignore ?? [])].sort(),
|
|
51
|
+
exposure: canonical({
|
|
52
|
+
...config.exposure,
|
|
53
|
+
// An adapter is code, not data: identify it by name and let the version
|
|
54
|
+
// field carry the rest.
|
|
55
|
+
adapters: (config.exposure?.adapters ?? []).map((a) => typeof a === 'string' ? a : a.name)
|
|
56
|
+
})
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `sha256:<hex>` over the resolved scoring surface and the safegres version.
|
|
61
|
+
* The version participates because a rule's *meaning* can change without its
|
|
62
|
+
* configuration changing, and two scores from different analyzers are no more
|
|
63
|
+
* comparable than two scores from different rule sets.
|
|
64
|
+
*/
|
|
65
|
+
export function configFingerprint(config, version) {
|
|
66
|
+
const payload = JSON.stringify({ version, config: scoringSurface(config) });
|
|
67
|
+
return `sha256:${createHash('sha256').update(payload).digest('hex')}`;
|
|
68
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ConfigLoader, type ExplainedValue, type LoadResult } from 'confstash';
|
|
2
|
+
import type { SafegresConfig } from './types';
|
|
3
|
+
export type { ConfigLoader, ExplainedValue, LoadResult };
|
|
4
|
+
/**
|
|
5
|
+
* The safegres config loader. Discovers `safegres.config.{ts,js,mjs,cjs}`,
|
|
6
|
+
* `.safegresrc{,.json,.yaml,.yml,.js}`, `safegres.json`, or the `"safegres"`
|
|
7
|
+
* package.json key by walking up from cwd; resolves `extends` against the
|
|
8
|
+
* built-in presets (or files / npm packages).
|
|
9
|
+
*/
|
|
10
|
+
export declare function safegresConfigLoader(): ConfigLoader<SafegresConfig>;
|
|
11
|
+
export interface LoadConfigParams {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
/** Explicit config file (skips discovery). */
|
|
14
|
+
configFile?: string;
|
|
15
|
+
/** Preset applied when no `extends` handling is needed from a file (CLI --preset). */
|
|
16
|
+
preset?: string;
|
|
17
|
+
/** Highest-precedence partial config (parsed CLI flags). */
|
|
18
|
+
overrides?: Partial<SafegresConfig>;
|
|
19
|
+
/**
|
|
20
|
+
* Grade under a built-in preset alone: skip config-file discovery entirely,
|
|
21
|
+
* so nothing in the working tree can move the score. For an evaluation
|
|
22
|
+
* harness, where the configuration is part of what is being tested.
|
|
23
|
+
*/
|
|
24
|
+
sealed?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Where a configured *path* should resolve from. A repository that splits its
|
|
28
|
+
* config into a shared base and a per-job file has two directories in play,
|
|
29
|
+
* and `ci/baseline.json` written in the base means the base's `ci/`, whichever
|
|
30
|
+
* file inherited it.
|
|
31
|
+
*/
|
|
32
|
+
export interface ConfigPathBase {
|
|
33
|
+
/** Directory to resolve a value declared at this dotted key against. */
|
|
34
|
+
dirFor(key: string): string;
|
|
35
|
+
}
|
|
36
|
+
/** Load and merge the effective safegres config. */
|
|
37
|
+
export declare function loadConfig(params?: LoadConfigParams): LoadResult<SafegresConfig>;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve each configured path against the file that declared it, so an
|
|
40
|
+
* inherited `perf.baseline` keeps pointing at the base file's directory.
|
|
41
|
+
* Values that came from a preset or from CLI overrides have no directory of
|
|
42
|
+
* their own and fall back to the discovered config file, then to `cwd`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function configPathBase(result: LoadResult<SafegresConfig>, cwd?: string): ConfigPathBase;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { createConfigLoader } from 'confstash';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { PRESETS } from './presets';
|
|
4
|
+
import { resolveRules } from './resolve';
|
|
5
|
+
import { validateConfigShape } from './validate';
|
|
6
|
+
/**
|
|
7
|
+
* The safegres config loader. Discovers `safegres.config.{ts,js,mjs,cjs}`,
|
|
8
|
+
* `.safegresrc{,.json,.yaml,.yml,.js}`, `safegres.json`, or the `"safegres"`
|
|
9
|
+
* package.json key by walking up from cwd; resolves `extends` against the
|
|
10
|
+
* built-in presets (or files / npm packages).
|
|
11
|
+
*/
|
|
12
|
+
export function safegresConfigLoader() {
|
|
13
|
+
return createConfigLoader({
|
|
14
|
+
tool: 'safegres',
|
|
15
|
+
presets: PRESETS,
|
|
16
|
+
defaults: {},
|
|
17
|
+
validate: (config) => {
|
|
18
|
+
validateConfigShape(config); // unknown keys, wrong value kinds
|
|
19
|
+
resolveRules(config); // throws ConfigValidationError on bad rules/overrides
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** Load and merge the effective safegres config. */
|
|
24
|
+
export function loadConfig(params = {}) {
|
|
25
|
+
if (params.sealed)
|
|
26
|
+
return loadSealedConfig(params.preset);
|
|
27
|
+
const loader = safegresConfigLoader();
|
|
28
|
+
const overrides = { ...(params.overrides ?? {}) };
|
|
29
|
+
if (params.preset) {
|
|
30
|
+
const name = params.preset.includes(':') ? params.preset : `safegres:${params.preset}`;
|
|
31
|
+
if (!(name in PRESETS)) {
|
|
32
|
+
throw new Error(`Unknown preset "${params.preset}". Available: ${Object.keys(PRESETS).join(', ')}`);
|
|
33
|
+
}
|
|
34
|
+
overrides.extends = name;
|
|
35
|
+
}
|
|
36
|
+
// `extends` in overrides isn't expanded by confstash (it only expands file
|
|
37
|
+
// layers), so a CLI --preset is merged in as a layer *below* the file config.
|
|
38
|
+
const result = describeExtendsFailure(() => loader.loadSync({
|
|
39
|
+
cwd: params.cwd,
|
|
40
|
+
configFile: params.configFile,
|
|
41
|
+
overrides: stripExtends(overrides)
|
|
42
|
+
}));
|
|
43
|
+
const config = withUnionedOverrides(result);
|
|
44
|
+
if (params.preset) {
|
|
45
|
+
const presetConfig = PRESETS[overrides.extends];
|
|
46
|
+
return { ...result, config: mergePreset(presetConfig, config) };
|
|
47
|
+
}
|
|
48
|
+
return { ...result, config };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A missing `extends` target surfaces from the loader as a bare `ENOENT` on a
|
|
52
|
+
* path nobody typed — the resolved one. Say what it was reached from, and that
|
|
53
|
+
* a name is a preset while a path is a file, which is the actual mistake.
|
|
54
|
+
*/
|
|
55
|
+
function describeExtendsFailure(load) {
|
|
56
|
+
try {
|
|
57
|
+
return load();
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
if (!message.includes('ENOENT'))
|
|
62
|
+
throw error;
|
|
63
|
+
throw new Error(`${message}\nsafegres: an "extends" target could not be read. A built-in preset is a name `
|
|
64
|
+
+ `(${Object.keys(PRESETS).join(', ')}); anything else is resolved as a path relative to `
|
|
65
|
+
+ 'the file that declared it, or as an npm package.');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `overrides` is a list of scoped exceptions, so inheriting one file from
|
|
70
|
+
* another has to *add* to it. Array-typed keys otherwise replace — a child
|
|
71
|
+
* that narrows `exposure.schemas` means the narrower list — but replacing here
|
|
72
|
+
* would silently drop the base file's exceptions, which is how a shared config
|
|
73
|
+
* turns into two divergent copies. Preset chains have always unioned it; this
|
|
74
|
+
* makes a file chain agree.
|
|
75
|
+
*/
|
|
76
|
+
function withUnionedOverrides(result) {
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const unioned = [];
|
|
79
|
+
for (const layer of result.layers) {
|
|
80
|
+
for (const override of layer.config.overrides ?? []) {
|
|
81
|
+
const key = JSON.stringify(override);
|
|
82
|
+
if (seen.has(key))
|
|
83
|
+
continue;
|
|
84
|
+
seen.add(key);
|
|
85
|
+
unioned.push(override);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (unioned.length === 0)
|
|
89
|
+
return result.config;
|
|
90
|
+
return { ...result.config, overrides: unioned };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve each configured path against the file that declared it, so an
|
|
94
|
+
* inherited `perf.baseline` keeps pointing at the base file's directory.
|
|
95
|
+
* Values that came from a preset or from CLI overrides have no directory of
|
|
96
|
+
* their own and fall back to the discovered config file, then to `cwd`.
|
|
97
|
+
*/
|
|
98
|
+
export function configPathBase(result, cwd = process.cwd()) {
|
|
99
|
+
const fallback = result.filepath ? path.dirname(result.filepath) : cwd;
|
|
100
|
+
return {
|
|
101
|
+
dirFor(key) {
|
|
102
|
+
// Highest precedence first: the layer that won the merge is the layer
|
|
103
|
+
// whose directory the value is written relative to.
|
|
104
|
+
for (let i = result.layers.length - 1; i >= 0; i--) {
|
|
105
|
+
const layer = result.layers[i];
|
|
106
|
+
if (valueAt(layer.config, key) === undefined)
|
|
107
|
+
continue;
|
|
108
|
+
return layer.source === 'file' && layer.origin ? path.dirname(layer.origin) : fallback;
|
|
109
|
+
}
|
|
110
|
+
return fallback;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function valueAt(config, key) {
|
|
115
|
+
let node = config;
|
|
116
|
+
for (const part of key.split('.')) {
|
|
117
|
+
if (node == null || typeof node !== 'object')
|
|
118
|
+
return undefined;
|
|
119
|
+
node = node[part];
|
|
120
|
+
}
|
|
121
|
+
return node;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The sealed config: one named preset, expanded, and nothing else. No
|
|
125
|
+
* discovery, no overrides, no `extends` to a local file — so the only thing
|
|
126
|
+
* that can change the score is the database.
|
|
127
|
+
*/
|
|
128
|
+
function loadSealedConfig(preset = 'recommended') {
|
|
129
|
+
const name = preset.includes(':') ? preset : `safegres:${preset}`;
|
|
130
|
+
if (!(name in PRESETS)) {
|
|
131
|
+
throw new Error(`Unknown preset "${preset}". Available: ${Object.keys(PRESETS).join(', ')}`);
|
|
132
|
+
}
|
|
133
|
+
return { config: expandPreset(PRESETS[name]), layers: [], isEmpty: true };
|
|
134
|
+
}
|
|
135
|
+
function stripExtends(config) {
|
|
136
|
+
const { extends: _extends, ...rest } = config;
|
|
137
|
+
return rest;
|
|
138
|
+
}
|
|
139
|
+
function mergePreset(preset, over) {
|
|
140
|
+
const base = expandPreset(preset);
|
|
141
|
+
return {
|
|
142
|
+
...base,
|
|
143
|
+
...over,
|
|
144
|
+
rules: { ...(base.rules ?? {}), ...(over.rules ?? {}) },
|
|
145
|
+
scoring: { ...(base.scoring ?? {}), ...(over.scoring ?? {}) },
|
|
146
|
+
failOn: { ...(base.failOn ?? {}), ...(over.failOn ?? {}) },
|
|
147
|
+
// Per name: a project adds its own card without dropping the preset's,
|
|
148
|
+
// and redefines one by naming it.
|
|
149
|
+
...(base.scorecards || over.scorecards
|
|
150
|
+
? { scorecards: { ...(base.scorecards ?? {}), ...(over.scorecards ?? {}) } }
|
|
151
|
+
: {}),
|
|
152
|
+
overrides: [...(base.overrides ?? []), ...(over.overrides ?? [])],
|
|
153
|
+
// Merged per-key so retuning `skipOwned` doesn't silently drop the
|
|
154
|
+
// preset's `ignore` list. Omitted entirely when neither side sets it.
|
|
155
|
+
...(base.extensions || over.extensions
|
|
156
|
+
? { extensions: { ...(base.extensions ?? {}), ...(over.extensions ?? {}) } }
|
|
157
|
+
: {})
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function expandPreset(preset) {
|
|
161
|
+
if (!preset.extends)
|
|
162
|
+
return preset;
|
|
163
|
+
const parents = Array.isArray(preset.extends) ? preset.extends : [preset.extends];
|
|
164
|
+
let acc = {};
|
|
165
|
+
for (const p of parents) {
|
|
166
|
+
const parent = PRESETS[p];
|
|
167
|
+
if (parent)
|
|
168
|
+
acc = mergePreset(expandPreset(parent), acc);
|
|
169
|
+
}
|
|
170
|
+
const { extends: _extends, ...rest } = preset;
|
|
171
|
+
return mergePreset(acc, rest);
|
|
172
|
+
}
|