@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/esm/cli/audit.js
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { Logger } from '@pgpmjs/logger';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { diffCallGraph, parseBaseline, serializeBaseline, toBaseline } from '../callgraph/baseline';
|
|
5
|
+
import { audit } from '../commands/audit';
|
|
6
|
+
import { configPathBase, loadConfig } from '../config/loader';
|
|
7
|
+
import { diffPerf, parsePerfBaseline, serializePerfBaseline, toPerfBaseline } from '../perf/baseline';
|
|
8
|
+
import { compareReports, parseSnapshot, serializeSnapshot, toSnapshot } from '../report/compare';
|
|
9
|
+
import { emitGithub, postStickyComment, renderGithubComment, renderGithubSummary } from '../report/github';
|
|
10
|
+
import { renderJson } from '../report/json';
|
|
11
|
+
import { renderMarkdown } from '../report/markdown';
|
|
12
|
+
import { renderPretty } from '../report/pretty';
|
|
13
|
+
import { buildSourceIndex, renderSarif } from '../report/sarif';
|
|
14
|
+
import { matchPlane, viewConfigFromReportConfig } from '../report/view';
|
|
15
|
+
import { meetsGrade } from '../score/score';
|
|
16
|
+
import { meetsThreshold, SEVERITY_ORDER, summarize } from '../types';
|
|
17
|
+
import { buildClient, configParamsFromArgv, csvList, extensionScopeFromArgv, resolveRunPaths } from './shared';
|
|
18
|
+
const log = new Logger('safegres');
|
|
19
|
+
/**
|
|
20
|
+
* `--pgpm` mode needs the optional peer dependency `pgsql-test`, so the
|
|
21
|
+
* helper module is loaded lazily with a friendly error when it's missing.
|
|
22
|
+
*/
|
|
23
|
+
function importPgpmTest() {
|
|
24
|
+
try {
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
|
+
return require('../pgpm-test');
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (err.code === 'MODULE_NOT_FOUND') {
|
|
30
|
+
log.error('--pgpm requires the optional peer dependency "pgsql-test" — install it (e.g. `npm i -D pgsql-test`) and retry');
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const usage = `
|
|
37
|
+
safegres audit — Postgres security and performance auditor
|
|
38
|
+
|
|
39
|
+
safegres audit [OPTIONS]
|
|
40
|
+
|
|
41
|
+
Connection (priority order, top wins):
|
|
42
|
+
--pgpm [dir] No connection needed: deploy the pgpm workspace at
|
|
43
|
+
[dir] (default: nearest from cwd) into an ephemeral
|
|
44
|
+
test database and audit it (requires pgsql-test)
|
|
45
|
+
--connection <url> Full PostgreSQL connection string
|
|
46
|
+
--host <host> PostgreSQL host (else PGHOST, default localhost)
|
|
47
|
+
--port <port> PostgreSQL port (else PGPORT, default 5432)
|
|
48
|
+
--user <user> PostgreSQL user (else PGUSER, default postgres)
|
|
49
|
+
--password <pw> PostgreSQL password (else PGPASSWORD,default password)
|
|
50
|
+
--database <db> PostgreSQL database (else PGDATABASE,default postgres)
|
|
51
|
+
|
|
52
|
+
Configuration:
|
|
53
|
+
--config <path> Explicit config file (else discovered: safegres.config.{ts,js,mjs,cjs},
|
|
54
|
+
.safegresrc{,.json,.yaml,.yml,.js}, safegres.json, package.json "safegres")
|
|
55
|
+
--preset <name> Apply a built-in preset. Posture: recommended, strict, minimal.
|
|
56
|
+
Stack: constructive, postgrest, supabase, graphile, hasura.
|
|
57
|
+
Composable: multi-tenant, oltp (extends: [stack, posture]).
|
|
58
|
+
--rule <CODE=SETTING> Retune a rule (repeatable), e.g. --rule A3=off --rule A5=high
|
|
59
|
+
--sealed Grade under a built-in preset ALONE: no config-file
|
|
60
|
+
discovery, and --config/--rule/--exposure-schemas and
|
|
61
|
+
every baseline flag are refused. For evaluating an
|
|
62
|
+
agent, where editing the rules is the cheapest way to
|
|
63
|
+
win. Pair with --preset; the resolved rule set is
|
|
64
|
+
fingerprinted into report.provenance either way
|
|
65
|
+
--verify-fingerprint <f> Exit non-zero unless the run's configuration
|
|
66
|
+
fingerprint is <f> — the harness's proof that the
|
|
67
|
+
score it is reading was produced under its own rules
|
|
68
|
+
|
|
69
|
+
Exposure (what the score is computed against):
|
|
70
|
+
--exposure-schemas <csv> Declare the API-exposed schemas; findings outside
|
|
71
|
+
them become unscored internal advisories
|
|
72
|
+
--exposed-only Hide internal (non-exposed) findings from output
|
|
73
|
+
--plane <name|glob> Expand a secondary access plane (repeatable; '*' for
|
|
74
|
+
all). Planes are declared in exposure.planes and
|
|
75
|
+
graded separately — the headline score is always the
|
|
76
|
+
primary (API) plane and never moves
|
|
77
|
+
|
|
78
|
+
Performance dimension (optional; scored separately from security):
|
|
79
|
+
--perf Also audit index hygiene (X1/X5/X6/X7/X8), policy-aware
|
|
80
|
+
index coverage (X2/X3/X4) and policy cost (P1/P1b),
|
|
81
|
+
scored on its own 0-100 axis (report.perf)
|
|
82
|
+
--fail-on-perf-score <n> Exit non-zero if the perf score is below n (0-100)
|
|
83
|
+
--fail-on-perf-grade <g> Exit non-zero if the perf grade is below g (A+|A|B|C|D)
|
|
84
|
+
--write-perf-baseline <file>
|
|
85
|
+
Snapshot today's perf findings to <file> as accepted
|
|
86
|
+
debt (implies --perf)
|
|
87
|
+
--perf-baseline <file> Diff perf findings against a committed baseline and
|
|
88
|
+
report only NEW debt (implies --perf)
|
|
89
|
+
--fail-on-new-perf Exit non-zero when --perf-baseline finds new debt
|
|
90
|
+
--stats Also run the runtime-statistics rules (S1-S4) from
|
|
91
|
+
pg_stat_user_tables and, when installed,
|
|
92
|
+
pg_stat_statements (implies --perf). Workload-
|
|
93
|
+
dependent: only meaningful against a database that
|
|
94
|
+
has served representative traffic
|
|
95
|
+
--explain Prove each probeable perf finding with
|
|
96
|
+
EXPLAIN (GENERIC_PLAN) and attach the plan as
|
|
97
|
+
evidence; findings the planner refutes are reported
|
|
98
|
+
but unscored (implies --perf, needs PostgreSQL 16+)
|
|
99
|
+
|
|
100
|
+
Comparison with a previous run (what changed, not just what is):
|
|
101
|
+
--compare <file> Diff scores, severity counts and per-rule counts
|
|
102
|
+
against a previous run and render the delta.
|
|
103
|
+
<file> is any earlier --format json output, or a
|
|
104
|
+
snapshot written by --write-snapshot
|
|
105
|
+
--compare-ref <label> How to name the previous run in the report
|
|
106
|
+
(e.g. "main", a commit sha). Default "previous run"
|
|
107
|
+
--compare-sha <sha> Commit the previous run was produced from
|
|
108
|
+
--compare-run-id <id> CI run that produced it
|
|
109
|
+
--compare-run-url <url> Link to that run
|
|
110
|
+
--compare-age <age> How old it is, pre-formatted ("10.2 hours").
|
|
111
|
+
Defaults to the age of its generatedAt
|
|
112
|
+
--compare-skipped <why> There is no baseline and this is the reason, shown
|
|
113
|
+
in place of the delta so an absent delta does not
|
|
114
|
+
read as "nothing changed"
|
|
115
|
+
--write-snapshot <file> Write the aggregate slice of this run (scores,
|
|
116
|
+
counts, per-rule counts) for a later --compare,
|
|
117
|
+
when keeping the whole report is too much
|
|
118
|
+
|
|
119
|
+
Call graph (unscored; human review):
|
|
120
|
+
--call-graph Analyze the functions reachable from the exposed entry
|
|
121
|
+
points and list trust boundaries: SECURITY DEFINER hops,
|
|
122
|
+
RLS-bypass paths, auth-context mutations, internal-table
|
|
123
|
+
reach, and opaque (dynamic SQL) nodes
|
|
124
|
+
--write-baseline <file> Snapshot the call-graph boundaries to <file>
|
|
125
|
+
(implies --call-graph)
|
|
126
|
+
--baseline <file> Diff the call graph against a committed snapshot and
|
|
127
|
+
report NEW trust boundaries (implies --call-graph)
|
|
128
|
+
--fail-on-new-boundaries Exit non-zero when --baseline finds new boundaries
|
|
129
|
+
|
|
130
|
+
Audit options:
|
|
131
|
+
--schemas <csv> Limit to these schemas (default: all non-system)
|
|
132
|
+
--exclude-schemas <csv> Skip these schemas
|
|
133
|
+
--ignore-extensions <csv> Skip everything in these extensions' schemas, for
|
|
134
|
+
objects an extension creates without registering a
|
|
135
|
+
dependency (e.g. pg_partman's child partitions)
|
|
136
|
+
--audit-extension-owned Audit relations owned by an installed extension too
|
|
137
|
+
(skipped by default — they are not yours to alter)
|
|
138
|
+
--roles <csv> Audit grants only for these roles (default: all)
|
|
139
|
+
--exclude-roles <csv> Skip grants for these roles
|
|
140
|
+
--format <fmt> "pretty" (default) | "json" | "json-pretty" | "markdown" | "sarif"
|
|
141
|
+
(markdown is for CI: a GitHub job summary or PR comment;
|
|
142
|
+
sarif uploads to GitHub code scanning)
|
|
143
|
+
--sarif-sources <dir> With --format sarif: scan <dir> for the CREATE TABLE /
|
|
144
|
+
CREATE POLICY that defines each object, so alerts point
|
|
145
|
+
at the SQL that produced the finding
|
|
146
|
+
--out <dir> Write safegres.json, safegres.md and safegres.sarif
|
|
147
|
+
into <dir> (created as needed)
|
|
148
|
+
--write-json <file> Also write the JSON report to <file>
|
|
149
|
+
--write-markdown <file> Also write the markdown report to <file>
|
|
150
|
+
--write-sarif <file> Also write the SARIF report to <file>
|
|
151
|
+
(one audit, as many outputs as CI needs)
|
|
152
|
+
--summary, -q Print only exposure, score, and severity counts (no findings)
|
|
153
|
+
--verbose Expand internal (non-exposed) advisories (listed as a count otherwise)
|
|
154
|
+
--fail-on <severity> Exit non-zero if any finding >= severity
|
|
155
|
+
(critical|high|medium|low|info; default: none)
|
|
156
|
+
--fail-on-score <n> Exit non-zero if the score is below n (0-100)
|
|
157
|
+
--fail-on-grade <g> Exit non-zero if the grade is below g (A+|A|B|C|D)
|
|
158
|
+
--report-only Evaluate and report every gate, then exit 0 anyway —
|
|
159
|
+
the way to run a gated config as an advisory job
|
|
160
|
+
--skip-ast Skip AST-level anti-pattern checks (faster)
|
|
161
|
+
--no-color Disable ANSI colors in pretty output
|
|
162
|
+
--help, -h Show this help message
|
|
163
|
+
|
|
164
|
+
GitHub Actions:
|
|
165
|
+
--github Write the job summary to $GITHUB_STEP_SUMMARY and
|
|
166
|
+
emit workflow annotations (auto-detected when
|
|
167
|
+
GITHUB_ACTIONS=true). Configure what appears with
|
|
168
|
+
report.github in the config file
|
|
169
|
+
--github-comment Upsert the sticky PR comment (needs GITHUB_TOKEN)
|
|
170
|
+
--write-github-comment <file>
|
|
171
|
+
Write the PR-comment markdown to <file> instead of
|
|
172
|
+
posting it
|
|
173
|
+
|
|
174
|
+
Everything a CI job repeats every run belongs in the config file instead, so
|
|
175
|
+
the job is just \`safegres audit\` (paths are relative to the config file):
|
|
176
|
+
|
|
177
|
+
{
|
|
178
|
+
"extends": "safegres:constructive",
|
|
179
|
+
"source": { "pgpm": "application/app" },
|
|
180
|
+
"callGraph": { "enabled": true },
|
|
181
|
+
"perf": { "enabled": true, "baseline": "ci/perf-baseline.json", "failOnNew": true },
|
|
182
|
+
"outputs": { "dir": "safegres-reports" },
|
|
183
|
+
"failOn": { "grade": "B" }
|
|
184
|
+
}
|
|
185
|
+
`;
|
|
186
|
+
export default async (argv, _prompter, _options) => {
|
|
187
|
+
if (argv.help || argv.h) {
|
|
188
|
+
process.stdout.write(usage);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// minimist parses `--no-color` as `color: false`.
|
|
192
|
+
const colorEnabled = argv.color !== false;
|
|
193
|
+
const pgpmCwd = typeof argv.pgpm === 'string' ? argv.pgpm : undefined;
|
|
194
|
+
const sealed = argv.sealed === true;
|
|
195
|
+
if (sealed) {
|
|
196
|
+
// Everything below is a way to change the score without changing the
|
|
197
|
+
// database. A sealed run refuses them outright rather than ignoring them:
|
|
198
|
+
// an evaluation that silently dropped the flag it was handed would be
|
|
199
|
+
// reporting a number for rules nobody chose.
|
|
200
|
+
const banned = [
|
|
201
|
+
'config',
|
|
202
|
+
'rule',
|
|
203
|
+
'baseline',
|
|
204
|
+
'write-baseline',
|
|
205
|
+
'perf-baseline',
|
|
206
|
+
'write-perf-baseline',
|
|
207
|
+
'exposure-schemas'
|
|
208
|
+
].filter((flag) => argv[flag] !== undefined);
|
|
209
|
+
if (banned.length > 0) {
|
|
210
|
+
log.error(`--sealed refuses local configuration: remove ${banned.map((f) => `--${f}`).join(', ')}`);
|
|
211
|
+
process.exit(2);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const loaded = loadConfig({ cwd: pgpmCwd, ...configParamsFromArgv(argv) });
|
|
215
|
+
const config = loaded.config;
|
|
216
|
+
const { pgpm: pgpmSource, usePgpm, perfBaseline, callGraphBaseline, outputs } = resolveRunPaths(argv, config, configPathBase(loaded));
|
|
217
|
+
const exposureSchemas = csvList(argv['exposure-schemas']);
|
|
218
|
+
const auditOptions = {
|
|
219
|
+
schemas: csvList(argv.schemas),
|
|
220
|
+
excludeSchemas: csvList(argv['exclude-schemas']),
|
|
221
|
+
extensions: extensionScopeFromArgv(argv, config),
|
|
222
|
+
includeRoles: csvList(argv.roles),
|
|
223
|
+
excludeRoles: csvList(argv['exclude-roles']),
|
|
224
|
+
skipAstChecks: argv['skip-ast'] === true,
|
|
225
|
+
perf: argv.perf === true
|
|
226
|
+
|| perfBaseline !== undefined
|
|
227
|
+
|| typeof argv['write-perf-baseline'] === 'string'
|
|
228
|
+
? true
|
|
229
|
+
: undefined,
|
|
230
|
+
stats: argv.stats === true ? true : undefined,
|
|
231
|
+
explain: argv.explain === true ? true : undefined,
|
|
232
|
+
callGraph: argv['call-graph'] === true
|
|
233
|
+
|| config.callGraph?.enabled === true
|
|
234
|
+
|| callGraphBaseline !== undefined
|
|
235
|
+
|| typeof argv['write-baseline'] === 'string',
|
|
236
|
+
exposure: exposureSchemas
|
|
237
|
+
? { ...config.exposure, schemas: exposureSchemas }
|
|
238
|
+
: undefined,
|
|
239
|
+
config,
|
|
240
|
+
sealed,
|
|
241
|
+
...(typeof argv.preset === 'string' ? { preset: argv.preset } : {})
|
|
242
|
+
};
|
|
243
|
+
let report;
|
|
244
|
+
if (usePgpm) {
|
|
245
|
+
const { auditPgpmWorkspace } = importPgpmTest();
|
|
246
|
+
report = await auditPgpmWorkspace({ ...auditOptions, cwd: pgpmSource });
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const client = buildClient(argv);
|
|
250
|
+
await client.connect();
|
|
251
|
+
try {
|
|
252
|
+
report = await audit(client, auditOptions);
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
await client.end();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (typeof argv['write-baseline'] === 'string' && report.callGraph) {
|
|
259
|
+
fs.writeFileSync(argv['write-baseline'], serializeBaseline(toBaseline(report.callGraph)));
|
|
260
|
+
log.info(`wrote call-graph baseline: ${argv['write-baseline']}`);
|
|
261
|
+
}
|
|
262
|
+
if (typeof argv['write-perf-baseline'] === 'string' && report.perf) {
|
|
263
|
+
fs.writeFileSync(argv['write-perf-baseline'], serializePerfBaseline(toPerfBaseline(report.perf.findings)));
|
|
264
|
+
log.info(`wrote perf baseline: ${argv['write-perf-baseline']}`);
|
|
265
|
+
}
|
|
266
|
+
if (perfBaseline !== undefined && report.perf) {
|
|
267
|
+
let raw;
|
|
268
|
+
try {
|
|
269
|
+
raw = fs.readFileSync(perfBaseline, 'utf8');
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
log.error(`cannot read perf baseline: ${perfBaseline} (create one with --write-perf-baseline)`);
|
|
273
|
+
process.exit(2);
|
|
274
|
+
}
|
|
275
|
+
report.perf.diff = diffPerf(report.perf.findings, parsePerfBaseline(raw));
|
|
276
|
+
}
|
|
277
|
+
if (outputs.snapshot !== undefined) {
|
|
278
|
+
writeOut(outputs.snapshot, serializeSnapshot(toSnapshot(report, typeof argv['compare-ref'] === 'string' ? { ref: argv['compare-ref'] } : {})));
|
|
279
|
+
log.info(`wrote snapshot: ${outputs.snapshot}`);
|
|
280
|
+
}
|
|
281
|
+
if (typeof argv.compare === 'string') {
|
|
282
|
+
let raw;
|
|
283
|
+
try {
|
|
284
|
+
raw = fs.readFileSync(argv.compare, 'utf8');
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
log.error(`cannot read --compare file: ${argv.compare} `
|
|
288
|
+
+ '(any earlier `--format json` report or `--write-snapshot` output works)');
|
|
289
|
+
process.exit(2);
|
|
290
|
+
}
|
|
291
|
+
let previous;
|
|
292
|
+
try {
|
|
293
|
+
previous = parseSnapshot(raw);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
log.error(`--compare file is not a safegres report: ${err.message}`);
|
|
297
|
+
process.exit(2);
|
|
298
|
+
}
|
|
299
|
+
if (typeof argv['compare-ref'] === 'string')
|
|
300
|
+
previous.ref = argv['compare-ref'];
|
|
301
|
+
const provenance = baselineProvenance(argv);
|
|
302
|
+
if (provenance)
|
|
303
|
+
previous.provenance = { ...previous.provenance, ...provenance };
|
|
304
|
+
report.comparison = compareReports(previous, report);
|
|
305
|
+
}
|
|
306
|
+
else if (typeof argv['compare-skipped'] === 'string' && argv['compare-skipped'] !== '') {
|
|
307
|
+
report.comparisonSkipped = argv['compare-skipped'];
|
|
308
|
+
}
|
|
309
|
+
if (callGraphBaseline !== undefined && report.callGraph) {
|
|
310
|
+
let raw;
|
|
311
|
+
try {
|
|
312
|
+
raw = fs.readFileSync(callGraphBaseline, 'utf8');
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
log.error(`cannot read call-graph baseline: ${callGraphBaseline} (create one with --write-baseline)`);
|
|
316
|
+
process.exit(2);
|
|
317
|
+
}
|
|
318
|
+
report.callGraphDiff = diffCallGraph(report.callGraph, parseBaseline(raw));
|
|
319
|
+
}
|
|
320
|
+
if (argv['exposed-only'] === true) {
|
|
321
|
+
report.findings = report.findings.filter((f) => f.exposed !== false);
|
|
322
|
+
report.summary = summarize(report.findings);
|
|
323
|
+
}
|
|
324
|
+
const planeSelectors = listArg(argv.plane);
|
|
325
|
+
const viewConfig = {
|
|
326
|
+
...viewConfigFromReportConfig(config.report),
|
|
327
|
+
...(planeSelectors.length > 0 ? { planes: planeSelectors } : {}),
|
|
328
|
+
...(argv['exposed-only'] === true ? { exposedOnly: true } : {})
|
|
329
|
+
};
|
|
330
|
+
const fmt = typeof argv.format === 'string' ? argv.format : 'pretty';
|
|
331
|
+
let output;
|
|
332
|
+
switch (fmt) {
|
|
333
|
+
case 'json':
|
|
334
|
+
output = renderJson(report);
|
|
335
|
+
break;
|
|
336
|
+
case 'json-pretty':
|
|
337
|
+
output = renderJson(report, { pretty: true });
|
|
338
|
+
break;
|
|
339
|
+
case 'sarif':
|
|
340
|
+
output = renderSarif(report, {
|
|
341
|
+
sources: outputs.sarifSources ? buildSourceIndex(outputs.sarifSources) : undefined
|
|
342
|
+
});
|
|
343
|
+
break;
|
|
344
|
+
case 'markdown':
|
|
345
|
+
case 'md':
|
|
346
|
+
output = renderMarkdown(report, {
|
|
347
|
+
summary: argv.summary === true,
|
|
348
|
+
verbose: argv.verbose === true,
|
|
349
|
+
view: viewConfig
|
|
350
|
+
});
|
|
351
|
+
break;
|
|
352
|
+
case 'pretty':
|
|
353
|
+
output = renderPretty(report, {
|
|
354
|
+
color: colorEnabled,
|
|
355
|
+
summary: argv.summary === true,
|
|
356
|
+
verbose: argv.verbose === true,
|
|
357
|
+
view: viewConfig
|
|
358
|
+
});
|
|
359
|
+
break;
|
|
360
|
+
default:
|
|
361
|
+
log.error(`Unknown --format: ${fmt}`);
|
|
362
|
+
process.exit(2);
|
|
363
|
+
}
|
|
364
|
+
process.stdout.write(output);
|
|
365
|
+
process.stdout.write('\n');
|
|
366
|
+
// One audit, as many renderings as CI asked for.
|
|
367
|
+
if (outputs.json !== undefined) {
|
|
368
|
+
writeOut(outputs.json, `${renderJson(report, { pretty: true })}\n`);
|
|
369
|
+
log.info(`wrote json report: ${outputs.json}`);
|
|
370
|
+
}
|
|
371
|
+
if (outputs.markdown !== undefined) {
|
|
372
|
+
writeOut(outputs.markdown, `${renderMarkdown(report, { verbose: argv.verbose === true, view: viewConfig })}\n`);
|
|
373
|
+
log.info(`wrote markdown report: ${outputs.markdown}`);
|
|
374
|
+
}
|
|
375
|
+
if (outputs.sarif !== undefined) {
|
|
376
|
+
writeOut(outputs.sarif, `${renderSarif(report, {
|
|
377
|
+
sources: outputs.sarifSources ? buildSourceIndex(outputs.sarifSources) : undefined
|
|
378
|
+
})}\n`);
|
|
379
|
+
log.info(`wrote sarif report: ${outputs.sarif}`);
|
|
380
|
+
}
|
|
381
|
+
// Gates are evaluated before anything exits, so the GitHub renderers can
|
|
382
|
+
// annotate exactly the findings that failed the build.
|
|
383
|
+
let failed = false;
|
|
384
|
+
const gateFailures = [];
|
|
385
|
+
const fail = (message) => {
|
|
386
|
+
log.error(message);
|
|
387
|
+
failed = true;
|
|
388
|
+
};
|
|
389
|
+
// Checked first: if the ruler is wrong, nothing measured with it is worth
|
|
390
|
+
// gating on.
|
|
391
|
+
const expected = argv['verify-fingerprint'];
|
|
392
|
+
if (typeof expected === 'string' && report.provenance?.fingerprint !== expected) {
|
|
393
|
+
fail(`configuration fingerprint ${report.provenance?.fingerprint ?? 'unknown'} `
|
|
394
|
+
+ `does not match --verify-fingerprint ${expected}`);
|
|
395
|
+
}
|
|
396
|
+
const failOnSeverity = typeof argv['fail-on'] === 'string' ? argv['fail-on'] : config.failOn?.severity;
|
|
397
|
+
if (failOnSeverity) {
|
|
398
|
+
if (!(failOnSeverity in SEVERITY_ORDER)) {
|
|
399
|
+
log.error(`Unknown --fail-on severity: ${failOnSeverity}`);
|
|
400
|
+
process.exit(2);
|
|
401
|
+
}
|
|
402
|
+
const over = report.findings.filter((f) => meetsThreshold(f.severity, failOnSeverity));
|
|
403
|
+
if (over.length > 0) {
|
|
404
|
+
gateFailures.push(...over);
|
|
405
|
+
fail(`${over.length} finding(s) at or above --fail-on ${failOnSeverity}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const failOnScore = typeof argv['fail-on-score'] === 'number' ? argv['fail-on-score'] : config.failOn?.score;
|
|
409
|
+
if (failOnScore != null && report.score && report.score.value < failOnScore) {
|
|
410
|
+
fail(`score ${report.score.value} is below --fail-on-score ${failOnScore}`);
|
|
411
|
+
}
|
|
412
|
+
const failOnGrade = typeof argv['fail-on-grade'] === 'string' ? argv['fail-on-grade'] : config.failOn?.grade;
|
|
413
|
+
if (failOnGrade && report.score && !meetsGrade(report.score.grade, failOnGrade)) {
|
|
414
|
+
fail(`grade ${report.score.grade} is below --fail-on-grade ${failOnGrade}`);
|
|
415
|
+
}
|
|
416
|
+
const failOnPerfScore = typeof argv['fail-on-perf-score'] === 'number'
|
|
417
|
+
? argv['fail-on-perf-score']
|
|
418
|
+
: config.failOn?.perfScore;
|
|
419
|
+
if (failOnPerfScore != null && report.perf && report.perf.score.value < failOnPerfScore) {
|
|
420
|
+
fail(`perf score ${report.perf.score.value} is below --fail-on-perf-score ${failOnPerfScore}`);
|
|
421
|
+
}
|
|
422
|
+
const failOnPerfGrade = typeof argv['fail-on-perf-grade'] === 'string'
|
|
423
|
+
? argv['fail-on-perf-grade']
|
|
424
|
+
: config.failOn?.perfGrade;
|
|
425
|
+
if (failOnPerfGrade && report.perf && !meetsGrade(report.perf.score.grade, failOnPerfGrade)) {
|
|
426
|
+
fail(`perf grade ${report.perf.score.grade} is below --fail-on-perf-grade ${failOnPerfGrade}`);
|
|
427
|
+
}
|
|
428
|
+
// Per-plane gates. A secondary plane gates nothing unless the config asks:
|
|
429
|
+
// the direct-connection surface is legitimately worse than the API's, and a
|
|
430
|
+
// parity gate would only get the plane deleted.
|
|
431
|
+
for (const [pattern, gate] of Object.entries(config.failOn?.planes ?? {})) {
|
|
432
|
+
const planes = (report.planes ?? []).filter((p) => !p.skipped && matchPlane(pattern, p.name));
|
|
433
|
+
for (const plane of planes) {
|
|
434
|
+
if (gate.score != null && plane.score.value < gate.score) {
|
|
435
|
+
fail(`plane ${plane.name} score ${plane.score.value} is below failOn.planes["${pattern}"].score ${gate.score}`);
|
|
436
|
+
}
|
|
437
|
+
if (gate.grade && !meetsGrade(plane.score.grade, gate.grade)) {
|
|
438
|
+
fail(`plane ${plane.name} grade ${plane.score.grade} is below failOn.planes["${pattern}"].grade ${gate.grade}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Per-scorecard gates. The point of a scorecard is that a team gates on
|
|
443
|
+
// its own question, so this is the gate that matters to them and the
|
|
444
|
+
// headline is someone else's number.
|
|
445
|
+
for (const [pattern, gate] of Object.entries(config.failOn?.scorecards ?? {})) {
|
|
446
|
+
const cards = (report.scorecards ?? []).filter((c) => matchPlane(pattern, c.name));
|
|
447
|
+
for (const card of cards) {
|
|
448
|
+
if (gate.score != null && card.score.value < gate.score) {
|
|
449
|
+
fail(`scorecard ${card.name} score ${card.score.value} is below failOn.scorecards["${pattern}"].score ${gate.score}`);
|
|
450
|
+
}
|
|
451
|
+
if (gate.grade && !meetsGrade(card.score.grade, gate.grade)) {
|
|
452
|
+
fail(`scorecard ${card.name} grade ${card.score.grade} is below failOn.scorecards["${pattern}"].grade ${gate.grade}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const failOnNewPerf = argv['fail-on-new-perf'] === true || config.perf?.failOnNew === true;
|
|
457
|
+
if (failOnNewPerf && report.perf?.diff && report.perf.diff.added.length > 0) {
|
|
458
|
+
const count = report.perf.diff.added.length;
|
|
459
|
+
gateFailures.push(...report.perf.diff.added);
|
|
460
|
+
fail(`${count} new performance finding${count === 1 ? '' : 's'} since the perf baseline — fix them, or re-baseline with --write-perf-baseline to accept`);
|
|
461
|
+
}
|
|
462
|
+
if ((argv['fail-on-new-boundaries'] === true || config.callGraph?.failOnNew === true)
|
|
463
|
+
&& report.callGraphDiff
|
|
464
|
+
&& report.callGraphDiff.added.length > 0) {
|
|
465
|
+
fail(`${report.callGraphDiff.added.length} new trust boundar${report.callGraphDiff.added.length === 1 ? 'y' : 'ies'} since the baseline — review and re-baseline to accept`);
|
|
466
|
+
}
|
|
467
|
+
// GitHub output: on by default inside Actions, because a report nobody has
|
|
468
|
+
// to wire up is a report people actually read.
|
|
469
|
+
const githubConfig = config.report?.github;
|
|
470
|
+
const githubMode = argv.github === true || (argv.github !== false && process.env.GITHUB_ACTIONS === 'true');
|
|
471
|
+
const githubOptions = {
|
|
472
|
+
...(githubConfig ? { config: githubConfig } : {}),
|
|
473
|
+
gateFailures,
|
|
474
|
+
view: viewConfig
|
|
475
|
+
};
|
|
476
|
+
if (githubMode) {
|
|
477
|
+
if (!emitGithub(report, githubOptions)) {
|
|
478
|
+
log.warn('--github: no $GITHUB_STEP_SUMMARY in the environment — summary not written');
|
|
479
|
+
process.stdout.write(`${renderGithubSummary(report, githubOptions)}\n`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (outputs.githubComment !== undefined) {
|
|
483
|
+
writeOut(outputs.githubComment, `${renderGithubComment(report, githubOptions)}\n`);
|
|
484
|
+
log.info(`wrote github comment: ${outputs.githubComment}`);
|
|
485
|
+
}
|
|
486
|
+
if (argv['github-comment'] === true) {
|
|
487
|
+
const result = await postStickyComment(renderGithubComment(report, githubOptions));
|
|
488
|
+
if (result.posted)
|
|
489
|
+
log.info('updated the safegres PR comment');
|
|
490
|
+
else
|
|
491
|
+
log.warn(`--github-comment: not posted — ${result.reason}`);
|
|
492
|
+
}
|
|
493
|
+
if (failed && argv['report-only'] !== true)
|
|
494
|
+
process.exit(1);
|
|
495
|
+
if (failed)
|
|
496
|
+
log.warn('--report-only: gates failed, exiting 0');
|
|
497
|
+
};
|
|
498
|
+
/**
|
|
499
|
+
* Where the baseline came from, as the provider reported it. A provider is
|
|
500
|
+
* whatever chose the file — a CI adapter, a script, a person — so every field
|
|
501
|
+
* is optional and none of them mean anything to safegres beyond being printed.
|
|
502
|
+
*/
|
|
503
|
+
function baselineProvenance(argv) {
|
|
504
|
+
const str = (flag) => typeof argv[flag] === 'string' && argv[flag] !== '' ? argv[flag] : undefined;
|
|
505
|
+
const provenance = {
|
|
506
|
+
...(str('compare-sha') && { sha: str('compare-sha') }),
|
|
507
|
+
...(str('compare-run-id') && { runId: str('compare-run-id') }),
|
|
508
|
+
...(str('compare-run-url') && { runUrl: str('compare-run-url') }),
|
|
509
|
+
...(str('compare-age') && { age: str('compare-age') })
|
|
510
|
+
};
|
|
511
|
+
return Object.keys(provenance).length > 0 ? provenance : undefined;
|
|
512
|
+
}
|
|
513
|
+
/** Write an output file, creating its directory: CI should not have to mkdir. */
|
|
514
|
+
function writeOut(file, contents) {
|
|
515
|
+
fs.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
516
|
+
fs.writeFileSync(file, contents);
|
|
517
|
+
}
|
|
518
|
+
/** A repeatable flag: `--plane a --plane b` (minimist gives an array). */
|
|
519
|
+
function listArg(value) {
|
|
520
|
+
if (typeof value === 'string')
|
|
521
|
+
return [value];
|
|
522
|
+
if (Array.isArray(value))
|
|
523
|
+
return value.filter((v) => typeof v === 'string');
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Logger } from '@pgpmjs/logger';
|
|
2
|
+
import { appendFileSync } from 'fs';
|
|
3
|
+
import { CANDIDATE_LIMIT, MAX_BASELINE_AGE_MS } from '../ci/baseline';
|
|
4
|
+
import { selectGithubBaseline } from '../ci/github';
|
|
5
|
+
const log = new Logger('safegres');
|
|
6
|
+
const usage = `
|
|
7
|
+
safegres baseline — choose the report a delta is measured against
|
|
8
|
+
|
|
9
|
+
safegres baseline --provider github [OPTIONS]
|
|
10
|
+
|
|
11
|
+
Finds the base branch's most recent run that is a sane comparison — succeeded,
|
|
12
|
+
young enough, and audited a commit in this branch's merge-base history — and
|
|
13
|
+
downloads its report. When nothing qualifies it says why and exits 0: the audit
|
|
14
|
+
then runs with no deltas, and the absolute gates still apply.
|
|
15
|
+
|
|
16
|
+
The result is written to $GITHUB_OUTPUT when the runner provides one, as
|
|
17
|
+
compare / compare-ref / compare-sha / compare-run-id / compare-run-url /
|
|
18
|
+
compare-age / compare-skipped — the same names \`safegres audit\` takes as
|
|
19
|
+
flags, so the next step is \`--compare "\${{ steps.<id>.outputs.compare }}"\`.
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--provider <name> Where the runs live. Only "github" today
|
|
23
|
+
--repo <owner/repo> Default $GITHUB_REPOSITORY
|
|
24
|
+
--base <ref> Base branch whose runs are candidates
|
|
25
|
+
(default: the pull request's base)
|
|
26
|
+
--head <sha> Branch tip to take the merge base of
|
|
27
|
+
(default: the pull request's head)
|
|
28
|
+
--workflow <file> Workflow file that produced the reports
|
|
29
|
+
(default: the workflow this runs in)
|
|
30
|
+
--artifact <name> Artifact holding safegres.json
|
|
31
|
+
(default: safegres-reports)
|
|
32
|
+
--dir <dir> Where to unpack it (default: previous)
|
|
33
|
+
--max-age-hours <n> Staleness limit (default: ${MAX_BASELINE_AGE_MS / 3600000})
|
|
34
|
+
--candidates <n> Runs to consider (default: ${CANDIDATE_LIMIT})
|
|
35
|
+
--json Print the selection as JSON
|
|
36
|
+
--help, -h Show this help message
|
|
37
|
+
|
|
38
|
+
Needs \`permissions: actions: read\` to list runs and download artifacts.
|
|
39
|
+
`;
|
|
40
|
+
/** `key=value` into a GitHub Actions file, or stdout when running locally. */
|
|
41
|
+
function emit(file, key, value) {
|
|
42
|
+
const line = `${key}=${value}`;
|
|
43
|
+
if (file)
|
|
44
|
+
appendFileSync(file, `${line}\n`);
|
|
45
|
+
else
|
|
46
|
+
process.stdout.write(`(${key}) ${value}\n`);
|
|
47
|
+
}
|
|
48
|
+
function publish(result) {
|
|
49
|
+
const file = process.env.GITHUB_OUTPUT;
|
|
50
|
+
if (result.chosen) {
|
|
51
|
+
emit(file, 'compare', result.chosen.path);
|
|
52
|
+
emit(file, 'compare-ref', result.base);
|
|
53
|
+
emit(file, 'compare-sha', result.chosen.headSha);
|
|
54
|
+
emit(file, 'compare-run-id', result.chosen.runId);
|
|
55
|
+
if (result.runUrl)
|
|
56
|
+
emit(file, 'compare-run-url', result.runUrl);
|
|
57
|
+
if (result.age)
|
|
58
|
+
emit(file, 'compare-age', result.age);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
emit(file, 'compare-skipped', result.reason ?? 'no baseline');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export default async (argv, _prompter, _options) => {
|
|
65
|
+
if (argv.help || argv.h) {
|
|
66
|
+
process.stdout.write(usage);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const provider = typeof argv.provider === 'string' ? argv.provider : 'github';
|
|
70
|
+
if (provider !== 'github') {
|
|
71
|
+
log.error(`unknown --provider ${provider} (only "github" is implemented)`);
|
|
72
|
+
process.exit(2);
|
|
73
|
+
}
|
|
74
|
+
const hours = Number(argv['max-age-hours']);
|
|
75
|
+
const candidates = Number(argv.candidates);
|
|
76
|
+
let result;
|
|
77
|
+
try {
|
|
78
|
+
result = selectGithubBaseline({
|
|
79
|
+
...(typeof argv.repo === 'string' && { repo: argv.repo }),
|
|
80
|
+
...(typeof argv.base === 'string' && { base: argv.base }),
|
|
81
|
+
...(typeof argv.head === 'string' && { headSha: argv.head }),
|
|
82
|
+
...(typeof argv.workflow === 'string' && { workflow: argv.workflow }),
|
|
83
|
+
...(typeof argv.artifact === 'string' && { artifact: argv.artifact }),
|
|
84
|
+
...(typeof argv.dir === 'string' && { dir: argv.dir }),
|
|
85
|
+
...(Number.isFinite(hours) && hours > 0 && { maxAgeMs: hours * 3600000 }),
|
|
86
|
+
...(Number.isFinite(candidates) && candidates > 0 && { candidateLimit: candidates })
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// Fail-soft, loudly: a missing PR context, a token without `actions: read`
|
|
91
|
+
// or a `gh` that is not installed must not fail an audit that is perfectly
|
|
92
|
+
// able to run without deltas — but the reason travels into the report as
|
|
93
|
+
// the skip message rather than being swallowed.
|
|
94
|
+
const message = err.message.trim();
|
|
95
|
+
log.error(`no comparison baseline: ${message}`);
|
|
96
|
+
publish({ chosen: null, rejected: [], reason: message, base: '', mergeBaseSha: '' });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
publish(result);
|
|
100
|
+
if (argv.json === true)
|
|
101
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
102
|
+
};
|