@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,329 @@
1
+ /**
2
+ * Role reachability: what a role can *actually* reach, not just what the ACL
3
+ * rows name.
4
+ *
5
+ * `effectiveGrants` (see `lattice.ts`) already folds the three ways a
6
+ * privilege arrives passively — a direct grant, a grant TO PUBLIC, and a grant
7
+ * to a role the caller INHERITs from. That closure answers "what privileges
8
+ * does this role hold?" but not "what can this role make Postgres do?", and on
9
+ * PG16+ the two diverge: a membership can confer `SET ROLE` (`set_option`)
10
+ * *without* inheritance, so a role that passively holds nothing can still
11
+ * execute with a target role's full privileges by assuming it.
12
+ *
13
+ * This module is the first step of the reachability lattice (planning
14
+ * #1358/#1361): it projects a role's reach into each relation as a set of
15
+ * cells, each carrying the role the access actually *executes as*
16
+ * (`effectiveRole`) and how that reach arrived (`path`). Stage 1 models two
17
+ * edge kinds — passive grants and `SET ROLE` — both catalog-proven
18
+ * (`proof: 'catalog'`). Stage 2 adds the view-ownership edge: a read through a
19
+ * non-`security_invoker` view executes as the view's owner, and the base
20
+ * relations it touches are read out of the view body, so those cells carry
21
+ * `proof: 'ast'`. SECURITY DEFINER function edges come later.
22
+ */
23
+ import type { FunctionGrant } from '../pg/functions';
24
+ import type { GrantInfo, PgPrivilege, TableSnapshot } from '../pg/introspect';
25
+ import { type GrantVia, type RoleGraph } from './lattice';
26
+ /** One hop in the path by which a role reaches a relation. */
27
+ export type RoleReachEdge =
28
+ /** A privilege held passively: directly, via PUBLIC, or via INHERIT. */
29
+ {
30
+ kind: 'grant';
31
+ via: GrantVia;
32
+ privilege: PgPrivilege;
33
+ }
34
+ /** The caller assumed `to` with `SET ROLE` (`pg_auth_members.set_option`). */
35
+ | {
36
+ kind: 'setrole';
37
+ to: string;
38
+ }
39
+ /**
40
+ * The caller read through a view that executes as `owner` — every relation
41
+ * the body names is read under the owner's privileges, not the caller's.
42
+ */
43
+ | {
44
+ kind: 'view';
45
+ view: string;
46
+ owner: string;
47
+ }
48
+ /**
49
+ * The caller read a materialized view whose rows were computed as `owner`
50
+ * at REFRESH time. Unlike the view edge this holds whatever the reader's
51
+ * privileges are: the rows are stored, so the bases are never consulted and
52
+ * their policies never run.
53
+ */
54
+ | {
55
+ kind: 'matview';
56
+ view: string;
57
+ owner: string;
58
+ }
59
+ /**
60
+ * The caller's command fired a rewrite rule on `view`. The rule's actions
61
+ * are permission-checked against the rule's table owner, and unlike the
62
+ * view edge this is *not* governed by `security_invoker`.
63
+ */
64
+ | {
65
+ kind: 'rule';
66
+ view: string;
67
+ rule: string;
68
+ owner: string;
69
+ }
70
+ /**
71
+ * The caller executed a function that runs as `owner` — a SECURITY DEFINER,
72
+ * or anything called from one. Every relation the body touches is touched
73
+ * with the owner's privileges, which is the function twin of the view edge.
74
+ */
75
+ | {
76
+ kind: 'function';
77
+ fn: string;
78
+ owner: string;
79
+ }
80
+ /**
81
+ * The caller's write against `view` fired an `INSTEAD OF` trigger. The
82
+ * write itself never reaches a base relation: it becomes the trigger
83
+ * function's body, checked against *that function's* effective user — the
84
+ * caller, unless the function is SECURITY DEFINER. The view's own owner and
85
+ * `security_invoker` do not enter into it.
86
+ */
87
+ | {
88
+ kind: 'trigger';
89
+ view: string;
90
+ trigger: string;
91
+ fn: string;
92
+ owner: string;
93
+ };
94
+ /**
95
+ * How well-founded a reach cell is. Stage 1 is entirely `catalog` — every
96
+ * edge is a row in `pg_auth_members` or an ACL entry. Later edges derived from
97
+ * parsing function/view bodies are `ast`; a body safegres cannot follow
98
+ * (dynamic SQL) taints everything downstream as `opaque-tainted`, which callers
99
+ * must treat as *unknown*, never as *safe*.
100
+ */
101
+ export type ReachProof = 'catalog' | 'ast' | 'opaque-tainted';
102
+ /** One relation a role reaches, under one effective role, by one path. */
103
+ export interface RoleReachCell {
104
+ schema: string;
105
+ table: string;
106
+ privileges: PgPrivilege[];
107
+ /**
108
+ * The role the access executes as. Equal to the querying role for passive
109
+ * grants; the assumed role once a `setrole` edge is on the path. Postgres
110
+ * checks the relation's ACLs and RLS policies against *this* role, which is
111
+ * why it has to be retained rather than collapsed back onto the caller.
112
+ */
113
+ effectiveRole: string;
114
+ /** The edges, caller-first, by which the reach arrived. */
115
+ path: RoleReachEdge[];
116
+ proof: ReachProof;
117
+ /**
118
+ * The relation was not introspected: it is outside the audited schemas.
119
+ * `privileges` is what the path exercises, not what the relation permits.
120
+ */
121
+ external?: boolean;
122
+ /** The columns of the relation the path reaches, where they are known. */
123
+ columns?: string[];
124
+ /**
125
+ * Why the path is only a lower bound, on a `opaque-tainted` cell: the body
126
+ * that produced it executes SQL this analysis cannot follow, so the relation
127
+ * it names is the *view itself* and what lies beyond it is unknown.
128
+ */
129
+ taint?: string;
130
+ }
131
+ /** Everything one role reaches, across the relations examined. */
132
+ export interface RoleReach {
133
+ role: string;
134
+ cells: RoleReachCell[];
135
+ }
136
+ /**
137
+ * Project each role's reach into `tables`. Stage 1 emits, per (role, table):
138
+ *
139
+ * - one `effectiveRole === role` cell for the passive `effectiveGrants`
140
+ * closure, when non-empty; and
141
+ * - one cell per role in the caller's transitive `SET ROLE` closure that
142
+ * itself holds privileges on the table — the access it gains by assuming
143
+ * that role.
144
+ *
145
+ * A relation the role cannot reach at all produces no cell.
146
+ */
147
+ export declare function computeRoleReach(tables: TableSnapshot[], graph: RoleGraph, roles: string[]): RoleReach[];
148
+ /** One relation a view body reads, with the roles the hops execute as. */
149
+ export interface ViewBaseRelation {
150
+ schema: string;
151
+ table: string;
152
+ /**
153
+ * The view hops the read passes through, outermost first. The last hop's
154
+ * owner is the role the base relation is actually read as.
155
+ */
156
+ hops: Array<{
157
+ view: string;
158
+ owner: string;
159
+ }>;
160
+ /**
161
+ * The relation lives outside the audited schemas, so nothing is known about
162
+ * its ACL, its RLS or its owner. The read is proven; its consequences are
163
+ * not, and a consumer must not grade it as if the relation were in scope.
164
+ */
165
+ external?: boolean;
166
+ /**
167
+ * The columns of the relation that actually escape through the view, from
168
+ * the catalog's own dependency rows. Absent means unknown — never "none".
169
+ */
170
+ columns?: string[];
171
+ }
172
+ /** A view, its own ACL, and the base relations its body was found to read. */
173
+ export interface ViewReachInput {
174
+ schema: string;
175
+ name: string;
176
+ owner: string;
177
+ /** ACL rows on the view itself — who can SELECT the view at all. */
178
+ grants: GrantInfo[];
179
+ baseRelations: ViewBaseRelation[];
180
+ /**
181
+ * The outermost relation is a materialized view, so its first hop is a
182
+ * `matview` edge: the rows were computed at REFRESH time rather than read
183
+ * through on demand.
184
+ */
185
+ materialized?: boolean;
186
+ /**
187
+ * The body was wholly or partly unreadable, and why. `baseRelations` is
188
+ * then a *lower bound*: everything in it is proven, but the body reaches
189
+ * relations this analysis cannot name.
190
+ */
191
+ unreadable?: string;
192
+ }
193
+ /**
194
+ * Project the view-ownership edge into the same reach model: for every role
195
+ * that can SELECT a view, one cell per base relation the view body reads,
196
+ * under the owner the hop executes as.
197
+ *
198
+ * Only SELECT is modelled here; the write half is
199
+ * {@link computeViewWriteReach}, which needs the catalog's updatability
200
+ * answer on top of the body's relation set.
201
+ *
202
+ * A view whose body could not be read (dynamic SQL, an unparseable body)
203
+ * still belongs in `views`, carrying {@link ViewReachInput.unreadable}: an
204
+ * unreadable body is unknown, not empty, and it yields one extra cell naming
205
+ * the *view* with `proof: 'opaque-tainted'` — the reach is real, its far end
206
+ * is not knowable here. Grading rules must skip those cells; only a coverage
207
+ * rule should report them.
208
+ */
209
+ export declare function computeViewReach(views: ViewReachInput[], graph: RoleGraph, roles: string[]): RoleReach[];
210
+ /** One hop of a call chain, and the role in force once it is taken. */
211
+ export interface FunctionHop {
212
+ /** `schema.name` — overloads collapse, as they do in the call graph. */
213
+ fn: string;
214
+ owner: string;
215
+ }
216
+ /** One relation a function body touches, under the role in force there. */
217
+ export interface FunctionAccess {
218
+ schema: string;
219
+ table: string;
220
+ privilege: PgPrivilege;
221
+ /** Call hops, entry first; the last hop's owner executes the access. */
222
+ hops: FunctionHop[];
223
+ /** Views the access passed through after the call chain, outermost first. */
224
+ viewHops?: Array<{
225
+ view: string;
226
+ owner: string;
227
+ }>;
228
+ /** The relation is outside the audited schemas: nothing about it is known. */
229
+ external?: boolean;
230
+ }
231
+ /** A function, its EXECUTE ACL, and the relations its body reaches. */
232
+ export interface FunctionReachInput {
233
+ schema: string;
234
+ name: string;
235
+ /** Identity arguments — what distinguishes one overload from another. */
236
+ args: string;
237
+ owner: string;
238
+ grants: FunctionGrant[];
239
+ /** The EXECUTE came from Postgres's default ACL, not a deliberate grant. */
240
+ defaultAcl: boolean;
241
+ accesses: FunctionAccess[];
242
+ /**
243
+ * The body was wholly or partly unreadable, and why. `accesses` is then a
244
+ * lower bound: what is there is proven, what is missing is unknowable.
245
+ */
246
+ unreadable?: string;
247
+ }
248
+ /**
249
+ * Project the function-execution edge into the reach model: for every role
250
+ * that can EXECUTE a function running as someone else, one cell per relation
251
+ * its body touches, under the role in force at that point in the call chain.
252
+ *
253
+ * The shape mirrors {@link computeViewReach} deliberately — a definer function
254
+ * and a definer view are the same escalation with different syntax — and, as
255
+ * there, a body that could not be read yields one `opaque-tainted` cell naming
256
+ * the *function*: the execution is proven, its far end is not.
257
+ */
258
+ export declare function computeFunctionReach(functions: FunctionReachInput[], graph: RoleGraph, roles: string[]): RoleReach[];
259
+ /** A view, its ACL, and the relations an `INSTEAD OF` trigger's body reaches. */
260
+ export interface TriggerWriteInput {
261
+ schema: string;
262
+ name: string;
263
+ /** ACL rows on the view — what the caller must hold to fire the trigger. */
264
+ grants: GrantInfo[];
265
+ trigger: string;
266
+ /** The commands on the view that fire it. */
267
+ events: PgPrivilege[];
268
+ /** `schema.name` of the trigger function. */
269
+ fn: string;
270
+ /** The role the trigger function's body executes as. */
271
+ fnOwner: string;
272
+ /** Relations the body touches, under `fnOwner`. */
273
+ accesses: Array<{
274
+ schema: string;
275
+ table: string;
276
+ privilege: PgPrivilege;
277
+ external?: boolean;
278
+ }>;
279
+ }
280
+ /**
281
+ * Project the `INSTEAD OF` trigger edge: for every role holding a command the
282
+ * trigger fires on, one cell per relation the trigger function's body touches.
283
+ *
284
+ * Callers must only pass triggers whose function actually re-owns the write —
285
+ * an invoker trigger function runs the body as the caller, which is no edge at
286
+ * all, and Postgres denies it exactly as it would deny the direct write.
287
+ */
288
+ export declare function computeTriggerWriteReach(triggers: TriggerWriteInput[], graph: RoleGraph, roles: string[]): RoleReach[];
289
+ /**
290
+ * One relation a *write* against a view lands on.
291
+ *
292
+ * The two privileges are distinct on purpose. `via` is what the caller must
293
+ * hold on the view to issue the command; `privilege` is what the rewritten
294
+ * command exercises on the target. They coincide for an auto-updatable view
295
+ * (an INSERT on the view is an INSERT on its base relation) and routinely
296
+ * differ for a rewrite rule, where `ON INSERT ... DO INSTEAD UPDATE other`
297
+ * turns one into the other.
298
+ */
299
+ export interface ViewWriteEdge {
300
+ schema: string;
301
+ table: string;
302
+ via: PgPrivilege;
303
+ privilege: PgPrivilege;
304
+ /** View hops, outermost first; the last hop's owner executes the write. */
305
+ hops: Array<{
306
+ view: string;
307
+ owner: string;
308
+ }>;
309
+ /** Set when the edge exists because of a rewrite rule, not auto-update. */
310
+ rule?: string;
311
+ }
312
+ /** A view, its own ACL, and the relations writes against it reach. */
313
+ export interface ViewWriteInput {
314
+ schema: string;
315
+ name: string;
316
+ owner: string;
317
+ grants: GrantInfo[];
318
+ writeEdges: ViewWriteEdge[];
319
+ }
320
+ /**
321
+ * Project write edges through views into the reach model: for every role that
322
+ * holds the triggering command on the view, one cell per relation that write
323
+ * lands on, under the role the landing executes as.
324
+ *
325
+ * As with {@link computeViewReach}, a view whose write target could not be
326
+ * proven must not appear in `views` — an `INSTEAD OF` trigger's body, a
327
+ * multi-relation body, or an unreadable rule action is unknown, not empty.
328
+ */
329
+ export declare function computeViewWriteReach(views: ViewWriteInput[], graph: RoleGraph, roles: string[]): RoleReach[];
@@ -0,0 +1,277 @@
1
+ "use strict";
2
+ /**
3
+ * Role reachability: what a role can *actually* reach, not just what the ACL
4
+ * rows name.
5
+ *
6
+ * `effectiveGrants` (see `lattice.ts`) already folds the three ways a
7
+ * privilege arrives passively — a direct grant, a grant TO PUBLIC, and a grant
8
+ * to a role the caller INHERITs from. That closure answers "what privileges
9
+ * does this role hold?" but not "what can this role make Postgres do?", and on
10
+ * PG16+ the two diverge: a membership can confer `SET ROLE` (`set_option`)
11
+ * *without* inheritance, so a role that passively holds nothing can still
12
+ * execute with a target role's full privileges by assuming it.
13
+ *
14
+ * This module is the first step of the reachability lattice (planning
15
+ * #1358/#1361): it projects a role's reach into each relation as a set of
16
+ * cells, each carrying the role the access actually *executes as*
17
+ * (`effectiveRole`) and how that reach arrived (`path`). Stage 1 models two
18
+ * edge kinds — passive grants and `SET ROLE` — both catalog-proven
19
+ * (`proof: 'catalog'`). Stage 2 adds the view-ownership edge: a read through a
20
+ * non-`security_invoker` view executes as the view's owner, and the base
21
+ * relations it touches are read out of the view body, so those cells carry
22
+ * `proof: 'ast'`. SECURITY DEFINER function edges come later.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.computeRoleReach = computeRoleReach;
26
+ exports.computeViewReach = computeViewReach;
27
+ exports.computeFunctionReach = computeFunctionReach;
28
+ exports.computeTriggerWriteReach = computeTriggerWriteReach;
29
+ exports.computeViewWriteReach = computeViewWriteReach;
30
+ const lattice_1 = require("./lattice");
31
+ const RLS_PRIVILEGES = ['SELECT', 'INSERT', 'UPDATE', 'DELETE'];
32
+ /**
33
+ * Project each role's reach into `tables`. Stage 1 emits, per (role, table):
34
+ *
35
+ * - one `effectiveRole === role` cell for the passive `effectiveGrants`
36
+ * closure, when non-empty; and
37
+ * - one cell per role in the caller's transitive `SET ROLE` closure that
38
+ * itself holds privileges on the table — the access it gains by assuming
39
+ * that role.
40
+ *
41
+ * A relation the role cannot reach at all produces no cell.
42
+ */
43
+ function computeRoleReach(tables, graph, roles) {
44
+ return roles.map((role) => {
45
+ const attrs = graph.get(role);
46
+ const cells = [];
47
+ for (const table of tables) {
48
+ const direct = (0, lattice_1.effectiveGrants)(table, role, graph).filter((g) => RLS_PRIVILEGES.includes(g.privilege));
49
+ if (direct.length > 0) {
50
+ cells.push({
51
+ schema: table.schema,
52
+ table: table.name,
53
+ privileges: direct.map((g) => g.privilege),
54
+ effectiveRole: role,
55
+ path: direct.map((g) => ({ kind: 'grant', via: g.via, privilege: g.privilege })),
56
+ proof: 'catalog'
57
+ });
58
+ }
59
+ for (const target of attrs?.canSetRole ?? []) {
60
+ const assumed = (0, lattice_1.effectiveGrants)(table, target, graph).filter((g) => RLS_PRIVILEGES.includes(g.privilege));
61
+ if (assumed.length === 0)
62
+ continue;
63
+ cells.push({
64
+ schema: table.schema,
65
+ table: table.name,
66
+ privileges: assumed.map((g) => g.privilege),
67
+ effectiveRole: target,
68
+ path: [
69
+ { kind: 'setrole', to: target },
70
+ ...assumed.map((g) => ({ kind: 'grant', via: g.via, privilege: g.privilege }))
71
+ ],
72
+ proof: 'catalog'
73
+ });
74
+ }
75
+ }
76
+ return { role, cells };
77
+ });
78
+ }
79
+ /**
80
+ * Project the view-ownership edge into the same reach model: for every role
81
+ * that can SELECT a view, one cell per base relation the view body reads,
82
+ * under the owner the hop executes as.
83
+ *
84
+ * Only SELECT is modelled here; the write half is
85
+ * {@link computeViewWriteReach}, which needs the catalog's updatability
86
+ * answer on top of the body's relation set.
87
+ *
88
+ * A view whose body could not be read (dynamic SQL, an unparseable body)
89
+ * still belongs in `views`, carrying {@link ViewReachInput.unreadable}: an
90
+ * unreadable body is unknown, not empty, and it yields one extra cell naming
91
+ * the *view* with `proof: 'opaque-tainted'` — the reach is real, its far end
92
+ * is not knowable here. Grading rules must skip those cells; only a coverage
93
+ * rule should report them.
94
+ */
95
+ function computeViewReach(views, graph, roles) {
96
+ return roles.map((role) => {
97
+ const cells = [];
98
+ for (const view of views) {
99
+ const select = (0, lattice_1.effectiveGrants)(view, role, graph).find((g) => g.privilege === 'SELECT');
100
+ if (!select)
101
+ continue;
102
+ for (const base of view.baseRelations) {
103
+ if (base.hops.length === 0)
104
+ continue;
105
+ cells.push({
106
+ schema: base.schema,
107
+ table: base.table,
108
+ privileges: ['SELECT'],
109
+ effectiveRole: base.hops[base.hops.length - 1].owner,
110
+ path: [
111
+ { kind: 'grant', via: select.via, privilege: 'SELECT' },
112
+ ...base.hops.map((h, i) => ({
113
+ kind: view.materialized && i === 0 ? 'matview' : 'view',
114
+ view: h.view,
115
+ owner: h.owner
116
+ }))
117
+ ],
118
+ proof: 'ast',
119
+ ...(base.external ? { external: true } : {}),
120
+ ...(base.columns ? { columns: base.columns } : {})
121
+ });
122
+ }
123
+ if (view.unreadable) {
124
+ cells.push({
125
+ schema: view.schema,
126
+ table: view.name,
127
+ privileges: ['SELECT'],
128
+ effectiveRole: view.owner,
129
+ path: [
130
+ { kind: 'grant', via: select.via, privilege: 'SELECT' },
131
+ { kind: 'view', view: `${view.schema}.${view.name}`, owner: view.owner }
132
+ ],
133
+ proof: 'opaque-tainted',
134
+ taint: view.unreadable
135
+ });
136
+ }
137
+ }
138
+ return { role, cells };
139
+ });
140
+ }
141
+ /**
142
+ * Project the function-execution edge into the reach model: for every role
143
+ * that can EXECUTE a function running as someone else, one cell per relation
144
+ * its body touches, under the role in force at that point in the call chain.
145
+ *
146
+ * The shape mirrors {@link computeViewReach} deliberately — a definer function
147
+ * and a definer view are the same escalation with different syntax — and, as
148
+ * there, a body that could not be read yields one `opaque-tainted` cell naming
149
+ * the *function*: the execution is proven, its far end is not.
150
+ */
151
+ function computeFunctionReach(functions, graph, roles) {
152
+ return roles.map((role) => {
153
+ const cells = [];
154
+ for (const fn of functions) {
155
+ const via = (0, lattice_1.effectiveExecute)(fn, role, graph);
156
+ if (!via)
157
+ continue;
158
+ for (const access of fn.accesses) {
159
+ if (access.hops.length === 0)
160
+ continue;
161
+ const viewHops = access.viewHops ?? [];
162
+ const last = viewHops.length > 0
163
+ ? viewHops[viewHops.length - 1].owner
164
+ : access.hops[access.hops.length - 1].owner;
165
+ cells.push({
166
+ schema: access.schema,
167
+ table: access.table,
168
+ privileges: [access.privilege],
169
+ effectiveRole: last,
170
+ path: [
171
+ { kind: 'grant', via, privilege: 'EXECUTE' },
172
+ ...access.hops.map((h) => ({ kind: 'function', fn: h.fn, owner: h.owner })),
173
+ ...viewHops.map((h) => ({ kind: 'view', view: h.view, owner: h.owner }))
174
+ ],
175
+ proof: 'ast',
176
+ ...(access.external ? { external: true } : {})
177
+ });
178
+ }
179
+ if (fn.unreadable) {
180
+ cells.push({
181
+ schema: fn.schema,
182
+ table: `${fn.name}(${fn.args})`,
183
+ privileges: ['EXECUTE'],
184
+ effectiveRole: fn.owner,
185
+ path: [
186
+ { kind: 'grant', via, privilege: 'EXECUTE' },
187
+ { kind: 'function', fn: `${fn.schema}.${fn.name}`, owner: fn.owner }
188
+ ],
189
+ proof: 'opaque-tainted',
190
+ taint: fn.unreadable
191
+ });
192
+ }
193
+ }
194
+ return { role, cells };
195
+ });
196
+ }
197
+ /**
198
+ * Project the `INSTEAD OF` trigger edge: for every role holding a command the
199
+ * trigger fires on, one cell per relation the trigger function's body touches.
200
+ *
201
+ * Callers must only pass triggers whose function actually re-owns the write —
202
+ * an invoker trigger function runs the body as the caller, which is no edge at
203
+ * all, and Postgres denies it exactly as it would deny the direct write.
204
+ */
205
+ function computeTriggerWriteReach(triggers, graph, roles) {
206
+ return roles.map((role) => {
207
+ const cells = [];
208
+ for (const trigger of triggers) {
209
+ const held = (0, lattice_1.effectiveGrants)(trigger, role, graph);
210
+ const fired = trigger.events.filter((e) => held.some((g) => g.privilege === e));
211
+ if (fired.length === 0)
212
+ continue;
213
+ const grant = held.find((g) => g.privilege === fired[0]);
214
+ for (const access of trigger.accesses) {
215
+ cells.push({
216
+ schema: access.schema,
217
+ table: access.table,
218
+ privileges: [access.privilege],
219
+ effectiveRole: trigger.fnOwner,
220
+ path: [
221
+ { kind: 'grant', via: grant.via, privilege: fired[0] },
222
+ {
223
+ kind: 'trigger',
224
+ view: `${trigger.schema}.${trigger.name}`,
225
+ trigger: trigger.trigger,
226
+ fn: trigger.fn,
227
+ owner: trigger.fnOwner
228
+ }
229
+ ],
230
+ proof: 'ast',
231
+ ...(access.external ? { external: true } : {})
232
+ });
233
+ }
234
+ }
235
+ return { role, cells };
236
+ });
237
+ }
238
+ /**
239
+ * Project write edges through views into the reach model: for every role that
240
+ * holds the triggering command on the view, one cell per relation that write
241
+ * lands on, under the role the landing executes as.
242
+ *
243
+ * As with {@link computeViewReach}, a view whose write target could not be
244
+ * proven must not appear in `views` — an `INSTEAD OF` trigger's body, a
245
+ * multi-relation body, or an unreadable rule action is unknown, not empty.
246
+ */
247
+ function computeViewWriteReach(views, graph, roles) {
248
+ return roles.map((role) => {
249
+ const cells = [];
250
+ for (const view of views) {
251
+ const held = (0, lattice_1.effectiveGrants)(view, role, graph);
252
+ for (const edge of view.writeEdges) {
253
+ if (edge.hops.length === 0)
254
+ continue;
255
+ const grant = held.find((g) => g.privilege === edge.via);
256
+ if (!grant)
257
+ continue;
258
+ const last = edge.hops[edge.hops.length - 1];
259
+ cells.push({
260
+ schema: edge.schema,
261
+ table: edge.table,
262
+ privileges: [edge.privilege],
263
+ effectiveRole: last.owner,
264
+ path: [
265
+ { kind: 'grant', via: grant.via, privilege: edge.via },
266
+ ...edge.hops.map((h) => ({ kind: 'view', view: h.view, owner: h.owner })),
267
+ ...(edge.rule
268
+ ? [{ kind: 'rule', view: last.view, rule: edge.rule, owner: last.owner }]
269
+ : [])
270
+ ],
271
+ proof: 'ast'
272
+ });
273
+ }
274
+ }
275
+ return { role, cells };
276
+ });
277
+ }
@@ -0,0 +1,45 @@
1
+ import type { TableSnapshot } from '../pg/introspect';
2
+ import type { Finding } from '../types';
3
+ /**
4
+ * Role-trust rules (R-series): findings driven by *who* is granted access,
5
+ * not just whether coverage exists. R1/R2 take a configurable list of
6
+ * untrusted roles (e.g. `anonymous`) via rule options; with no roles
7
+ * configured they are no-ops, so they cost nothing on databases without
8
+ * such a role model.
9
+ */
10
+ export interface RoleTrustOptions {
11
+ /** Role names considered untrusted (exact match). */
12
+ roles?: string[];
13
+ /**
14
+ * Take the untrusted roles from the resolved exposure surface instead of
15
+ * naming them — their names are per-deployment (`myapp_visitor`, a
16
+ * Constructive API's `anon_role`), so a preset cannot hardcode them.
17
+ *
18
+ * - `anon`: only the roles an *unauthenticated* caller arrives as. The right
19
+ * default for a stack whose signed-in role legitimately writes: flagging
20
+ * `authenticated` for holding an INSERT grant would flag the product.
21
+ * - `exposure`: every role at the API edge, signed-in ones included. The
22
+ * stricter reading, for a surface where no role should write directly.
23
+ *
24
+ * Unions with `roles`.
25
+ */
26
+ rolesFrom?: 'exposure' | 'anon';
27
+ }
28
+ /**
29
+ * R1: an untrusted role holds a write privilege on a table. Even with
30
+ * airtight policies, write access for e.g. `anonymous` is almost always a
31
+ * grant mistake — unauthenticated actors can INSERT/UPDATE/DELETE.
32
+ */
33
+ export declare function checkUntrustedRoleWrites(table: TableSnapshot, options?: RoleTrustOptions): Finding[];
34
+ /**
35
+ * R2: a permissive policy makes write operations pass RLS for an untrusted
36
+ * role (directly or via PUBLIC). Pairs with R1: the grant is the door, the
37
+ * policy is the unlocked latch.
38
+ */
39
+ export declare function checkUntrustedRolePolicies(table: TableSnapshot, options?: RoleTrustOptions): Finding[];
40
+ /**
41
+ * R3: a table with RLS enabled has grants TO PUBLIC. PUBLIC includes every
42
+ * present and future role, which silently widens access as roles are added
43
+ * and defeats role-scoped policy reasoning.
44
+ */
45
+ export declare function checkPublicGrants(table: TableSnapshot): Finding[];