@safegres/test 0.0.1 → 1.30.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (483) hide show
  1. package/README.md +825 -412
  2. package/adapters.d.ts +8 -0
  3. package/adapters.js +18 -0
  4. package/ast/helpers.d.ts +16 -0
  5. package/ast/helpers.js +40 -0
  6. package/ast/parse.d.ts +31 -0
  7. package/ast/parse.js +87 -0
  8. package/ast/walk.d.ts +27 -0
  9. package/ast/walk.js +79 -0
  10. package/callgraph/baseline.d.ts +28 -0
  11. package/callgraph/baseline.js +55 -0
  12. package/callgraph/extract.d.ts +93 -0
  13. package/callgraph/extract.js +340 -0
  14. package/callgraph/graph.d.ts +82 -0
  15. package/callgraph/graph.js +276 -0
  16. package/checks/anti-patterns.d.ts +53 -0
  17. package/checks/anti-patterns.js +247 -0
  18. package/checks/column-grants.d.ts +20 -0
  19. package/checks/column-grants.js +74 -0
  20. package/checks/coverage.d.ts +43 -0
  21. package/checks/coverage.js +110 -0
  22. package/checks/definer-function.d.ts +91 -0
  23. package/checks/definer-function.js +458 -0
  24. package/checks/definer-view.d.ts +167 -0
  25. package/checks/definer-view.js +392 -0
  26. package/checks/indexes.d.ts +82 -0
  27. package/checks/indexes.js +263 -0
  28. package/checks/lattice.d.ts +179 -0
  29. package/checks/lattice.js +487 -0
  30. package/checks/object-acls.d.ts +32 -0
  31. package/checks/object-acls.js +129 -0
  32. package/checks/policy-index.d.ts +78 -0
  33. package/checks/policy-index.js +349 -0
  34. package/checks/revocable-grants.d.ts +140 -0
  35. package/checks/revocable-grants.js +377 -0
  36. package/checks/rls-flags.d.ts +25 -0
  37. package/checks/rls-flags.js +82 -0
  38. package/checks/role-reach.d.ts +329 -0
  39. package/checks/role-reach.js +277 -0
  40. package/checks/role-trust.d.ts +45 -0
  41. package/checks/role-trust.js +91 -0
  42. package/checks/set-role.d.ts +21 -0
  43. package/checks/set-role.js +66 -0
  44. package/checks/stats.d.ts +52 -0
  45. package/checks/stats.js +192 -0
  46. package/checks/view-exposure.d.ts +68 -0
  47. package/checks/view-exposure.js +236 -0
  48. package/checks/view-writes.d.ts +88 -0
  49. package/checks/view-writes.js +350 -0
  50. package/ci/baseline.d.ts +104 -0
  51. package/ci/baseline.js +111 -0
  52. package/ci/github.d.ts +47 -0
  53. package/ci/github.js +197 -0
  54. package/cli/audit.d.ts +3 -0
  55. package/cli/audit.js +560 -0
  56. package/cli/baseline.d.ts +3 -0
  57. package/cli/baseline.js +104 -0
  58. package/cli/commands.d.ts +2 -0
  59. package/cli/commands.js +60 -0
  60. package/cli/doctor.d.ts +3 -0
  61. package/cli/doctor.js +72 -0
  62. package/cli/eval.d.ts +3 -0
  63. package/cli/eval.js +153 -0
  64. package/cli/print-config.d.ts +3 -0
  65. package/cli/print-config.js +54 -0
  66. package/cli/shared.d.ts +43 -0
  67. package/cli/shared.js +153 -0
  68. package/cli.d.ts +2 -0
  69. package/cli.js +69 -0
  70. package/commands/audit.d.ts +65 -0
  71. package/commands/audit.js +779 -0
  72. package/commands/doctor.d.ts +19 -0
  73. package/commands/doctor.js +181 -0
  74. package/commands/eval.d.ts +67 -0
  75. package/commands/eval.js +93 -0
  76. package/config/fingerprint.d.ts +40 -0
  77. package/config/fingerprint.js +71 -0
  78. package/config/loader.d.ts +44 -0
  79. package/config/loader.js +210 -0
  80. package/config/presets.d.ts +95 -0
  81. package/config/presets.js +327 -0
  82. package/config/resolve.d.ts +47 -0
  83. package/config/resolve.js +192 -0
  84. package/config/schema.d.ts +54 -0
  85. package/config/schema.js +269 -0
  86. package/config/types.d.ts +562 -0
  87. package/config/validate.d.ts +14 -0
  88. package/config/validate.js +125 -0
  89. package/corpus/README.md +83 -0
  90. package/corpus/bootstrap.sql +14 -0
  91. package/corpus/cases/01-anon-write-grant/case.json +26 -0
  92. package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
  93. package/corpus/cases/02-grant-to-public/case.json +25 -0
  94. package/corpus/cases/02-grant-to-public/schema.sql +18 -0
  95. package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
  96. package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
  97. package/corpus/cases/04-permissive-write-policy/case.json +30 -0
  98. package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
  99. package/corpus/cases/05-public-read-policy/case.json +25 -0
  100. package/corpus/cases/05-public-read-policy/schema.sql +16 -0
  101. package/corpus/cases/06-update-without-with-check/case.json +25 -0
  102. package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
  103. package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
  104. package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
  105. package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
  106. package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
  107. package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
  108. package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
  109. package/corpus/cases/10-rls-not-forced/case.json +24 -0
  110. package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
  111. package/corpus/cases/11-session-user-in-policy/case.json +24 -0
  112. package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
  113. package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
  114. package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
  115. package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
  116. package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
  117. package/corpus/cases/14-dead-policy/case.json +24 -0
  118. package/corpus/cases/14-dead-policy/schema.sql +18 -0
  119. package/corpus/cases/15-unreachable-grant/case.json +24 -0
  120. package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
  121. package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
  122. package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
  123. package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
  124. package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
  125. package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
  126. package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
  127. package/corpus/cases/19-policy-column-cast/case.json +24 -0
  128. package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
  129. package/corpus/cases/20-stable-function-per-row/case.json +24 -0
  130. package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
  131. package/corpus/cases/21-table-without-primary-key/case.json +24 -0
  132. package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
  133. package/corpus/cases/22-redundant-index/case.json +24 -0
  134. package/corpus/cases/22-redundant-index/schema.sql +16 -0
  135. package/corpus/cases/23-tsvector-without-index/case.json +24 -0
  136. package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
  137. package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
  138. package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
  139. package/corpus/cases/25-definer-view-bypass/case.json +31 -0
  140. package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
  141. package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
  142. package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
  143. package/corpus/cases/27-definer-view-write/case.json +36 -0
  144. package/corpus/cases/27-definer-view-write/schema.sql +40 -0
  145. package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
  146. package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
  147. package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
  148. package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
  149. package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
  150. package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
  151. package/corpus/cases/31-matview-snapshot/case.json +32 -0
  152. package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
  153. package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
  154. package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
  155. package/corpus/cases/33-leaky-filter-view/case.json +36 -0
  156. package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
  157. package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
  158. package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
  159. package/corpus/cases/35-column-grant-projection/case.json +27 -0
  160. package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
  161. package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
  162. package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
  163. package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
  164. package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
  165. package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
  166. package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
  167. package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
  168. package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
  169. package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
  170. package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
  171. package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
  172. package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
  173. package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
  174. package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
  175. package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
  176. package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
  177. package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
  178. package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
  179. package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
  180. package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
  181. package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
  182. package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
  183. package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
  184. package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
  185. package/corpus/cases/41-unreadable-view-body/case.json +33 -0
  186. package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
  187. package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
  188. package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
  189. package/corpus/cases/42-search-column-indexed/case.json +21 -0
  190. package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
  191. package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
  192. package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
  193. package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
  194. package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
  195. package/corpus/cases/45-anon-foreign-table/case.json +27 -0
  196. package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
  197. package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
  198. package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
  199. package/corpus/cases/47-checked-writable-view/case.json +37 -0
  200. package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
  201. package/corpus/cases/48-definer-function-reach/case.json +38 -0
  202. package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
  203. package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
  204. package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
  205. package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
  206. package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
  207. package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
  208. package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
  209. package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
  210. package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
  211. package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
  212. package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
  213. package/corpus/index.d.ts +79 -0
  214. package/corpus/index.js +125 -0
  215. package/esm/adapters.d.ts +8 -0
  216. package/esm/adapters.js +6 -0
  217. package/esm/ast/helpers.d.ts +16 -0
  218. package/esm/ast/helpers.js +36 -0
  219. package/esm/ast/parse.d.ts +31 -0
  220. package/esm/ast/parse.js +82 -0
  221. package/esm/ast/walk.d.ts +27 -0
  222. package/esm/ast/walk.js +72 -0
  223. package/esm/callgraph/baseline.d.ts +28 -0
  224. package/esm/callgraph/baseline.js +48 -0
  225. package/esm/callgraph/extract.d.ts +93 -0
  226. package/esm/callgraph/extract.js +333 -0
  227. package/esm/callgraph/graph.d.ts +82 -0
  228. package/esm/callgraph/graph.js +273 -0
  229. package/esm/checks/anti-patterns.d.ts +53 -0
  230. package/esm/checks/anti-patterns.js +240 -0
  231. package/esm/checks/column-grants.d.ts +20 -0
  232. package/esm/checks/column-grants.js +71 -0
  233. package/esm/checks/coverage.d.ts +43 -0
  234. package/esm/checks/coverage.js +104 -0
  235. package/esm/checks/definer-function.d.ts +91 -0
  236. package/esm/checks/definer-function.js +452 -0
  237. package/esm/checks/definer-view.d.ts +167 -0
  238. package/esm/checks/definer-view.js +382 -0
  239. package/esm/checks/indexes.d.ts +82 -0
  240. package/esm/checks/indexes.js +256 -0
  241. package/esm/checks/lattice.d.ts +179 -0
  242. package/esm/checks/lattice.js +475 -0
  243. package/esm/checks/object-acls.d.ts +32 -0
  244. package/esm/checks/object-acls.js +125 -0
  245. package/esm/checks/policy-index.d.ts +78 -0
  246. package/esm/checks/policy-index.js +342 -0
  247. package/esm/checks/revocable-grants.d.ts +140 -0
  248. package/esm/checks/revocable-grants.js +373 -0
  249. package/esm/checks/rls-flags.d.ts +25 -0
  250. package/esm/checks/rls-flags.js +77 -0
  251. package/esm/checks/role-reach.d.ts +329 -0
  252. package/esm/checks/role-reach.js +270 -0
  253. package/esm/checks/role-trust.d.ts +45 -0
  254. package/esm/checks/role-trust.js +86 -0
  255. package/esm/checks/set-role.d.ts +21 -0
  256. package/esm/checks/set-role.js +63 -0
  257. package/esm/checks/stats.d.ts +52 -0
  258. package/esm/checks/stats.js +184 -0
  259. package/esm/checks/view-exposure.d.ts +68 -0
  260. package/esm/checks/view-exposure.js +231 -0
  261. package/esm/checks/view-writes.d.ts +88 -0
  262. package/esm/checks/view-writes.js +344 -0
  263. package/esm/ci/baseline.d.ts +104 -0
  264. package/esm/ci/baseline.js +107 -0
  265. package/esm/ci/github.d.ts +47 -0
  266. package/esm/ci/github.js +161 -0
  267. package/esm/cli/audit.d.ts +3 -0
  268. package/esm/cli/audit.js +525 -0
  269. package/esm/cli/baseline.d.ts +3 -0
  270. package/esm/cli/baseline.js +102 -0
  271. package/esm/cli/commands.d.ts +2 -0
  272. package/esm/cli/commands.js +53 -0
  273. package/esm/cli/doctor.d.ts +3 -0
  274. package/esm/cli/doctor.js +67 -0
  275. package/esm/cli/eval.d.ts +3 -0
  276. package/esm/cli/eval.js +115 -0
  277. package/esm/cli/print-config.d.ts +3 -0
  278. package/esm/cli/print-config.js +52 -0
  279. package/esm/cli/shared.d.ts +43 -0
  280. package/esm/cli/shared.js +112 -0
  281. package/esm/cli.d.ts +2 -0
  282. package/esm/cli.js +67 -0
  283. package/esm/commands/audit.d.ts +65 -0
  284. package/esm/commands/audit.js +776 -0
  285. package/esm/commands/doctor.d.ts +19 -0
  286. package/esm/commands/doctor.js +178 -0
  287. package/esm/commands/eval.d.ts +67 -0
  288. package/esm/commands/eval.js +90 -0
  289. package/esm/config/fingerprint.d.ts +40 -0
  290. package/esm/config/fingerprint.js +68 -0
  291. package/esm/config/loader.d.ts +44 -0
  292. package/esm/config/loader.js +172 -0
  293. package/esm/config/presets.d.ts +95 -0
  294. package/esm/config/presets.js +324 -0
  295. package/esm/config/resolve.d.ts +47 -0
  296. package/esm/config/resolve.js +180 -0
  297. package/esm/config/schema.d.ts +54 -0
  298. package/esm/config/schema.js +265 -0
  299. package/esm/config/types.d.ts +562 -0
  300. package/esm/config/validate.d.ts +14 -0
  301. package/esm/config/validate.js +122 -0
  302. package/esm/corpus/index.d.ts +79 -0
  303. package/esm/corpus/index.js +85 -0
  304. package/esm/exposure/adapters.d.ts +152 -0
  305. package/esm/exposure/adapters.js +463 -0
  306. package/esm/exposure/planes.d.ts +59 -0
  307. package/esm/exposure/planes.js +144 -0
  308. package/esm/exposure/reach.d.ts +78 -0
  309. package/esm/exposure/reach.js +105 -0
  310. package/esm/exposure/routines.d.ts +36 -0
  311. package/esm/exposure/routines.js +54 -0
  312. package/esm/index.d.ts +96 -0
  313. package/esm/index.js +57 -6
  314. package/esm/lint/index.d.ts +11 -0
  315. package/esm/lint/index.js +10 -0
  316. package/esm/perf/baseline.d.ts +40 -0
  317. package/esm/perf/baseline.js +83 -0
  318. package/esm/perf/explain.d.ts +46 -0
  319. package/esm/perf/explain.js +201 -0
  320. package/esm/pg/acl.d.ts +72 -0
  321. package/esm/pg/acl.js +186 -0
  322. package/esm/pg/behaviors.d.ts +114 -0
  323. package/esm/pg/behaviors.js +206 -0
  324. package/esm/pg/exposure.d.ts +74 -0
  325. package/esm/pg/exposure.js +198 -0
  326. package/esm/pg/functions.d.ts +52 -0
  327. package/esm/pg/functions.js +128 -0
  328. package/esm/pg/indexes.d.ts +192 -0
  329. package/esm/pg/indexes.js +395 -0
  330. package/esm/pg/introspect.d.ts +134 -0
  331. package/esm/pg/introspect.js +274 -0
  332. package/esm/pg/objects.d.ts +37 -0
  333. package/esm/pg/objects.js +108 -0
  334. package/esm/pg/paths.d.ts +114 -0
  335. package/esm/pg/paths.js +159 -0
  336. package/esm/pg/proc.d.ts +23 -0
  337. package/esm/pg/proc.js +55 -0
  338. package/esm/pg/reach-inputs.d.ts +47 -0
  339. package/esm/pg/reach-inputs.js +133 -0
  340. package/esm/pg/roles.d.ts +28 -0
  341. package/esm/pg/roles.js +50 -0
  342. package/esm/pg/stats.d.ts +65 -0
  343. package/esm/pg/stats.js +129 -0
  344. package/esm/pgpm-test.d.ts +22 -0
  345. package/esm/pgpm-test.js +39 -0
  346. package/esm/report/callgraph.d.ts +9 -0
  347. package/esm/report/callgraph.js +74 -0
  348. package/esm/report/compare.d.ts +143 -0
  349. package/esm/report/compare.js +195 -0
  350. package/esm/report/github.d.ts +74 -0
  351. package/esm/report/github.js +290 -0
  352. package/esm/report/json.d.ts +4 -0
  353. package/esm/report/json.js +3 -0
  354. package/esm/report/markdown.d.ts +25 -0
  355. package/esm/report/markdown.js +352 -0
  356. package/esm/report/pretty.d.ts +14 -0
  357. package/esm/report/pretty.js +318 -0
  358. package/esm/report/sarif.d.ts +40 -0
  359. package/esm/report/sarif.js +189 -0
  360. package/esm/report/view.d.ts +82 -0
  361. package/esm/report/view.js +126 -0
  362. package/esm/rules/registry.d.ts +66 -0
  363. package/esm/rules/registry.js +562 -0
  364. package/esm/score/score.d.ts +84 -0
  365. package/esm/score/score.js +233 -0
  366. package/esm/score/scorecards.d.ts +68 -0
  367. package/esm/score/scorecards.js +157 -0
  368. package/esm/types.d.ts +292 -0
  369. package/esm/types.js +26 -0
  370. package/esm/version.d.ts +8 -0
  371. package/esm/version.js +9 -0
  372. package/exposure/adapters.d.ts +152 -0
  373. package/exposure/adapters.js +468 -0
  374. package/exposure/planes.d.ts +59 -0
  375. package/exposure/planes.js +151 -0
  376. package/exposure/reach.d.ts +78 -0
  377. package/exposure/reach.js +109 -0
  378. package/exposure/routines.d.ts +36 -0
  379. package/exposure/routines.js +57 -0
  380. package/index.d.ts +96 -6
  381. package/index.js +229 -6
  382. package/lint/index.d.ts +11 -0
  383. package/lint/index.js +19 -0
  384. package/package.json +54 -48
  385. package/perf/baseline.d.ts +40 -0
  386. package/perf/baseline.js +92 -0
  387. package/perf/explain.d.ts +46 -0
  388. package/perf/explain.js +204 -0
  389. package/pg/acl.d.ts +72 -0
  390. package/pg/acl.js +191 -0
  391. package/pg/behaviors.d.ts +114 -0
  392. package/pg/behaviors.js +217 -0
  393. package/pg/exposure.d.ts +74 -0
  394. package/pg/exposure.js +205 -0
  395. package/pg/functions.d.ts +52 -0
  396. package/pg/functions.js +131 -0
  397. package/pg/indexes.d.ts +192 -0
  398. package/pg/indexes.js +400 -0
  399. package/pg/introspect.d.ts +134 -0
  400. package/pg/introspect.js +280 -0
  401. package/pg/objects.d.ts +37 -0
  402. package/pg/objects.js +111 -0
  403. package/pg/paths.d.ts +114 -0
  404. package/pg/paths.js +166 -0
  405. package/pg/proc.d.ts +23 -0
  406. package/pg/proc.js +58 -0
  407. package/pg/reach-inputs.d.ts +47 -0
  408. package/pg/reach-inputs.js +136 -0
  409. package/pg/roles.d.ts +28 -0
  410. package/pg/roles.js +54 -0
  411. package/pg/stats.d.ts +65 -0
  412. package/pg/stats.js +133 -0
  413. package/pgpm-test.d.ts +22 -0
  414. package/pgpm-test.js +42 -0
  415. package/report/callgraph.d.ts +9 -0
  416. package/report/callgraph.js +81 -0
  417. package/report/compare.d.ts +143 -0
  418. package/report/compare.js +205 -0
  419. package/report/github.d.ts +74 -0
  420. package/report/github.js +333 -0
  421. package/report/json.d.ts +4 -0
  422. package/report/json.js +6 -0
  423. package/report/markdown.d.ts +25 -0
  424. package/report/markdown.js +355 -0
  425. package/report/pretty.d.ts +14 -0
  426. package/report/pretty.js +324 -0
  427. package/report/sarif.d.ts +40 -0
  428. package/report/sarif.js +193 -0
  429. package/report/view.d.ts +82 -0
  430. package/report/view.js +132 -0
  431. package/rules/registry.d.ts +66 -0
  432. package/rules/registry.js +569 -0
  433. package/schema/safegres.schema.json +1119 -0
  434. package/score/score.d.ts +84 -0
  435. package/score/score.js +238 -0
  436. package/score/scorecards.d.ts +68 -0
  437. package/score/scorecards.js +162 -0
  438. package/types.d.ts +292 -0
  439. package/types.js +32 -0
  440. package/version.d.ts +8 -0
  441. package/version.js +12 -0
  442. package/LICENSE +0 -23
  443. package/admin.d.ts +0 -26
  444. package/admin.js +0 -182
  445. package/connect.d.ts +0 -19
  446. package/connect.js +0 -83
  447. package/dist/README.md +0 -557
  448. package/dist/package.json +0 -72
  449. package/esm/admin.js +0 -178
  450. package/esm/connect.js +0 -78
  451. package/esm/manager.js +0 -136
  452. package/esm/roles.js +0 -32
  453. package/esm/seed/adapters.js +0 -23
  454. package/esm/seed/csv.js +0 -44
  455. package/esm/seed/index.js +0 -16
  456. package/esm/seed/json.js +0 -18
  457. package/esm/seed/launchql.js +0 -19
  458. package/esm/seed/sqitch.js +0 -17
  459. package/esm/stream.js +0 -43
  460. package/esm/test-client.js +0 -150
  461. package/manager.d.ts +0 -25
  462. package/manager.js +0 -140
  463. package/roles.d.ts +0 -17
  464. package/roles.js +0 -38
  465. package/seed/adapters.d.ts +0 -4
  466. package/seed/adapters.js +0 -28
  467. package/seed/csv.d.ts +0 -9
  468. package/seed/csv.js +0 -49
  469. package/seed/index.d.ts +0 -16
  470. package/seed/index.js +0 -33
  471. package/seed/json.d.ts +0 -6
  472. package/seed/json.js +0 -21
  473. package/seed/launchql.d.ts +0 -2
  474. package/seed/launchql.js +0 -22
  475. package/seed/sqitch.d.ts +0 -2
  476. package/seed/sqitch.js +0 -20
  477. package/seed/types.d.ts +0 -13
  478. package/stream.d.ts +0 -2
  479. package/stream.js +0 -46
  480. package/test-client.d.ts +0 -49
  481. package/test-client.js +0 -154
  482. /package/{seed → config}/types.js +0 -0
  483. /package/esm/{seed → config}/types.js +0 -0
