@safegres/test 0.0.1 → 1.30.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +825 -412
- package/adapters.d.ts +8 -0
- package/adapters.js +18 -0
- package/ast/helpers.d.ts +16 -0
- package/ast/helpers.js +40 -0
- package/ast/parse.d.ts +31 -0
- package/ast/parse.js +87 -0
- package/ast/walk.d.ts +27 -0
- package/ast/walk.js +79 -0
- package/callgraph/baseline.d.ts +28 -0
- package/callgraph/baseline.js +55 -0
- package/callgraph/extract.d.ts +93 -0
- package/callgraph/extract.js +340 -0
- package/callgraph/graph.d.ts +82 -0
- package/callgraph/graph.js +276 -0
- package/checks/anti-patterns.d.ts +53 -0
- package/checks/anti-patterns.js +247 -0
- package/checks/column-grants.d.ts +20 -0
- package/checks/column-grants.js +74 -0
- package/checks/coverage.d.ts +43 -0
- package/checks/coverage.js +110 -0
- package/checks/definer-function.d.ts +91 -0
- package/checks/definer-function.js +458 -0
- package/checks/definer-view.d.ts +167 -0
- package/checks/definer-view.js +392 -0
- package/checks/indexes.d.ts +82 -0
- package/checks/indexes.js +263 -0
- package/checks/lattice.d.ts +179 -0
- package/checks/lattice.js +487 -0
- package/checks/object-acls.d.ts +32 -0
- package/checks/object-acls.js +129 -0
- package/checks/policy-index.d.ts +78 -0
- package/checks/policy-index.js +349 -0
- package/checks/revocable-grants.d.ts +140 -0
- package/checks/revocable-grants.js +377 -0
- package/checks/rls-flags.d.ts +25 -0
- package/checks/rls-flags.js +82 -0
- package/checks/role-reach.d.ts +329 -0
- package/checks/role-reach.js +277 -0
- package/checks/role-trust.d.ts +45 -0
- package/checks/role-trust.js +91 -0
- package/checks/set-role.d.ts +21 -0
- package/checks/set-role.js +66 -0
- package/checks/stats.d.ts +52 -0
- package/checks/stats.js +192 -0
- package/checks/view-exposure.d.ts +68 -0
- package/checks/view-exposure.js +236 -0
- package/checks/view-writes.d.ts +88 -0
- package/checks/view-writes.js +350 -0
- package/ci/baseline.d.ts +104 -0
- package/ci/baseline.js +111 -0
- package/ci/github.d.ts +47 -0
- package/ci/github.js +197 -0
- package/cli/audit.d.ts +3 -0
- package/cli/audit.js +560 -0
- package/cli/baseline.d.ts +3 -0
- package/cli/baseline.js +104 -0
- package/cli/commands.d.ts +2 -0
- package/cli/commands.js +60 -0
- package/cli/doctor.d.ts +3 -0
- package/cli/doctor.js +72 -0
- package/cli/eval.d.ts +3 -0
- package/cli/eval.js +153 -0
- package/cli/print-config.d.ts +3 -0
- package/cli/print-config.js +54 -0
- package/cli/shared.d.ts +43 -0
- package/cli/shared.js +153 -0
- package/cli.d.ts +2 -0
- package/cli.js +69 -0
- package/commands/audit.d.ts +65 -0
- package/commands/audit.js +779 -0
- package/commands/doctor.d.ts +19 -0
- package/commands/doctor.js +181 -0
- package/commands/eval.d.ts +67 -0
- package/commands/eval.js +93 -0
- package/config/fingerprint.d.ts +40 -0
- package/config/fingerprint.js +71 -0
- package/config/loader.d.ts +44 -0
- package/config/loader.js +210 -0
- package/config/presets.d.ts +95 -0
- package/config/presets.js +327 -0
- package/config/resolve.d.ts +47 -0
- package/config/resolve.js +192 -0
- package/config/schema.d.ts +54 -0
- package/config/schema.js +269 -0
- package/config/types.d.ts +562 -0
- package/config/validate.d.ts +14 -0
- package/config/validate.js +125 -0
- package/corpus/README.md +83 -0
- package/corpus/bootstrap.sql +14 -0
- package/corpus/cases/01-anon-write-grant/case.json +26 -0
- package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
- package/corpus/cases/02-grant-to-public/case.json +25 -0
- package/corpus/cases/02-grant-to-public/schema.sql +18 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
- package/corpus/cases/04-permissive-write-policy/case.json +30 -0
- package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
- package/corpus/cases/05-public-read-policy/case.json +25 -0
- package/corpus/cases/05-public-read-policy/schema.sql +16 -0
- package/corpus/cases/06-update-without-with-check/case.json +25 -0
- package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
- package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
- package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
- package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
- package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
- package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
- package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
- package/corpus/cases/10-rls-not-forced/case.json +24 -0
- package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
- package/corpus/cases/11-session-user-in-policy/case.json +24 -0
- package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
- package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
- package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
- package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
- package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
- package/corpus/cases/14-dead-policy/case.json +24 -0
- package/corpus/cases/14-dead-policy/schema.sql +18 -0
- package/corpus/cases/15-unreachable-grant/case.json +24 -0
- package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
- package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
- package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
- package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
- package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
- package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
- package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
- package/corpus/cases/19-policy-column-cast/case.json +24 -0
- package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
- package/corpus/cases/20-stable-function-per-row/case.json +24 -0
- package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
- package/corpus/cases/21-table-without-primary-key/case.json +24 -0
- package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
- package/corpus/cases/22-redundant-index/case.json +24 -0
- package/corpus/cases/22-redundant-index/schema.sql +16 -0
- package/corpus/cases/23-tsvector-without-index/case.json +24 -0
- package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
- package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
- package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
- package/corpus/cases/25-definer-view-bypass/case.json +31 -0
- package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
- package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
- package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
- package/corpus/cases/27-definer-view-write/case.json +36 -0
- package/corpus/cases/27-definer-view-write/schema.sql +40 -0
- package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
- package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
- package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
- package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
- package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
- package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
- package/corpus/cases/31-matview-snapshot/case.json +32 -0
- package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
- package/corpus/cases/33-leaky-filter-view/case.json +36 -0
- package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
- package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
- package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
- package/corpus/cases/35-column-grant-projection/case.json +27 -0
- package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
- package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
- package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
- package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
- package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
- package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
- package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
- package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
- package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
- package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
- package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
- package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
- package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
- package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
- package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
- package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
- package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
- package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
- package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
- package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
- package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
- package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
- package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
- package/corpus/cases/41-unreadable-view-body/case.json +33 -0
- package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
- package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
- package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
- package/corpus/cases/42-search-column-indexed/case.json +21 -0
- package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
- package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
- package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
- package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
- package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
- package/corpus/cases/45-anon-foreign-table/case.json +27 -0
- package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
- package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
- package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
- package/corpus/cases/47-checked-writable-view/case.json +37 -0
- package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
- package/corpus/cases/48-definer-function-reach/case.json +38 -0
- package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
- package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
- package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
- package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
- package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
- package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
- package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
- package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
- package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
- package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
- package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
- package/corpus/index.d.ts +79 -0
- package/corpus/index.js +125 -0
- package/esm/adapters.d.ts +8 -0
- package/esm/adapters.js +6 -0
- package/esm/ast/helpers.d.ts +16 -0
- package/esm/ast/helpers.js +36 -0
- package/esm/ast/parse.d.ts +31 -0
- package/esm/ast/parse.js +82 -0
- package/esm/ast/walk.d.ts +27 -0
- package/esm/ast/walk.js +72 -0
- package/esm/callgraph/baseline.d.ts +28 -0
- package/esm/callgraph/baseline.js +48 -0
- package/esm/callgraph/extract.d.ts +93 -0
- package/esm/callgraph/extract.js +333 -0
- package/esm/callgraph/graph.d.ts +82 -0
- package/esm/callgraph/graph.js +273 -0
- package/esm/checks/anti-patterns.d.ts +53 -0
- package/esm/checks/anti-patterns.js +240 -0
- package/esm/checks/column-grants.d.ts +20 -0
- package/esm/checks/column-grants.js +71 -0
- package/esm/checks/coverage.d.ts +43 -0
- package/esm/checks/coverage.js +104 -0
- package/esm/checks/definer-function.d.ts +91 -0
- package/esm/checks/definer-function.js +452 -0
- package/esm/checks/definer-view.d.ts +167 -0
- package/esm/checks/definer-view.js +382 -0
- package/esm/checks/indexes.d.ts +82 -0
- package/esm/checks/indexes.js +256 -0
- package/esm/checks/lattice.d.ts +179 -0
- package/esm/checks/lattice.js +475 -0
- package/esm/checks/object-acls.d.ts +32 -0
- package/esm/checks/object-acls.js +125 -0
- package/esm/checks/policy-index.d.ts +78 -0
- package/esm/checks/policy-index.js +342 -0
- package/esm/checks/revocable-grants.d.ts +140 -0
- package/esm/checks/revocable-grants.js +373 -0
- package/esm/checks/rls-flags.d.ts +25 -0
- package/esm/checks/rls-flags.js +77 -0
- package/esm/checks/role-reach.d.ts +329 -0
- package/esm/checks/role-reach.js +270 -0
- package/esm/checks/role-trust.d.ts +45 -0
- package/esm/checks/role-trust.js +86 -0
- package/esm/checks/set-role.d.ts +21 -0
- package/esm/checks/set-role.js +63 -0
- package/esm/checks/stats.d.ts +52 -0
- package/esm/checks/stats.js +184 -0
- package/esm/checks/view-exposure.d.ts +68 -0
- package/esm/checks/view-exposure.js +231 -0
- package/esm/checks/view-writes.d.ts +88 -0
- package/esm/checks/view-writes.js +344 -0
- package/esm/ci/baseline.d.ts +104 -0
- package/esm/ci/baseline.js +107 -0
- package/esm/ci/github.d.ts +47 -0
- package/esm/ci/github.js +161 -0
- package/esm/cli/audit.d.ts +3 -0
- package/esm/cli/audit.js +525 -0
- package/esm/cli/baseline.d.ts +3 -0
- package/esm/cli/baseline.js +102 -0
- package/esm/cli/commands.d.ts +2 -0
- package/esm/cli/commands.js +53 -0
- package/esm/cli/doctor.d.ts +3 -0
- package/esm/cli/doctor.js +67 -0
- package/esm/cli/eval.d.ts +3 -0
- package/esm/cli/eval.js +115 -0
- package/esm/cli/print-config.d.ts +3 -0
- package/esm/cli/print-config.js +52 -0
- package/esm/cli/shared.d.ts +43 -0
- package/esm/cli/shared.js +112 -0
- package/esm/cli.d.ts +2 -0
- package/esm/cli.js +67 -0
- package/esm/commands/audit.d.ts +65 -0
- package/esm/commands/audit.js +776 -0
- package/esm/commands/doctor.d.ts +19 -0
- package/esm/commands/doctor.js +178 -0
- package/esm/commands/eval.d.ts +67 -0
- package/esm/commands/eval.js +90 -0
- package/esm/config/fingerprint.d.ts +40 -0
- package/esm/config/fingerprint.js +68 -0
- package/esm/config/loader.d.ts +44 -0
- package/esm/config/loader.js +172 -0
- package/esm/config/presets.d.ts +95 -0
- package/esm/config/presets.js +324 -0
- package/esm/config/resolve.d.ts +47 -0
- package/esm/config/resolve.js +180 -0
- package/esm/config/schema.d.ts +54 -0
- package/esm/config/schema.js +265 -0
- package/esm/config/types.d.ts +562 -0
- package/esm/config/validate.d.ts +14 -0
- package/esm/config/validate.js +122 -0
- package/esm/corpus/index.d.ts +79 -0
- package/esm/corpus/index.js +85 -0
- package/esm/exposure/adapters.d.ts +152 -0
- package/esm/exposure/adapters.js +463 -0
- package/esm/exposure/planes.d.ts +59 -0
- package/esm/exposure/planes.js +144 -0
- package/esm/exposure/reach.d.ts +78 -0
- package/esm/exposure/reach.js +105 -0
- package/esm/exposure/routines.d.ts +36 -0
- package/esm/exposure/routines.js +54 -0
- package/esm/index.d.ts +96 -0
- package/esm/index.js +57 -6
- package/esm/lint/index.d.ts +11 -0
- package/esm/lint/index.js +10 -0
- package/esm/perf/baseline.d.ts +40 -0
- package/esm/perf/baseline.js +83 -0
- package/esm/perf/explain.d.ts +46 -0
- package/esm/perf/explain.js +201 -0
- package/esm/pg/acl.d.ts +72 -0
- package/esm/pg/acl.js +186 -0
- package/esm/pg/behaviors.d.ts +114 -0
- package/esm/pg/behaviors.js +206 -0
- package/esm/pg/exposure.d.ts +74 -0
- package/esm/pg/exposure.js +198 -0
- package/esm/pg/functions.d.ts +52 -0
- package/esm/pg/functions.js +128 -0
- package/esm/pg/indexes.d.ts +192 -0
- package/esm/pg/indexes.js +395 -0
- package/esm/pg/introspect.d.ts +134 -0
- package/esm/pg/introspect.js +274 -0
- package/esm/pg/objects.d.ts +37 -0
- package/esm/pg/objects.js +108 -0
- package/esm/pg/paths.d.ts +114 -0
- package/esm/pg/paths.js +159 -0
- package/esm/pg/proc.d.ts +23 -0
- package/esm/pg/proc.js +55 -0
- package/esm/pg/reach-inputs.d.ts +47 -0
- package/esm/pg/reach-inputs.js +133 -0
- package/esm/pg/roles.d.ts +28 -0
- package/esm/pg/roles.js +50 -0
- package/esm/pg/stats.d.ts +65 -0
- package/esm/pg/stats.js +129 -0
- package/esm/pgpm-test.d.ts +22 -0
- package/esm/pgpm-test.js +39 -0
- package/esm/report/callgraph.d.ts +9 -0
- package/esm/report/callgraph.js +74 -0
- package/esm/report/compare.d.ts +143 -0
- package/esm/report/compare.js +195 -0
- package/esm/report/github.d.ts +74 -0
- package/esm/report/github.js +290 -0
- package/esm/report/json.d.ts +4 -0
- package/esm/report/json.js +3 -0
- package/esm/report/markdown.d.ts +25 -0
- package/esm/report/markdown.js +352 -0
- package/esm/report/pretty.d.ts +14 -0
- package/esm/report/pretty.js +318 -0
- package/esm/report/sarif.d.ts +40 -0
- package/esm/report/sarif.js +189 -0
- package/esm/report/view.d.ts +82 -0
- package/esm/report/view.js +126 -0
- package/esm/rules/registry.d.ts +66 -0
- package/esm/rules/registry.js +562 -0
- package/esm/score/score.d.ts +84 -0
- package/esm/score/score.js +233 -0
- package/esm/score/scorecards.d.ts +68 -0
- package/esm/score/scorecards.js +157 -0
- package/esm/types.d.ts +292 -0
- package/esm/types.js +26 -0
- package/esm/version.d.ts +8 -0
- package/esm/version.js +9 -0
- package/exposure/adapters.d.ts +152 -0
- package/exposure/adapters.js +468 -0
- package/exposure/planes.d.ts +59 -0
- package/exposure/planes.js +151 -0
- package/exposure/reach.d.ts +78 -0
- package/exposure/reach.js +109 -0
- package/exposure/routines.d.ts +36 -0
- package/exposure/routines.js +57 -0
- package/index.d.ts +96 -6
- package/index.js +229 -6
- package/lint/index.d.ts +11 -0
- package/lint/index.js +19 -0
- package/package.json +54 -48
- package/perf/baseline.d.ts +40 -0
- package/perf/baseline.js +92 -0
- package/perf/explain.d.ts +46 -0
- package/perf/explain.js +204 -0
- package/pg/acl.d.ts +72 -0
- package/pg/acl.js +191 -0
- package/pg/behaviors.d.ts +114 -0
- package/pg/behaviors.js +217 -0
- package/pg/exposure.d.ts +74 -0
- package/pg/exposure.js +205 -0
- package/pg/functions.d.ts +52 -0
- package/pg/functions.js +131 -0
- package/pg/indexes.d.ts +192 -0
- package/pg/indexes.js +400 -0
- package/pg/introspect.d.ts +134 -0
- package/pg/introspect.js +280 -0
- package/pg/objects.d.ts +37 -0
- package/pg/objects.js +111 -0
- package/pg/paths.d.ts +114 -0
- package/pg/paths.js +166 -0
- package/pg/proc.d.ts +23 -0
- package/pg/proc.js +58 -0
- package/pg/reach-inputs.d.ts +47 -0
- package/pg/reach-inputs.js +136 -0
- package/pg/roles.d.ts +28 -0
- package/pg/roles.js +54 -0
- package/pg/stats.d.ts +65 -0
- package/pg/stats.js +133 -0
- package/pgpm-test.d.ts +22 -0
- package/pgpm-test.js +42 -0
- package/report/callgraph.d.ts +9 -0
- package/report/callgraph.js +81 -0
- package/report/compare.d.ts +143 -0
- package/report/compare.js +205 -0
- package/report/github.d.ts +74 -0
- package/report/github.js +333 -0
- package/report/json.d.ts +4 -0
- package/report/json.js +6 -0
- package/report/markdown.d.ts +25 -0
- package/report/markdown.js +355 -0
- package/report/pretty.d.ts +14 -0
- package/report/pretty.js +324 -0
- package/report/sarif.d.ts +40 -0
- package/report/sarif.js +193 -0
- package/report/view.d.ts +82 -0
- package/report/view.js +132 -0
- package/rules/registry.d.ts +66 -0
- package/rules/registry.js +569 -0
- package/schema/safegres.schema.json +1119 -0
- package/score/score.d.ts +84 -0
- package/score/score.js +238 -0
- package/score/scorecards.d.ts +68 -0
- package/score/scorecards.js +162 -0
- package/types.d.ts +292 -0
- package/types.js +32 -0
- package/version.d.ts +8 -0
- package/version.js +12 -0
- package/LICENSE +0 -23
- package/admin.d.ts +0 -26
- package/admin.js +0 -182
- package/connect.d.ts +0 -19
- package/connect.js +0 -83
- package/dist/README.md +0 -557
- package/dist/package.json +0 -72
- package/esm/admin.js +0 -178
- package/esm/connect.js +0 -78
- package/esm/manager.js +0 -136
- package/esm/roles.js +0 -32
- package/esm/seed/adapters.js +0 -23
- package/esm/seed/csv.js +0 -44
- package/esm/seed/index.js +0 -16
- package/esm/seed/json.js +0 -18
- package/esm/seed/launchql.js +0 -19
- package/esm/seed/sqitch.js +0 -17
- package/esm/stream.js +0 -43
- package/esm/test-client.js +0 -150
- package/manager.d.ts +0 -25
- package/manager.js +0 -140
- package/roles.d.ts +0 -17
- package/roles.js +0 -38
- package/seed/adapters.d.ts +0 -4
- package/seed/adapters.js +0 -28
- package/seed/csv.d.ts +0 -9
- package/seed/csv.js +0 -49
- package/seed/index.d.ts +0 -16
- package/seed/index.js +0 -33
- package/seed/json.d.ts +0 -6
- package/seed/json.js +0 -21
- package/seed/launchql.d.ts +0 -2
- package/seed/launchql.js +0 -22
- package/seed/sqitch.d.ts +0 -2
- package/seed/sqitch.js +0 -20
- package/seed/types.d.ts +0 -13
- package/stream.d.ts +0 -2
- package/stream.js +0 -46
- package/test-client.d.ts +0 -49
- package/test-client.js +0 -154
- /package/{seed → config}/types.js +0 -0
- /package/esm/{seed → config}/types.js +0 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import yanse from 'yanse';
|
|
2
|
+
import { renderCallGraph, renderCallGraphDiff } from './callgraph';
|
|
3
|
+
import { baselineLabel, describeBaseline, formatDelta } from './compare';
|
|
4
|
+
import { selectView } from './view';
|
|
5
|
+
const SEV_LABEL = {
|
|
6
|
+
critical: 'CRIT',
|
|
7
|
+
high: 'HIGH',
|
|
8
|
+
medium: 'MED ',
|
|
9
|
+
low: 'LOW ',
|
|
10
|
+
info: 'INFO'
|
|
11
|
+
};
|
|
12
|
+
const SEV_PAINT = {
|
|
13
|
+
critical: (s) => yanse.bold(yanse.red(s)),
|
|
14
|
+
high: yanse.red,
|
|
15
|
+
medium: yanse.yellow,
|
|
16
|
+
low: yanse.cyan,
|
|
17
|
+
info: yanse.gray
|
|
18
|
+
};
|
|
19
|
+
const noop = (s) => s;
|
|
20
|
+
export function renderPretty(report, options = {}) {
|
|
21
|
+
const colorEnabled = options.color ?? process.stdout.isTTY === true;
|
|
22
|
+
const paint = (sev, s) => (colorEnabled ? SEV_PAINT[sev](s) : noop(s));
|
|
23
|
+
const view = selectView(report, {
|
|
24
|
+
detail: options.summary ? 'summary' : options.verbose ? 'verbose' : 'normal',
|
|
25
|
+
...(options.planes ? { planes: options.planes } : {}),
|
|
26
|
+
...options.view
|
|
27
|
+
});
|
|
28
|
+
const verbose = view.detail === 'verbose';
|
|
29
|
+
const { summary: s, score, exposure } = report;
|
|
30
|
+
const lines = [
|
|
31
|
+
`safegres ${report.version} (${report.generatedAt})`,
|
|
32
|
+
...(report.provenance?.sealed
|
|
33
|
+
? [`sealed: ${report.provenance.preset ?? 'recommended'} ${report.provenance.fingerprint}`]
|
|
34
|
+
: []),
|
|
35
|
+
''
|
|
36
|
+
];
|
|
37
|
+
if (exposure) {
|
|
38
|
+
if (exposure.known) {
|
|
39
|
+
lines.push(`exposure: ${exposure.schemas.length} schema(s) via ${exposure.source}`
|
|
40
|
+
+ ` — ${exposure.exposedTables}/${exposure.totalTables} tables exposed`);
|
|
41
|
+
if (exposure.roles && exposure.roles.length > 0) {
|
|
42
|
+
// Marking the anonymous ones answers the question a reader actually
|
|
43
|
+
// has: which of these does a caller reach with no credentials?
|
|
44
|
+
const anon = new Set(exposure.anonRoles ?? []);
|
|
45
|
+
const roles = exposure.roles.map((r) => (anon.has(r) ? `${r} (anon)` : r));
|
|
46
|
+
lines.push(` api roles: ${roles.join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
if (exposure.unaddressable && exposure.unaddressable.length > 0) {
|
|
49
|
+
lines.push(` ${exposure.unaddressable.length} relation(s) in those schemas the API cannot address: `
|
|
50
|
+
+ exposure.unaddressable.map((r) => `${r.schema}.${r.table}`).join(', '));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
lines.push(paint('medium', 'exposure: unknown — entire database assumed reachable (score capped)'));
|
|
55
|
+
}
|
|
56
|
+
lines.push('');
|
|
57
|
+
}
|
|
58
|
+
if (score) {
|
|
59
|
+
lines.push(...scoreLines('score', score, colorEnabled));
|
|
60
|
+
if (report.comparison?.security) {
|
|
61
|
+
lines.push(` ${deltaLine(report.comparison.security, baselineLabel(report.comparison.previous), colorEnabled)}`);
|
|
62
|
+
}
|
|
63
|
+
lines.push('');
|
|
64
|
+
}
|
|
65
|
+
if (report.perf) {
|
|
66
|
+
lines.push(...scoreLines('perf score', report.perf.score, colorEnabled));
|
|
67
|
+
if (report.comparison?.perf) {
|
|
68
|
+
lines.push(` ${deltaLine(report.comparison.perf, baselineLabel(report.comparison.previous), colorEnabled)}`);
|
|
69
|
+
}
|
|
70
|
+
lines.push('');
|
|
71
|
+
}
|
|
72
|
+
if (view.has('scorecards')) {
|
|
73
|
+
lines.push(...scorecardLines(view, colorEnabled), '');
|
|
74
|
+
}
|
|
75
|
+
if (view.has('planes')) {
|
|
76
|
+
lines.push(...planeLines(view, colorEnabled), '');
|
|
77
|
+
}
|
|
78
|
+
lines.push(`summary: ${paint('critical', String(s.critical))} critical `
|
|
79
|
+
+ `${paint('high', String(s.high))} high `
|
|
80
|
+
+ `${paint('medium', String(s.medium))} medium `
|
|
81
|
+
+ `${paint('low', String(s.low))} low `
|
|
82
|
+
+ `${paint('info', String(s.info))} info`);
|
|
83
|
+
// Which run the Δ above is against, or why there is none — a delta whose
|
|
84
|
+
// baseline the reader cannot see is a number they cannot check.
|
|
85
|
+
if (report.comparison) {
|
|
86
|
+
lines.push(`baseline: ${describeBaseline(report.comparison.previous, { link: false })}`);
|
|
87
|
+
}
|
|
88
|
+
else if (report.comparisonSkipped) {
|
|
89
|
+
lines.push(`no delta baseline: ${report.comparisonSkipped}`
|
|
90
|
+
+ ' (absolute score and the perf baseline still gate this run)');
|
|
91
|
+
}
|
|
92
|
+
// --summary: score + counts only, no per-finding lines.
|
|
93
|
+
if (view.detail === 'summary') {
|
|
94
|
+
if (report.callGraph) {
|
|
95
|
+
lines.push('', renderCallGraph(report.callGraph, { color: colorEnabled, summary: true }));
|
|
96
|
+
}
|
|
97
|
+
if (report.callGraphDiff) {
|
|
98
|
+
lines.push('', renderCallGraphDiff(report.callGraphDiff, { color: colorEnabled }));
|
|
99
|
+
}
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|
|
102
|
+
lines.push('');
|
|
103
|
+
// Perf findings render in their own section so the two dimensions never
|
|
104
|
+
// read as one list.
|
|
105
|
+
const { exposed, internal } = view.security;
|
|
106
|
+
for (const f of exposed) {
|
|
107
|
+
lines.push(renderFinding(f, paint));
|
|
108
|
+
}
|
|
109
|
+
if (internal.length > 0) {
|
|
110
|
+
// Collapse internal advisories to a count by default; --verbose lists them.
|
|
111
|
+
if (verbose) {
|
|
112
|
+
lines.push('', `internal advisories (${internal.length}) — not exposed via any API, excluded from the score:`, '');
|
|
113
|
+
for (const f of internal) {
|
|
114
|
+
lines.push(renderFinding(f, paint));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
lines.push('', paint('info', `internal advisories (${internal.length}) — not exposed via any API, `
|
|
119
|
+
+ 'excluded from the score; run with --verbose to list.'));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (exposed.length + internal.length === 0) {
|
|
123
|
+
lines.push('no findings.');
|
|
124
|
+
}
|
|
125
|
+
if (report.roleAccess && report.roleAccess.roles.length > 0) {
|
|
126
|
+
lines.push('', 'role access — effective grants (direct, PUBLIC, inherited):', '');
|
|
127
|
+
for (const entry of report.roleAccess.roles) {
|
|
128
|
+
lines.push(` ${entry.role}: ${entry.accessibleTables} relation(s) accessible — `
|
|
129
|
+
+ `${entry.unmediated.length} unmediated (no RLS), ${entry.mediated} policy-mediated`
|
|
130
|
+
+ `${entry.dead > 0 ? `, ${entry.dead} dead (RLS default-deny)` : ''}`);
|
|
131
|
+
const shown = verbose ? entry.unmediated : entry.unmediated.slice(0, 20);
|
|
132
|
+
for (const r of shown) {
|
|
133
|
+
lines.push(paint('medium', ` ${r.schema}.${r.table} ${r.privileges.join(', ')}`
|
|
134
|
+
+ `${r.columns ? ` on (${r.columns.join(', ')})` : ''} (via ${r.via})`));
|
|
135
|
+
}
|
|
136
|
+
if (shown.length < entry.unmediated.length) {
|
|
137
|
+
lines.push(paint('info', ` … ${entry.unmediated.length - shown.length} more — run with --verbose to list.`));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (report.perf?.diff) {
|
|
142
|
+
const { added, removed, accepted } = report.perf.diff;
|
|
143
|
+
lines.push('', 'performance vs baseline:', '');
|
|
144
|
+
if (added.length === 0) {
|
|
145
|
+
lines.push(`no new perf debt (${accepted.length} accepted, ${removed.length} fixed).`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
lines.push(paint('high', `${added.length} new perf finding${added.length === 1 ? '' : 's'} since the baseline:`));
|
|
149
|
+
for (const f of added)
|
|
150
|
+
lines.push(renderFinding(f, paint));
|
|
151
|
+
lines.push(paint('info', ` (${accepted.length} accepted, ${removed.length} fixed)`));
|
|
152
|
+
}
|
|
153
|
+
if (removed.length > 0) {
|
|
154
|
+
lines.push(paint('info', ` fixed since the baseline: ${removed.map((r) => `${r.code} ${r.schema}.${r.table}`).join(', ')} — re-baseline to lock the win in`));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (report.perf) {
|
|
158
|
+
const perfExposed = report.perf.findings.filter((f) => f.exposed !== false);
|
|
159
|
+
const perfInternal = report.perf.findings.length - perfExposed.length;
|
|
160
|
+
lines.push('', 'performance (index hygiene) — scored separately from security:', '');
|
|
161
|
+
if (perfExposed.length === 0) {
|
|
162
|
+
lines.push('no perf findings.');
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
for (const f of perfExposed)
|
|
166
|
+
lines.push(renderFinding(f, paint));
|
|
167
|
+
}
|
|
168
|
+
if (perfInternal > 0 && !verbose) {
|
|
169
|
+
lines.push(paint('info', ` (${perfInternal} internal perf advisor${perfInternal === 1 ? 'y' : 'ies'} excluded from the perf score)`));
|
|
170
|
+
}
|
|
171
|
+
else if (perfInternal > 0) {
|
|
172
|
+
for (const f of report.perf.findings.filter((f) => f.exposed === false)) {
|
|
173
|
+
lines.push(renderFinding(f, paint));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const { paths, stats, explain } = report.perf;
|
|
177
|
+
if (paths && paths.writeOnceShaped > 0) {
|
|
178
|
+
const acted = { report: 'reported', demote: 'demoted to info', suppress: 'suppressed' }[paths.onWriteOncePointer];
|
|
179
|
+
lines.push(paint('info', ` access paths: ${paths.total} foreign keys — ${paths.read} read by a policy or view, `
|
|
180
|
+
+ `${paths.writeOnceShaped} on ${paths.tables} tables write-once shaped (X1 ${acted})`));
|
|
181
|
+
}
|
|
182
|
+
if (stats) {
|
|
183
|
+
lines.push(paint('info', ` runtime statistics: ${stats.tables} tables, counters since ${stats.statsReset ?? 'server start'}`
|
|
184
|
+
+ `${stats.scored ? '' : ' (S* findings advisory — perf.scoring.includeStats is false)'}`));
|
|
185
|
+
for (const note of stats.notes ?? [])
|
|
186
|
+
lines.push(paint('info', ` ${note}`));
|
|
187
|
+
}
|
|
188
|
+
if (explain) {
|
|
189
|
+
lines.push(paint('info', explain.unavailable
|
|
190
|
+
?? ` planner proof: ${explain.confirmed} confirmed, ${explain.refuted} refuted, ${explain.inconclusive} inconclusive of ${explain.probed} probed`));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (report.callGraph) {
|
|
194
|
+
lines.push('', renderCallGraph(report.callGraph, { color: colorEnabled }));
|
|
195
|
+
}
|
|
196
|
+
if (report.callGraphDiff) {
|
|
197
|
+
lines.push('', renderCallGraphDiff(report.callGraphDiff, { color: colorEnabled }));
|
|
198
|
+
}
|
|
199
|
+
return lines.join('\n');
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The other named scores. One line each: the headline answers the question
|
|
203
|
+
* the preset was written for, and these answer the ones a particular team
|
|
204
|
+
* actually gates on. `raw` sits among them deliberately — a number no
|
|
205
|
+
* configuration softened, next to the one that was.
|
|
206
|
+
*/
|
|
207
|
+
function scorecardLines(view, colorEnabled) {
|
|
208
|
+
const lines = ['scorecards — the same findings, scored by question:'];
|
|
209
|
+
for (const card of view.scorecards) {
|
|
210
|
+
const grade = colorEnabled
|
|
211
|
+
? gradePaintFor(card.score)(`${card.score.value} (${card.score.grade})`)
|
|
212
|
+
: `${card.score.value} (${card.score.grade})`;
|
|
213
|
+
lines.push(` ${card.name}: ${grade} — ${card.findings} finding(s)`
|
|
214
|
+
+ (card.description ? ` ${card.description}` : ''));
|
|
215
|
+
}
|
|
216
|
+
return lines;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Secondary planes: one line each, because they are not the headline. The
|
|
220
|
+
* primary score answers "how safe is the product"; these answer "and what if
|
|
221
|
+
* somebody doesn't come through it" — worth seeing, not worth competing with
|
|
222
|
+
* the number the team is being graded on. `--plane <name>` expands one.
|
|
223
|
+
*/
|
|
224
|
+
function planeLines(view, colorEnabled) {
|
|
225
|
+
const lines = ['other access planes — advisory, not part of the score above:'];
|
|
226
|
+
for (const plane of view.planes) {
|
|
227
|
+
lines.push(` ${planeLine(plane, colorEnabled)}`);
|
|
228
|
+
}
|
|
229
|
+
for (const plane of view.expandedPlanes) {
|
|
230
|
+
if (plane.skipped)
|
|
231
|
+
continue;
|
|
232
|
+
lines.push('', `plane ${plane.name}:`, ...scoreLines(' score', plane.score, colorEnabled));
|
|
233
|
+
}
|
|
234
|
+
if (view.expandedPlanes.length === 0 && view.planes.some((p) => !p.skipped)) {
|
|
235
|
+
lines.push(' (run with --plane <name> to expand one, or --plane \'*\' for all)');
|
|
236
|
+
}
|
|
237
|
+
return lines;
|
|
238
|
+
}
|
|
239
|
+
function planeLine(plane, colorEnabled) {
|
|
240
|
+
const who = plane.roles && plane.roles.length > 0
|
|
241
|
+
? ` (${plane.roles.join(', ')})`
|
|
242
|
+
: plane.schemas.length > 0
|
|
243
|
+
? ` (${plane.schemas.join(', ')})`
|
|
244
|
+
: '';
|
|
245
|
+
if (plane.skipped)
|
|
246
|
+
return `${plane.name}${who}: not graded — ${plane.skipped}`;
|
|
247
|
+
const grade = colorEnabled ? gradePaintFor(plane.score)(`${plane.score.value} (${plane.score.grade})`) : `${plane.score.value} (${plane.score.grade})`;
|
|
248
|
+
const via = plane.reachedVia ? `, reached via ${plane.reachedVia}` : '';
|
|
249
|
+
return `${plane.name} [${plane.kind}]${who}: ${grade}`
|
|
250
|
+
+ ` — ${plane.exposedTables} relation(s)${via}`;
|
|
251
|
+
}
|
|
252
|
+
function gradePaintFor(score) {
|
|
253
|
+
if (score.grade.startsWith('A'))
|
|
254
|
+
return yanse.green;
|
|
255
|
+
return score.grade === 'B' || score.grade === 'C' ? yanse.yellow : yanse.red;
|
|
256
|
+
}
|
|
257
|
+
function scoreLines(label, score, colorEnabled) {
|
|
258
|
+
const gradePaint = colorEnabled ? gradePaintFor(score) : noop;
|
|
259
|
+
const capNote = score.cappedByUnknownExposure ? ' (capped: exposure unknown)' : '';
|
|
260
|
+
const lines = [
|
|
261
|
+
`${label}: ${gradePaint(`${score.value} (${score.grade})`)} — model: ${score.model}${capNote}`
|
|
262
|
+
];
|
|
263
|
+
const scored = score.deductions.filter((d) => !d.unscored);
|
|
264
|
+
const top = scored.slice(0, 3);
|
|
265
|
+
if (top.length > 0) {
|
|
266
|
+
lines.push(` top deductions: ${top.map(deductionLabel).join(' ')}`);
|
|
267
|
+
// Payoff, not points: what the score becomes if the rule goes to zero.
|
|
268
|
+
lines.push(` by rule: ${scored
|
|
269
|
+
.map((d) => `${d.code} ${d.grade} (+${d.potential.toFixed(1)})`)
|
|
270
|
+
.join(' ')}`);
|
|
271
|
+
}
|
|
272
|
+
const capped = scored.filter((d) => d.capped);
|
|
273
|
+
if (capped.length > 0) {
|
|
274
|
+
lines.push(` capped: ${capped.map((d) => d.code).join(', ')}`
|
|
275
|
+
+ ' — one rule cannot decide the grade; the finding count is unchanged');
|
|
276
|
+
}
|
|
277
|
+
const unscored = score.deductions.filter((d) => d.unscored);
|
|
278
|
+
if (unscored.length > 0) {
|
|
279
|
+
lines.push(` unscored: ${unscored.map((d) => `${d.code} (×${d.count})`).join(' ')}`
|
|
280
|
+
+ ' — zero-weight, fixing these cannot move the score');
|
|
281
|
+
}
|
|
282
|
+
return lines;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* `L19 −664 (×568 → 166 fixes)` — the finding count and what it costs to
|
|
286
|
+
* clear are different numbers, and only the second one is charged.
|
|
287
|
+
*/
|
|
288
|
+
function deductionLabel(d) {
|
|
289
|
+
const count = d.units === undefined
|
|
290
|
+
? `×${d.count}`
|
|
291
|
+
: `×${d.count} → ${d.units} fix${d.units === 1 ? '' : 'es'}`;
|
|
292
|
+
return `${d.code} −${d.points} (${count})`;
|
|
293
|
+
}
|
|
294
|
+
function deltaLine(d, ref, colorEnabled) {
|
|
295
|
+
const since = ref ? ` vs ${ref}` : ' vs previous run';
|
|
296
|
+
const arrow = d.delta > 0 ? '▲' : d.delta < 0 ? '▼' : '=';
|
|
297
|
+
// Same convention as the markdown report: green is the direction you want.
|
|
298
|
+
const paint = (better, s) => colorEnabled ? (better ? yanse.green(s) : yanse.red(s)) : s;
|
|
299
|
+
const movement = `${arrow} ${formatDelta(d.delta, 1)}`;
|
|
300
|
+
const score = d.delta === 0 ? movement : paint(d.delta > 0, movement);
|
|
301
|
+
const grade = d.gradeBefore === d.gradeAfter ? '' : `, ${d.gradeBefore} → ${d.gradeAfter}`;
|
|
302
|
+
const findingMove = `${d.findingsBefore} → ${d.findingsAfter}`;
|
|
303
|
+
const findings = d.findingsAfter === d.findingsBefore
|
|
304
|
+
? findingMove
|
|
305
|
+
: paint(d.findingsAfter < d.findingsBefore, findingMove);
|
|
306
|
+
return `Δ${since}: ${score} (from ${d.before}${grade}) findings ${findings}`;
|
|
307
|
+
}
|
|
308
|
+
function renderFinding(f, paint) {
|
|
309
|
+
const label = paint(f.severity, SEV_LABEL[f.severity]);
|
|
310
|
+
const loc = [f.schema, f.table].filter(Boolean).join('.') + (f.policy ? ` (${f.policy})` : '');
|
|
311
|
+
const head = `[${label}] ${f.code} ${loc}`;
|
|
312
|
+
const body = ` ${f.message}`;
|
|
313
|
+
const hint = f.hint ? ` hint: ${f.hint}` : '';
|
|
314
|
+
const evidence = f.evidence
|
|
315
|
+
? ` plan (${f.evidence.status}): ${f.evidence.plan}${f.evidence.note ? ` — ${f.evidence.note}` : ''}`
|
|
316
|
+
: '';
|
|
317
|
+
return [head, body, hint, evidence].filter(Boolean).join('\n');
|
|
318
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SARIF 2.1.0 rendering (`--format sarif`), so findings land in GitHub code
|
|
3
|
+
* scanning (Security tab + inline PR annotations) instead of only in a log.
|
|
4
|
+
*
|
|
5
|
+
* A code-scanning alert is a *file and a line*, but safegres reads the
|
|
6
|
+
* catalog — a live database has no source location. `buildSourceIndex()`
|
|
7
|
+
* closes that gap by scanning the SQL that produced the database
|
|
8
|
+
* (pgpm deploy scripts, migrations) for the `CREATE TABLE` / `CREATE POLICY`
|
|
9
|
+
* that defines each object, so `app_public.widgets` resolves to the line that
|
|
10
|
+
* created it. Findings that don't resolve are still emitted, without a
|
|
11
|
+
* location: GitHub drops those from the Security tab, but any other SARIF
|
|
12
|
+
* consumer keeps them, and the JSON format carries everything regardless.
|
|
13
|
+
*/
|
|
14
|
+
import type { Report } from '../types';
|
|
15
|
+
export interface SourceLocation {
|
|
16
|
+
/** Repo-relative POSIX path, as GitHub expects it. */
|
|
17
|
+
file: string;
|
|
18
|
+
line: number;
|
|
19
|
+
}
|
|
20
|
+
/** `schema.table` → definition site, plus `schema.table:policy` for policies. */
|
|
21
|
+
export type SourceIndex = Map<string, SourceLocation>;
|
|
22
|
+
export interface RenderSarifOptions {
|
|
23
|
+
/** Definition sites for relations and policies (see `buildSourceIndex`). */
|
|
24
|
+
sources?: SourceIndex;
|
|
25
|
+
/** Absolute URI of the scanned repository root, if known. */
|
|
26
|
+
repoRoot?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function renderSarif(report: Report, options?: RenderSarifOptions): string;
|
|
29
|
+
export interface BuildSourceIndexOptions {
|
|
30
|
+
/** Directory names to skip. Defaults to node_modules, .git, dist. */
|
|
31
|
+
skipDirs?: string[];
|
|
32
|
+
/** Paths in the index are relative to this. Defaults to `dir`. */
|
|
33
|
+
root?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Scan a directory tree of `.sql` files for the statements that define the
|
|
37
|
+
* objects safegres reports on. First definition wins: a pgpm module deploys a
|
|
38
|
+
* table once, and a later `ALTER` is not where a reviewer wants the alert.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildSourceIndex(dir: string, options?: BuildSourceIndexOptions): SourceIndex;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SARIF 2.1.0 rendering (`--format sarif`), so findings land in GitHub code
|
|
3
|
+
* scanning (Security tab + inline PR annotations) instead of only in a log.
|
|
4
|
+
*
|
|
5
|
+
* A code-scanning alert is a *file and a line*, but safegres reads the
|
|
6
|
+
* catalog — a live database has no source location. `buildSourceIndex()`
|
|
7
|
+
* closes that gap by scanning the SQL that produced the database
|
|
8
|
+
* (pgpm deploy scripts, migrations) for the `CREATE TABLE` / `CREATE POLICY`
|
|
9
|
+
* that defines each object, so `app_public.widgets` resolves to the line that
|
|
10
|
+
* created it. Findings that don't resolve are still emitted, without a
|
|
11
|
+
* location: GitHub drops those from the Security tab, but any other SARIF
|
|
12
|
+
* consumer keeps them, and the JSON format carries everything regardless.
|
|
13
|
+
*/
|
|
14
|
+
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
15
|
+
import { join, relative, sep } from 'path';
|
|
16
|
+
import { findingKey, subjectOf } from '../perf/baseline';
|
|
17
|
+
import { dimensionOf, RULES } from '../rules/registry';
|
|
18
|
+
const SARIF_VERSION = '2.1.0';
|
|
19
|
+
const SCHEMA = 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json';
|
|
20
|
+
/** SARIF has three visible levels; five severities have to fold into them. */
|
|
21
|
+
const LEVEL = {
|
|
22
|
+
critical: 'error',
|
|
23
|
+
high: 'error',
|
|
24
|
+
medium: 'warning',
|
|
25
|
+
low: 'note',
|
|
26
|
+
info: 'note'
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* GitHub reads `security-severity` (a CVSS-shaped number) to bucket an alert
|
|
30
|
+
* as critical/high/medium/low in the Security tab.
|
|
31
|
+
*/
|
|
32
|
+
const SECURITY_SEVERITY = {
|
|
33
|
+
critical: '9.0',
|
|
34
|
+
high: '7.0',
|
|
35
|
+
medium: '5.0',
|
|
36
|
+
low: '3.0',
|
|
37
|
+
info: '1.0'
|
|
38
|
+
};
|
|
39
|
+
export function renderSarif(report, options = {}) {
|
|
40
|
+
const findings = report.findings;
|
|
41
|
+
const emitted = new Set(findings.map((f) => f.code));
|
|
42
|
+
const sarif = {
|
|
43
|
+
$schema: SCHEMA,
|
|
44
|
+
version: SARIF_VERSION,
|
|
45
|
+
runs: [
|
|
46
|
+
{
|
|
47
|
+
tool: {
|
|
48
|
+
driver: {
|
|
49
|
+
name: 'safegres',
|
|
50
|
+
informationUri: 'https://github.com/constructive-io/constructive/tree/main/packages/safegres',
|
|
51
|
+
semanticVersion: report.version,
|
|
52
|
+
version: report.version,
|
|
53
|
+
rules: RULES.filter((r) => emitted.has(r.code)).map((rule) => ({
|
|
54
|
+
id: rule.code,
|
|
55
|
+
name: rule.code,
|
|
56
|
+
shortDescription: { text: rule.title },
|
|
57
|
+
fullDescription: { text: rule.title },
|
|
58
|
+
defaultConfiguration: { level: LEVEL[rule.defaultSeverity] },
|
|
59
|
+
properties: {
|
|
60
|
+
tags: [
|
|
61
|
+
dimensionOf(rule) === 'perf' ? 'performance' : 'security',
|
|
62
|
+
rule.category,
|
|
63
|
+
rule.direction
|
|
64
|
+
],
|
|
65
|
+
'security-severity': SECURITY_SEVERITY[rule.defaultSeverity]
|
|
66
|
+
}
|
|
67
|
+
}))
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
// Scores have nowhere to live in a SARIF result, so they ride along as
|
|
71
|
+
// run properties — enough for a consumer to gate on.
|
|
72
|
+
properties: {
|
|
73
|
+
generatedAt: report.generatedAt,
|
|
74
|
+
...(report.score ? { score: report.score.value, grade: report.score.grade } : {}),
|
|
75
|
+
...(report.perf ? { perfScore: report.perf.score.value, perfGrade: report.perf.score.grade } : {})
|
|
76
|
+
},
|
|
77
|
+
results: findings.map((finding) => result(finding, options))
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
};
|
|
81
|
+
return JSON.stringify(sarif, null, 2);
|
|
82
|
+
}
|
|
83
|
+
function result(finding, options) {
|
|
84
|
+
const location = resolve(finding, options.sources);
|
|
85
|
+
const relation = [finding.schema, finding.table].filter(Boolean).join('.');
|
|
86
|
+
return {
|
|
87
|
+
ruleId: finding.code,
|
|
88
|
+
level: LEVEL[finding.severity],
|
|
89
|
+
message: { text: relation ? `${relation}: ${finding.message}` : finding.message },
|
|
90
|
+
...(location
|
|
91
|
+
? {
|
|
92
|
+
locations: [
|
|
93
|
+
{
|
|
94
|
+
physicalLocation: {
|
|
95
|
+
artifactLocation: {
|
|
96
|
+
uri: location.file,
|
|
97
|
+
...(options.repoRoot ? { uriBaseId: '%SRCROOT%' } : {})
|
|
98
|
+
},
|
|
99
|
+
region: { startLine: location.line }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
: { locations: [] }),
|
|
105
|
+
// Identity, not text: the same key the perf baseline uses, so rewording a
|
|
106
|
+
// rule doesn't resolve-and-reopen every alert.
|
|
107
|
+
partialFingerprints: {
|
|
108
|
+
safegresFindingKey: findingKey({
|
|
109
|
+
code: finding.code,
|
|
110
|
+
schema: finding.schema,
|
|
111
|
+
table: finding.table,
|
|
112
|
+
policy: finding.policy,
|
|
113
|
+
subject: subjectOf(finding)
|
|
114
|
+
})
|
|
115
|
+
},
|
|
116
|
+
properties: {
|
|
117
|
+
severity: finding.severity,
|
|
118
|
+
category: finding.category,
|
|
119
|
+
dimension: finding.dimension ?? 'security',
|
|
120
|
+
...(finding.direction ? { direction: finding.direction } : {}),
|
|
121
|
+
...(finding.exposed === undefined ? {} : { exposed: finding.exposed }),
|
|
122
|
+
...(finding.acknowledged ? { acknowledged: true } : {}),
|
|
123
|
+
...(finding.policy ? { policy: finding.policy } : {}),
|
|
124
|
+
...(finding.role ? { role: finding.role } : {}),
|
|
125
|
+
...(finding.hint ? { hint: finding.hint } : {})
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Policy site first — it's the precise line — then the table that owns it. */
|
|
130
|
+
function resolve(finding, sources) {
|
|
131
|
+
if (!sources || !finding.schema || !finding.table)
|
|
132
|
+
return undefined;
|
|
133
|
+
const relation = `${finding.schema}.${finding.table}`;
|
|
134
|
+
if (finding.policy) {
|
|
135
|
+
const policy = sources.get(`${relation}:${finding.policy}`);
|
|
136
|
+
if (policy)
|
|
137
|
+
return policy;
|
|
138
|
+
}
|
|
139
|
+
return sources.get(relation);
|
|
140
|
+
}
|
|
141
|
+
const CREATE_TABLE = /^\s*create\s+(?:unlogged\s+|temp\w*\s+)?table\s+(?:if\s+not\s+exists\s+)?("?[\w$]+"?)\.("?[\w$]+"?)/i;
|
|
142
|
+
const CREATE_POLICY = /^\s*create\s+policy\s+("?[\w$]+"?)\s+on\s+("?[\w$]+"?)\.("?[\w$]+"?)/i;
|
|
143
|
+
/**
|
|
144
|
+
* Scan a directory tree of `.sql` files for the statements that define the
|
|
145
|
+
* objects safegres reports on. First definition wins: a pgpm module deploys a
|
|
146
|
+
* table once, and a later `ALTER` is not where a reviewer wants the alert.
|
|
147
|
+
*/
|
|
148
|
+
export function buildSourceIndex(dir, options = {}) {
|
|
149
|
+
const skip = new Set(options.skipDirs ?? ['node_modules', '.git', 'dist']);
|
|
150
|
+
const root = options.root ?? dir;
|
|
151
|
+
const index = new Map();
|
|
152
|
+
const walk = (current) => {
|
|
153
|
+
for (const entry of readdirSync(current)) {
|
|
154
|
+
if (skip.has(entry))
|
|
155
|
+
continue;
|
|
156
|
+
const path = join(current, entry);
|
|
157
|
+
const stat = statSync(path);
|
|
158
|
+
if (stat.isDirectory())
|
|
159
|
+
walk(path);
|
|
160
|
+
else if (entry.toLowerCase().endsWith('.sql'))
|
|
161
|
+
scan(path, root, index);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
walk(dir);
|
|
165
|
+
return index;
|
|
166
|
+
}
|
|
167
|
+
function scan(path, root, index) {
|
|
168
|
+
const uri = relative(root, path).split(sep).join('/');
|
|
169
|
+
const lines = readFileSync(path, 'utf8').split('\n');
|
|
170
|
+
lines.forEach((line, i) => {
|
|
171
|
+
const table = CREATE_TABLE.exec(line);
|
|
172
|
+
if (table) {
|
|
173
|
+
add(index, `${unquote(table[1])}.${unquote(table[2])}`, uri, i + 1);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const policy = CREATE_POLICY.exec(line);
|
|
177
|
+
if (policy) {
|
|
178
|
+
const relation = `${unquote(policy[2])}.${unquote(policy[3])}`;
|
|
179
|
+
add(index, `${relation}:${unquote(policy[1])}`, uri, i + 1);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function add(index, key, file, line) {
|
|
184
|
+
if (!index.has(key))
|
|
185
|
+
index.set(key, { file, line });
|
|
186
|
+
}
|
|
187
|
+
function unquote(identifier) {
|
|
188
|
+
return identifier.startsWith('"') ? identifier.slice(1, -1) : identifier.toLowerCase();
|
|
189
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selection: the layer between analysis and rendering.
|
|
3
|
+
*
|
|
4
|
+
* A `Report` is what the audit found — pure data, the same for everybody. A
|
|
5
|
+
* `ReportView` is what one reader should see: which planes, which dimensions,
|
|
6
|
+
* which sections, how much detail. Renderers consume the view and decide only
|
|
7
|
+
* how it *looks*, so "the team wants the perf score and their direct-role
|
|
8
|
+
* planes in the PR comment" is answered once here, rather than four times in
|
|
9
|
+
* four renderers that drift apart.
|
|
10
|
+
*/
|
|
11
|
+
import type { ReportConfig } from '../config/types';
|
|
12
|
+
import type { ScorecardReport } from '../score/scorecards';
|
|
13
|
+
import type { Finding, PlaneReport, Report } from '../types';
|
|
14
|
+
import type { ScoreDelta } from './compare';
|
|
15
|
+
export type ViewSection = 'scores' | 'delta' | 'findings' | 'planes' | 'scorecards' | 'role-access' | 'call-graph';
|
|
16
|
+
export declare const ALL_SECTIONS: ViewSection[];
|
|
17
|
+
export type ViewDimension = 'security' | 'perf';
|
|
18
|
+
export interface ViewConfig {
|
|
19
|
+
/**
|
|
20
|
+
* Planes to render in full, by name or glob (`*`, `direct:*`). The primary
|
|
21
|
+
* plane is always included. Default: primary only — secondaries are
|
|
22
|
+
* summarized, not expanded.
|
|
23
|
+
*/
|
|
24
|
+
planes?: string[];
|
|
25
|
+
/** Dimensions to render. Default: whatever the report has. */
|
|
26
|
+
dimensions?: ViewDimension[];
|
|
27
|
+
/** Sections to render. Default: all of them the report has data for. */
|
|
28
|
+
sections?: ViewSection[];
|
|
29
|
+
/** How much of each section. Default `normal`. */
|
|
30
|
+
detail?: 'summary' | 'normal' | 'verbose';
|
|
31
|
+
/** Drop findings that are not on the primary surface entirely. */
|
|
32
|
+
exposedOnly?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** One score to render, with the delta that belongs to it. */
|
|
35
|
+
export interface ViewScore {
|
|
36
|
+
/** `security`, `perf`, or `plane:<name>`. */
|
|
37
|
+
id: string;
|
|
38
|
+
label: string;
|
|
39
|
+
dimension: ViewDimension;
|
|
40
|
+
score: import('../score/score').Score;
|
|
41
|
+
/** Present for the primary security/perf scores when the run was compared. */
|
|
42
|
+
delta?: ScoreDelta;
|
|
43
|
+
/** The plane this score describes, when it is not the headline. */
|
|
44
|
+
plane?: PlaneReport;
|
|
45
|
+
}
|
|
46
|
+
export interface ViewFindings {
|
|
47
|
+
/** Reachable on the primary surface — the scored ones. */
|
|
48
|
+
exposed: Finding[];
|
|
49
|
+
/** Off the primary surface: reported, unscored. */
|
|
50
|
+
internal: Finding[];
|
|
51
|
+
}
|
|
52
|
+
export interface ReportView {
|
|
53
|
+
report: Report;
|
|
54
|
+
detail: 'summary' | 'normal' | 'verbose';
|
|
55
|
+
/** Headline scores, in render order. */
|
|
56
|
+
scores: ViewScore[];
|
|
57
|
+
/** Secondary planes to mention. Expanded ones are in `expandedPlanes`. */
|
|
58
|
+
planes: PlaneReport[];
|
|
59
|
+
/** Planes the view config asked to see in full. */
|
|
60
|
+
expandedPlanes: PlaneReport[];
|
|
61
|
+
/**
|
|
62
|
+
* Named scores other than `default`, which is the headline and is already
|
|
63
|
+
* rendered as the score. Kept in config order so the file decides what a
|
|
64
|
+
* reader sees first.
|
|
65
|
+
*/
|
|
66
|
+
scorecards: ScorecardReport[];
|
|
67
|
+
security: ViewFindings;
|
|
68
|
+
perf?: ViewFindings;
|
|
69
|
+
has(section: ViewSection): boolean;
|
|
70
|
+
}
|
|
71
|
+
/** Glob match over plane names: `*` matches any run of characters. */
|
|
72
|
+
export declare function matchPlane(pattern: string, name: string): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Everything presentation-adjacent about a report, decided once.
|
|
75
|
+
*
|
|
76
|
+
* Selection never edits the report: `exposedOnly` narrows what the view
|
|
77
|
+
* exposes, and the underlying `report.findings` stays whole so a JSON dump
|
|
78
|
+
* beside a pretty print is still the full picture.
|
|
79
|
+
*/
|
|
80
|
+
export declare function selectView(report: Report, config?: ViewConfig): ReportView;
|
|
81
|
+
/** The config-file half of a view: `report.planes` / `report.dimensions`. */
|
|
82
|
+
export declare function viewConfigFromReportConfig(config: ReportConfig | undefined): ViewConfig;
|