@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.
Files changed (483) hide show
  1. package/README.md +825 -412
  2. package/adapters.d.ts +8 -0
  3. package/adapters.js +18 -0
  4. package/ast/helpers.d.ts +16 -0
  5. package/ast/helpers.js +40 -0
  6. package/ast/parse.d.ts +31 -0
  7. package/ast/parse.js +87 -0
  8. package/ast/walk.d.ts +27 -0
  9. package/ast/walk.js +79 -0
  10. package/callgraph/baseline.d.ts +28 -0
  11. package/callgraph/baseline.js +55 -0
  12. package/callgraph/extract.d.ts +93 -0
  13. package/callgraph/extract.js +340 -0
  14. package/callgraph/graph.d.ts +82 -0
  15. package/callgraph/graph.js +276 -0
  16. package/checks/anti-patterns.d.ts +53 -0
  17. package/checks/anti-patterns.js +247 -0
  18. package/checks/column-grants.d.ts +20 -0
  19. package/checks/column-grants.js +74 -0
  20. package/checks/coverage.d.ts +43 -0
  21. package/checks/coverage.js +110 -0
  22. package/checks/definer-function.d.ts +91 -0
  23. package/checks/definer-function.js +458 -0
  24. package/checks/definer-view.d.ts +167 -0
  25. package/checks/definer-view.js +392 -0
  26. package/checks/indexes.d.ts +82 -0
  27. package/checks/indexes.js +263 -0
  28. package/checks/lattice.d.ts +179 -0
  29. package/checks/lattice.js +487 -0
  30. package/checks/object-acls.d.ts +32 -0
  31. package/checks/object-acls.js +129 -0
  32. package/checks/policy-index.d.ts +78 -0
  33. package/checks/policy-index.js +349 -0
  34. package/checks/revocable-grants.d.ts +140 -0
  35. package/checks/revocable-grants.js +377 -0
  36. package/checks/rls-flags.d.ts +25 -0
  37. package/checks/rls-flags.js +82 -0
  38. package/checks/role-reach.d.ts +329 -0
  39. package/checks/role-reach.js +277 -0
  40. package/checks/role-trust.d.ts +45 -0
  41. package/checks/role-trust.js +91 -0
  42. package/checks/set-role.d.ts +21 -0
  43. package/checks/set-role.js +66 -0
  44. package/checks/stats.d.ts +52 -0
  45. package/checks/stats.js +192 -0
  46. package/checks/view-exposure.d.ts +68 -0
  47. package/checks/view-exposure.js +236 -0
  48. package/checks/view-writes.d.ts +88 -0
  49. package/checks/view-writes.js +350 -0
  50. package/ci/baseline.d.ts +104 -0
  51. package/ci/baseline.js +111 -0
  52. package/ci/github.d.ts +47 -0
  53. package/ci/github.js +197 -0
  54. package/cli/audit.d.ts +3 -0
  55. package/cli/audit.js +560 -0
  56. package/cli/baseline.d.ts +3 -0
  57. package/cli/baseline.js +104 -0
  58. package/cli/commands.d.ts +2 -0
  59. package/cli/commands.js +60 -0
  60. package/cli/doctor.d.ts +3 -0
  61. package/cli/doctor.js +72 -0
  62. package/cli/eval.d.ts +3 -0
  63. package/cli/eval.js +153 -0
  64. package/cli/print-config.d.ts +3 -0
  65. package/cli/print-config.js +54 -0
  66. package/cli/shared.d.ts +43 -0
  67. package/cli/shared.js +153 -0
  68. package/cli.d.ts +2 -0
  69. package/cli.js +69 -0
  70. package/commands/audit.d.ts +65 -0
  71. package/commands/audit.js +779 -0
  72. package/commands/doctor.d.ts +19 -0
  73. package/commands/doctor.js +181 -0
  74. package/commands/eval.d.ts +67 -0
  75. package/commands/eval.js +93 -0
  76. package/config/fingerprint.d.ts +40 -0
  77. package/config/fingerprint.js +71 -0
  78. package/config/loader.d.ts +44 -0
  79. package/config/loader.js +210 -0
  80. package/config/presets.d.ts +95 -0
  81. package/config/presets.js +327 -0
  82. package/config/resolve.d.ts +47 -0
  83. package/config/resolve.js +192 -0
  84. package/config/schema.d.ts +54 -0
  85. package/config/schema.js +269 -0
  86. package/config/types.d.ts +562 -0
  87. package/config/validate.d.ts +14 -0
  88. package/config/validate.js +125 -0
  89. package/corpus/README.md +83 -0
  90. package/corpus/bootstrap.sql +14 -0
  91. package/corpus/cases/01-anon-write-grant/case.json +26 -0
  92. package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
  93. package/corpus/cases/02-grant-to-public/case.json +25 -0
  94. package/corpus/cases/02-grant-to-public/schema.sql +18 -0
  95. package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
  96. package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
  97. package/corpus/cases/04-permissive-write-policy/case.json +30 -0
  98. package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
  99. package/corpus/cases/05-public-read-policy/case.json +25 -0
  100. package/corpus/cases/05-public-read-policy/schema.sql +16 -0
  101. package/corpus/cases/06-update-without-with-check/case.json +25 -0
  102. package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
  103. package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
  104. package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
  105. package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
  106. package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
  107. package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
  108. package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
  109. package/corpus/cases/10-rls-not-forced/case.json +24 -0
  110. package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
  111. package/corpus/cases/11-session-user-in-policy/case.json +24 -0
  112. package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
  113. package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
  114. package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
  115. package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
  116. package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
  117. package/corpus/cases/14-dead-policy/case.json +24 -0
  118. package/corpus/cases/14-dead-policy/schema.sql +18 -0
  119. package/corpus/cases/15-unreachable-grant/case.json +24 -0
  120. package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
  121. package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
  122. package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
  123. package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
  124. package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
  125. package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
  126. package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
  127. package/corpus/cases/19-policy-column-cast/case.json +24 -0
  128. package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
  129. package/corpus/cases/20-stable-function-per-row/case.json +24 -0
  130. package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
  131. package/corpus/cases/21-table-without-primary-key/case.json +24 -0
  132. package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
  133. package/corpus/cases/22-redundant-index/case.json +24 -0
  134. package/corpus/cases/22-redundant-index/schema.sql +16 -0
  135. package/corpus/cases/23-tsvector-without-index/case.json +24 -0
  136. package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
  137. package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
  138. package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
  139. package/corpus/cases/25-definer-view-bypass/case.json +31 -0
  140. package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
  141. package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
  142. package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
  143. package/corpus/cases/27-definer-view-write/case.json +36 -0
  144. package/corpus/cases/27-definer-view-write/schema.sql +40 -0
  145. package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
  146. package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
  147. package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
  148. package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
  149. package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
  150. package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
  151. package/corpus/cases/31-matview-snapshot/case.json +32 -0
  152. package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
  153. package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
  154. package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
  155. package/corpus/cases/33-leaky-filter-view/case.json +36 -0
  156. package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
  157. package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
  158. package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
  159. package/corpus/cases/35-column-grant-projection/case.json +27 -0
  160. package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
  161. package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
  162. package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
  163. package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
  164. package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
  165. package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
  166. package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
  167. package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
  168. package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
  169. package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
  170. package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
  171. package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
  172. package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
  173. package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
  174. package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
  175. package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
  176. package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
  177. package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
  178. package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
  179. package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
  180. package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
  181. package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
  182. package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
  183. package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
  184. package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
  185. package/corpus/cases/41-unreadable-view-body/case.json +33 -0
  186. package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
  187. package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
  188. package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
  189. package/corpus/cases/42-search-column-indexed/case.json +21 -0
  190. package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
  191. package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
  192. package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
  193. package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
  194. package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
  195. package/corpus/cases/45-anon-foreign-table/case.json +27 -0
  196. package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
  197. package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
  198. package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
  199. package/corpus/cases/47-checked-writable-view/case.json +37 -0
  200. package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
  201. package/corpus/cases/48-definer-function-reach/case.json +38 -0
  202. package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
  203. package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
  204. package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
  205. package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
  206. package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
  207. package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
  208. package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
  209. package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
  210. package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
  211. package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
  212. package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
  213. package/corpus/index.d.ts +79 -0
  214. package/corpus/index.js +125 -0
  215. package/esm/adapters.d.ts +8 -0
  216. package/esm/adapters.js +6 -0
  217. package/esm/ast/helpers.d.ts +16 -0
  218. package/esm/ast/helpers.js +36 -0
  219. package/esm/ast/parse.d.ts +31 -0
  220. package/esm/ast/parse.js +82 -0
  221. package/esm/ast/walk.d.ts +27 -0
  222. package/esm/ast/walk.js +72 -0
  223. package/esm/callgraph/baseline.d.ts +28 -0
  224. package/esm/callgraph/baseline.js +48 -0
  225. package/esm/callgraph/extract.d.ts +93 -0
  226. package/esm/callgraph/extract.js +333 -0
  227. package/esm/callgraph/graph.d.ts +82 -0
  228. package/esm/callgraph/graph.js +273 -0
  229. package/esm/checks/anti-patterns.d.ts +53 -0
  230. package/esm/checks/anti-patterns.js +240 -0
  231. package/esm/checks/column-grants.d.ts +20 -0
  232. package/esm/checks/column-grants.js +71 -0
  233. package/esm/checks/coverage.d.ts +43 -0
  234. package/esm/checks/coverage.js +104 -0
  235. package/esm/checks/definer-function.d.ts +91 -0
  236. package/esm/checks/definer-function.js +452 -0
  237. package/esm/checks/definer-view.d.ts +167 -0
  238. package/esm/checks/definer-view.js +382 -0
  239. package/esm/checks/indexes.d.ts +82 -0
  240. package/esm/checks/indexes.js +256 -0
  241. package/esm/checks/lattice.d.ts +179 -0
  242. package/esm/checks/lattice.js +475 -0
  243. package/esm/checks/object-acls.d.ts +32 -0
  244. package/esm/checks/object-acls.js +125 -0
  245. package/esm/checks/policy-index.d.ts +78 -0
  246. package/esm/checks/policy-index.js +342 -0
  247. package/esm/checks/revocable-grants.d.ts +140 -0
  248. package/esm/checks/revocable-grants.js +373 -0
  249. package/esm/checks/rls-flags.d.ts +25 -0
  250. package/esm/checks/rls-flags.js +77 -0
  251. package/esm/checks/role-reach.d.ts +329 -0
  252. package/esm/checks/role-reach.js +270 -0
  253. package/esm/checks/role-trust.d.ts +45 -0
  254. package/esm/checks/role-trust.js +86 -0
  255. package/esm/checks/set-role.d.ts +21 -0
  256. package/esm/checks/set-role.js +63 -0
  257. package/esm/checks/stats.d.ts +52 -0
  258. package/esm/checks/stats.js +184 -0
  259. package/esm/checks/view-exposure.d.ts +68 -0
  260. package/esm/checks/view-exposure.js +231 -0
  261. package/esm/checks/view-writes.d.ts +88 -0
  262. package/esm/checks/view-writes.js +344 -0
  263. package/esm/ci/baseline.d.ts +104 -0
  264. package/esm/ci/baseline.js +107 -0
  265. package/esm/ci/github.d.ts +47 -0
  266. package/esm/ci/github.js +161 -0
  267. package/esm/cli/audit.d.ts +3 -0
  268. package/esm/cli/audit.js +525 -0
  269. package/esm/cli/baseline.d.ts +3 -0
  270. package/esm/cli/baseline.js +102 -0
  271. package/esm/cli/commands.d.ts +2 -0
  272. package/esm/cli/commands.js +53 -0
  273. package/esm/cli/doctor.d.ts +3 -0
  274. package/esm/cli/doctor.js +67 -0
  275. package/esm/cli/eval.d.ts +3 -0
  276. package/esm/cli/eval.js +115 -0
  277. package/esm/cli/print-config.d.ts +3 -0
  278. package/esm/cli/print-config.js +52 -0
  279. package/esm/cli/shared.d.ts +43 -0
  280. package/esm/cli/shared.js +112 -0
  281. package/esm/cli.d.ts +2 -0
  282. package/esm/cli.js +67 -0
  283. package/esm/commands/audit.d.ts +65 -0
  284. package/esm/commands/audit.js +776 -0
  285. package/esm/commands/doctor.d.ts +19 -0
  286. package/esm/commands/doctor.js +178 -0
  287. package/esm/commands/eval.d.ts +67 -0
  288. package/esm/commands/eval.js +90 -0
  289. package/esm/config/fingerprint.d.ts +40 -0
  290. package/esm/config/fingerprint.js +68 -0
  291. package/esm/config/loader.d.ts +44 -0
  292. package/esm/config/loader.js +172 -0
  293. package/esm/config/presets.d.ts +95 -0
  294. package/esm/config/presets.js +324 -0
  295. package/esm/config/resolve.d.ts +47 -0
  296. package/esm/config/resolve.js +180 -0
  297. package/esm/config/schema.d.ts +54 -0
  298. package/esm/config/schema.js +265 -0
  299. package/esm/config/types.d.ts +562 -0
  300. package/esm/config/validate.d.ts +14 -0
  301. package/esm/config/validate.js +122 -0
  302. package/esm/corpus/index.d.ts +79 -0
  303. package/esm/corpus/index.js +85 -0
  304. package/esm/exposure/adapters.d.ts +152 -0
  305. package/esm/exposure/adapters.js +463 -0
  306. package/esm/exposure/planes.d.ts +59 -0
  307. package/esm/exposure/planes.js +144 -0
  308. package/esm/exposure/reach.d.ts +78 -0
  309. package/esm/exposure/reach.js +105 -0
  310. package/esm/exposure/routines.d.ts +36 -0
  311. package/esm/exposure/routines.js +54 -0
  312. package/esm/index.d.ts +96 -0
  313. package/esm/index.js +57 -6
  314. package/esm/lint/index.d.ts +11 -0
  315. package/esm/lint/index.js +10 -0
  316. package/esm/perf/baseline.d.ts +40 -0
  317. package/esm/perf/baseline.js +83 -0
  318. package/esm/perf/explain.d.ts +46 -0
  319. package/esm/perf/explain.js +201 -0
  320. package/esm/pg/acl.d.ts +72 -0
  321. package/esm/pg/acl.js +186 -0
  322. package/esm/pg/behaviors.d.ts +114 -0
  323. package/esm/pg/behaviors.js +206 -0
  324. package/esm/pg/exposure.d.ts +74 -0
  325. package/esm/pg/exposure.js +198 -0
  326. package/esm/pg/functions.d.ts +52 -0
  327. package/esm/pg/functions.js +128 -0
  328. package/esm/pg/indexes.d.ts +192 -0
  329. package/esm/pg/indexes.js +395 -0
  330. package/esm/pg/introspect.d.ts +134 -0
  331. package/esm/pg/introspect.js +274 -0
  332. package/esm/pg/objects.d.ts +37 -0
  333. package/esm/pg/objects.js +108 -0
  334. package/esm/pg/paths.d.ts +114 -0
  335. package/esm/pg/paths.js +159 -0
  336. package/esm/pg/proc.d.ts +23 -0
  337. package/esm/pg/proc.js +55 -0
  338. package/esm/pg/reach-inputs.d.ts +47 -0
  339. package/esm/pg/reach-inputs.js +133 -0
  340. package/esm/pg/roles.d.ts +28 -0
  341. package/esm/pg/roles.js +50 -0
  342. package/esm/pg/stats.d.ts +65 -0
  343. package/esm/pg/stats.js +129 -0
  344. package/esm/pgpm-test.d.ts +22 -0
  345. package/esm/pgpm-test.js +39 -0
  346. package/esm/report/callgraph.d.ts +9 -0
  347. package/esm/report/callgraph.js +74 -0
  348. package/esm/report/compare.d.ts +143 -0
  349. package/esm/report/compare.js +195 -0
  350. package/esm/report/github.d.ts +74 -0
  351. package/esm/report/github.js +290 -0
  352. package/esm/report/json.d.ts +4 -0
  353. package/esm/report/json.js +3 -0
  354. package/esm/report/markdown.d.ts +25 -0
  355. package/esm/report/markdown.js +352 -0
  356. package/esm/report/pretty.d.ts +14 -0
  357. package/esm/report/pretty.js +318 -0
  358. package/esm/report/sarif.d.ts +40 -0
  359. package/esm/report/sarif.js +189 -0
  360. package/esm/report/view.d.ts +82 -0
  361. package/esm/report/view.js +126 -0
  362. package/esm/rules/registry.d.ts +66 -0
  363. package/esm/rules/registry.js +562 -0
  364. package/esm/score/score.d.ts +84 -0
  365. package/esm/score/score.js +233 -0
  366. package/esm/score/scorecards.d.ts +68 -0
  367. package/esm/score/scorecards.js +157 -0
  368. package/esm/types.d.ts +292 -0
  369. package/esm/types.js +26 -0
  370. package/esm/version.d.ts +8 -0
  371. package/esm/version.js +9 -0
  372. package/exposure/adapters.d.ts +152 -0
  373. package/exposure/adapters.js +468 -0
  374. package/exposure/planes.d.ts +59 -0
  375. package/exposure/planes.js +151 -0
  376. package/exposure/reach.d.ts +78 -0
  377. package/exposure/reach.js +109 -0
  378. package/exposure/routines.d.ts +36 -0
  379. package/exposure/routines.js +57 -0
  380. package/index.d.ts +96 -6
  381. package/index.js +229 -6
  382. package/lint/index.d.ts +11 -0
  383. package/lint/index.js +19 -0
  384. package/package.json +54 -48
  385. package/perf/baseline.d.ts +40 -0
  386. package/perf/baseline.js +92 -0
  387. package/perf/explain.d.ts +46 -0
  388. package/perf/explain.js +204 -0
  389. package/pg/acl.d.ts +72 -0
  390. package/pg/acl.js +191 -0
  391. package/pg/behaviors.d.ts +114 -0
  392. package/pg/behaviors.js +217 -0
  393. package/pg/exposure.d.ts +74 -0
  394. package/pg/exposure.js +205 -0
  395. package/pg/functions.d.ts +52 -0
  396. package/pg/functions.js +131 -0
  397. package/pg/indexes.d.ts +192 -0
  398. package/pg/indexes.js +400 -0
  399. package/pg/introspect.d.ts +134 -0
  400. package/pg/introspect.js +280 -0
  401. package/pg/objects.d.ts +37 -0
  402. package/pg/objects.js +111 -0
  403. package/pg/paths.d.ts +114 -0
  404. package/pg/paths.js +166 -0
  405. package/pg/proc.d.ts +23 -0
  406. package/pg/proc.js +58 -0
  407. package/pg/reach-inputs.d.ts +47 -0
  408. package/pg/reach-inputs.js +136 -0
  409. package/pg/roles.d.ts +28 -0
  410. package/pg/roles.js +54 -0
  411. package/pg/stats.d.ts +65 -0
  412. package/pg/stats.js +133 -0
  413. package/pgpm-test.d.ts +22 -0
  414. package/pgpm-test.js +42 -0
  415. package/report/callgraph.d.ts +9 -0
  416. package/report/callgraph.js +81 -0
  417. package/report/compare.d.ts +143 -0
  418. package/report/compare.js +205 -0
  419. package/report/github.d.ts +74 -0
  420. package/report/github.js +333 -0
  421. package/report/json.d.ts +4 -0
  422. package/report/json.js +6 -0
  423. package/report/markdown.d.ts +25 -0
  424. package/report/markdown.js +355 -0
  425. package/report/pretty.d.ts +14 -0
  426. package/report/pretty.js +324 -0
  427. package/report/sarif.d.ts +40 -0
  428. package/report/sarif.js +193 -0
  429. package/report/view.d.ts +82 -0
  430. package/report/view.js +132 -0
  431. package/rules/registry.d.ts +66 -0
  432. package/rules/registry.js +569 -0
  433. package/schema/safegres.schema.json +1119 -0
  434. package/score/score.d.ts +84 -0
  435. package/score/score.js +238 -0
  436. package/score/scorecards.d.ts +68 -0
  437. package/score/scorecards.js +162 -0
  438. package/types.d.ts +292 -0
  439. package/types.js +32 -0
  440. package/version.d.ts +8 -0
  441. package/version.js +12 -0
  442. package/LICENSE +0 -23
  443. package/admin.d.ts +0 -26
  444. package/admin.js +0 -182
  445. package/connect.d.ts +0 -19
  446. package/connect.js +0 -83
  447. package/dist/README.md +0 -557
  448. package/dist/package.json +0 -72
  449. package/esm/admin.js +0 -178
  450. package/esm/connect.js +0 -78
  451. package/esm/manager.js +0 -136
  452. package/esm/roles.js +0 -32
  453. package/esm/seed/adapters.js +0 -23
  454. package/esm/seed/csv.js +0 -44
  455. package/esm/seed/index.js +0 -16
  456. package/esm/seed/json.js +0 -18
  457. package/esm/seed/launchql.js +0 -19
  458. package/esm/seed/sqitch.js +0 -17
  459. package/esm/stream.js +0 -43
  460. package/esm/test-client.js +0 -150
  461. package/manager.d.ts +0 -25
  462. package/manager.js +0 -140
  463. package/roles.d.ts +0 -17
  464. package/roles.js +0 -38
  465. package/seed/adapters.d.ts +0 -4
  466. package/seed/adapters.js +0 -28
  467. package/seed/csv.d.ts +0 -9
  468. package/seed/csv.js +0 -49
  469. package/seed/index.d.ts +0 -16
  470. package/seed/index.js +0 -33
  471. package/seed/json.d.ts +0 -6
  472. package/seed/json.js +0 -21
  473. package/seed/launchql.d.ts +0 -2
  474. package/seed/launchql.js +0 -22
  475. package/seed/sqitch.d.ts +0 -2
  476. package/seed/sqitch.js +0 -20
  477. package/seed/types.d.ts +0 -13
  478. package/stream.d.ts +0 -2
  479. package/stream.js +0 -46
  480. package/test-client.d.ts +0 -49
  481. package/test-client.js +0 -154
  482. /package/{seed → config}/types.js +0 -0
  483. /package/esm/{seed → config}/types.js +0 -0
