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