@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
package/README.md CHANGED
@@ -1,557 +1,970 @@
1
- # safegres: secure postgres
1
+ # safegres
2
2
 
3
3
  <p align="center" width="100%">
4
- <img height="250" src="https://raw.githubusercontent.com/launchql/launchql/refs/heads/main/assets/outline-logo.svg" />
4
+ <img src="https://raw.githubusercontent.com/Safegres/brand/refs/heads/main/safegres.svg" alt="safegres" width="120" />
5
5
  </p>
6
6
 
7
7
  <p align="center" width="100%">
8
- <a href="https://github.com/launchql/launchql/actions/workflows/run-tests.yaml">
9
- <img height="20" src="https://github.com/launchql/launchql/actions/workflows/run-tests.yaml/badge.svg" />
10
- </a>
11
- <a href="https://github.com/launchql/launchql/blob/main/LICENSE">
12
- <img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/>
13
- </a>
14
- <a href="https://www.npmjs.com/package/pgsql-test">
15
- <img height="20" src="https://img.shields.io/github/package-json/v/launchql/launchql?filename=packages%2Fpgsql-test%2Fpackage.json"/>
8
+ <a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
9
+ <img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
16
10
  </a>
11
+ <a href="https://github.com/constructive-io/constructive/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
12
+ <a href="https://www.npmjs.com/package/safegres"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=packages%2Fsafegres%2Fpackage.json"/></a>
17
13
  </p>
18
14
 
19
- `pgsql-test` gives you instant, isolated PostgreSQL databases for each test — with automatic transaction rollbacks, context switching, and clean seeding. Forget flaky tests and brittle environments. Write real SQL. Get real coverage. Stay fast.
15
+ <p align="center"><strong>Safe <em>and</em> fast Postgres. Graded.</strong></p>
20
16
 
21
- ## Install
17
+ ## Agent skill
22
18
 
23
- ```sh
24
- npm install pgsql-test
25
- ```
19
+ The fastest start: give your coding agent the safegres skill and ask it to audit your database.
26
20
 
27
- ## Features
21
+ ```bash
22
+ npx skills add constructive-io/constructive --skill safegres
23
+ ```
28
24
 
29
- * ⚡ **Instant test DBs** — each one seeded, isolated, and UUID-named
30
- * 🔄 **Per-test rollback** — every test runs in its own transaction or savepoint
31
- * 🛡️ **RLS-friendly** — test with role-based auth via `.setContext()`
32
- * 🌱 **Flexible seeding** — run `.sql` files, programmatic seeds, or even load fixtures
33
- * 🧪 **Compatible with any async runner** — works with `Jest`, `Mocha`, etc.
34
- * 🧹 **Auto teardown** — no residue, no reboots, just clean exits
25
+ ## Install
35
26
 
36
- ### LaunchQL migrations
27
+ ```bash
28
+ npm i -D safegres
29
+ npx safegres doctor # connection, catalog visibility, and what's missing
30
+ npx safegres audit --perf # two scores, from the catalog
31
+ ```
37
32
 