@@ -0,0 +1,233 @@
1
+ import { RULES_BY_CODE } from '../rules/registry';
2
+ import { meetsThreshold } from '../types';
3
+ export const DEFAULT_WEIGHTS = {
4
+ critical: 25,
5
+ high: 10,
6
+ medium: 4,
7
+ low: 1,
8
+ info: 0
9
+ };
10
+ export const DEFAULT_GRADE_BANDS = {
11
+ 'A+': 97,
12
+ A: 90,
13
+ B: 80,
14
+ C: 65,
15
+ D: 50
16
+ };
17
+ const DEFAULT_MAX_DEDUCTION_PER_RULE = 40;
18
+ const DEFAULT_MAX_RULE_DENSITY = 0.5;
19
+ const DEFAULT_DENSITY_K = 0.17;
20
+ const DEFAULT_UNKNOWN_EXPOSURE_CAP = 80;
21
+ const DEFAULT_FAIL_CLOSED_WEIGHT = 0;
22
+ /**
23
+ * Compute the audit score. The score is config-driven by design: disabling
24
+ * or retuning rules changes the findings and therefore the score, and only
25
+ * findings on the exposed surface count (see `Finding.exposed`).
26
+ */
27
+ export function computeScore(findings, config = {}, context = {}) {
28
+ const model = config.model ?? 'density';
29
+ return model === 'weighted'
30
+ ? computeWeightedScore(findings, config, context)
31
+ : computeDensityScore(findings, config, context);
32
+ }
33
+ /**
34
+ * Density scoring: severity-weighted *scorable* findings per exposed table
35
+ * with exponential falloff — does not saturate on large schemas.
36
+ *
37
+ * score = 100 · exp(−k · riskPoints / exposedTables)
38
+ *
39
+ * Scorable findings are those on the exposed surface (`exposed !== false`)
40
+ * that are not fail-closed (fail-closed weights are multiplied by
41
+ * `failClosedWeight`, default 0 — denied-at-runtime hygiene doesn't reduce
42
+ * the score). With the default k, one critical per ~10 exposed tables lands
43
+ * around a C; one critical per table is an F.
44
+ *
45
+ * Two things keep a rule's *shape* out of the arithmetic. Points are charged
46
+ * per unit of repair, not per finding, for rules that declare one — a rule
47
+ * emitting a row per (relation × function) pair costs what fixing it costs.
48
+ * And each rule's total is capped at a fraction of the points that would fail
49
+ * an audit alone (`maxRuleDensity`), so no single rule can decide the grade.
50
+ */
51
+ function computeDensityScore(findings, config, context) {
52
+ const weights = { ...DEFAULT_WEIGHTS, ...(config.weights ?? {}) };
53
+ const perRule = config.perRuleWeights ?? {};
54
+ const bands = { ...DEFAULT_GRADE_BANDS, ...(config.gradeBands ?? {}) };
55
+ const floor = config.floorOnCritical === undefined ? 'C' : config.floorOnCritical;
56
+ const k = config.densityK ?? DEFAULT_DENSITY_K;
57
+ const failClosedWeight = config.failClosedWeight ?? DEFAULT_FAIL_CLOSED_WEIGHT;
58
+ // Meta findings (e.g. W1) are advisories about the audit itself — the
59
+ // unknown-exposure cap is their penalty, not weighted points.
60
+ const scorable = context.prefiltered
61
+ ? findings
62
+ : findings.filter((f) => f.exposed !== false && !f.acknowledged && f.category !== 'meta');
63
+ const exposedTables = Math.max(1, context.exposedTables ?? 1);
64
+ const byRule = new Map();
65
+ for (const f of scorable) {
66
+ let weight = perRule[f.code] ?? weights[f.severity] ?? 0;
67
+ if (f.direction === 'fail-closed')
68
+ weight *= failClosedWeight;
69
+ const entry = byRule.get(f.code) ?? { count: 0, points: 0, units: new Set() };
70
+ entry.count += 1;
71
+ // A rule with a declared unit of repair is charged once per unit: the
72
+ // second finding against the same function is the same fix, and letting
73
+ // it charge again grades fan-out rather than risk.
74
+ const unit = repairUnit(f);
75
+ if (unit === undefined || !entry.units.has(unit)) {
76
+ entry.points += Math.max(0, weight);
77
+ if (unit !== undefined)
78
+ entry.units.add(unit);
79
+ }
80
+ byRule.set(f.code, entry);
81
+ }
82
+ // No single rule may contribute more than `maxRuleDensity` of the points
83
+ // that would take the score to F alone — at the default half, one rule can
84
+ // cost two grade bands but an F still takes breadth. Solved from the curve
85
+ // rather than fixed, so retuning `k` or the bands moves the cap with them.
86
+ const maxRuleDensity = config.maxRuleDensity ?? DEFAULT_MAX_RULE_DENSITY;
87
+ const pointsToF = (Math.log(100 / (bands.D || 50)) / k) * exposedTables;
88
+ const ruleCap = maxRuleDensity === false ? Infinity : maxRuleDensity * pointsToF;
89
+ const capped = new Map();
90
+ for (const [code, tally] of byRule) {
91
+ capped.set(code, {
92
+ count: tally.count,
93
+ points: Math.min(tally.points, ruleCap),
94
+ raw: tally.points,
95
+ ...(tally.units.size > 0 ? { units: tally.units.size } : {})
96
+ });
97
+ }
98
+ const riskPoints = [...capped.values()].reduce((sum, r) => sum + r.points, 0);
99
+ const density = riskPoints / exposedTables;
100
+ const curve = (points) => round1(100 * Math.exp((-k * points) / exposedTables));
101
+ let value = round1(100 * Math.exp(-k * density));
102
+ const deductions = [...capped.entries()]
103
+ .map(([code, { count, points, raw, units }]) => ({
104
+ code,
105
+ count,
106
+ points,
107
+ score: curve(points),
108
+ grade: gradeFor(curve(points), bands),
109
+ potential: round1(curve(riskPoints - points) - value),
110
+ ...(units !== undefined && units !== count ? { units } : {}),
111
+ ...(raw > points ? { capped: true } : {}),
112
+ ...(points === 0 ? { unscored: true } : {})
113
+ }))
114
+ .sort(byPayoff);
115
+ const unknownCap = config.unknownExposureCap ?? DEFAULT_UNKNOWN_EXPOSURE_CAP;
116
+ const cappedByUnknown = context.exposureKnown === false && unknownCap !== false && value > unknownCap;
117
+ if (cappedByUnknown)
118
+ value = unknownCap;
119
+ let grade = gradeFor(value, bands);
120
+ if (floor
121
+ && scorable.some((f) => f.direction !== 'fail-closed' && meetsThreshold(f.severity, 'critical'))) {
122
+ grade = worseOf(grade, floor);
123
+ }
124
+ return {
125
+ value,
126
+ grade,
127
+ model: 'density',
128
+ deductions,
129
+ density: Math.round(density * 1000) / 1000,
130
+ exposedTables,
131
+ ...(cappedByUnknown ? { cappedByUnknownExposure: true } : {})
132
+ };
133
+ }
134
+ /**
135
+ * The finding's unit of repair, from the rule's declared `dedupBy` path.
136
+ * `undefined` when the rule declares none — one finding, one unit.
137
+ */
138
+ function repairUnit(finding) {
139
+ const path = RULES_BY_CODE.get(finding.code)?.dedupBy;
140
+ if (!path)
141
+ return undefined;
142
+ let value = finding;
143
+ for (const key of path.split('.')) {
144
+ if (value === null || typeof value !== 'object')
145
+ return undefined;
146
+ value = value[key];
147
+ }
148
+ return typeof value === 'string' ? value : undefined;
149
+ }
150
+ /**
151
+ * Legacy weighted-deduction scoring (Lighthouse-style): each finding deducts
152
+ * points by severity (or per-rule override), capped per rule so one noisy
153
+ * rule can't zero the score alone. Any critical finding floors the grade
154
+ * (default 'C'). Saturates on large schemas — prefer the density model.
155
+ */
156
+ function computeWeightedScore(findings, config, context) {
157
+ const weights = { ...DEFAULT_WEIGHTS, ...(config.weights ?? {}) };
158
+ const perRule = config.perRuleWeights ?? {};
159
+ const cap = config.maxDeductionPerRule ?? DEFAULT_MAX_DEDUCTION_PER_RULE;
160
+ const bands = { ...DEFAULT_GRADE_BANDS, ...(config.gradeBands ?? {}) };
161
+ const floor = config.floorOnCritical === undefined ? 'C' : config.floorOnCritical;
162
+ const scorable = context.prefiltered
163
+ ? findings
164
+ : findings.filter((f) => f.exposed !== false && !f.acknowledged && f.category !== 'meta');
165
+ const byRule = new Map();
166
+ for (const f of scorable) {
167
+ const weight = perRule[f.code] ?? weights[f.severity] ?? 0;
168
+ const entry = byRule.get(f.code) ?? { count: 0, points: 0 };
169
+ entry.count += 1;
170
+ entry.points += weight;
171
+ byRule.set(f.code, entry);
172
+ }
173
+ const byRuleCapped = [...byRule.entries()].map(([code, { count, points }]) => [code, { count, points: Math.min(points, cap) }]);
174
+ const total = byRuleCapped.reduce((sum, [, r]) => sum + r.points, 0);
175
+ let value = Math.max(0, round1(100 - total));
176
+ const remainder = (points) => Math.max(0, round1(100 - (total - points)));
177
+ const deductions = byRuleCapped
178
+ .map(([code, { count, points }]) => ({
179
+ code,
180
+ count,
181
+ points,
182
+ score: Math.max(0, round1(100 - points)),
183
+ grade: gradeFor(Math.max(0, round1(100 - points)), bands),
184
+ potential: round1(remainder(points) - value),
185
+ ...(points === 0 ? { unscored: true } : {})
186
+ }))
187
+ .sort(byPayoff);
188
+ const unknownCap = config.unknownExposureCap ?? DEFAULT_UNKNOWN_EXPOSURE_CAP;
189
+ const capped = context.exposureKnown === false && unknownCap !== false && value > unknownCap;
190
+ if (capped)
191
+ value = unknownCap;
192
+ let grade = gradeFor(value, bands);
193
+ if (floor && scorable.some((f) => meetsThreshold(f.severity, 'critical'))) {
194
+ grade = worseOf(grade, floor);
195
+ }
196
+ return {
197
+ value,
198
+ grade,
199
+ model: 'weighted',
200
+ deductions,
201
+ ...(capped ? { cappedByUnknownExposure: true } : {})
202
+ };
203
+ }
204
+ function round1(n) {
205
+ return Math.round(n * 10) / 10;
206
+ }
207
+ /**
208
+ * Scored rules first, largest deduction at the top; unscored rules last, in
209
+ * code order. Points and payoff rank identically under a monotone curve, so
210
+ * this keeps the existing "top deductions" output stable.
211
+ */
212
+ function byPayoff(a, b) {
213
+ if (!!a.unscored !== !!b.unscored)
214
+ return a.unscored ? 1 : -1;
215
+ return b.points - a.points || a.code.localeCompare(b.code);
216
+ }
217
+ const GRADE_ORDER = ['A+', 'A', 'B', 'C', 'D', 'F'];
218
+ function gradeFor(value, bands) {
219
+ for (const grade of GRADE_ORDER) {
220
+ if (grade === 'F')
221
+ break;
222
+ if (value >= bands[grade])
223
+ return grade;
224
+ }
225
+ return 'F';
226
+ }
227
+ function worseOf(a, b) {
228
+ return GRADE_ORDER.indexOf(a) >= GRADE_ORDER.indexOf(b) ? a : b;
229
+ }
230
+ /** True when `grade` is at least as good as `minimum`. */
231
+ export function meetsGrade(grade, minimum) {
232
+ return GRADE_ORDER.indexOf(grade) <= GRADE_ORDER.indexOf(minimum);
233
+ }
@@ -0,0 +1,68 @@
1
+ import type { ScorecardConfig, ScorecardSelector } from '../config/types';
2
+ import { type Finding, type Summary } from '../types';
3
+ import { type Score, type ScoreContext } from './score';
4
+ /**
5
+ * One named score over a selected slice of the findings.
6
+ *
7
+ * The headline score answers one question well and every other question
8
+ * badly: a platform team gating on what `anonymous` reaches and an
9
+ * application team gating on house style are not arguing about the same
10
+ * number, and forcing them to is how a single number becomes something
11
+ * nobody trusts. A scorecard is that question, written down — a selector
12
+ * over the findings plus the weighting to grade them with.
13
+ */
14
+ export interface ScorecardReport {
15
+ /** Config key, e.g. `anon-surface`. */
16
+ name: string;
17
+ /** Human-readable heading for reports. */
18
+ title?: string;
19
+ /**
20
+ * Why this scorecard exists — what decision it informs. Rendered with the
21
+ * score, because a number whose question is unstated invites the reader to
22
+ * supply their own.
23
+ */
24
+ description?: string;
25
+ /** The findings it selected, graded by its own weighting. */
26
+ score: Score;
27
+ /** How many findings the selector matched. */
28
+ findings: number;
29
+ /** Severity counts over the selected findings. */
30
+ summary: Summary;
31
+ /** The resolved selector, so a report explains its own arithmetic. */
32
+ select: ScorecardSelector;
33
+ /**
34
+ * Reserved scorecards safegres computes itself: `default` is the headline
35
+ * (`report.score`, verbatim) and `raw` is every finding at its declared
36
+ * severity with no exposure filter. Neither can be redefined into
37
+ * something flattering.
38
+ */
39
+ reserved?: boolean;
40
+ }
41
+ export interface ScorecardContext extends ScoreContext {
42
+ /** Relations on the exposed surface — the default density denominator. */
43
+ exposedTables?: number;
44
+ /** Every relation in the audited schemas — the `all` denominator. */
45
+ totalTables?: number;
46
+ }
47
+ /**
48
+ * The two scorecards every run carries, whatever the config says.
49
+ *
50
+ * `raw` is the honest one: no exposure filter, no acknowledgements, no
51
+ * preset severity retuning, fail-closed findings weighted like anything
52
+ * else. It is not flattering and it is not meant to be — it is the number
53
+ * that is comparable between two databases, and the one a preset cannot
54
+ * talk down.
55
+ */
56
+ export declare const RESERVED_SCORECARDS: Record<string, ScorecardConfig>;
57
+ /**
58
+ * Grade each configured scorecard. `default` is not recomputed — it is the
59
+ * headline score passed in, so a scorecard block can never move the number
60
+ * on the badge by redefining what `default` means.
61
+ */
62
+ export declare function computeScorecards(findings: Finding[], headline: Score, cards: Record<string, ScorecardConfig>, context: ScorecardContext): ScorecardReport[];
63
+ /**
64
+ * The findings a scorecard grades. Every clause is a narrowing filter, so an
65
+ * empty selector selects what the headline does: exposed, non-acknowledged,
66
+ * security findings.
67
+ */
68
+ export declare function selectFindings(findings: Finding[], select: ScorecardSelector): Finding[];
@@ -0,0 +1,157 @@
1
+ import { RULES_BY_CODE } from '../rules/registry';
2
+ import { summarize } from '../types';
3
+ import { computeScore } from './score';
4
+ /**
5
+ * The two scorecards every run carries, whatever the config says.
6
+ *
7
+ * `raw` is the honest one: no exposure filter, no acknowledgements, no
8
+ * preset severity retuning, fail-closed findings weighted like anything
9
+ * else. It is not flattering and it is not meant to be — it is the number
10
+ * that is comparable between two databases, and the one a preset cannot
11
+ * talk down.
12
+ */
13
+ export const RESERVED_SCORECARDS = {
14
+ default: {
15
+ title: 'Safegres score',
16
+ description: 'The headline: exposed, fail-open findings at their configured severities.'
17
+ },
18
+ raw: {
19
+ title: 'Raw score',
20
+ description: 'Every finding at its declared severity, exposure ignored — comparable across databases.',
21
+ select: {
22
+ severities: 'declared',
23
+ exposure: 'all',
24
+ acknowledged: 'include',
25
+ denominator: 'all'
26
+ },
27
+ failClosedWeight: 1,
28
+ maxRuleDensity: false
29
+ }
30
+ };
31
+ /**
32
+ * Grade each configured scorecard. `default` is not recomputed — it is the
33
+ * headline score passed in, so a scorecard block can never move the number
34
+ * on the badge by redefining what `default` means.
35
+ */
36
+ export function computeScorecards(findings, headline, cards, context) {
37
+ const out = [];
38
+ for (const [name, card] of Object.entries(cards)) {
39
+ const reserved = name in RESERVED_SCORECARDS;
40
+ const select = { ...(RESERVED_SCORECARDS[name]?.select ?? {}), ...(card.select ?? {}) };
41
+ if (name === 'default') {
42
+ out.push({
43
+ name,
44
+ ...titleOf(name, card),
45
+ score: headline,
46
+ findings: findings.filter((f) => f.exposed !== false && !f.acknowledged).length,
47
+ summary: summarize(findings.filter((f) => f.exposed !== false && !f.acknowledged)),
48
+ select,
49
+ reserved: true
50
+ });
51
+ continue;
52
+ }
53
+ const selected = selectFindings(findings, select);
54
+ const denominator = select.denominator === 'all'
55
+ ? (context.totalTables ?? context.exposedTables)
56
+ : context.exposedTables;
57
+ out.push({
58
+ name,
59
+ ...titleOf(name, card),
60
+ score: computeScore(selected, card, {
61
+ ...context,
62
+ exposedTables: denominator,
63
+ // The selector has already decided what counts; scoring must not
64
+ // filter again, or `exposure: 'all'` would be silently overruled.
65
+ prefiltered: true
66
+ }),
67
+ findings: selected.length,
68
+ summary: summarize(selected),
69
+ select,
70
+ ...(reserved ? { reserved: true } : {})
71
+ });
72
+ }
73
+ return out;
74
+ }
75
+ function titleOf(name, card) {
76
+ const base = RESERVED_SCORECARDS[name] ?? {};
77
+ const title = card.title ?? base.title;
78
+ const description = card.description ?? base.description;
79
+ return { ...(title ? { title } : {}), ...(description ? { description } : {}) };
80
+ }
81
+ /**
82
+ * The findings a scorecard grades. Every clause is a narrowing filter, so an
83
+ * empty selector selects what the headline does: exposed, non-acknowledged,
84
+ * security findings.
85
+ */
86
+ export function selectFindings(findings, select) {
87
+ const dimension = select.dimension ?? 'security';
88
+ const rules = select.rules?.map(matcher);
89
+ const exclude = select.exclude?.map(matcher);
90
+ const roles = select.roles ? new Set(select.roles) : undefined;
91
+ const planes = select.planes ? new Set(select.planes) : undefined;
92
+ const schemas = select.schemas ? new Set(select.schemas) : undefined;
93
+ const severity = select.minSeverity ? SEVERITY_RANK[select.minSeverity] : undefined;
94
+ const selected = findings.filter((f) => {
95
+ if (f.category === 'meta')
96
+ return false;
97
+ if (dimension !== 'all' && (f.dimension ?? 'security') !== dimension)
98
+ return false;
99
+ if (select.exposure !== 'all' && f.exposed === false)
100
+ return false;
101
+ if (select.acknowledged !== 'include' && f.acknowledged)
102
+ return false;
103
+ if (select.direction && select.direction !== 'any' && f.direction !== select.direction) {
104
+ return false;
105
+ }
106
+ if (rules && !rules.some((m) => m(f.code)))
107
+ return false;
108
+ if (exclude?.some((m) => m(f.code)))
109
+ return false;
110
+ if (schemas && (f.schema === undefined || !schemas.has(f.schema)))
111
+ return false;
112
+ if (roles && !touchesRole(f, roles))
113
+ return false;
114
+ if (planes && !f.planes?.some((p) => planes.has(p)))
115
+ return false;
116
+ if (severity !== undefined && SEVERITY_RANK[f.severity] < severity)
117
+ return false;
118
+ return true;
119
+ });
120
+ // Declared severities answer "how bad is this really", independent of what
121
+ // a preset chose to quiet down. Copies, never mutation: the findings in
122
+ // the report keep the severity the config resolved.
123
+ return select.severities === 'declared'
124
+ ? selected.map((f) => {
125
+ const declared = RULES_BY_CODE.get(f.code)?.defaultSeverity;
126
+ return declared === undefined || declared === f.severity ? f : { ...f, severity: declared };
127
+ })
128
+ : selected;
129
+ }
130
+ const SEVERITY_RANK = {
131
+ info: 0,
132
+ low: 1,
133
+ medium: 2,
134
+ high: 3,
135
+ critical: 4
136
+ };
137
+ /**
138
+ * A role clause asks "is this finding about one of these roles" — which the
139
+ * finding answers in one of three places: the graded `role` column, a
140
+ * grantee list, or the reachability context a lattice rule attached.
141
+ */
142
+ function touchesRole(finding, roles) {
143
+ if (finding.role && roles.has(finding.role))
144
+ return true;
145
+ const context = finding.context;
146
+ if (context?.role && roles.has(context.role))
147
+ return true;
148
+ return Array.isArray(context?.roles)
149
+ && context.roles.some((r) => typeof r === 'string' && roles.has(r));
150
+ }
151
+ /** Rule code matcher: exact, or a `C*` prefix wildcard. */
152
+ function matcher(pattern) {
153
+ if (!pattern.endsWith('*'))
154
+ return (code) => code === pattern;
155
+ const prefix = pattern.slice(0, -1);
156
+ return (code) => code.startsWith(prefix);
157
+ }