@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
package/pg/stats.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Runtime statistics snapshot (`--stats`).
|
|
4
|
+
*
|
|
5
|
+
* Everything here reads the cumulative statistics views — `pg_stat_user_tables`,
|
|
6
|
+
* `pg_stat_user_indexes`, and (when installed) `pg_stat_statements`. Unlike the
|
|
7
|
+
* catalog checks, these numbers describe a *workload*: they are only as good as
|
|
8
|
+
* the traffic the database has seen since the counters were last reset, which is
|
|
9
|
+
* why the snapshot carries `statsReset` and why the checks take floors.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DEFAULT_STATEMENT_LIMIT = void 0;
|
|
13
|
+
exports.introspectStats = introspectStats;
|
|
14
|
+
const introspect_1 = require("./introspect");
|
|
15
|
+
const DEFAULT_EXCLUDES = ['pg_catalog', 'information_schema', 'pg_toast'];
|
|
16
|
+
/** Statements slower than this in aggregate are worth reporting at all. */
|
|
17
|
+
exports.DEFAULT_STATEMENT_LIMIT = 20;
|
|
18
|
+
async function introspectStats(exec, options = {}) {
|
|
19
|
+
const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
|
|
20
|
+
const schemaFilter = options.schemas && options.schemas.length > 0
|
|
21
|
+
? `AND s.schemaname = ANY($1::text[])`
|
|
22
|
+
: `AND NOT (s.schemaname = ANY($2::text[]))`;
|
|
23
|
+
const extFilter = (0, introspect_1.extensionFilter)(options.extensions, 3);
|
|
24
|
+
// Every parameter is referenced (even when only one filters) so Postgres
|
|
25
|
+
// can infer their types — an unused $N errors out at bind time.
|
|
26
|
+
const sql = `
|
|
27
|
+
WITH _params AS (
|
|
28
|
+
SELECT
|
|
29
|
+
$1::text[] AS include_schemas,
|
|
30
|
+
$2::text[] AS exclude_schemas,
|
|
31
|
+
$3::text[] AS ignore_extensions
|
|
32
|
+
)
|
|
33
|
+
SELECT
|
|
34
|
+
s.schemaname AS schema_name,
|
|
35
|
+
s.relname AS table_name,
|
|
36
|
+
s.seq_scan AS seq_scans,
|
|
37
|
+
s.seq_tup_read AS seq_tuples_read,
|
|
38
|
+
COALESCE(s.idx_scan, 0) AS index_scans,
|
|
39
|
+
s.n_live_tup AS live_tuples,
|
|
40
|
+
s.n_dead_tup AS dead_tuples,
|
|
41
|
+
pg_relation_size(s.relid) AS size_bytes,
|
|
42
|
+
GREATEST(s.last_vacuum, s.last_autovacuum) AS last_vacuum,
|
|
43
|
+
GREATEST(s.last_analyze, s.last_autoanalyze) AS last_analyze,
|
|
44
|
+
COALESCE(
|
|
45
|
+
(SELECT jsonb_agg(jsonb_build_object(
|
|
46
|
+
'name', i.indexrelname,
|
|
47
|
+
'scans', COALESCE(i.idx_scan, 0),
|
|
48
|
+
'sizeBytes', pg_relation_size(i.indexrelid),
|
|
49
|
+
'unique', ix.indisunique,
|
|
50
|
+
'constraint', (con.oid IS NOT NULL)
|
|
51
|
+
) ORDER BY i.indexrelname)
|
|
52
|
+
FROM pg_stat_user_indexes i
|
|
53
|
+
JOIN pg_index ix ON ix.indexrelid = i.indexrelid
|
|
54
|
+
LEFT JOIN pg_constraint con
|
|
55
|
+
ON con.conindid = i.indexrelid AND con.contype IN ('p', 'u', 'x')
|
|
56
|
+
WHERE i.relid = s.relid),
|
|
57
|
+
'[]'::jsonb
|
|
58
|
+
) AS indexes
|
|
59
|
+
FROM pg_stat_user_tables s
|
|
60
|
+
JOIN pg_class c ON c.oid = s.relid
|
|
61
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
62
|
+
WHERE true
|
|
63
|
+
${schemaFilter}${extFilter}
|
|
64
|
+
ORDER BY s.schemaname, s.relname
|
|
65
|
+
`;
|
|
66
|
+
const { rows } = await exec.query(sql, [options.schemas ?? [], excludes, options.extensions?.ignore ?? []]);
|
|
67
|
+
const tables = rows.map((r) => ({
|
|
68
|
+
schema: r.schema_name,
|
|
69
|
+
name: r.table_name,
|
|
70
|
+
seqScans: num(r.seq_scans),
|
|
71
|
+
seqTuplesRead: num(r.seq_tuples_read),
|
|
72
|
+
indexScans: num(r.index_scans),
|
|
73
|
+
liveTuples: num(r.live_tuples),
|
|
74
|
+
deadTuples: num(r.dead_tuples),
|
|
75
|
+
sizeBytes: num(r.size_bytes),
|
|
76
|
+
lastVacuum: iso(r.last_vacuum),
|
|
77
|
+
lastAnalyze: iso(r.last_analyze),
|
|
78
|
+
indexes: r.indexes.map((i) => ({ ...i, scans: num(i.scans), sizeBytes: num(i.sizeBytes) }))
|
|
79
|
+
}));
|
|
80
|
+
const { rows: resetRows } = await exec.query(`SELECT stats_reset FROM pg_stat_database WHERE datname = current_database()`);
|
|
81
|
+
const resetRow = resetRows[0];
|
|
82
|
+
const snapshot = {
|
|
83
|
+
tables,
|
|
84
|
+
statsReset: iso(resetRow?.stats_reset ?? null)
|
|
85
|
+
};
|
|
86
|
+
const statements = await introspectStatements(exec, options.statementLimit);
|
|
87
|
+
if (typeof statements === 'string')
|
|
88
|
+
snapshot.statementsUnavailable = statements;
|
|
89
|
+
else
|
|
90
|
+
snapshot.statements = statements;
|
|
91
|
+
return snapshot;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Top statements by total execution time. Returns a reason string instead of
|
|
95
|
+
* rows when `pg_stat_statements` is not installed or not readable by the
|
|
96
|
+
* connected role — an optional extension is a normal state, not an error.
|
|
97
|
+
*/
|
|
98
|
+
async function introspectStatements(exec, limit = exports.DEFAULT_STATEMENT_LIMIT) {
|
|
99
|
+
const { rows: installedRows } = await exec.query(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements') AS present`);
|
|
100
|
+
if (!installedRows[0]?.present) {
|
|
101
|
+
return 'pg_stat_statements is not installed — statement-level findings (S4) were skipped';
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const { rows } = await exec.query(`SELECT
|
|
105
|
+
query,
|
|
106
|
+
calls,
|
|
107
|
+
total_exec_time AS total_time_ms,
|
|
108
|
+
mean_exec_time AS mean_time_ms,
|
|
109
|
+
rows
|
|
110
|
+
FROM pg_stat_statements
|
|
111
|
+
ORDER BY total_exec_time DESC
|
|
112
|
+
LIMIT $1`, [limit]);
|
|
113
|
+
return rows.map((r) => ({
|
|
114
|
+
query: r.query,
|
|
115
|
+
calls: num(r.calls),
|
|
116
|
+
totalTimeMs: Number(r.total_time_ms),
|
|
117
|
+
meanTimeMs: Number(r.mean_time_ms),
|
|
118
|
+
rows: num(r.rows)
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
123
|
+
return `pg_stat_statements is installed but unreadable (${message}) — S4 was skipped`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function num(value) {
|
|
127
|
+
return typeof value === 'number' ? value : Number(value);
|
|
128
|
+
}
|
|
129
|
+
function iso(value) {
|
|
130
|
+
if (value === null || value === undefined)
|
|
131
|
+
return null;
|
|
132
|
+
return value instanceof Date ? value.toISOString() : String(value);
|
|
133
|
+
}
|
package/pgpm-test.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type AuditOptions } from './commands/audit';
|
|
2
|
+
import type { SafegresConfig } from './config/types';
|
|
3
|
+
import type { Report } from './types';
|
|
4
|
+
export interface AuditPgpmWorkspaceOptions extends Omit<AuditOptions, 'config'> {
|
|
5
|
+
/**
|
|
6
|
+
* pgpm workspace or module directory to deploy. Defaults to the nearest
|
|
7
|
+
* pgpm module/workspace discovered from `process.cwd()`.
|
|
8
|
+
*/
|
|
9
|
+
cwd?: string;
|
|
10
|
+
/** Explicit safegres config file (else discovered by walk-up from `cwd`). */
|
|
11
|
+
configFile?: string;
|
|
12
|
+
/** Built-in preset name (recommended|strict|constructive|minimal). */
|
|
13
|
+
preset?: string;
|
|
14
|
+
/** Pre-resolved config — skips discovery entirely. */
|
|
15
|
+
config?: SafegresConfig;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Deploy the pgpm workspace at `cwd` into an ephemeral test database, run
|
|
19
|
+
* `safegres audit` against it with the project's safegres config, tear the
|
|
20
|
+
* database down, and return the report.
|
|
21
|
+
*/
|
|
22
|
+
export declare function auditPgpmWorkspace(options?: AuditPgpmWorkspaceOptions): Promise<Report>;
|
package/pgpm-test.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.auditPgpmWorkspace = auditPgpmWorkspace;
|
|
4
|
+
/**
|
|
5
|
+
* Test helper for pgpm projects: deploy the workspace into an ephemeral
|
|
6
|
+
* database (via pgsql-test) and audit it with the project's discovered
|
|
7
|
+
* safegres config.
|
|
8
|
+
*
|
|
9
|
+
* `pgsql-test` is an optional peer dependency — only needed when this
|
|
10
|
+
* entrypoint is imported.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { auditPgpmWorkspace } from 'safegres/pgpm-test';
|
|
14
|
+
*
|
|
15
|
+
* it('passes the security audit', async () => {
|
|
16
|
+
* const report = await auditPgpmWorkspace();
|
|
17
|
+
* expect(report.score.grade).toBe('A+');
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
const pgsql_test_1 = require("pgsql-test");
|
|
22
|
+
const audit_1 = require("./commands/audit");
|
|
23
|
+
const loader_1 = require("./config/loader");
|
|
24
|
+
/**
|
|
25
|
+
* Deploy the pgpm workspace at `cwd` into an ephemeral test database, run
|
|
26
|
+
* `safegres audit` against it with the project's safegres config, tear the
|
|
27
|
+
* database down, and return the report.
|
|
28
|
+
*/
|
|
29
|
+
async function auditPgpmWorkspace(options = {}) {
|
|
30
|
+
const { cwd, configFile, preset, config: explicitConfig, ...auditOptions } = options;
|
|
31
|
+
const config = explicitConfig ?? (0, loader_1.loadConfig)({ cwd, configFile, preset }).config;
|
|
32
|
+
const { pg, teardown } = await (0, pgsql_test_1.getConnections)({}, [pgsql_test_1.seed.pgpm(cwd)]);
|
|
33
|
+
try {
|
|
34
|
+
return await (0, audit_1.audit)(pg.client, {
|
|
35
|
+
...auditOptions,
|
|
36
|
+
config
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
await teardown();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CallGraphDiff } from '../callgraph/baseline';
|
|
2
|
+
import type { CallGraphReport } from '../callgraph/graph';
|
|
3
|
+
export interface RenderCallGraphOptions {
|
|
4
|
+
color?: boolean;
|
|
5
|
+
/** Only the stats line — no checklist items. */
|
|
6
|
+
summary?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function renderCallGraph(cg: CallGraphReport, options?: RenderCallGraphOptions): string;
|
|
9
|
+
export declare function renderCallGraphDiff(diff: CallGraphDiff, options?: RenderCallGraphOptions): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.renderCallGraph = renderCallGraph;
|
|
7
|
+
exports.renderCallGraphDiff = renderCallGraphDiff;
|
|
8
|
+
const yanse_1 = __importDefault(require("yanse"));
|
|
9
|
+
const baseline_1 = require("../callgraph/baseline");
|
|
10
|
+
const CODE_META = {
|
|
11
|
+
CF1: { title: 'DEFINER without pinned search_path (provable — fix these)', paint: yanse_1.default.red },
|
|
12
|
+
CF2: { title: 'DEFINER executable by anonymous/PUBLIC (provable — confirm intent)', paint: yanse_1.default.red },
|
|
13
|
+
CG2: { title: 'RLS-bypass paths (RLS does not protect the table on this path)', paint: yanse_1.default.yellow },
|
|
14
|
+
CG3: { title: 'auth-context mutations (functions that change who you are)', paint: yanse_1.default.yellow },
|
|
15
|
+
CG1: { title: 'trust hops (execution crosses into SECURITY DEFINER)', paint: yanse_1.default.cyan },
|
|
16
|
+
CG4: { title: 'internal tables reached via DEFINER paths', paint: yanse_1.default.cyan },
|
|
17
|
+
CG5: { title: 'opaque nodes (static analysis ends here — audit manually)', paint: yanse_1.default.gray }
|
|
18
|
+
};
|
|
19
|
+
const CODE_ORDER = ['CF1', 'CF2', 'CG2', 'CG3', 'CG1', 'CG4', 'CG5'];
|
|
20
|
+
function renderCallGraph(cg, options = {}) {
|
|
21
|
+
const color = options.color === true;
|
|
22
|
+
const paint = (p, s) => (color ? p(s) : s);
|
|
23
|
+
const s = cg.stats;
|
|
24
|
+
const lines = [
|
|
25
|
+
paint(yanse_1.default.bold, 'call graph — trust boundaries reachable from the exposed surface (unscored; human review)'),
|
|
26
|
+
` ${s.entryPoints} entry point(s) → ${s.reachableFunctions} reachable function(s)`
|
|
27
|
+
+ ` | ${s.trustHops} trust hop(s) ${s.rlsBypassPaths} RLS-bypass `
|
|
28
|
+
+ `${s.authContextMutations} auth-context ${s.internalReach} internal-reach ${s.opaqueNodes} opaque`
|
|
29
|
+
];
|
|
30
|
+
if (options.summary || cg.checklist.length === 0) {
|
|
31
|
+
if (cg.checklist.length === 0)
|
|
32
|
+
lines.push(' no trust boundaries found.');
|
|
33
|
+
return lines.join('\n');
|
|
34
|
+
}
|
|
35
|
+
for (const code of CODE_ORDER) {
|
|
36
|
+
const items = cg.checklist.filter((i) => i.code === code);
|
|
37
|
+
if (items.length === 0)
|
|
38
|
+
continue;
|
|
39
|
+
const meta = CODE_META[code];
|
|
40
|
+
lines.push('', paint(meta.paint, `${code} — ${meta.title} (${items.length})`));
|
|
41
|
+
for (const item of items) {
|
|
42
|
+
lines.push(...renderItem(item));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
function renderCallGraphDiff(diff, options = {}) {
|
|
48
|
+
const color = options.color === true;
|
|
49
|
+
const paint = (p, s) => (color ? p(s) : s);
|
|
50
|
+
if (diff.added.length === 0 && diff.removed.length === 0) {
|
|
51
|
+
return paint(yanse_1.default.green, 'baseline: no new trust boundaries.');
|
|
52
|
+
}
|
|
53
|
+
const lines = [];
|
|
54
|
+
if (diff.added.length > 0) {
|
|
55
|
+
lines.push(paint(yanse_1.default.red, `baseline: ${diff.added.length} NEW trust boundar${diff.added.length === 1 ? 'y' : 'ies'} — review and re-baseline to accept:`));
|
|
56
|
+
for (const item of diff.added) {
|
|
57
|
+
lines.push(` + [${item.code}] ${item.table ? `${item.fn} → ${item.table}` : item.fn}`);
|
|
58
|
+
lines.push(` ${item.message}`);
|
|
59
|
+
if (item.path.length > 1)
|
|
60
|
+
lines.push(` via: ${item.path.join(' → ')}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (diff.removed.length > 0) {
|
|
64
|
+
lines.push(paint(yanse_1.default.green, `baseline: ${diff.removed.length} boundar${diff.removed.length === 1 ? 'y' : 'ies'} resolved (no longer present) — re-baseline to prune:`));
|
|
65
|
+
for (const b of diff.removed) {
|
|
66
|
+
lines.push(` - ${(0, baseline_1.boundaryKey)(b)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
71
|
+
function renderItem(item) {
|
|
72
|
+
const subject = item.table ? `${item.fn} → ${item.table}` : item.fn;
|
|
73
|
+
const out = [` • ${subject}`, ` ${item.message}`];
|
|
74
|
+
if (item.path.length > 1) {
|
|
75
|
+
out.push(` via: ${item.path.join(' → ')}`);
|
|
76
|
+
}
|
|
77
|
+
else if (item.entry !== item.fn) {
|
|
78
|
+
out.push(` via: ${item.entry}`);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparison against a previous run (`--compare <file>`).
|
|
3
|
+
*
|
|
4
|
+
* A report says what the database is; it never says what changed. In CI that
|
|
5
|
+
* is the only question a reviewer has — the PR comment reads "Performance 75.0
|
|
6
|
+
* (C)" whether the branch improved it by ten points or cost ten, and the
|
|
7
|
+
* numbers are only legible to someone who remembers yesterday's.
|
|
8
|
+
*
|
|
9
|
+
* The previous run is supplied as a file rather than inferred: a scanner has
|
|
10
|
+
* no memory and no business acquiring one, so CI decides what "previous" means
|
|
11
|
+
* (the artifact from the base branch, a committed scoreboard) and passes it in.
|
|
12
|
+
* Any `renderJson` output works as input, so nothing extra has to be produced
|
|
13
|
+
* for this to be usable — but `toSnapshot` exists for the case where a full
|
|
14
|
+
* report is too large to keep around, since only the aggregates are read.
|
|
15
|
+
*/
|
|
16
|
+
import type { Grade } from '../config/types';
|
|
17
|
+
import type { Report, Severity, Summary } from '../types';
|
|
18
|
+
/** The aggregate slice of a report that a comparison reads. */
|
|
19
|
+
export interface ReportSnapshot {
|
|
20
|
+
/** safegres version that produced the run. */
|
|
21
|
+
version?: string;
|
|
22
|
+
generatedAt?: string;
|
|
23
|
+
/** Free-form label for the run — a git ref, a commit sha, "main". */
|
|
24
|
+
ref?: string;
|
|
25
|
+
/** Where the previous run came from, when CI knows. */
|
|
26
|
+
provenance?: BaselineProvenance;
|
|
27
|
+
summary: Summary;
|
|
28
|
+
security?: DimensionSnapshot;
|
|
29
|
+
perf?: DimensionSnapshot;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Which run the delta is measured against.
|
|
33
|
+
*
|
|
34
|
+
* `ref` alone renders as "Changes since main", which is what a stale baseline
|
|
35
|
+
* hides behind: it is true of a report produced an hour ago and of one produced
|
|
36
|
+
* a fortnight and 150 merges ago, and the reader cannot tell. Every field is a
|
|
37
|
+
* plain string a provider fills in, so this stays provider-agnostic — a run id
|
|
38
|
+
* and a run URL are as meaningful on GitLab or Buildkite as on GitHub Actions.
|
|
39
|
+
*/
|
|
40
|
+
export interface BaselineProvenance {
|
|
41
|
+
/** The commit the baseline report was produced from. */
|
|
42
|
+
sha?: string;
|
|
43
|
+
/** The CI run that produced it. */
|
|
44
|
+
runId?: string;
|
|
45
|
+
/** A link to that run. */
|
|
46
|
+
runUrl?: string;
|
|
47
|
+
/** Pre-formatted age ("10.2 hours"), when the provider measured it. */
|
|
48
|
+
age?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface DimensionSnapshot {
|
|
51
|
+
value: number;
|
|
52
|
+
grade: Grade;
|
|
53
|
+
findings: number;
|
|
54
|
+
/** Finding count per rule code. */
|
|
55
|
+
rules: Record<string, number>;
|
|
56
|
+
}
|
|
57
|
+
export interface ScoreDelta {
|
|
58
|
+
before: number;
|
|
59
|
+
after: number;
|
|
60
|
+
/** `after - before`, rounded to one decimal. Positive is an improvement. */
|
|
61
|
+
delta: number;
|
|
62
|
+
gradeBefore: Grade;
|
|
63
|
+
gradeAfter: Grade;
|
|
64
|
+
/** Findings before → after. */
|
|
65
|
+
findingsBefore: number;
|
|
66
|
+
findingsAfter: number;
|
|
67
|
+
}
|
|
68
|
+
export interface RuleDelta {
|
|
69
|
+
code: string;
|
|
70
|
+
before: number;
|
|
71
|
+
after: number;
|
|
72
|
+
/** `after - before`. Positive means the rule fires more than it used to. */
|
|
73
|
+
delta: number;
|
|
74
|
+
dimension: 'security' | 'perf';
|
|
75
|
+
/**
|
|
76
|
+
* The previous run reported nothing for this code — a rule that did not
|
|
77
|
+
* exist in that version, or a dimension it never ran. Distinct from a real
|
|
78
|
+
* zero: `before: 0` means the rule ran and found nothing, and an increase
|
|
79
|
+
* off it is a regression; this means the movement is unknown, and rendering
|
|
80
|
+
* it as one is how a scanner upgrade comes out looking like a catastrophe.
|
|
81
|
+
*/
|
|
82
|
+
unmeasuredBefore?: boolean;
|
|
83
|
+
}
|
|
84
|
+
export interface ReportComparison {
|
|
85
|
+
/** Where the previous numbers came from, for the reader. */
|
|
86
|
+
previous: {
|
|
87
|
+
ref?: string;
|
|
88
|
+
generatedAt?: string;
|
|
89
|
+
version?: string;
|
|
90
|
+
provenance?: BaselineProvenance;
|
|
91
|
+
};
|
|
92
|
+
security?: ScoreDelta;
|
|
93
|
+
perf?: ScoreDelta;
|
|
94
|
+
/** Severity counts, before → after. */
|
|
95
|
+
summary: Record<Severity, {
|
|
96
|
+
before: number;
|
|
97
|
+
after: number;
|
|
98
|
+
delta: number;
|
|
99
|
+
}>;
|
|
100
|
+
/** Rules whose finding count moved, largest regression first. */
|
|
101
|
+
rules: RuleDelta[];
|
|
102
|
+
/** Nothing moved: same scores, same counts, same rules. */
|
|
103
|
+
unchanged: boolean;
|
|
104
|
+
}
|
|
105
|
+
/** Reduce a report to the aggregates a comparison needs. */
|
|
106
|
+
export declare function toSnapshot(report: Report, meta?: {
|
|
107
|
+
ref?: string;
|
|
108
|
+
provenance?: BaselineProvenance;
|
|
109
|
+
}): ReportSnapshot;
|
|
110
|
+
export declare function serializeSnapshot(snapshot: ReportSnapshot): string;
|
|
111
|
+
/**
|
|
112
|
+
* Read a previous run from JSON. Accepts either a snapshot or a full report —
|
|
113
|
+
* the two are distinguished by the presence of `findings`, so CI can hand over
|
|
114
|
+
* whatever it already has on disk.
|
|
115
|
+
*/
|
|
116
|
+
export declare function parseSnapshot(json: string): ReportSnapshot;
|
|
117
|
+
/** Diff a fresh report against a previous run. */
|
|
118
|
+
export declare function compareReports(previous: ReportSnapshot, current: Report): ReportComparison;
|
|
119
|
+
/**
|
|
120
|
+
* `+2.3` / `−1.4` / `0`, with a real minus sign. Rendering-only, but shared:
|
|
121
|
+
* the markdown and pretty renderers must not disagree about which direction a
|
|
122
|
+
* number moved.
|
|
123
|
+
*/
|
|
124
|
+
export declare function formatDelta(delta: number, digits?: number): string;
|
|
125
|
+
/** `13.6 days` / `3.2 hours` / `41 minutes` — an age a human reads at a glance. */
|
|
126
|
+
export declare function formatAge(ms: number): string;
|
|
127
|
+
/**
|
|
128
|
+
* "main@af904c90d — [run 33373344814](…), 10.2 hours old" — the one line every
|
|
129
|
+
* renderer puts above a delta, so the job summary, the PR comment and the
|
|
130
|
+
* terminal cannot describe different baselines.
|
|
131
|
+
*
|
|
132
|
+
* The age falls back to `generatedAt` when the provider did not measure one:
|
|
133
|
+
* the report timestamp is what a scanner always has.
|
|
134
|
+
*/
|
|
135
|
+
export declare function describeBaseline(previous: ReportComparison['previous'], options?: {
|
|
136
|
+
link?: boolean;
|
|
137
|
+
now?: number;
|
|
138
|
+
}): string;
|
|
139
|
+
/**
|
|
140
|
+
* `main@af904c90d`, or just `main` when no commit is known — the short name of
|
|
141
|
+
* the baseline, for a place too narrow for the run and the age.
|
|
142
|
+
*/
|
|
143
|
+
export declare function baselineLabel(previous: ReportComparison['previous']): string;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Comparison against a previous run (`--compare <file>`).
|
|
4
|
+
*
|
|
5
|
+
* A report says what the database is; it never says what changed. In CI that
|
|
6
|
+
* is the only question a reviewer has — the PR comment reads "Performance 75.0
|
|
7
|
+
* (C)" whether the branch improved it by ten points or cost ten, and the
|
|
8
|
+
* numbers are only legible to someone who remembers yesterday's.
|
|
9
|
+
*
|
|
10
|
+
* The previous run is supplied as a file rather than inferred: a scanner has
|
|
11
|
+
* no memory and no business acquiring one, so CI decides what "previous" means
|
|
12
|
+
* (the artifact from the base branch, a committed scoreboard) and passes it in.
|
|
13
|
+
* Any `renderJson` output works as input, so nothing extra has to be produced
|
|
14
|
+
* for this to be usable — but `toSnapshot` exists for the case where a full
|
|
15
|
+
* report is too large to keep around, since only the aggregates are read.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.toSnapshot = toSnapshot;
|
|
19
|
+
exports.serializeSnapshot = serializeSnapshot;
|
|
20
|
+
exports.parseSnapshot = parseSnapshot;
|
|
21
|
+
exports.compareReports = compareReports;
|
|
22
|
+
exports.formatDelta = formatDelta;
|
|
23
|
+
exports.formatAge = formatAge;
|
|
24
|
+
exports.describeBaseline = describeBaseline;
|
|
25
|
+
exports.baselineLabel = baselineLabel;
|
|
26
|
+
const types_1 = require("../types");
|
|
27
|
+
const SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
|
|
28
|
+
function dimensionSnapshot(score, findings) {
|
|
29
|
+
if (!score)
|
|
30
|
+
return undefined;
|
|
31
|
+
const rules = {};
|
|
32
|
+
for (const d of score.deductions)
|
|
33
|
+
rules[d.code] = d.count;
|
|
34
|
+
return { value: score.value, grade: score.grade, findings, rules };
|
|
35
|
+
}
|
|
36
|
+
/** Reduce a report to the aggregates a comparison needs. */
|
|
37
|
+
function toSnapshot(report, meta = {}) {
|
|
38
|
+
const perfFindings = report.perf?.findings.length ?? 0;
|
|
39
|
+
return {
|
|
40
|
+
version: report.version,
|
|
41
|
+
generatedAt: report.generatedAt,
|
|
42
|
+
...(meta.ref !== undefined && { ref: meta.ref }),
|
|
43
|
+
...(meta.provenance !== undefined && { provenance: meta.provenance }),
|
|
44
|
+
summary: report.summary,
|
|
45
|
+
...(report.score && {
|
|
46
|
+
security: dimensionSnapshot(report.score, report.findings.length - perfFindings)
|
|
47
|
+
}),
|
|
48
|
+
...(report.perf && { perf: dimensionSnapshot(report.perf.score, perfFindings) })
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function serializeSnapshot(snapshot) {
|
|
52
|
+
return `${JSON.stringify(snapshot, null, 2)}\n`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Read a previous run from JSON. Accepts either a snapshot or a full report —
|
|
56
|
+
* the two are distinguished by the presence of `findings`, so CI can hand over
|
|
57
|
+
* whatever it already has on disk.
|
|
58
|
+
*/
|
|
59
|
+
function parseSnapshot(json) {
|
|
60
|
+
const parsed = JSON.parse(json);
|
|
61
|
+
if (Array.isArray(parsed.findings))
|
|
62
|
+
return toSnapshot(parsed);
|
|
63
|
+
if (!parsed.summary) {
|
|
64
|
+
throw new Error('not a safegres report or snapshot: no "summary"');
|
|
65
|
+
}
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
68
|
+
function round(value) {
|
|
69
|
+
return Math.round(value * 10) / 10;
|
|
70
|
+
}
|
|
71
|
+
function scoreDelta(before, after) {
|
|
72
|
+
if (!before || !after)
|
|
73
|
+
return undefined;
|
|
74
|
+
return {
|
|
75
|
+
before: before.value,
|
|
76
|
+
after: after.value,
|
|
77
|
+
delta: round(after.value - before.value),
|
|
78
|
+
gradeBefore: before.grade,
|
|
79
|
+
gradeAfter: after.grade,
|
|
80
|
+
findingsBefore: before.findings,
|
|
81
|
+
findingsAfter: after.findings
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function ruleDeltas(dimension, before, after) {
|
|
85
|
+
if (!before && !after)
|
|
86
|
+
return [];
|
|
87
|
+
const codes = new Set([
|
|
88
|
+
...Object.keys(before?.rules ?? {}),
|
|
89
|
+
...Object.keys(after?.rules ?? {})
|
|
90
|
+
]);
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const code of codes) {
|
|
93
|
+
const measured = before?.rules[code] !== undefined;
|
|
94
|
+
const b = before?.rules[code] ?? 0;
|
|
95
|
+
const a = after?.rules[code] ?? 0;
|
|
96
|
+
if (a === b)
|
|
97
|
+
continue;
|
|
98
|
+
out.push({
|
|
99
|
+
code,
|
|
100
|
+
before: b,
|
|
101
|
+
after: a,
|
|
102
|
+
delta: a - b,
|
|
103
|
+
dimension,
|
|
104
|
+
...(measured ? {} : { unmeasuredBefore: true })
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/** Diff a fresh report against a previous run. */
|
|
110
|
+
function compareReports(previous, current) {
|
|
111
|
+
const now = toSnapshot(current);
|
|
112
|
+
const summary = {};
|
|
113
|
+
const prevSummary = previous.summary ?? (0, types_1.newSummary)();
|
|
114
|
+
for (const sev of SEVERITIES) {
|
|
115
|
+
const before = prevSummary[sev] ?? 0;
|
|
116
|
+
const after = now.summary[sev] ?? 0;
|
|
117
|
+
summary[sev] = { before, after, delta: after - before };
|
|
118
|
+
}
|
|
119
|
+
const rules = [
|
|
120
|
+
...ruleDeltas('security', previous.security, now.security),
|
|
121
|
+
...ruleDeltas('perf', previous.perf, now.perf)
|
|
122
|
+
]
|
|
123
|
+
// Regressions first, then improvements; rules with no previous reading
|
|
124
|
+
// last, since they are not movement and should not lead the table.
|
|
125
|
+
.sort((a, b) => Number(a.unmeasuredBefore ?? false) - Number(b.unmeasuredBefore ?? false)
|
|
126
|
+
|| b.delta - a.delta
|
|
127
|
+
|| a.code.localeCompare(b.code));
|
|
128
|
+
const security = scoreDelta(previous.security, now.security);
|
|
129
|
+
const perf = scoreDelta(previous.perf, now.perf);
|
|
130
|
+
return {
|
|
131
|
+
previous: {
|
|
132
|
+
...(previous.ref !== undefined && { ref: previous.ref }),
|
|
133
|
+
...(previous.generatedAt !== undefined && { generatedAt: previous.generatedAt }),
|
|
134
|
+
...(previous.version !== undefined && { version: previous.version }),
|
|
135
|
+
...(previous.provenance !== undefined && { provenance: previous.provenance })
|
|
136
|
+
},
|
|
137
|
+
...(security && { security }),
|
|
138
|
+
...(perf && { perf }),
|
|
139
|
+
summary,
|
|
140
|
+
rules,
|
|
141
|
+
unchanged: rules.length === 0
|
|
142
|
+
&& (security?.delta ?? 0) === 0
|
|
143
|
+
&& (perf?.delta ?? 0) === 0
|
|
144
|
+
&& SEVERITIES.every((sev) => summary[sev].delta === 0)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* `+2.3` / `−1.4` / `0`, with a real minus sign. Rendering-only, but shared:
|
|
149
|
+
* the markdown and pretty renderers must not disagree about which direction a
|
|
150
|
+
* number moved.
|
|
151
|
+
*/
|
|
152
|
+
function formatDelta(delta, digits = 0) {
|
|
153
|
+
if (delta === 0)
|
|
154
|
+
return '0';
|
|
155
|
+
const magnitude = Math.abs(delta).toFixed(digits);
|
|
156
|
+
return delta > 0 ? `+${magnitude}` : `−${magnitude}`;
|
|
157
|
+
}
|
|
158
|
+
/** `13.6 days` / `3.2 hours` / `41 minutes` — an age a human reads at a glance. */
|
|
159
|
+
function formatAge(ms) {
|
|
160
|
+
const minutes = ms / 60000;
|
|
161
|
+
if (minutes < 90) {
|
|
162
|
+
const rounded = Math.round(minutes);
|
|
163
|
+
return `${rounded} minute${rounded === 1 ? '' : 's'}`;
|
|
164
|
+
}
|
|
165
|
+
const hours = minutes / 60;
|
|
166
|
+
if (hours < 48)
|
|
167
|
+
return `${hours.toFixed(1)} hours`;
|
|
168
|
+
return `${(hours / 24).toFixed(1)} days`;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* "main@af904c90d — [run 33373344814](…), 10.2 hours old" — the one line every
|
|
172
|
+
* renderer puts above a delta, so the job summary, the PR comment and the
|
|
173
|
+
* terminal cannot describe different baselines.
|
|
174
|
+
*
|
|
175
|
+
* The age falls back to `generatedAt` when the provider did not measure one:
|
|
176
|
+
* the report timestamp is what a scanner always has.
|
|
177
|
+
*/
|
|
178
|
+
function describeBaseline(previous, options = {}) {
|
|
179
|
+
const p = previous.provenance ?? {};
|
|
180
|
+
const label = baselineLabel(previous);
|
|
181
|
+
const parts = [];
|
|
182
|
+
if (p.runId) {
|
|
183
|
+
parts.push(p.runUrl && options.link !== false ? `[run ${p.runId}](${p.runUrl})` : `run ${p.runId}`);
|
|
184
|
+
}
|
|
185
|
+
const age = p.age ?? ageOf(previous.generatedAt, options.now);
|
|
186
|
+
if (age)
|
|
187
|
+
parts.push(`${age} old`);
|
|
188
|
+
const detail = parts.length > 0 ? ` — ${parts.join(', ')}` : '';
|
|
189
|
+
return `${label}${detail}`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* `main@af904c90d`, or just `main` when no commit is known — the short name of
|
|
193
|
+
* the baseline, for a place too narrow for the run and the age.
|
|
194
|
+
*/
|
|
195
|
+
function baselineLabel(previous) {
|
|
196
|
+
const sha = previous.provenance?.sha;
|
|
197
|
+
const ref = previous.ref ?? previous.generatedAt ?? 'the previous run';
|
|
198
|
+
return sha ? `${previous.ref ?? 'previous'}@${sha.slice(0, 9)}` : ref;
|
|
199
|
+
}
|
|
200
|
+
function ageOf(generatedAt, now = Date.now()) {
|
|
201
|
+
if (!generatedAt)
|
|
202
|
+
return undefined;
|
|
203
|
+
const ms = now - Date.parse(generatedAt);
|
|
204
|
+
return ms >= 0 ? formatAge(ms) : undefined;
|
|
205
|
+
}
|