@@ -0,0 +1,41 @@
1
+ DROP SCHEMA IF EXISTS c_definer_fn CASCADE;
2
+ CREATE SCHEMA c_definer_fn;
3
+
4
+ -- The role the function executes as, and the owner of the table behind it.
5
+ DO $$ BEGIN
6
+ CREATE ROLE c_definer_fn_owner NOLOGIN;
7
+ EXCEPTION WHEN duplicate_object THEN NULL;
8
+ END $$;
9
+
10
+ GRANT USAGE ON SCHEMA c_definer_fn TO corpus_anon, corpus_user, c_definer_fn_owner;
11
+
12
+ CREATE TABLE c_definer_fn.secrets (
13
+ id bigserial PRIMARY KEY,
14
+ subject text NOT NULL,
15
+ body text NOT NULL
16
+ );
17
+ ALTER TABLE c_definer_fn.secrets OWNER TO c_definer_fn_owner;
18
+ ALTER TABLE c_definer_fn.secrets ENABLE ROW LEVEL SECURITY;
19
+
20
+ -- Signed-in users see their own rows. corpus_anon holds no grant on the table
21
+ -- at all, and no policy names it.
22
+ CREATE POLICY secrets_own_rows ON c_definer_fn.secrets
23
+ FOR SELECT TO corpus_user
24
+ USING (subject = (SELECT current_setting('app.subject', true)));
25
+ CREATE INDEX secrets_subject_idx ON c_definer_fn.secrets (subject);
26
+ GRANT SELECT ON c_definer_fn.secrets TO corpus_user;
27
+
28
+ -- The flaw: the function is SECURITY DEFINER, so its body runs as
29
+ -- c_definer_fn_owner — who owns `secrets` and, because RLS is not FORCEd, is
30
+ -- not filtered by its policy. EXECUTE is the only grant corpus_anon needs, and
31
+ -- nothing in the ACL of `secrets` names it. Postgres grants EXECUTE to PUBLIC
32
+ -- by default, so this reach exists even where nobody wrote a grant.
33
+ CREATE FUNCTION c_definer_fn.recent_subjects()
34
+ RETURNS TABLE (id bigint, subject text)
35
+ LANGUAGE sql
36
+ SECURITY DEFINER
37
+ SET search_path TO c_definer_fn, pg_catalog
38
+ AS $$
39
+ SELECT s.id, s.subject FROM c_definer_fn.secrets s ORDER BY s.id DESC LIMIT 20;
40
+ $$;
41
+ ALTER FUNCTION c_definer_fn.recent_subjects() OWNER TO c_definer_fn_owner;
@@ -0,0 +1,32 @@
1
+ {
2
+ "title": "An invoker function over the same body is not a reach edge",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_invoker_fn"
7
+ ],
8
+ "roles": [
9
+ "corpus_anon",
10
+ "corpus_user"
11
+ ],
12
+ "anonRoles": [
13
+ "corpus_anon"
14
+ ]
15
+ },
16
+ "expect": [
17
+ {
18
+ "code": "A3",
19
+ "relation": "c_invoker_fn.secrets",
20
+ "note": "the only real finding left: RLS is not FORCEd, so the table owner still reads every row"
21
+ }
22
+ ],
23
+ "forbid": [
24
+ "L8",
25
+ "L15",
26
+ "L19",
27
+ "L20"
28
+ ],
29
+ "worstSeverity": "low",
30
+ "fix": "ALTER TABLE c_invoker_fn.secrets FORCE ROW LEVEL SECURITY. The function needs no fix — running as the caller is what the definer-function reach (case 48) is fixed *into*.",
31
+ "id": "49-invoker-function-no-reach"
32
+ }
@@ -0,0 +1,38 @@
1
+ DROP SCHEMA IF EXISTS c_invoker_fn CASCADE;
2
+ CREATE SCHEMA c_invoker_fn;
3
+
4
+ DO $$ BEGIN
5
+ CREATE ROLE c_invoker_fn_owner NOLOGIN;
6
+ EXCEPTION WHEN duplicate_object THEN NULL;
7
+ END $$;
8
+
9
+ GRANT USAGE ON SCHEMA c_invoker_fn TO corpus_anon, corpus_user, c_invoker_fn_owner;
10
+
11
+ CREATE TABLE c_invoker_fn.secrets (
12
+ id bigserial PRIMARY KEY,
13
+ subject text NOT NULL,
14
+ body text NOT NULL
15
+ );
16
+ ALTER TABLE c_invoker_fn.secrets OWNER TO c_invoker_fn_owner;
17
+ ALTER TABLE c_invoker_fn.secrets ENABLE ROW LEVEL SECURITY;
18
+
19
+ CREATE POLICY secrets_own_rows ON c_invoker_fn.secrets
20
+ FOR SELECT TO corpus_user
21
+ USING (subject = (SELECT current_setting('app.subject', true)));
22
+ CREATE INDEX secrets_subject_idx ON c_invoker_fn.secrets (subject);
23
+ GRANT SELECT ON c_invoker_fn.secrets TO corpus_user;
24
+
25
+ -- The same body over the same shape, with the one difference that matters:
26
+ -- a function is SECURITY INVOKER unless it says otherwise, so the body runs
27
+ -- with the caller's privileges and corpus_anon is denied on `secrets` exactly
28
+ -- as a direct SELECT would be. There is no reach edge here, and a rule that
29
+ -- reported one would be flagging a correct schema — which is why this case
30
+ -- forbids L19 rather than merely omitting it.
31
+ CREATE FUNCTION c_invoker_fn.recent_subjects()
32
+ RETURNS TABLE (id bigint, subject text)
33
+ LANGUAGE sql
34
+ SET search_path TO c_invoker_fn, pg_catalog
35
+ AS $$
36
+ SELECT s.id, s.subject FROM c_invoker_fn.secrets s ORDER BY s.id DESC LIMIT 20;
37
+ $$;
38
+ ALTER FUNCTION c_invoker_fn.recent_subjects() OWNER TO c_invoker_fn_owner;
@@ -0,0 +1,38 @@
1
+ {
2
+ "title": "Anonymous role writes a locked table through an INSTEAD OF trigger's definer function",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_trigger_write"
7
+ ],
8
+ "roles": [
9
+ "corpus_anon",
10
+ "corpus_user"
11
+ ],
12
+ "anonRoles": [
13
+ "corpus_anon"
14
+ ]
15
+ },
16
+ "expect": [
17
+ {
18
+ "code": "L20",
19
+ "relation": "c_trigger_write.submissions",
20
+ "role": "corpus_anon",
21
+ "note": "the write never reaches `submissions` directly — it becomes the trigger function's body, which is SECURITY DEFINER, so it lands as c_trigger_write_owner. This is the write L9 suppressed as unfollowable"
22
+ },
23
+ {
24
+ "code": "A3",
25
+ "relation": "c_trigger_write.submissions",
26
+ "note": "RLS is not FORCEd, which is why the owner's insert inside the trigger body is not checked against the table's own policy"
27
+ }
28
+ ],
29
+ "forbid": [
30
+ "L8",
31
+ "L9",
32
+ "L15",
33
+ "L19"
34
+ ],
35
+ "worstSeverity": "low",
36
+ "fix": "Make c_trigger_write.tg_submission_inbox() SECURITY INVOKER so the caller's own grants apply, authorize the caller inside the body, or give the function an owner whose reach matches what the view is meant to accept. Revoking corpus_anon's INSERT on the view is not the fix — that grant is what the API serves.",
37
+ "id": "50-instead-of-trigger-write"
38
+ }
@@ -0,0 +1,58 @@
1
+ DROP SCHEMA IF EXISTS c_trigger_write CASCADE;
2
+ CREATE SCHEMA c_trigger_write;
3
+
4
+ DO $$ BEGIN
5
+ CREATE ROLE c_trigger_write_owner NOLOGIN;
6
+ EXCEPTION WHEN duplicate_object THEN NULL;
7
+ END $$;
8
+
9
+ GRANT USAGE ON SCHEMA c_trigger_write TO corpus_anon, corpus_user, c_trigger_write_owner;
10
+
11
+ CREATE TABLE c_trigger_write.submissions (
12
+ id bigserial PRIMARY KEY,
13
+ author text NOT NULL,
14
+ body text NOT NULL
15
+ );
16
+ ALTER TABLE c_trigger_write.submissions OWNER TO c_trigger_write_owner;
17
+ ALTER TABLE c_trigger_write.submissions ENABLE ROW LEVEL SECURITY;
18
+
19
+ CREATE POLICY submissions_own_rows ON c_trigger_write.submissions
20
+ FOR ALL TO corpus_user
21
+ USING (author = (SELECT current_setting('app.author', true)))
22
+ WITH CHECK (author = (SELECT current_setting('app.author', true)));
23
+ CREATE INDEX submissions_author_idx ON c_trigger_write.submissions (author);
24
+ GRANT SELECT, INSERT ON c_trigger_write.submissions TO corpus_user;
25
+ GRANT USAGE ON SEQUENCE c_trigger_write.submissions_id_seq TO corpus_user;
26
+
27
+ -- The flaw, in the function rather than the view: an INSTEAD OF trigger
28
+ -- replaces the write against the view with this body, and the body is
29
+ -- permission-checked against whoever the *function* runs as. SECURITY DEFINER
30
+ -- makes that c_trigger_write_owner, so corpus_anon's INSERT on the view lands
31
+ -- in `submissions` under the table owner. The view's own owner and its
32
+ -- security_invoker setting do not enter into it.
33
+ CREATE FUNCTION c_trigger_write.tg_submission_inbox()
34
+ RETURNS trigger
35
+ LANGUAGE plpgsql
36
+ SECURITY DEFINER
37
+ SET search_path TO c_trigger_write, pg_catalog
38
+ AS $$
39
+ BEGIN
40
+ INSERT INTO c_trigger_write.submissions (author, body) VALUES (NEW.author, NEW.body);
41
+ RETURN NEW;
42
+ END;
43
+ $$;
44
+ ALTER FUNCTION c_trigger_write.tg_submission_inbox() OWNER TO c_trigger_write_owner;
45
+
46
+ -- security_invoker, precisely so the finding cannot be attributed to the view:
47
+ -- the escalation is the trigger function's, and L9's auto-update path does not
48
+ -- apply to a view carrying INSTEAD OF triggers at all.
49
+ CREATE VIEW c_trigger_write.submission_inbox
50
+ WITH (security_invoker = true) AS
51
+ SELECT id, author, body FROM c_trigger_write.submissions;
52
+ ALTER VIEW c_trigger_write.submission_inbox OWNER TO c_trigger_write_owner;
53
+
54
+ CREATE TRIGGER submission_inbox_insert
55
+ INSTEAD OF INSERT ON c_trigger_write.submission_inbox
56
+ FOR EACH ROW EXECUTE FUNCTION c_trigger_write.tg_submission_inbox();
57
+
58
+ GRANT INSERT ON c_trigger_write.submission_inbox TO corpus_anon;
@@ -0,0 +1,33 @@
1
+ {
2
+ "title": "An INSTEAD OF trigger whose function runs as the caller is not a write bypass",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_invoker_trigger"
7
+ ],
8
+ "roles": [
9
+ "corpus_anon",
10
+ "corpus_user"
11
+ ],
12
+ "anonRoles": [
13
+ "corpus_anon"
14
+ ]
15
+ },
16
+ "expect": [
17
+ {
18
+ "code": "A3",
19
+ "relation": "c_invoker_trigger.submissions",
20
+ "note": "the only real finding left: RLS is not FORCEd, so the table owner still writes every row"
21
+ }
22
+ ],
23
+ "forbid": [
24
+ "L8",
25
+ "L9",
26
+ "L15",
27
+ "L19",
28
+ "L20"
29
+ ],
30
+ "worstSeverity": "low",
31
+ "fix": "ALTER TABLE c_invoker_trigger.submissions FORCE ROW LEVEL SECURITY. Neither the trigger nor the view needs a fix — an invoker trigger function is what case 50 is fixed *into*, and the view being a definer changes nothing, because it is the function's security attribute that decides who the write runs as.",
32
+ "id": "51-invoker-trigger-no-write"
33
+ }
@@ -0,0 +1,54 @@
1
+ DROP SCHEMA IF EXISTS c_invoker_trigger CASCADE;
2
+ CREATE SCHEMA c_invoker_trigger;
3
+
4
+ DO $$ BEGIN
5
+ CREATE ROLE c_invoker_trigger_owner NOLOGIN;
6
+ EXCEPTION WHEN duplicate_object THEN NULL;
7
+ END $$;
8
+
9
+ GRANT USAGE ON SCHEMA c_invoker_trigger
10
+ TO corpus_anon, corpus_user, c_invoker_trigger_owner;
11
+
12
+ CREATE TABLE c_invoker_trigger.submissions (
13
+ id bigserial PRIMARY KEY,
14
+ author text NOT NULL,
15
+ body text NOT NULL
16
+ );
17
+ ALTER TABLE c_invoker_trigger.submissions OWNER TO c_invoker_trigger_owner;
18
+ ALTER TABLE c_invoker_trigger.submissions ENABLE ROW LEVEL SECURITY;
19
+
20
+ CREATE POLICY submissions_own_rows ON c_invoker_trigger.submissions
21
+ FOR ALL TO corpus_user
22
+ USING (author = (SELECT current_setting('app.author', true)))
23
+ WITH CHECK (author = (SELECT current_setting('app.author', true)));
24
+ CREATE INDEX submissions_author_idx ON c_invoker_trigger.submissions (author);
25
+ GRANT SELECT, INSERT ON c_invoker_trigger.submissions TO corpus_user;
26
+ GRANT USAGE ON SEQUENCE c_invoker_trigger.submissions_id_seq TO corpus_user;
27
+
28
+ -- The same trigger over the same shape, with the one difference that matters:
29
+ -- the trigger function is SECURITY INVOKER, so its body runs as the caller and
30
+ -- Postgres denies corpus_anon on `submissions` exactly as it would deny a
31
+ -- direct insert. The view here is *not* security_invoker, which is the point:
32
+ -- a view's owner does not decide who a trigger function's body runs as, and a
33
+ -- rule that read the view's setting instead of the function's would fire here.
34
+ CREATE FUNCTION c_invoker_trigger.tg_submission_inbox()
35
+ RETURNS trigger
36
+ LANGUAGE plpgsql
37
+ SET search_path TO c_invoker_trigger, pg_catalog
38
+ AS $$
39
+ BEGIN
40
+ INSERT INTO c_invoker_trigger.submissions (author, body) VALUES (NEW.author, NEW.body);
41
+ RETURN NEW;
42
+ END;
43
+ $$;
44
+ ALTER FUNCTION c_invoker_trigger.tg_submission_inbox() OWNER TO c_invoker_trigger_owner;
45
+
46
+ CREATE VIEW c_invoker_trigger.submission_inbox AS
47
+ SELECT id, author, body FROM c_invoker_trigger.submissions;
48
+ ALTER VIEW c_invoker_trigger.submission_inbox OWNER TO c_invoker_trigger_owner;
49
+
50
+ CREATE TRIGGER submission_inbox_insert
51
+ INSTEAD OF INSERT ON c_invoker_trigger.submission_inbox
52
+ FOR EACH ROW EXECUTE FUNCTION c_invoker_trigger.tg_submission_inbox();
53
+
54
+ GRANT INSERT ON c_invoker_trigger.submission_inbox TO corpus_anon;
@@ -0,0 +1,30 @@
1
+ {
2
+ "title": "A role holds EXECUTE on a trigger function it can neither call nor fire — revocable",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_revocable"
7
+ ],
8
+ "roles": [
9
+ "corpus_anon",
10
+ "corpus_user"
11
+ ],
12
+ "anonRoles": [
13
+ "corpus_anon"
14
+ ]
15
+ },
16
+ "expect": [
17
+ {
18
+ "code": "L21",
19
+ "relation": "c_revocable.stamp_created_by",
20
+ "role": "corpus_anon",
21
+ "note": "corpus_anon's EXECUTE arrives from a blanket default-privileges grant, but the reachability closure never lands on the function: a trigger function is not directly callable, and corpus_anon holds no write grant on `events` to fire the trigger. Granted minus reachable is non-empty, so the grant is revocable."
22
+ }
23
+ ],
24
+ "forbid": [
25
+ "L19",
26
+ "L20"
27
+ ],
28
+ "fix": "REVOKE EXECUTE ON FUNCTION c_revocable.stamp_created_by() FROM corpus_anon. No path exercises the grant — corpus_anon cannot call a trigger function directly, and cannot fire the trigger without a write grant on c_revocable.events — so dropping it changes nothing corpus_anon can do.",
29
+ "id": "52-revocable-execute-grant"
30
+ }
@@ -0,0 +1,45 @@
1
+ DROP SCHEMA IF EXISTS c_revocable CASCADE;
2
+ CREATE SCHEMA c_revocable;
3
+
4
+ DO $$ BEGIN
5
+ CREATE ROLE c_revocable_owner NOLOGIN;
6
+ EXCEPTION WHEN duplicate_object THEN NULL;
7
+ END $$;
8
+
9
+ GRANT USAGE ON SCHEMA c_revocable TO corpus_anon, corpus_user, c_revocable_owner;
10
+
11
+ CREATE TABLE c_revocable.events (
12
+ id bigserial PRIMARY KEY,
13
+ body text NOT NULL
14
+ );
15
+ ALTER TABLE c_revocable.events OWNER TO c_revocable_owner;
16
+ ALTER TABLE c_revocable.events ENABLE ROW LEVEL SECURITY;
17
+ ALTER TABLE c_revocable.events FORCE ROW LEVEL SECURITY;
18
+
19
+ -- corpus_anon may read, nothing more.
20
+ CREATE POLICY events_read ON c_revocable.events
21
+ FOR SELECT TO corpus_anon USING (id > 0);
22
+ GRANT SELECT ON c_revocable.events TO corpus_anon;
23
+
24
+ -- A SECURITY DEFINER trigger function. It fires only on write, and corpus_anon
25
+ -- holds no write grant on `events`, so it can never make the trigger run. A
26
+ -- trigger function is not directly callable either: Postgres refuses a direct
27
+ -- call however wide its EXECUTE ACL is.
28
+ CREATE FUNCTION c_revocable.stamp_created_by()
29
+ RETURNS trigger
30
+ LANGUAGE plpgsql
31
+ AS $$
32
+ BEGIN
33
+ RETURN NEW;
34
+ END;
35
+ $$;
36
+ ALTER FUNCTION c_revocable.stamp_created_by() OWNER TO c_revocable_owner;
37
+
38
+ CREATE TRIGGER stamp_created_by BEFORE INSERT ON c_revocable.events
39
+ FOR EACH ROW EXECUTE FUNCTION c_revocable.stamp_created_by();
40
+
41
+ -- The blanket grant: corpus_anon gets EXECUTE the way a schema created with
42
+ -- `ALTER DEFAULT PRIVILEGES ... GRANT ALL ON FUNCTIONS TO anonymous` hands it
43
+ -- out. corpus_anon now holds EXECUTE on a trigger function it can neither call
44
+ -- nor fire — a grant `granted − reachable` should prove revocable.
45
+ GRANT EXECUTE ON FUNCTION c_revocable.stamp_created_by() TO corpus_anon;
@@ -0,0 +1,23 @@
1
+ {
2
+ "title": "A grant that looks dead but is reached through an RLS policy predicate — retained, not revocable",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_policy_reach",
7
+ "c_policy_priv"
8
+ ],
9
+ "roles": [
10
+ "corpus_anon",
11
+ "corpus_user"
12
+ ],
13
+ "anonRoles": [
14
+ "corpus_anon"
15
+ ]
16
+ },
17
+ "expect": [],
18
+ "forbid": [
19
+ "L21"
20
+ ],
21
+ "fix": "Do not revoke. corpus_anon's EXECUTE on c_policy_priv.can_read(bigint) is exercised by the docs_visible policy predicate on every read of c_policy_reach.docs, which evaluates as the querying role. Revoking it makes the policy raise `permission denied for function can_read` and fails authorization closed for every anonymous read — the load-bearing grant the rule is built to protect.",
22
+ "id": "53-policy-predicate-retains-execute"
23
+ }
@@ -0,0 +1,49 @@
1
+ DROP SCHEMA IF EXISTS c_policy_reach CASCADE;
2
+ DROP SCHEMA IF EXISTS c_policy_priv CASCADE;
3
+ CREATE SCHEMA c_policy_reach;
4
+ CREATE SCHEMA c_policy_priv;
5
+
6
+ DO $$ BEGIN
7
+ CREATE ROLE c_policy_owner NOLOGIN;
8
+ EXCEPTION WHEN duplicate_object THEN NULL;
9
+ END $$;
10
+
11
+ -- corpus_anon can reach the API schema, but holds no USAGE on the private one —
12
+ -- exactly the constructive-db shape, where the `*_private` schemas hold the
13
+ -- check functions the policies call. The private schema is still audited (it is
14
+ -- in scope), it is simply not part of the surface corpus_anon addresses.
15
+ GRANT USAGE ON SCHEMA c_policy_reach TO corpus_anon, c_policy_owner;
16
+ GRANT USAGE ON SCHEMA c_policy_priv TO c_policy_owner;
17
+
18
+ CREATE TABLE c_policy_reach.docs (
19
+ id bigserial PRIMARY KEY,
20
+ owner_id bigint NOT NULL,
21
+ body text NOT NULL
22
+ );
23
+ ALTER TABLE c_policy_reach.docs OWNER TO c_policy_owner;
24
+ ALTER TABLE c_policy_reach.docs ENABLE ROW LEVEL SECURITY;
25
+ ALTER TABLE c_policy_reach.docs FORCE ROW LEVEL SECURITY;
26
+
27
+ -- The load-bearing check. It looks dead: corpus_anon never calls it, holds no
28
+ -- USAGE on its schema, and no ACL on `docs` names it. But it is the SELECT
29
+ -- policy's predicate, which evaluates as corpus_anon on every read of `docs`.
30
+ CREATE FUNCTION c_policy_priv.can_read(owner_id bigint)
31
+ RETURNS boolean
32
+ LANGUAGE sql
33
+ STABLE
34
+ AS $$
35
+ SELECT owner_id = NULLIF(current_setting('app.uid', true), '')::bigint;
36
+ $$;
37
+ ALTER FUNCTION c_policy_priv.can_read(bigint) OWNER TO c_policy_owner;
38
+
39
+ CREATE POLICY docs_visible ON c_policy_reach.docs
40
+ FOR SELECT TO corpus_anon
41
+ USING (c_policy_priv.can_read(owner_id));
42
+
43
+ GRANT SELECT ON c_policy_reach.docs TO corpus_anon;
44
+
45
+ -- The blanket grant again: corpus_anon holds EXECUTE on the private check
46
+ -- function. `granted − reachable` must NOT report it revocable — the policy
47
+ -- predicate is a reachable path, and losing EXECUTE would break authorization
48
+ -- silently, for every anonymous read. This is the case the rule exists for.
49
+ GRANT EXECUTE ON FUNCTION c_policy_priv.can_read(bigint) TO corpus_anon;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The evaluation corpus: small schemas with a deliberate flaw and a written-down
3
+ * answer.
4
+ *
5
+ * It exists because "the score went up" is not evidence of anything on its own.
6
+ * A corpus with known answers turns safegres into a measuring instrument you
7
+ * can calibrate: every case names the findings a correct audit must produce, so
8
+ * a run can be graded on recall (did it find the flaw?) rather than on a number
9
+ * whose provenance nobody can check. It is used three ways —
10
+ *
11
+ * 1. safegres's own regression suite (`__tests__/corpus.test.ts`);
12
+ * 2. an evaluation harness for an agent asked to *fix* the case: audit,
13
+ * hand the findings over, re-audit, and require the expected findings to
14
+ * be gone without new ones appearing;
15
+ * 3. worked examples — each case is short enough to read.
16
+ *
17
+ * Cases live in `corpus/cases/<id>/` as `schema.sql` plus `case.json`, i.e.
18
+ * data, not code: another tool can consume the corpus without running ours.
19
+ */
20
+ import type { ExposureConfig } from '../config/types';
21
+ import type { Report, Severity } from '../types';
22
+ /** One expected finding: a rule code, optionally pinned to a relation. */
23
+ export interface ExpectedFinding {
24
+ code: string;
25
+ /** `schema.table`, when the case has more than one relation in play. */
26
+ relation?: string;
27
+ /** Why this is the right answer — the part a reader learns from. */
28
+ note?: string;
29
+ }
30
+ export interface CorpusCase {
31
+ id: string;
32
+ title: string;
33
+ /** Which axis the flaw is on. A case may be graded on both. */
34
+ dimension: 'security' | 'perf';
35
+ /** The SQL that builds the case, as committed. */
36
+ sql: string;
37
+ /** The surface the case is graded against — its whole point, for most cases. */
38
+ exposure: ExposureConfig;
39
+ /** Findings a correct audit must produce. */
40
+ expect: ExpectedFinding[];
41
+ /** Rule codes that must NOT fire: the false positives this case guards. */
42
+ forbid?: string[];
43
+ /** Worst severity the case should produce, as a coarse sanity check. */
44
+ worstSeverity?: Severity;
45
+ /** How the flaw is fixed, in one sentence — the answer key. */
46
+ fix: string;
47
+ }
48
+ /**
49
+ * The corpus shipped with the package. Two candidates because the package
50
+ * publishes from `dist/`, where the corpus sits one directory closer.
51
+ */
52
+ export declare function corpusDir(): string;
53
+ /**
54
+ * The SQL creating the roles every case is graded against (`corpus_anon`,
55
+ * `corpus_user`). Idempotent, so a harness can run it before every case.
56
+ */
57
+ export declare function corpusBootstrap(dir?: string): string;
58
+ /** Load every case in `dir` (default: the shipped corpus), ordered by id. */
59
+ export declare function loadCorpus(dir?: string): CorpusCase[];
60
+ /** Load a single case directory (`case.json` + `schema.sql`). */
61
+ export declare function loadCase(dir: string): CorpusCase;
62
+ export interface CaseResult {
63
+ id: string;
64
+ /** Expected findings the audit produced. */
65
+ found: ExpectedFinding[];
66
+ /** Expected findings the audit missed — recall failures. */
67
+ missed: ExpectedFinding[];
68
+ /** Forbidden rule codes the audit produced — precision failures. */
69
+ falsePositives: string[];
70
+ /** True when nothing was missed and nothing forbidden fired. */
71
+ passed: boolean;
72
+ }
73
+ /**
74
+ * Grade a report against the case it was produced from. Deliberately not a
75
+ * strict equality check on the finding set: a case pins the findings that
76
+ * define it, and a later release adding an unrelated advisory should not
77
+ * invalidate the corpus. What a case *does* pin negatively goes in `forbid`.
78
+ */
79
+ export declare function gradeCase(report: Report, testCase: CorpusCase): CaseResult;
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /**
3
+ * The evaluation corpus: small schemas with a deliberate flaw and a written-down
4
+ * answer.
5
+ *
6
+ * It exists because "the score went up" is not evidence of anything on its own.
7
+ * A corpus with known answers turns safegres into a measuring instrument you
8
+ * can calibrate: every case names the findings a correct audit must produce, so
9
+ * a run can be graded on recall (did it find the flaw?) rather than on a number
10
+ * whose provenance nobody can check. It is used three ways —
11
+ *
12
+ * 1. safegres's own regression suite (`__tests__/corpus.test.ts`);
13
+ * 2. an evaluation harness for an agent asked to *fix* the case: audit,
14
+ * hand the findings over, re-audit, and require the expected findings to
15
+ * be gone without new ones appearing;
16
+ * 3. worked examples — each case is short enough to read.
17
+ *
18
+ * Cases live in `corpus/cases/<id>/` as `schema.sql` plus `case.json`, i.e.
19
+ * data, not code: another tool can consume the corpus without running ours.
20
+ */
21
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ var desc = Object.getOwnPropertyDescriptor(m, k);
24
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
25
+ desc = { enumerable: true, get: function() { return m[k]; } };
26
+ }
27
+ Object.defineProperty(o, k2, desc);
28
+ }) : (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ o[k2] = m[k];
31
+ }));
32
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
33
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
34
+ }) : function(o, v) {
35
+ o["default"] = v;
36
+ });
37
+ var __importStar = (this && this.__importStar) || (function () {
38
+ var ownKeys = function(o) {
39
+ ownKeys = Object.getOwnPropertyNames || function (o) {
40
+ var ar = [];
41
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
42
+ return ar;
43
+ };
44
+ return ownKeys(o);
45
+ };
46
+ return function (mod) {
47
+ if (mod && mod.__esModule) return mod;
48
+ var result = {};
49
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
50
+ __setModuleDefault(result, mod);
51
+ return result;
52
+ };
53
+ })();
54
+ Object.defineProperty(exports, "__esModule", { value: true });
55
+ exports.corpusDir = corpusDir;
56
+ exports.corpusBootstrap = corpusBootstrap;
57
+ exports.loadCorpus = loadCorpus;
58
+ exports.loadCase = loadCase;
59
+ exports.gradeCase = gradeCase;
60
+ const fs = __importStar(require("fs"));
61
+ const path = __importStar(require("path"));
62
+ /**
63
+ * The corpus shipped with the package. Two candidates because the package
64
+ * publishes from `dist/`, where the corpus sits one directory closer.
65
+ */
66
+ function corpusDir() {
67
+ const candidates = [
68
+ path.resolve(__dirname, '..', 'corpus', 'cases'),
69
+ path.resolve(__dirname, '..', '..', 'corpus', 'cases')
70
+ ];
71
+ const found = candidates.find((dir) => fs.existsSync(dir));
72
+ if (!found)
73
+ throw new Error(`Corpus not found. Looked in: ${candidates.join(', ')}`);
74
+ return found;
75
+ }
76
+ /**
77
+ * The SQL creating the roles every case is graded against (`corpus_anon`,
78
+ * `corpus_user`). Idempotent, so a harness can run it before every case.
79
+ */
80
+ function corpusBootstrap(dir = corpusDir()) {
81
+ return fs.readFileSync(path.resolve(dir, '..', 'bootstrap.sql'), 'utf8');
82
+ }
83
+ /** Load every case in `dir` (default: the shipped corpus), ordered by id. */
84
+ function loadCorpus(dir = corpusDir()) {
85
+ return fs
86
+ .readdirSync(dir, { withFileTypes: true })
87
+ .filter((e) => e.isDirectory())
88
+ .map((e) => loadCase(path.join(dir, e.name)))
89
+ .sort((a, b) => a.id.localeCompare(b.id));
90
+ }
91
+ /** Load a single case directory (`case.json` + `schema.sql`). */
92
+ function loadCase(dir) {
93
+ const meta = JSON.parse(fs.readFileSync(path.join(dir, 'case.json'), 'utf8'));
94
+ return {
95
+ ...meta,
96
+ id: meta.id ?? path.basename(dir),
97
+ sql: fs.readFileSync(path.join(dir, 'schema.sql'), 'utf8')
98
+ };
99
+ }
100
+ function matches(finding, expected) {
101
+ if (finding.code !== expected.code)
102
+ return false;
103
+ if (!expected.relation)
104
+ return true;
105
+ return `${finding.schema}.${finding.table}` === expected.relation;
106
+ }
107
+ /**
108
+ * Grade a report against the case it was produced from. Deliberately not a
109
+ * strict equality check on the finding set: a case pins the findings that
110
+ * define it, and a later release adding an unrelated advisory should not
111
+ * invalidate the corpus. What a case *does* pin negatively goes in `forbid`.
112
+ */
113
+ function gradeCase(report, testCase) {
114
+ const findings = report.findings;
115
+ const found = testCase.expect.filter((e) => findings.some((f) => matches(f, e)));
116
+ const missed = testCase.expect.filter((e) => !findings.some((f) => matches(f, e)));
117
+ const falsePositives = [...new Set(findings.map((f) => f.code))].filter((code) => (testCase.forbid ?? []).includes(code));
118
+ return {
119
+ id: testCase.id,
120
+ found,
121
+ missed,
122
+ falsePositives,
123
+ passed: missed.length === 0 && falsePositives.length === 0
124
+ };
125
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `safegres/adapters` — the built-in exposure adapters and the interface a
3
+ * custom one implements. Kept as its own entry point so a config file can
4
+ * import an adapter without pulling in the whole auditor.
5
+ */
6
+ export type { ExposureAdapter, PlaneInput, ReachContext } from './exposure/adapters';
7
+ export { BUILTIN_ADAPTERS, constructiveAdapter, definePlanes, graphileAdapter, hasuraAdapter, postgraphileAdapter, postgrestAdapter, resolveAdapters, supabaseAdapter } from './exposure/adapters';
8
+ export type { ApiReach, UnreachableRelation } from './exposure/reach';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `safegres/adapters` — the built-in exposure adapters and the interface a
3
+ * custom one implements. Kept as its own entry point so a config file can
4
+ * import an adapter without pulling in the whole auditor.
5
+ */
6
+ export { BUILTIN_ADAPTERS, constructiveAdapter, definePlanes, graphileAdapter, hasuraAdapter, postgraphileAdapter, postgrestAdapter, resolveAdapters, supabaseAdapter } from './exposure/adapters';