38
- Part of the [LaunchQL](https://github.com/launchql) ecosystem, `pgsql-test` is built to pair seamlessly with our TypeScript-based [Sqitch](https://sqitch.org/) engine rewrite:
33
+ Standard libpq environment (`PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`), a full
34
+ `--connection <url>`, or per-field flags. No agent, no traffic, no application framework, nothing
35
+ executed and nothing written — safe to point at a production replica.
39
36
 
40
- * 🚀 **Lightning-fast migrations** — powered by LaunchQL’s native deployer (10x faster than legacy Sqitch)
41
- * 🔧 **Composable test scaffolds** — integrate with full LaunchQL stacks or use standalone
37
+ *No database handy? See [CI](#ci-in-one-job): one service container plus the migration command you
38
+ already have.*
42
39
 
40
+ ## Features
43
41
 
44
- ## Table of Contents
42
+ * 🛡️ **Complete RLS Auditing** – Grants, RLS enforcement, policy coverage and policy behavior, across every schema and role in the database
43
+ * 🎯 **Operation-Level Coverage** – Every granted `SELECT`/`INSERT`/`UPDATE`/`DELETE` checked against the policies that actually cover it, per role
44
+ * 🔍 **Risky Policy Detection** – Permissive checks, volatile and session-dependent predicates, definer escalation and role bypass, found in the parsed AST rather than by regex
45
+ * ⚡ **Performance on Its Own Axis** – Policy predicates that no index can serve, per-row function calls, missing foreign-key indexes — scored separately, so hardening never reads as a regression
46
+ * 🧭 **Exposure Planes** – The declared API surface is the headline grade; direct-connection and per-role planes are graded beside it, so "what can an anonymous caller reach?" is a number
47
+ * 📊 **Security Scoring & Grades** – Scores, grades, severity counts and per-rule deductions with the payoff of fixing each one
48
+ * 🐘 **Pure PostgreSQL Analysis** – Reads the catalog and parses SQL; no agent, no traffic, no ORM, no application changes, nothing executed
49
+ * ⚙️ **Built for CI** – Summaries, Markdown, JSON and SARIF, GitHub job summaries, annotations and a sticky PR comment, with configurable failure thresholds
50
+ * Δ **Change-Aware Audits** – Compare two reports to show exactly what a branch introduced, fixed or worsened; baselines accept inherited debt and gate only what's new
51
+ * 🧪 **Graded Against a Corpus** – `safegres eval` scores the auditor itself on fixtures with known answers, sealed against config that could game the number
52
+ * 📦 **CLI and Library APIs** – Run it from the command line, or import the analysis, report and renderers into your own tooling
45
53
 
46
- 1. [Install](#install)
47
- 2. [Features](#features)
48
- 3. [Quick Start](#-quick-start)
49
- 4. [`getConnections()` Overview](#getconnections-overview)
50
- 5. [PgTestClient API Overview](#pgtestclient-api-overview)
51
- 6. [Usage Examples](#usage-examples)
52
- * [Basic Setup](#-basic-setup)
53
- * [Role-Based Context](#-role-based-context)
54
- * [Seeding System](#-seeding-system)
55
- * [SQL File Seeding](#-sql-file-seeding)
56
- * [Programmatic Seeding](#-programmatic-seeding)
57
- * [CSV Seeding](#️-csv-seeding)
58
- * [JSON Seeding](#️-json-seeding)
59
- * [Sqitch Seeding](#️-sqitch-seeding)
60
- * [LaunchQL Seeding](#-launchql-seeding)
61
- 7. [`getConnections() Options` ](#getconnections-options)
62
- 8. [Disclaimer](#disclaimer)
54
+ ## What you get
63
55
 
56
+ ```
57
+ safegres 1.16.1 (2026-02-14T09:31:07.884Z)
64
58
 
65
- ## ✨ Quick Start
59
+ exposure: 2 schema(s) via config — 41/318 tables exposed
66
60
 
67
- ```ts
68
- import { getConnections } from 'pgsql-test';
61
+ score: 91.4 (A) — model: density
62
+ top deductions: A2 −10 (×1) R3 −4 (×1)
63
+ by rule: A2 B (+8.4) R3 A (+3.5)
64
+ unscored: A4 (×3) A6 (×7) — zero-weight, fixing these cannot move the score
69
65
 
70
- let db, teardown;
66
+ perf score: 78.1 (C) — model: density
67
+ top deductions: X2 −12 (×3) X1 −8 (×2) X9 −4 (×1)
68
+ by rule: X2 C (+11.2) X1 D (+7.6) X9 B (+3.8)
71
69
 
72
- beforeAll(async () => {
73
- ({ db, teardown } = await getConnections());
74
- await db.query(`SELECT 1`); // ✅ Ready to run queries
75
- });
70
+ summary: 0 critical 1 high 4 medium 3 low 7 info
76
71
 
77
- afterAll(() => teardown());
72
+ [HIGH] A2 app_public.webhook_deliveries
73
+ Roles [authenticated] have grants on app_public.webhook_deliveries but RLS is disabled
74
+ [MED ] X2 app_public.posts (posts_tenant)
75
+ Policy "posts_tenant" on app_public.posts filters by tenant_id, which is not the
76
+ leading column of any index
78
77
  ```
79
78
 
80
- ## `getConnections()` Overview
79
+ Also available as `--format json`, `json-pretty`, `markdown` (job summaries and PR comments) and
80
+ `sarif` (GitHub code scanning).
81
+
82
+ ## How it works
83
+
84
+ safegres is a static analyzer for a live PostgreSQL database. It reads the system catalog and
85
+ parses every policy predicate and function body into an AST, then answers two questions the
86
+ catalog already contains the answers to — *who can actually reach this row*, and *what does
87
+ checking that cost per row* — and scores them on **two independent axes**.
88
+
89
+ Two scores, never mixed. Hardening a table can't show up as a performance regression; adding an
90
+ index can't show up as a security win. That is the whole design: the trade you make between them
91
+ becomes visible instead of netting out to a single number.
92
+
93
+ Three analyses over three different representations. None of them run your queries.
94
+
95
+ **1. The catalog, as a relation.** `pg_class`, `pg_namespace`, `pg_policy`, `pg_roles`,
96
+ `pg_auth_members`, `pg_index` and `aclexplode()` are joined into one snapshot per relation:
97
+ grants, RLS flags, policies with their expressions, indexes, ownership.
98
+
99
+ **2. Policy predicates, as ASTs.** Every `USING` / `WITH CHECK` expression is parsed with
100
+ [`pgsql-parser`](https://github.com/constructive-io/pgsql-parser) (libpg_query — the *actual* PostgreSQL
101
+ grammar, not a regex) and traversed. That is what makes the interesting rules possible: a literal
102
+ `true` in a write policy (A7), a cast wrapping the policy's own column so no plain index can serve
103
+ it (X3), a non-`IMMUTABLE` call whose arguments reference no column of the row and which sits
104
+ outside an uncorrelated scalar sub-select — i.e. a per-row call the planner will not hoist into an
105
+ InitPlan (X9). Structural detection, not a list of known function names: a GUC-reading helper
106
+ somebody adds next year is caught without configuration.
107
+
108
+ **3. Effective access, as a lattice.** ACL rows are not access. A privilege also arrives via
109
+ `GRANT … TO PUBLIC` and via role inheritance (`pg_auth_members`, INHERIT-following), and it is
110
+ only *reachable* if the role also holds `USAGE` on the schema. safegres computes the effective
111
+ cell each `(relation, role, privilege)` triple lands in, with `pg_has_role` membership semantics,
112
+ and reports the ones that are inconsistent in either direction:
113
+
114
+ | grant | RLS | policy | verdict |
115
+ | --- | --- | --- | --- |
116
+ | yes | on | yes | normal — policy-mediated access |
117
+ | yes | on | no | dead grant (L1 indirect; A4/A5 direct) |
118
+ | yes | off | — | unmediated (A2 table-level; L5 per untrusted role, indirect) |
119
+ | no | — | yes | dead policy (L2) |
120
+
121
+ The same closure answers the direct question — *what can role X actually reach?* — as
122
+ `report.roleAccess`, with provenance (`direct`, `PUBLIC`, `member of <role>`) per relation.
123
+
124
+ Two optional analyses go further: `--explain` plans each finding's query shape with
125
+ `EXPLAIN (GENERIC_PLAN)` and lets the planner **refute** the tool's own static claim; `--call-graph`
126
+ computes the transitive closure of function calls from the exposed entry points and enumerates the
127
+ trust boundaries on the way.
128
+
129
+ ## What it checks
130
+
131
+ 45 rules across two dimensions — 34 security, 11 performance — plus four opt-in runtime-statistics
132
+ rules and a four-rule source-level convention linter. The prefix letter is a family, **not** the
133
+ dimension: `P1`/`P1b` are performance, `P5` is security.
134
+
135
+ ### Security (34 rules)
136
+
137
+ | Code | Severity | Direction | Check |
138
+ | --- | --- | --- | --- |
139
+ | A1 | low | fail-closed | RLS enabled but **0 policies** (deny-all — confirm the lock is intended) |
140
+ | A2 | high | fail-open | Grants exist on a table with **RLS disabled** |
141
+ | A3 | low | fail-open | RLS enabled but **`FORCE ROW LEVEL SECURITY` not set** (table-owner bypass) |
142
+ | A4 | low | fail-closed | INSERT/UPDATE/DELETE grant with **no covering policy** — writes denied at runtime |
143
+ | A5 | low | fail-closed | SELECT grant with **no policy** — queries silently return 0 rows |
144
+ | A6 | info | fail-closed | UPDATE has `USING` but **no `WITH CHECK`** (row-smuggling surface) |
145
+ | A7 | critical | fail-open | Trivially-permissive **write** policy (literal `true`) |
146
+ | A8 | low | fail-open | Trivially-permissive **SELECT** policy (`USING (true)`) |
147
+ | P5 | high | fail-open | Policy references **`session_user`** / `current_user` / `pg_has_role(...)` |
148
+ | R1 | critical | fail-open | An **untrusted role** holds a write privilege † |
149
+ | R2 | high | fail-open | A permissive write policy applies to an untrusted role or PUBLIC † |
150
+ | R3 | medium | fail-open | An RLS table has grants **TO PUBLIC** |
151
+ | L1 | low | fail-closed | **Dead indirect grant** — arrives via PUBLIC/inheritance, no policy admits it |
152
+ | L2 | low | fail-closed | **Dead policy** — applies to a role holding no corresponding grant |
153
+ | L3 | low | fail-closed | **Unreachable grant** — object privilege without schema `USAGE` |
154
+ | L4 | info | neutral | **Dead schema `USAGE`** — reaches no relation and no function |
155
+ | L5 | info | fail-open | An untrusted role reaches an **RLS-off table** via PUBLIC/inheritance † |
156
+ | L6 | info | neutral | **Unaddressable grant** — an API role holds privileges on a relation its API cannot name ‡ |
157
+ | L7 | info | fail-open | **Role escalation** — an untrusted role can `SET ROLE` to a role with more reach † |
158
+ | L8 | info | fail-open | **DEFINER view bypass** — an untrusted role reads a base relation, and named columns of it, as the view's owner † |
159
+ | L9 | info | fail-open | **DEFINER view write** — an auto-updatable definer view writes a base relation as its owner † |
160
+ | L10 | info | fail-open | **Rewrite-rule bypass** — a rule on a view writes a relation as the view's owner, `security_invoker` notwithstanding † |
161
+ | L11 | info | fail-open | **Materialized-view snapshot** — stored rows serve an untrusted role what the base relation's grants and policies would not † |
162
+ | L12 | info | fail-open | **Non-barrier filtering view** — a view is an untrusted role's only path to a relation, but its row filter is not a boundary † |
163
+ | L13 | info | fail-open | **Column-level grant** — an untrusted role reaches a relation through `pg_attribute.attacl`, which no relation ACL shows † |
164
+ | L14 | info | neutral | **Unaudited base relation** — a definer view reads a relation in a schema the audit never introspected † |
165
+ | L15 | info | neutral | **Unreadable body** — an untrusted role reaches through a definer view or function whose body the analysis could not follow † |
166
+ | L16 | info | fail-open | **Sequence privilege** — an untrusted role can advance or read a sequence, which no policy filters † |
167
+ | L17 | info | fail-open | **Foreign-table grant** — an untrusted role reaches a relation that cannot carry RLS at all † |
168
+ | L18 | info | fail-open | **Writable filtering view without `WITH CHECK OPTION`** — an untrusted role writes rows the view's own filter excludes † |
169
+ | L19 | info | fail-open | **Definer-function reach** — an untrusted role touches a relation by executing a `SECURITY DEFINER` function, which runs as its owner † |
170
+ | L20 | info | fail-open | **`INSTEAD OF` trigger write** — a write against a view becomes a trigger function's body, and a definer one lands it as the function's owner † |
171
+ | L21 | info | fail-closed | **Revocable grant** — a role holds `EXECUTE` that no reachable path exercises † |
172
+ | W1 | medium | — | **No exposure surface configured** — whole database assumed reachable, score capped |
173
+
174
+ † The untrusted-role rules are no-ops until there is an untrusted role: named outright
175
+ (`"R1": ["critical", { "roles": ["anonymous"] }]`) or resolved from the exposure surface
176
+ (`rolesFrom: "anon"`, which `safegres:recommended` and the stack presets set — see
177
+ [Configuration](#configuration)). They cost nothing on databases without an untrusted-role model.
178
+
179
+ ‡ L6 needs an adapter that can compute [API reach](#api-reach--the-relations-the-api-can-actually-name);
180
+ without one nothing is unaddressable and it never fires.
181
+
182
+ **Exposure is asked per subject.** A relation finding is exposed when the API can address the
183
+ relation. A *routine* finding — the convention rules, L19, L20 — is exposed when an untrusted role
184
+ can **call the function**: `USAGE` on its schema and `EXECUTE` on the function, directly, by
185
+ inheritance, or through Postgres's default `EXECUTE TO PUBLIC`. Grading a definer function by the
186
+ schema it sits in is not conservative, it is wrong in the dangerous direction: the function is
187
+ private precisely so that calling it is the only way in.
188
+
189
+ **Direction is the load-bearing idea.** `fail-open` findings are exposure — the untrusted side
190
+ reaches more than intended. `fail-closed` findings are *denied by Postgres at runtime*: an
191
+ availability and hygiene concern, not a leak. They contribute **zero** to the score by default
192
+ (`scoring.failClosedWeight`). safegres does not cry wolf about a grant the database already refuses.
193
+
194
+ ### Convention (source-level lint, 4 rules — `safegres:constructive`)
195
+
196
+ House-style rules that read function **definitions** (`pg_get_functiondef`), not the catalog. They
197
+ are pure `source → findings` — they come from [`@pgsql/lint`](https://www.npmjs.com/package/@pgsql/lint),
198
+ which has no `pg` dependency — and are **off outside the `safegres:constructive` preset**, which
199
+ enables them. A function on a non-exposed schema costs nothing, like every other rule.
200
+
201
+ | Code | Severity | Direction | Check |
202
+ | --- | --- | --- | --- |
203
+ | C1 | high | fail-open | Function **sets `search_path`** (house rule: never set it — fully-qualify instead) |
204
+ | C2 | medium | neutral | Function uses a **`#variable_conflict`** directive |
205
+ | C3 | low | neutral | Function has an **unqualified relation reference** (relies on `search_path`) |
206
+ | C4 | high | fail-open | Function uses **dynamic SQL** (`EXECUTE` / `EXECUTE … USING` / `FOR … IN EXECUTE`) |
207
+
208
+ C3 ships at `low` (adoption severity — ratchet to error once the tree is clean). C4 cannot be
209
+ statically proven read-only, so every dynamic-SQL site is flagged and must be **waived inline with a
210
+ categorized reason** (`lookup-only`, `codegen`); a reasonless waiver does not suppress it.
211
+
212
+ **Inline suppressions** (ESLint/Prettier style), written as SQL comments inside the body:
213
+
214
+ ```sql
215
+ -- safegres-disable-next-line no-dynamic-sql -- lookup-only: building an IN-list of integers
216
+ EXECUTE format('SELECT ... WHERE id = ANY(%L)', ids);
217
+
218
+ EXECUTE 'REFRESH MATERIALIZED VIEW app.mv'; -- safegres-disable-line no-dynamic-sql -- codegen: fixed DDL
219
+
220
+ -- safegres-disable no-dynamic-sql -- lookup-only: this whole block probes the catalog
221
+ ...
222
+ -- safegres-enable no-dynamic-sql
223
+
224
+ -- safegres-disable-file no-set-search-path -- vendored extension shim
225
+ ```
81
226
 
82
- ```ts
83
- import { getConnections } from 'pgsql-test';
227
+ The `pgsql-lint-` prefix (as written for the standalone `pgsql-lint` CLI) is read the same way, so
228
+ one waiver covers both tools. A directive with no rule id applies to every convention rule. Waived
229
+ findings are **not dropped** — they surface as `acknowledged` (accepted-risk) findings carrying their reason, off the score.
230
+
231
+ ### Performance (11 rules, `--perf`)
232
+
233
+ | Code | Severity | Check |
234
+ | --- | --- | --- |
235
+ | X1 | medium | **Foreign key with no covering index** — joins and cascading deletes seq-scan the child |
236
+ | X2 | medium | **RLS policy filters on a column that leads no index** — the security qual seq-scans, for every caller |
237
+ | X3 | medium | **Policy casts or wraps its own column** (`tenant_id::text = …`) with no matching expression index |
238
+ | X4 | low | **Policy calls a non-LEAKPROOF function** — the qual can't be pushed below joins or subquery scans |
239
+ | X5 | low | **Redundant index** — exact duplicate of, or leading-column prefix of, another |
240
+ | X6 | low | **No primary key** and no usable replica identity |
241
+ | X7 | medium | **Search column with no index the search can use** — `tsvector` without GIN/GiST, `vector` without HNSW/IVFFlat |
242
+ | X8 | info | **Sort-shaped column leads no index** (`timestamptz`/`date`) — heuristic, scores 0 |
243
+ | X9 | medium | **Policy calls a STABLE function per row** — not wrapped in a scalar sub-select, so no InitPlan |
244
+ | P1 | high | Policy body calls a **VOLATILE function** — re-evaluated per row |
245
+ | P1b | medium | Policy body calls a **`SECURITY DEFINER` function** — the planner cannot inline it |
246
+
247
+ X2/X3/X4/X9 are the checks a generic index linter structurally cannot make, because they require
248
+ the policy predicate's AST. RLS quals are evaluated **before** user quals, on every candidate row,
249
+ for every caller — so an unindexed or cast-wrapped policy column is a whole-table tax rather than
250
+ one slow query. This is where the two dimensions physically intersect, and why they belong in one
251
+ tool.
252
+
253
+ X9, measured on 200k rows with a policy function that counts its own invocations:
254
+
255
+ | Policy qual | Calls | Time |
256
+ | --- | --- | --- |
257
+ | `other_id = current_principal_id()` (Filter) | 200,000 | 424 ms |
258
+ | `other_id = (SELECT current_principal_id())` (InitPlan) | 1 | 22 ms |
259
+
260
+ Four more rules (`S1`–`S4`) read runtime counters rather than the schema and are opt-in with
261
+ `--stats`. Full rationale for every rule, including the access-path signals behind X1 and the
262
+ plan-dependence caveat on X9: **[docs/rules.md](https://github.com/constructive-io/constructive/blob/main/packages/safegres/docs/rules.md)**.
263
+
264
+ ## Exposure surface
265
+
266
+ A database-wide score is meaningless if most of the database isn't reachable. Declare the
267
+ **exposure surface** and safegres partitions every finding:
268
+
269
+ - **Exposed** findings (on API-reachable schemas) drive the score.
270
+ - **Internal** findings are reported as unscored advisories (`--exposed-only` hides them).
271
+ - **No exposure configured** → `W1`, and the score is capped at 80/B (`scoring.unknownExposureCap`).
272
+
273
+ ```jsonc
274
+ {
275
+ "exposure": {
276
+ "schemas": ["app_public", "app_hidden"] // static surface
277
+ // or, on a Constructive database:
278
+ // "resolver": "constructive" // introspects routing_public.apis → api_schemas
279
+ }
280
+ }
281
+ ```
84
282
 
85
- // Complete object destructuring
86
- const { pg, db, admin, teardown, manager } = await getConnections();
283
+ The score improves by being **explicit** (declaring exposure and intent) or by **fixing** a leak —
284
+ never by renaming. A `*_public` schema name is not a declaration; the config is.
285
+ CLI: `--exposure-schemas <csv>`, `--exposed-only`.
286
+
287
+ Two related declarations, both of which mark findings *acknowledged* (reported as info, excluded
288
+ from the score): `public.read` for deliberately open reads (pricing tables, a public directory)
289
+ and `perf.ignore` for accepted performance debt. Extension-owned relations are skipped by default,
290
+ and an extension that creates objects at runtime can be skipped wholesale with
291
+ `extensions.ignore` — see [docs/rules.md](https://github.com/constructive-io/constructive/blob/main/packages/safegres/docs/rules.md#extension-objects).
292
+
293
+ ### Planes: the other ways in
294
+
295
+ An API is one way into a database, not the only one. Declare the others as **planes** and each
296
+ gets its own grade — while the headline score stays exactly what it was:
297
+
298
+ ```jsonc
299
+ {
300
+ "exposure": {
301
+ "schemas": ["app_public"],
302
+ "planes": [
303
+ { "name": "direct:reporting", "kind": "role", "roles": ["reporting"] },
304
+ { "name": "internal", "kind": "schema", "schemas": ["app_private"] }
305
+ ]
306
+ }
307
+ }
308
+ ```
87
309
 
88
- // Most common pattern
89
- const { db, teardown } = await getConnections();
310
+ ```
311
+ score 87 (B) security ← still the declared API surface, unchanged
312
+ other access planes — advisory, not part of the score above:
313
+ direct:reporting [role] (reporting): 41 (F) — 12 relation(s), reached via grant
314
+ internal [schema] (app_private): 63 (D) — 9 relation(s)
90
315
  ```
91
316
 
92
- The `getConnections()` helper sets up a fresh PostgreSQL test database and returns a structured object with:
317
+ A role plane is resolved through the same effective-access lattice the `L*` rules use — direct
318
+ grants, `PUBLIC`, and role inheritance — so it answers the question a reviewer actually asks: *if
319
+ this connection string leaks, what does it reach?* Roles with `BYPASSRLS` or superuser are
320
+ reported as not graded rather than given a meaningless F.
93
321
 
94
- * `pg`: a `PgTestClient` connected as the root or superuser — useful for administrative setup or introspection
95
- * `db`: a `PgTestClient` connected as the app-level user — used for running tests with RLS and granted permissions
96
- * `admin`: a `DbAdmin` utility for managing database state, extensions, roles, and templates
97
- * `teardown()`: a function that shuts down the test environment and database pool
98
- * `manager`: a shared connection pool manager (`PgTestConnector`) behind both clients
322
+ Secondary planes never move the headline and never touch finding identity, so adding one cannot
323
+ invalidate a baseline. They gate nothing unless you ask — `failOn.planes` takes a name or glob:
99
324
 
100
- Together, these allow fast, isolated, role-aware test environments with per-test rollback and full control over setup and teardown.
325
+ ```jsonc
326
+ { "failOn": { "grade": "B", "planes": { "direct:*": { "grade": "D" } } } }
327
+ ```
101
328
 
102
- The `PgTestClient` returned by `getConnections()` is a fully-featured wrapper around `pg.Pool`. It provides:
329
+ `--plane direct:reporting` (or `--plane '*'`) expands one in the terminal and markdown output;
330
+ `--format json` always carries them all in `report.planes`.
103
331
 
104
- * Automatic transaction and savepoint management for test isolation
105
- * Easy switching of role-based contexts for RLS testing
106
- * A clean, high-level API for integration testing PostgreSQL systems
332
+ ### Adapters
107
333
 
108
- ## `PgTestClient` API Overview
334
+ Where the exposed surface comes from is an interface, not a hard-coded integration. An adapter is
335
+ an object — no plugin resolution, nothing load-bearing about a package name:
109
336
 
110
337
  ```ts
111
- let pg: PgTestClient;
112
- let teardown: () => Promise<void>;
338
+ // safegres.config.ts
339
+ import type { SafegresConfig } from 'safegres';
340
+ import { constructiveAdapter } from 'safegres/adapters';
113
341
 
114
- beforeAll(async () => {
115
- ({ pg, teardown } = await getConnections());
116
- });
342
+ import { myGatewayAdapter } from './my-gateway-adapter';
343
+
344
+ const config: SafegresConfig = {
345
+ exposure: { adapters: [constructiveAdapter, myGatewayAdapter] }
346
+ };
347
+ export default config;
348
+ ```
117
349
 
118
- beforeEach(() => pg.beforeEach());
119
- afterEach(() => pg.afterEach());
120
- afterAll(() => teardown());
350
+ ```ts
351
+ interface ExposureAdapter {
352
+ name: string;
353
+ detect(exec: QueryExecutor): Promise<boolean>; // is this stack present?
354
+ resolve(exec: QueryExecutor): Promise<PlaneInput[]>; // one or more planes
355
+ reach?(exec: QueryExecutor, ctx: ReachContext): Promise<ApiReach>; // optional precision
356
+ }
121
357
  ```
122
358
 
123
- The `PgTestClient` returned by `getConnections()` wraps a `pg.Client` and provides convenient helpers for query execution, test isolation, and context switching.
359
+ Built-ins ship for `constructive`, `postgrest`, `supabase`, `hasura` and `graphile` — each reading
360
+ the signal its stack actually leaves in the catalog (see [Configuration](#configuration)), and
361
+ each emitting a primary `api` plane plus whatever secondary planes it can prove: one `api:<name>`
362
+ per API for Constructive, a `direct:<authenticator>` role plane for PostgREST, `app_private` as an
363
+ internal plane for graphile-starter. `postgraphile` is the exception: it contributes no plane at
364
+ all and only supplies [reach](#api-reach--the-relations-the-api-can-actually-name). JSON configs may name a built-in
365
+ (`"adapters": ["supabase"]`); anything else is an error rather than a silent no-op — a typo'd
366
+ adapter would otherwise present as an unexposed database. The old `"resolver": "constructive"`
367
+ still works.
368
+
369
+ ### API reach — the relations the API can actually name
370
+
371
+ A plane made of schemas answers *is this relation in the API's schemas?*, which over-counts: a
372
+ generated API exposes fields, and a schema routinely holds relations it deliberately does not
373
+ surface — join tables, denormalized shadows, machine-only back-pointers. `reach()` is where an
374
+ adapter narrows a plane from its schemas to its **relations**. The built-in `postgraphile` adapter
375
+ reads the `@behavior` / `@forwardBehavior` / `@backwardBehavior` smart tags to do it, and both
376
+ `graphile` and `constructive` delegate to it — those two answer *which schemas are served*, which
377
+ is a different question from *what the served schemas expose*:
378
+
379
+ ```jsonc
380
+ { "exposure": { "schemas": ["app_public"], "adapters": ["postgraphile"] } }
381
+ ```
124
382
 
125
- ### Common Methods
383
+ Three properties keep it from quietly deleting findings:
384
+
385
+ - **Only an explicit denial counts.** Presets grant most behaviors by default, so the *absence* of
386
+ `+list` says nothing. Silence is never read as denial.
387
+ - **Unreachable means unreachable by every route.** A relation with no root entry is still
388
+ addressable by traversing a relation field from one that has, so reach is graph traversal over
389
+ foreign keys, not a per-table test. Hiding one reverse relation is one missing path, not proof.
390
+ - **A role plane is never narrowed.** The API not exposing a table says nothing about a role
391
+ holding a direct connection. Behavior only ever refines `api`/`schema` planes.
392
+
393
+ Anything subtracted is listed in `report.exposure.unaddressable` rather than silently dropped, and
394
+ `"reach": false` turns the whole thing off. Where an API-edge role still holds privileges on a
395
+ relation its own API cannot name, **L6** reports the grant — unless some RLS policy predicate
396
+ references the relation, since a grant a policy subqueries under the querying role is load-bearing
397
+ however invisible it is to the API.
398
+
399
+ ## CI in one job
400
+
401
+ One service container, your existing migration command, one audit:
402
+
403
+ ```yaml
404
+ jobs:
405
+ database-audit:
406
+ runs-on: ubuntu-latest
407
+ services:
408
+ postgres:
409
+ image: postgres:16
410
+ env: { POSTGRES_PASSWORD: postgres }
411
+ options: --health-cmd pg_isready --health-interval 5s --health-retries 10
412
+ ports: ['5432:5432']
413
+ env:
414
+ PGHOST: localhost
415
+ PGUSER: postgres
416
+ PGPASSWORD: postgres
417
+ PGDATABASE: app_audit
418
+ steps:
419
+ - uses: actions/checkout@v4
420
+ # …install deps, then apply your schema with whatever already builds it:
421
+ # rails db:schema:load · manage.py migrate · prisma migrate deploy
422
+ # drizzle-kit push · sqitch deploy · pgpm deploy · psql -f schema.sql
423
+ - run: npx safegres audit --perf --format markdown >> "$GITHUB_STEP_SUMMARY"
424
+ - run: |
425
+ npx safegres audit --perf --summary \
426
+ --fail-on-grade B \
427
+ --perf-baseline ci/safegres-perf-baseline.json --fail-on-new-perf
428
+ ```
126
429
 
127
- * `query(sql, values?)` – Run a raw SQL query and get the `QueryResult`
128
- * `beforeEach()` – Begins a transaction and sets a savepoint (called at the start of each test)
129
- * `afterEach()` – Rolls back to the savepoint and commits the outer transaction (cleans up test state)
130
- * `setContext({ key: value })` – Sets PostgreSQL config variables (like `role`) to simulate RLS contexts
131
- * `any`, `one`, `oneOrNone`, `many`, `manyOrNone`, `none`, `result` – Typed query helpers for specific result expectations
430
+ Everything a job repeats every run belongs in the config file instead, so the job is one word. A
431
+ repository whose schema is a pgpm workspace does not need a database at all — `source.pgpm`
432
+ deploys it into an ephemeral one:
433
+
434
+ ```jsonc
435
+ // .safegresrc.json — paths are relative to this file
436
+ {
437
+ "extends": "safegres:constructive",
438
+ "source": { "pgpm": "application/app" },
439
+ "perf": { "enabled": true, "baseline": "ci/perf-baseline.json", "failOnNew": true },
440
+ "outputs": { "dir": "safegres-reports" },
441
+ "failOn": { "grade": "B" }
442
+ }
443
+ ```
132
444
 
133
- These methods make it easier to build expressive and isolated integration tests with strong typing and error handling.
445
+ ```yaml
446
+ - run: npx safegres audit # or: "audit": "safegres lint" in package.json
447
+ ```
134
448
 
135
- The `PgTestClient` returned by `getConnections()` is a fully-featured wrapper around `pg.Pool`. It provides:
449
+ Or the first-party action, which installs the CLI, runs that config, and turns the report into a
450
+ job summary, annotations and a sticky PR comment:
136
451
 
137
- * Automatic transaction and savepoint management for test isolation
138
- * Easy switching of role-based contexts for RLS testing
139
- * A clean, high-level API for integration testing PostgreSQL systems
452
+ ```yaml
453
+ - uses: constructive-io/constructive/packages/safegres@safegres-action-v1
454
+ with:
455
+ comment: true # sticky PR comment (needs pull-requests: write)
456
+ upload-sarif: true # code scanning (needs security-events: write)
457
+ ```
140
458
 
141
- ## Usage Examples
459
+ `safegres-action-v1` is a moving tag on the action's latest v1; `@main` tracks the monorepo tip.
460
+ The reports are uploaded as the `safegres-reports` artifact by default — that artifact *is* the
461
+ next run's baseline, so a job that suppresses it (`upload-reports: false`) can never show a delta.
142
462
 
143
- ### ⚡ Basic Setup
463
+ Auditing a pgpm workspace (`source.pgpm`) adds `version: local`, which runs the repo's own safegres
464
+ rather than installing one: that path deploys through `pgsql-test`, an optional peer only the
465
+ workspace has.
144
466
 
145
- ```ts
146
- import { getConnections } from 'pgsql-test';
467
+ On a pull request it also measures the delta for you: it picks the base branch's most recent run
468
+ that *is* a sane comparison — succeeded, younger than 48 hours, and audited a commit in this
469
+ branch's merge-base history — downloads its report, and names the run it chose in the summary and
470
+ the comment (`actions: read`). "The last successful run" is not that: an unvalidated `gh run list
471
+ --limit 1` once handed a PR a 13.6-day-old baseline and billed it for ~150 merges it never
472
+ contained. Nothing qualifying is not a failure — no deltas, a line saying why, gates unchanged.
147
473
 
148
- let db; // A fully wrapped PgTestClient using pg.Pool with savepoint-based rollback per test
149
- let teardown;
474
+ It exposes `security-score`, `security-grade` and `perf-score` as step outputs — on a failing run
475
+ too, which is when they get read. Everything else stays in the config file; see
476
+ **[docs/reporting.md](https://github.com/constructive-io/constructive/blob/main/packages/safegres/docs/reporting.md#the-action)**.
150
477
 
151
- beforeAll(async () => {
152
- ({ db, teardown } = await getConnections());
478
+ `outputs.dir` writes `safegres.json`, `safegres.md` and `safegres.sarif` into one directory — name
479
+ an individual file (`outputs.json`) only when the name matters. Directories are created as needed,
480
+ and a flag still wins over the file for a one-off run (`safegres audit --out reports`). Naming a
481
+ connection wins too, so the same config audits a database you already have:
482
+ `safegres audit --database staging`.
153
483
 
154
- await db.query(`
155
- CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT);
156
- CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INT REFERENCES users(id), content TEXT);
484
+ Scores lead the markdown, then severity counts, then a table per dimension; internal advisories
485
+ and accepted baseline debt fold into `<details>`. Pipe it to `gh pr comment --body-file -` to post
486
+ it on the pull request instead. Library callers get the same renderer as `renderMarkdown(report)`.
157
487
 
158
- INSERT INTO users (name) VALUES ('Alice'), ('Bob');
159
- INSERT INTO posts (user_id, content) VALUES (1, 'Hello world!'), (2, 'Graphile is cool!');
160
- `);
161
- });
488
+ `--format sarif` turns findings into GitHub code-scanning alerts, and `--compare` renders the
489
+ delta against a previous run's JSON. Both in **[docs/reporting.md](https://github.com/constructive-io/constructive/blob/main/packages/safegres/docs/reporting.md)**.
162
490
 
163
- afterAll(() => teardown());
491
+ ## The ratchet
164
492
 
165
- beforeEach(() => db.beforeEach());
166
- afterEach(() => db.afterEach());
493
+ An established schema will not be clean on the first run — but it can refuse to get worse. Commit
494
+ today's performance findings as accepted debt and gate on what is *not* in the baseline:
167
495
 
168
- test('user count starts at 2', async () => {
169
- const res = await db.query('SELECT COUNT(*) FROM users');
170
- expect(res.rows[0].count).toBe('2');
171
- });
496
+ ```bash
497
+ safegres audit --write-perf-baseline ci/safegres-perf-baseline.json # snapshot (implies --perf)
498
+ safegres audit --perf-baseline ci/safegres-perf-baseline.json # diff: new / accepted / fixed
499
+ safegres audit --perf-baseline ci/safegres-perf-baseline.json --fail-on-new-perf # gate
172
500
  ```
173
501
 
174
- ### 🔐 Role-Based Context
502
+ ```
503
+ performance vs baseline:
175
504
 
505
+ 1 new perf finding since the baseline:
506
+ [medium] X1 app_public.comments — foreign key with no covering index
507
+ (14 accepted, 2 fixed)
508
+ ```
176
509
 
177
- The `pgsql-test` framework provides powerful tools to simulate authentication contexts during tests, which is particularly useful when testing Row-Level Security (RLS) policies.
510
+ Entries are keyed by finding *identity* — `code` + relation + policy + subject (the constraint,
511
+ index, expression, column or function it is about) — never by message text, so rewording a rule or
512
+ retuning a severity in a later release cannot invalidate a committed baseline. Findings that
513
+ disappear are reported as fixed; re-baseline to lock the win in.
178
514
 
179
- #### Setting Test Context
515
+ On an established schema this is the enforceable gate, and a perf *grade* gate is not: a score
516
+ dominated by inherited debt either fails forever or means nothing. Security has no baseline —
517
+ gate it on a grade you can hold today and raise it as the score climbs.
180
518
 
181
- Use `setContext()` to simulate different user roles and JWT claims:
519
+ The same mechanism exists for call-graph trust boundaries (`--write-baseline`, `--baseline`,
520
+ `--fail-on-new-boundaries`).
182
521
 
183
- ```ts
184
- db.setContext({
185
- role: 'authenticated',
186
- 'jwt.claims.user_id': '123',
187
- 'jwt.claims.org_id': 'acme'
188
- });
189
- ```
522
+ ## Sealed runs
190
523
 
191
- This applies the settings using `SET LOCAL` statements, ensuring they persist only for the current transaction and maintain proper isolation between tests.
524
+ Every knob above is deliberate when a team declares its intent in CI, and is the cheapest possible
525
+ cheat when a *score* is the thing being evaluated — of an agent's migration, of a template, of a
526
+ submission. Turning off the rule and re-baselining the debt both raise the number without touching
527
+ the database.
192
528
 
193
- #### Testing Role-Based Access
529
+ So every report carries the ruler it was measured with:
194
530
 
195
- ```ts
196
- describe('authenticated role', () => {
197
- beforeEach(async () => {
198
- db.setContext({ role: 'authenticated' });
199
- await db.beforeEach();
200
- });
201
-
202
- afterEach(() => db.afterEach());
203
-
204
- it('runs as authenticated', async () => {
205
- const res = await db.query(`SELECT current_setting('role', true) AS role`);
206
- expect(res.rows[0].role).toBe('authenticated');
207
- });
208
- });
531
+ ```jsonc
532
+ "provenance": {
533
+ "version": "1.17.0",
534
+ "fingerprint": "sha256:8f14e45fceea167a…", // over the *resolved* rules, overrides,
535
+ "sealed": true, // scoring weights, exposure and ignores
536
+ "preset": "strict"
537
+ }
209
538
  ```
210
539
 
211
- #### Database Connection Options
540
+ `--sealed` grades under a built-in preset alone: no config-file discovery, and `--config`,
541
+ `--rule`, `--exposure-schemas` and every baseline flag are *refused* rather than ignored — a run
542
+ that silently dropped a flag would report a number for rules nobody chose. A harness pins the
543
+ answer it expects:
212
544
 
213
- For non-superuser testing, use the connection options described in the [options](#getconnections-options) section. The `db.connection` property allows you to customize the non-privileged user account for your tests.
545
+ ```bash
546
+ safegres audit --sealed --preset strict --verify-fingerprint sha256:8f14e45fceea167a… --format json
547
+ ```
214
548
 
215
- Use `setContext()` to simulate Role-Based Access Control (RBAC) during tests. This is useful when testing Row-Level Security (RLS) policies. Your actual server should manage role/user claims via secure tokens (e.g., setting `current_setting('jwt.claims.user_id')`), but this interface helps emulate those behaviors in test environments.
549
+ The fingerprint covers the resolved rule set rather than the config text, so it is invariant to how
550
+ a posture was spelled (preset vs. explicit rules, key order) and sensitive to anything that changes
551
+ it — including the safegres version, because a rule's meaning can change without its configuration
552
+ changing. Reports whose fingerprints differ are not comparable, and `--verify-fingerprint` exits
553
+ non-zero rather than let one be read as if it were.
216
554
 
217
- #### Common Testing Scenarios
555
+ None of this constrains ordinary use: an unsealed run still gets a fingerprint, and `sealed: false`
556
+ is simply the honest statement that local configuration participated.
218
557
 
219
- This approach enables testing various access patterns:
220
- - Authenticated vs. anonymous user access
221
- - Per-user data filtering
222
- - Admin privilege bypass behavior
223
- - Custom claim-based restrictions (organization membership, admin status)
558
+ ## The evaluation corpus
224
559
 
225
- > **Note:** While this interface helps simulate RBAC for testing, your production server should manage user/role claims via secure authentication tokens, typically by setting values like `current_setting('jwt.claims.user_id')` through proper authentication middleware.
560
+ A sealed score says the ruler did not move. It does not say the ruler is right. That is what the
561
+ corpus in [`corpus/`](corpus/README.md) is for: small schemas, each with one deliberate flaw and a
562
+ written-down answer — the findings a correct audit must produce, the false positives it must not,
563
+ and the one-sentence fix. Many come in pairs: 18 cases pin what must *not* fire, and six of those
564
+ are the earlier case with its flaw fixed — an answer key consisting entirely of silence and a score
565
+ of 100, because a rule that survives its own fix is worse than a rule that never existed.
226
566
 
227
- ### 🌱 Seeding System
567
+ `safegres eval` is the whole loop in one command: it deploys each case into the connected
568
+ database, audits it under a sealed preset, grades the report against the answer key, drops the
569
+ case's schemas again, and exits non-zero if any case fails.
228
570
 
229
- The second argument to `getConnections()` is an optional array of `SeedAdapter` objects:
571
+ ```console
572
+ $ safegres eval --database scratch
573
+ PASS 01-anon-write-grant security 1.2 (F ) R1
574
+ PASS 17-foreign-key-without-index perf 71.2 (C ) X1
575
+ FAIL 18-policy-column-unindexed perf 100 (A+) missed X2
230
576
 
231
- ```ts
232
- const { db, teardown } = await getConnections(getConnectionOptions, seedAdapters);
577
+ 41/42 cases passed · recall 98% · precision 100%
233
578
  ```
234
579
 
235
- This array lets you fully customize how your test database is seeded. You can compose multiple strategies:
236
-
237
- * [`seed.sqlfile()`](#-sql-file-seeding) – Execute raw `.sql` files from disk
238
- * [`seed.fn()`](#-programmatic-seeding) – Run JavaScript/TypeScript logic to programmatically insert data
239
- * [`seed.csv()`](#️-csv-seeding) – Load tabular data from CSV files
240
- * [`seed.json()`](#️-json-seeding) – Use in-memory objects as seed data
241
- * [`seed.sqitch()`](#️-sqitch-seeding) – Deploy a Sqitch-compatible migration project
242
- * [`seed.launchql()`](#-launchql-seeding) – Apply a LaunchQL module using `deployFast()` (compatible with sqitch)
243
-
244
- > ✨ **Default Behavior:** If no `SeedAdapter[]` is passed, LaunchQL seeding is assumed. This makes `pgsql-test` zero-config for LaunchQL-based projects.
245
-
246
- This composable system allows you to mix-and-match data setup strategies for flexible, realistic, and fast database tests.
247
-
248
- ### 🔌 SQL File Seeding
580
+ | Flag | |
581
+ | --- | --- |
582
+ | `--preset <name>` | the preset every case is graded under (default `recommended`) |
583
+ | `--corpus <dir>` | your own corpus of `<id>/{case.json,schema.sql}` |
584
+ | `--case <id>` | run one case, or an id prefix — comma-separated |
585
+ | `--list` | print the corpus without touching a database |
586
+ | `--json` | the `EvalReport`: per-case recall, precision, score, fingerprint |
587
+ | `--keep` | leave the case schemas behind, to poke at one by hand |
249
588
 
250
- Use `.sql` files to set up your database state before tests:
589
+ A config file may set `eval.corpus`, `eval.preset` and `eval.cases` — *what* to run. It cannot
590
+ retune the rules for a run: cases are always graded by the named preset alone, or the corpus would
591
+ be grading itself. The same loop is a library call (`runEval`), and its pieces are public:
251
592
 
252
593
  ```ts
253
- import path from 'path';
254
- import { getConnections, seed } from 'pgsql-test';
594
+ import { audit, gradeCase, loadConfig, loadCorpus } from 'safegres';
255
595
 
256
- const sql = (f: string) => path.join(__dirname, 'sql', f);
257
-
258
- let db;
259
- let teardown;
260
-
261
- beforeAll(async () => {
262
- ({ db, teardown } = await getConnections({}, [
263
- seed.sqlfile([
264
- sql('schema.sql'),
265
- sql('fixtures.sql')
266
- ])
267
- ]));
268
- });
596
+ const { config } = loadConfig({ sealed: true, preset: 'recommended' });
597
+ for (const c of loadCorpus()) {
598
+ await client.query(c.sql);
599
+ const { missed, falsePositives } = gradeCase(await audit(client, { config, ...c }), c);
600
+ }
601
+ ```
269
602
 
270
- afterAll(async () => {
271
- await teardown();
272
- });
603
+ Cases are data — `schema.sql` plus a `case.json` answer key — so a harness that never runs safegres
604
+ can still use them. Three uses: safegres's own regression suite, worked examples short enough to
605
+ read, and an agent evaluation — hand the agent a case, ask for a fix, and require the expected
606
+ findings to be *gone* with the dimension back at 100 rather than merely a better number.
607
+
608
+ ## Configuration
609
+
610
+ Configured like a linter. Discovered by walking up from the current directory:
611
+ `safegres.config.{ts,js,mjs,cjs}`, `.safegresrc{,.json,.yaml,.yml,.js}`, `safegres.json`, or a
612
+ `"safegres"` key in `package.json` (via
613
+ [confstash](https://github.com/constructive-io/dev-utils/tree/main/packages/confstash)).
614
+ Precedence: **CLI > project config > preset > built-in defaults**.
615
+
616
+ ```jsonc
617
+ // .safegresrc.json
618
+ {
619
+ "extends": "safegres:recommended",
620
+ "exposure": { "schemas": ["app_public"], "roles": ["anonymous", "authenticated"] },
621
+ "public": { "read": ["app_public.plans*", "app_public.event_types"] },
622
+ "extensions": { "ignore": ["pg_partman"] },
623
+ "rules": {
624
+ "A3": "info", // demote — still reported, contributes nothing
625
+ "A5": "high", // retune a severity
626
+ "A7": "off", // disable outright
627
+ "P*": "medium" // prefix wildcard (exact codes win)
628
+ },
629
+ "overrides": [
630
+ { "tables": ["app_public.audit_*"], "rules": { "A2": "off" } }
631
+ ],
632
+ "perf": { "enabled": true, "ignore": ["app_public.audit_*"], "rules": { "X6": "off" } },
633
+ "scoring": { "densityK": 0.17, "maxRuleDensity": 0.5, "unknownExposureCap": 80 },
634
+ "failOn": { "grade": "B", "perfGrade": "B" }
635
+ }
273
636
  ```
274
637
 
275
- ### 🧠 Programmatic Seeding
638
+ A typed `safegres.config.ts` with `defineConfig` from `confstash` works identically.
276
639
 
277
- Use JavaScript functions to insert seed data:
640
+ Presets come in three kinds, and they compose. A **stack** preset knows how your framework
641
+ declares exposure and what its role names mean; a **posture** preset says how harshly to read
642
+ what it finds; both are just partial configs, so `extends` takes an array:
278
643
 
279
- ```ts
280
- import { getConnections, seed } from 'pgsql-test';
644
+ ```jsonc
645
+ { "extends": ["safegres:supabase", "safegres:multi-tenant"] }
646
+ ```
281
647
 
282
- let db;
283
- let teardown;
648
+ | Stack | Resolves exposure from | Treats as untrusted |
649
+ | --- | --- | --- |
650
+ | `safegres:constructive` | `routing_public.apis` → `api_schemas` → `metaschema_public.schema` | `anonymous` |
651
+ | `safegres:postgrest` | `pgrst.db_schemas` in `pg_db_role_setting` | `pgrst.db_anon_role` |
652
+ | `safegres:supabase` | the GUCs if self-hosted, else Supabase's fixed surface | `anon`, `authenticated` |
653
+ | `safegres:hasura` | tracked tables in `hdb_catalog` | `anonymous`, `public` |
654
+ | `safegres:graphile` | the `graphile-starter` layout (`app_public` + `app_hidden`) | `<app>_visitor` |
655
+
656
+ Each reads a real catalog signal — not a schema name. Two carry a caveat worth knowing:
657
+
658
+ - **`graphile`** — PostGraphile's schema list is a process argument that leaves no trace in the
659
+ database, so this preset resolves the starter layout by convention. Naming it *is* the
660
+ declaration that the convention holds; when it doesn't, `exposure.schemas` still wins.
661
+ - **`supabase`** — Supabase configures PostgREST outside the database, so the GUCs are usually
662
+ absent. Its adapter falls back to the platform's fixed surface, but only after proving it is
663
+ looking at Supabase (`auth.users` plus the `anon`/`authenticated`/`service_role` trio). The
664
+ fallback lives in that adapter alone: plain `postgrest` never guesses, and an unconfigured
665
+ PostgREST resolves nothing — reported as unknown exposure, not as a surface.
666
+
667
+ Untrusted roles are usually *resolved*, not named. `pgrst.db_anon_role` and graphile's
668
+ `<app>_visitor` are per-deployment, so those presets point the rules at the surface instead of
669
+ guessing a name:
670
+
671
+ ```jsonc
672
+ { "rules": { "R1": ["critical", { "rolesFrom": "anon" }] } }
673
+ ```
284
674
 
285
- beforeAll(async () => {
286
- ({ db, teardown } = await getConnections({}, [
287
- seed.fn(async ({ pg }) => {
288
- await pg.query(`
289
- INSERT INTO users (name) VALUES ('Seeded User');
290
- `);
291
- })
292
- ]));
293
- });
675
+ An adapter resolves two role sets, because "at the API edge" and "reachable without credentials"
676
+ are different questions:
677
+
678
+ | `rolesFrom` | Roles | Use when |
679
+ | --- | --- | --- |
680
+ | `anon` | Only what an unauthenticated caller arrives as — `apis.anon_role`, `pgrst.db_anon_role`, Supabase's `anon`, graphile's visitor | Almost always. A signed-in role holding a write grant is the product; the anon role holding one is the bug. |
681
+ | `exposure` | Every role at the edge, signed-in ones included | A surface where no role should be writing directly. |
682
+
683
+ Both union with any explicit `roles`, so a preset can name a platform default *and* pick up a
684
+ custom one. Rules that ask for neither stay inert — resolved roles never leak into a rule that
685
+ didn't opt in. `report.exposure.anonRoles` carries the anonymous subset, and the pretty renderer
686
+ marks it inline (`api roles: authenticated, anonymous (anon)`).
687
+
688
+ | Posture | Behavior |
689
+ | --- | --- |
690
+ | `safegres:recommended` | Every rule at its default severity (the no-config behavior) |
691
+ | `safegres:strict` | Everything escalated; fail-closed counts 25%; `failOn: high` |
692
+ | `safegres:multi-tenant` | Row-visibility rules (A1/A2, L1–L3, L5) escalated — in a shared-table database an RLS gap is a cross-tenant read; one critical floors the grade at D |
693
+ | `safegres:oltp` | Perf axis first: the policy-shape rules that turn an index scan into a per-row function call (X2–X4, X9) escalated; `failOn: perfGrade C` |
694
+ | `safegres:minimal` | Structural flags only (A1–A3) — fast smoke check |
695
+
696
+ Presets **retune**, they don't delete: a rule that doesn't apply to a stack is demoted to `info`
697
+ (zero weight, so the score is unchanged) rather than switched off, so it stays in the report and
698
+ stays re-tunable. `minimal` is the deliberate exception — being a smoke check is its whole job.
699
+
700
+ `extends` also takes a **path**, which is how a repository with more than one audit job keeps one
701
+ copy of its rules. The gated PR job and the nightly advisory run against a deployed database
702
+ differ by their gates and their baseline, not by their 19-entry `public.read` list:
703
+
704
+ ```jsonc
705
+ // ci/nightly/.safegresrc.json
706
+ { "extends": "../../safegres.base.json",
707
+ "perf": { "baseline": "ci/nightly/perf.json" },
708
+ "failOn": { "grade": "D" } }
294
709
  ```
295
710
 
296
- ## 🗃️ CSV Seeding
711
+ ### Your own preset
297
712
 
298
- You can load tables from CSV files using `seed.csv({ ... })`. CSV headers must match the table column names exactly. This is useful for loading stable fixture data for integration tests or CI environments.
713
+ A preset is a partial config file, so a team's own preset is just a file it `extends`. That is
714
+ a path inside one repo, or an npm package when several repos share it. A bare name that is not
715
+ a built-in is resolved as an installed package (its `main`, JSON or JS):
299
716
 
300
- ```ts
301
- import path from 'path';
302
- import { getConnections, seed } from 'pgsql-test';
303
-
304
- const csv = (file: string) => path.resolve(__dirname, '../csv', file);
305
-
306
- let db;
307
- let teardown;
308
-
309
- beforeAll(async () => {
310
- ({ db, teardown } = await getConnections({}, [
311
- // Create schema
312
- seed.fn(async ({ pg }) => {
313
- await pg.query(`
314
- CREATE TABLE users (
315
- id SERIAL PRIMARY KEY,
316
- name TEXT NOT NULL
317
- );
318
-
319
- CREATE TABLE posts (
320
- id SERIAL PRIMARY KEY,
321
- user_id INT REFERENCES users(id),
322
- content TEXT NOT NULL
323
- );
324
- `);
325
- }),
326
- // Load from CSV
327
- seed.csv({
328
- users: csv('users.csv'),
329
- posts: csv('posts.csv')
330
- }),
331
- // Adjust SERIAL sequences to avoid conflicts
332
- seed.fn(async ({ pg }) => {
333
- await pg.query(`SELECT setval(pg_get_serial_sequence('users', 'id'), (SELECT MAX(id) FROM users));`);
334
- await pg.query(`SELECT setval(pg_get_serial_sequence('posts', 'id'), (SELECT MAX(id) FROM posts));`);
335
- })
336
- ]));
337
- });
338
-
339
- afterAll(() => teardown());
340
-
341
- it('has loaded rows', async () => {
342
- const res = await db.query('SELECT COUNT(*) FROM users');
343
- expect(+res.rows[0].count).toBeGreaterThan(0);
344
- });
345
- ```
346
-
347
- ## 🗃️ JSON Seeding
348
-
349
- You can seed tables using in-memory JSON objects. This is useful when you want fast, inline fixtures without managing external files.
717
+ ```jsonc
718
+ // @acme/safegres-preset/safegres.json (package.json: "main": "safegres.json")
719
+ {
720
+ "extends": ["safegres:postgrest", "safegres:multi-tenant"],
721
+ "exposure": { "anonRoles": ["web_anon"] },
722
+ "rules": { "A5": "high" },
723
+ "failOn": { "grade": "B" }
724
+ }
725
+ ```
350
726
 
351
- ```ts
352
- import { getConnections, seed } from 'pgsql-test';
353
-
354
- let db;
355
- let teardown;
356
-
357
- beforeAll(async () => {
358
- ({ db, teardown } = await getConnections({}, [
359
- // Create schema
360
- seed.fn(async ({ pg }) => {
361
- await pg.query(`
362
- CREATE SCHEMA custom;
363
- CREATE TABLE custom.users (
364
- id SERIAL PRIMARY KEY,
365
- name TEXT NOT NULL
366
- );
367
-
368
- CREATE TABLE custom.posts (
369
- id SERIAL PRIMARY KEY,
370
- user_id INT REFERENCES custom.users(id),
371
- content TEXT NOT NULL
372
- );
373
- `);
374
- }),
375
- // Seed with in-memory JSON
376
- seed.json({
377
- 'custom.users': [
378
- { id: 1, name: 'Alice' },
379
- { id: 2, name: 'Bob' }
380
- ],
381
- 'custom.posts': [
382
- { id: 1, user_id: 1, content: 'Hello world!' },
383
- { id: 2, user_id: 2, content: 'Graphile is cool!' }
384
- ]
385
- }),
386
- // Fix SERIAL sequences
387
- seed.fn(async ({ pg }) => {
388
- await pg.query(`SELECT setval(pg_get_serial_sequence('custom.users', 'id'), (SELECT MAX(id) FROM custom.users));`);
389
- await pg.query(`SELECT setval(pg_get_serial_sequence('custom.posts', 'id'), (SELECT MAX(id) FROM custom.posts));`);
390
- })
391
- ]));
392
- });
393
-
394
- afterAll(() => teardown());
395
-
396
- it('has loaded rows', async () => {
397
- const res = await db.query('SELECT COUNT(*) FROM custom.users');
398
- expect(+res.rows[0].count).toBeGreaterThan(0);
399
- });
400
- ```
401
-
402
- ## 🏗️ Sqitch Seeding
403
-
404
- *Note: While compatible with Sqitch syntax, LaunchQL uses its own high-performance [TypeScript-based deploy engine.](#-launchql-seeding) that we encourage using for sqitch projects*
405
-
406
- You can seed your test database using a Sqitch project but with significantly improved performance by leveraging LaunchQL's TypeScript deployment engine:
727
+ ```jsonc
728
+ // each project's .safegresrc.json
729
+ { "extends": "@acme/safegres-preset", "public": { "read": ["api.plans"] } }
730
+ ```
407
731
 
408
- ```ts
409
- import path from 'path';
410
- import { getConnections, seed } from 'pgsql-test';
732
+ The project file wins over the preset, key by key; `overrides` unions across the chain.
733
+ `safegres print-config --explain` prints the merged result and where each value came from.
411
734
 
412
- const cwd = path.resolve(__dirname, '../path/to/sqitch');
735
+ A path in an inherited file resolves against **the file that wrote it**, so a base file's
736
+ `"baseline": "ci/perf.json"` keeps meaning the base file's `ci/`, whichever job inherits it —
737
+ the same rule as a discovered config, applied per key. Objects merge per key and arrays replace,
738
+ except `overrides`, which is a list of scoped exceptions and so unions across the chain:
739
+ inheriting a config can't silently drop the exceptions that came with it. `--sealed` reaches none
740
+ of this — it does no discovery at all, so there is no file to extend from.
413
741
 
414
- beforeAll(async () => {
415
- ({ db, teardown } = await getConnections({}, [
416
- seed.sqitch(cwd)
417
- ]));
418
- });
419
- ```
742
+ The file is **validated on load**, against a schema derived from the same declaration the editor
743
+ completes against — an unknown key is an error naming the key it thinks you meant, not a silent
744
+ no-op, because `"failon": { "grade": "B" }` otherwise reads as a passing build rather than as a
745
+ typo. Point an editor at the schema for completion and inline documentation:
420
746
 
421
- This works for any Sqitch-compatible module, now accelerated by LaunchQL's deployment tooling.
747
+ ```jsonc
748
+ { "$schema": "https://raw.githubusercontent.com/constructive-io/constructive/main/packages/safegres/schema/safegres.schema.json" }
749
+ ```
422
750
 
423
- ## 🚀 LaunchQL Seeding
751
+ It also ships in the package (`safegres/schema/safegres.schema.json`), and
752
+ `safegres print-config --schema` writes it to stdout for an offline copy.
424
753
 
425
- If your project uses LaunchQL modules with a precompiled `launchql.plan`, you can use `pgsql-test` with **zero configuration**. Just call `getConnections()` — and it *just works*:
754
+ CLI: `--config <path>`, `--preset <name>`, `--rule CODE=off|severity` (repeatable).
426
755
 
427
- ```ts
428
- import { getConnections } from 'pgsql-test';
756
+ ## Scoring
429
757
 
430
- let db, teardown;
758
+ Each dimension is scored independently by the same function over disjoint finding sets. The
759
+ default **density** model normalizes by the exposed surface, so a large schema doesn't saturate
760
+ to 0/F:
431
761
 
432
- beforeAll(async () => {
433
- ({ db, teardown } = await getConnections()); // 🚀 LaunchQL deployFast() is used automatically - up to 10x faster than traditional Sqitch!
434
- });
762
+ ```
763
+ score = 100 · exp(−k · riskPoints / exposedTables)
435
764
  ```
436
765
 
437
- This works out of the box because `pgsql-test` uses the high-speed `deployFast()` function by default, applying any compiled LaunchQL schema located in the current working directory (`process.cwd()`).
766
+ `riskPoints` is the severity-weighted sum (critical 25, high 10, medium 4, low 1, info 0) of
767
+ *exposed, fail-open, non-acknowledged* findings; `k` defaults to 0.17 (≈ one critical per ten
768
+ exposed tables lands at a C). Non-exposed findings score 0; fail-closed findings score 0 unless
769
+ `scoring.failClosedWeight` is raised; unknown exposure caps the score; any exposed critical floors
770
+ the grade at C (`scoring.floorOnCritical`). Grades: A+ 97 · A 90 · B 80 · C 65 · D 50 · F below.
771
+ The legacy flat-deduction model is `scoring.model: "weighted"`.
772
+
773
+ Two corrections keep a rule's **shape** out of the arithmetic, because a rule's fan-out is a
774
+ property of how it reports, not of how much risk it found.
775
+
776
+ - **Points are charged per unit of repair.** A rule that emits one finding per (relation ×
777
+ function) pair — L19 does, and on one real database that is 853 findings over 166 functions —
778
+ costs what fixing it costs: 166 revokes. The finding count is unchanged in the report; the
779
+ deduction reads `−664 (×853 → 166 fixes)`.
780
+ - **No single rule can decide the grade.** Each rule's contribution is capped at
781
+ `scoring.maxRuleDensity` (default 0.5) of the points that would score an F alone, so one rule
782
+ at its ceiling costs two grade bands and an F still takes breadth. `false` disables it.
783
+
784
+ Every report carries its own arithmetic: per-rule points, grade, and the **payoff** — how far the
785
+ score would move if that rule's findings went away. Gate with `--fail-on <severity>`,
786
+ `--fail-on-score <n>`, `--fail-on-grade <g>` and their `--fail-on-perf-*` counterparts.
787
+
788
+ ### Scorecards: one report, several questions
789
+
790
+ "How secure is this database" is not one question, and a single grade answers it for at most one
791
+ reader. A platform team gates on what an unauthenticated caller reaches; an application team gates
792
+ on house style; a reviewer wants the number no preset softened. A **scorecard** is that question
793
+ written down — a selector over the findings, plus the weighting to grade what it selects:
794
+
795
+ ```jsonc
796
+ {
797
+ "scorecards": {
798
+ "anon-surface": {
799
+ "description": "What an unauthenticated caller reaches.",
800
+ "select": { "roles": ["anonymous"], "direction": "fail-open", "exposure": "all" },
801
+ "perRuleWeights": { "L19": 10 },
802
+ "floorOnCritical": "C"
803
+ },
804
+ "sql-conventions": {
805
+ "select": { "rules": ["C*"], "exposure": "all", "denominator": "all" }
806
+ }
807
+ },
808
+ "failOn": { "scorecards": { "anon-surface": { "grade": "A" } } }
809
+ }
810
+ ```
438
811
 
439
- If you want to specify a custom path to your LaunchQL module, use `seed.launchql()` explicitly:
812
+ Selectors narrow on `rules` (with `C*` wildcards) and `exclude`, `dimension`, `roles`, `planes`,
813
+ `schemas`, `direction`, `minSeverity`, `exposure`, `acknowledged`, `severities` and `denominator`.
814
+ Every scoring key is available per card, and `failOn.scorecards` gates on the one that matters to
815
+ you rather than on somebody else's headline.
816
+
817
+ Two cards always run and cannot be redefined into something flattering:
818
+
819
+ - **`default`** — the Safegres score: the headline, exposure-scoped and preset-tuned, i.e. graded
820
+ the way you actually use the database. Naming it in the config cannot move it.
821
+ - **`raw`** — every finding at its *declared* (registry) severity, exposure ignored,
822
+ acknowledgements included, fail-closed findings weighted like anything else. Not flattering, and
823
+ not meant to be: it is the number that is comparable between two databases and that no
824
+ configuration talked down.
825
+
826
+ A scorecard decides what a number is *about* — never what is reported. `report.findings` is always
827
+ the complete set, so the JSON is the raw data and the scores are queries over it.
828
+
829
+ ## Commands
830
+
831
+ ```bash
832
+ safegres audit # audit the connected database (default command)
833
+ safegres lint # alias for audit, for a package.json script
834
+ safegres perf # audit + the performance dimension (= audit --perf)
835
+ safegres doctor # diagnose config, parser, connection, catalog visibility, exposure
836
+ safegres eval # grade the auditor against a corpus with known answers
837
+ safegres baseline # CI: pick the report this run's delta is measured against
838
+ safegres print-config # the resolved effective config (--explain for per-key provenance)
839
+ ```
440
840
 
841
+ | Group | Flags |
842
+ | --- | --- |
843
+ | Connection | `--connection <url>`, `--host`, `--port`, `--user`, `--password`, `--database`, `--pgpm [dir]` |
844
+ | Config | `--config <path>`, `--preset <name>`, `--rule CODE=sev` |
845
+ | Exposure | `--exposure-schemas <csv>`, `--exposed-only` |
846
+ | Scope | `--schemas`, `--exclude-schemas`, `--roles`, `--exclude-roles`, `--ignore-extensions`, `--audit-extension-owned` |
847
+ | Performance | `--perf`, `--stats`, `--explain`, `--perf-baseline <f>`, `--write-perf-baseline <f>`, `--fail-on-new-perf` |
848
+ | Reporting | `--format pretty\|json\|json-pretty\|markdown\|sarif`, `--out <dir>`, `--sarif-sources <dir>`, `--summary`/`-q`, `--verbose`, `--compare <f>`, `--compare-ref <label>`, `--write-snapshot <f>` |
849
+ | Delta provenance | `--compare-sha <sha>`, `--compare-run-id <id>`, `--compare-run-url <url>`, `--compare-age <age>`, `--compare-skipped <why>` |
850
+ | Call graph | `--call-graph`, `--baseline <f>`, `--write-baseline <f>`, `--fail-on-new-boundaries` |
851
+ | Gating | `--fail-on <severity>`, `--fail-on-score <n>`, `--fail-on-grade <g>`, `--fail-on-perf-score`, `--fail-on-perf-grade`, `--report-only` |
852
+ | Misc | `--skip-ast`, `--no-color`, `--help`, `--version` |
853
+
854
+ The paths among those — `--pgpm`, the two baselines, and every `--write-*` — have config-file
855
+ equivalents (`source.pgpm`, `perf.baseline`, `callGraph.baseline`, `outputs.dir`/`outputs.*`), so CI can carry
856
+ them in version control rather than in a command line.
857
+
858
+ ## Going further
859
+
860
+ - **[docs/rules.md](https://github.com/constructive-io/constructive/blob/main/packages/safegres/docs/rules.md)** — every rule's rationale: the lattice semantics, the access-path
861
+ signals behind X1, the X7/X8/X9 arguments, extension objects.
862
+ - **[docs/reporting.md](https://github.com/constructive-io/constructive/blob/main/packages/safegres/docs/reporting.md)** — SARIF and code scanning, `--compare` deltas and snapshots.
863
+ - **[docs/advanced.md](https://github.com/constructive-io/constructive/blob/main/packages/safegres/docs/advanced.md)** — runtime statistics (`--stats`), planner proof
864
+ (`--explain`), library use, pgpm workspaces.
865
+ - **[docs/call-graph.md](https://github.com/constructive-io/constructive/blob/main/packages/safegres/docs/call-graph.md)** — the trust-boundary checklist (`--call-graph`) and
866
+ its baseline.
867
+
868
+ ## Library use
441
869
 
442
870
  ```ts
443
- import path from 'path';
444
- import { getConnections, seed } from 'pgsql-test';
871
+ import { Client } from 'pg';
872
+ import { getPgEnvOptions } from 'pg-env';
873
+ import { audit, renderPretty } from 'safegres';
445
874
 
446
- const cwd = path.resolve(__dirname, '../path/to/launchql');
875
+ const client = new Client(getPgEnvOptions());
876
+ await client.connect();
447
877
 
448
- beforeAll(async () => {
449
- ({ db, teardown } = await getConnections({}, [
450
- seed.launchql(cwd) // uses deployFast() - up to 10x faster than traditional Sqitch!
451
- ]));
452
- });
878
+ const report = await audit(client, { perf: true });
879
+ console.log(renderPretty(report));
880
+ console.log(report.score.grade, report.perf?.score.grade);
453
881
  ```
454
882
 
455
- ## Why LaunchQL's Approach?
456
-
457
- LaunchQL provides the best of both worlds:
883
+ ## Requirements
458
884
 
459
- 1. **Sqitch Compatibility**: Keep your familiar Sqitch syntax and migration approach
460
- 2. **TypeScript Performance**: Our TS-rewritten deployment engine delivers up to 10x faster schema deployments
461
- 3. **Developer Experience**: Tight feedback loops with near-instant schema setup for tests
462
- 4. **CI Optimization**: Dramatically reduced test suite run times with optimized deployment
885
+ - **PostgreSQL 14+.** `--explain` needs **16+** (`GENERIC_PLAN`); on older servers it reports
886
+ `perf.explain.unavailable` and leaves findings untouched.
887
+ - **A connection.** No extension is required. `pg_stat_statements` is optional and only powers S4;
888
+ its absence is a note in the report, never an error.
889
+ - **Catalog visibility.** Any role can run the audit, but a role that is neither superuser nor
890
+ `BYPASSRLS` may not see every policy and grant — `safegres doctor` says so explicitly. For a
891
+ score you can gate on, connect as the owner (in CI, that is the default anyway).
892
+ - **`--stats` describes a workload**, so it is meaningless against a freshly provisioned CI
893
+ database. The `X*` and `A*`/`L*`/`R*` rules are deterministic and work fine on an empty one.
894
+ - safegres **reads**. It never creates, alters, or drops anything, and `--explain` plans without
895
+ executing.
463
896
 
464
- By maintaining Sqitch compatibility while supercharging performance, LaunchQL enables you to keep your existing migration patterns while enjoying the speed benefits of our TypeScript engine.
897
+ ---
465
898
 
466
- ## `getConnections` Options
899
+ ## Education and Tutorials
467
900
 
468
- This table documents the available options for the `getConnections` function. The options are passed as a combination of `pg` and `db` configuration objects.
901
+ 1. 🚀 [Quickstart: Getting Up and Running](https://constructive.io/learn/quickstart)
902
+ Get started with modular databases in minutes. Install prerequisites and deploy your first module.
469
903
 
470
- ### `db` Options (PgTestConnectionOptions)
904
+ 2. 📦 [Modular PostgreSQL Development with Database Packages](https://constructive.io/learn/modular-postgres)
905
+ Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.
471
906
 
472
- | Option | Type | Default | Description |
473
- | ------------------------ | ---------- | ---------------- | --------------------------------------------------------------------------- |
474
- | `db.extensions` | `string[]` | `[]` | Array of PostgreSQL extensions to include in the test database |
475
- | `db.cwd` | `string` | `process.cwd()` | Working directory used for LaunchQL/Sqitch projects |
476
- | `db.connection.user` | `string` | `'app_user'` | User for simulating RLS via `setContext()` |
477
- | `db.connection.password` | `string` | `'app_password'` | Password for RLS test user |
478
- | `db.connection.role` | `string` | `'anonymous'` | Default role used during `setContext()` |
479
- | `db.template` | `string` | `undefined` | Template database used for faster test DB creation |
480
- | `db.rootDb` | `string` | `'postgres'` | Root database used for administrative operations (e.g., creating databases) |
481
- | `db.prefix` | `string` | `'db-'` | Prefix used when generating test database names |
907
+ 3. ✏️ [Authoring Database Changes](https://constructive.io/learn/authoring-database-changes)
908
+ Master the workflow for adding, organizing, and managing database changes with pgpm.
482
909
 
483
- ### `pg` Options (PgConfig)
910
+ 4. 🧪 [End-to-End PostgreSQL Testing with TypeScript](https://constructive.io/learn/e2e-postgres-testing)
911
+ Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.
484
912
 
485
- Environment variables will override these options when available:
913
+ 5. ⚡ [Supabase Testing](https://constructive.io/learn/supabase)
914
+ Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.
486
915
 
487
- * `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`
916
+ 6. 💧 [Drizzle ORM Testing](https://constructive.io/learn/drizzle-testing)
917
+ Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.
488
918
 
489
- | Option | Type | Default | Description |
490
- | ------------- | -------- | ------------- | ----------------------------------------------- |
491
- | `pg.user` | `string` | `'postgres'` | Superuser for PostgreSQL |
492
- | `pg.password` | `string` | `'password'` | Password for the PostgreSQL superuser |
493
- | `pg.host` | `string` | `'localhost'` | Hostname for PostgreSQL |
494
- | `pg.port` | `number` | `5423` | Port for PostgreSQL |
495
- | `pg.database` | `string` | `'postgres'` | Default database used when connecting initially |
919
+ 7. 🔧 [Troubleshooting](https://constructive.io/learn/troubleshooting)
920
+ Common issues and solutions for pgpm, PostgreSQL, and testing.
496
921
 
497
- ### Usage
922
+ ## Related Constructive Tooling
498
923
 
499
- ```ts
500
- const { conn, db, teardown } = await getConnections({
501
- pg: { user: 'postgres', password: 'secret' },
502
- db: {
503
- extensions: ['uuid-ossp'],
504
- cwd: '/path/to/project',
505
- connection: { user: 'test_user', password: 'secret', role: 'authenticated' },
506
- template: 'test_template',
507
- prefix: 'test_',
508
- rootDb: 'postgres'
509
- }
510
- });
511
- ```
924
+ ### 📦 Package Management
512
925
 
513
- ## Related LaunchQL Tooling
926
+ * [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
514
927
 
515
928
  ### 🧪 Testing
516
929
 
517
- * [launchql/pgsql-test](https://github.com/launchql/launchql/tree/main/packages/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
518
- * [launchql/graphile-test](https://github.com/launchql/launchql/tree/main/packages/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
519
- * [launchql/pg-query-context](https://github.com/launchql/launchql/tree/main/packages/pg-query-context): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
930
+ * [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
931
+ * [pglite-test](https://github.com/constructive-io/constructive/tree/main/postgres/pglite-test): **🪶 Drop-in pgsql-test replacement backed by PGlite** — in-process Postgres, no server required, instance-per-suite isolation.
932
+ * [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
933
+ * [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
934
+ * [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
935
+ * [pg-query-context](https://github.com/constructive-io/constructive/tree/main/postgres/pg-query-context): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
520
936
 
521
937
  ### 🧠 Parsing & AST
522
938
 
523
- * [launchql/pgsql-parser](https://github.com/launchql/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
524
- * [launchql/libpg-query-node](https://github.com/launchql/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
525
- * [launchql/pg-proto-parser](https://github.com/launchql/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
526
- * [@pgsql/enums](https://github.com/launchql/pgsql-parser/tree/main/packages/enums): **🏷️ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
527
- * [@pgsql/types](https://github.com/launchql/pgsql-parser/tree/main/packages/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
528
- * [@pgsql/utils](https://github.com/launchql/pgsql-parser/tree/main/packages/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
529
- * [launchql/pg-ast](https://github.com/launchql/launchql/tree/main/packages/pg-ast): **🔍 Low-level AST tools** and transformations for Postgres query structures.
939
+ * [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
940
+ * [libpg-query-node](https://www.npmjs.com/package/libpg-query): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
941
+ * [pg-proto-parser](https://www.npmjs.com/package/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
942
+ * [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): **🏷️ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
943
+ * [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
944
+ * [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
945
+
946
+ ### 📚 Documentation & Skills
530
947
 
531
- ### 🚀 API & Dev Tools
948
+ * [constructive-skills](https://github.com/constructive-io/constructive-skills): **📖 Platform documentation and AI agent skills** — feature catalog, blueprint reference, SDK guides (i18n, billing, limits, events, uploads, security, entities, search, AI), and deployment guides.
532
949
 
533
- * [launchql/server](https://github.com/launchql/launchql/tree/main/packages/server): **⚡ Express-based API server** powered by PostGraphile to expose a secure, scalable GraphQL API over your Postgres database.
534
- * [launchql/explorer](https://github.com/launchql/launchql/tree/main/packages/explorer): **🔎 Visual API explorer** with GraphiQL for browsing across all databases and schemas—useful for debugging, documentation, and API prototyping.
950
+ Install skills for AI coding agents:
535
951
 
536
- ### 🔁 Streaming & Uploads
952
+ ```bash
953
+ # All platform skills (security, blueprints, codegen, billing, etc.)
954
+ npx skills add constructive-io/constructive-skills
537
955
 
538
- * [launchql/s3-streamer](https://github.com/launchql/launchql/tree/main/packages/s3-streamer): **📤 Direct S3 streaming** for large files with support for metadata injection and content validation.
539
- * [launchql/etag-hash](https://github.com/launchql/launchql/tree/main/packages/etag-hash): **🏷️ S3-compatible ETags** created by streaming and hashing file uploads in chunks.
540
- * [launchql/etag-stream](https://github.com/launchql/launchql/tree/main/packages/etag-stream): **🔄 ETag computation** via Node stream transformer during upload or transfer.
541
- * [launchql/uuid-hash](https://github.com/launchql/launchql/tree/main/packages/uuid-hash): **🆔 Deterministic UUIDs** generated from hashed content, great for deduplication and asset referencing.
542
- * [launchql/uuid-stream](https://github.com/launchql/launchql/tree/main/packages/uuid-stream): **🌊 Streaming UUID generation** based on piped file content—ideal for upload pipelines.
543
- * [launchql/upload-names](https://github.com/launchql/launchql/tree/main/packages/upload-names): **📂 Collision-resistant filenames** utility for structured and unique file names for uploads.
956
+ # Individual repo skills (pgpm, safegres, testing, CLI, search, etc.)
957
+ npx skills add constructive-io/constructive --skill pgpm
958
+ npx skills add constructive-io/constructive --skill safegres
959
+ npx skills add constructive-io/constructive --skill constructive-testing
960
+ ```
544
961
 
545
- ### 🧰 CLI & Codegen
962
+ ## Credits
546
963
 
547
- * [@launchql/cli](https://github.com/launchql/launchql/tree/main/packages/cli): **🖥️ Command-line toolkit** for managing LaunchQL projects—supports database scaffolding, migrations, seeding, code generation, and automation.
548
- * [launchql/launchql-gen](https://github.com/launchql/launchql/tree/main/packages/launchql-gen): **✨ Auto-generated GraphQL** mutations and queries dynamically built from introspected schema data.
549
- * [@launchql/query-builder](https://github.com/launchql/launchql/tree/main/packages/query-builder): **🏗️ SQL constructor** providing a robust TypeScript-based query builder for dynamic generation of `SELECT`, `INSERT`, `UPDATE`, `DELETE`, and stored procedure calls—supports advanced SQL features like `JOIN`, `GROUP BY`, and schema-qualified queries.
550
- * [@launchql/query](https://github.com/launchql/launchql/tree/main/packages/query): **🧩 Fluent GraphQL builder** for PostGraphile schemas. ⚡ Schema-aware via introspection, 🧩 composable and ergonomic for building deeply nested queries.
964
+ **🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
551
965
 
552
966
  ## Disclaimer
553
967
 
554
968
  AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
555
969
 
556
970
  No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
557
-