@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,340 @@
1
+ "use strict";
2
+ /**
3
+ * Static body extraction for the call graph.
4
+ *
5
+ * Given a function's source, extract the function calls, table references
6
+ * (read vs write), and auth-context mutations it contains. Dynamic SQL
7
+ * (`EXECUTE format(...)`) and unparseable bodies are surfaced as `opaque`
8
+ * rather than silently dropped — static analysis ends there.
9
+ *
10
+ * A view body is the same question asked of a different object, so
11
+ * {@link extractQuery} exposes the plain-SQL half directly.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.extractBody = extractBody;
15
+ exports.extractFunctionAccess = extractFunctionAccess;
16
+ exports.extractQuery = extractQuery;
17
+ exports.extractAccess = extractAccess;
18
+ exports.bodyFiltersRows = bodyFiltersRows;
19
+ const libpg_query_1 = require("libpg-query");
20
+ const pgsql_parser_1 = require("pgsql-parser");
21
+ const walk_1 = require("../ast/walk");
22
+ const EMPTY = { calls: [], tables: [], settings: [], opaque: false };
23
+ /**
24
+ * Functions that run SQL of their own. The relations they touch are in a
25
+ * string argument, not in this AST, so a body calling one has a relation set
26
+ * that is a lower bound rather than an answer.
27
+ */
28
+ const SQL_EXECUTING = new Set(['query_to_xml', 'dblink', 'dblink_exec', 'dblink_send_query']);
29
+ /** Languages whose bodies we can statically analyze. */
30
+ const ANALYZABLE = new Set(['sql', 'plpgsql']);
31
+ async function extractBody(fn) {
32
+ if (!ANALYZABLE.has(fn.language)) {
33
+ if (fn.language === 'internal' || fn.language === 'c')
34
+ return EMPTY;
35
+ return { ...EMPTY, opaque: true, opaqueReason: `language "${fn.language}" is not statically analyzable` };
36
+ }
37
+ if (fn.language === 'sql') {
38
+ if (!fn.source || fn.source.trim() === '')
39
+ return EMPTY;
40
+ return extractQuery(fn.source);
41
+ }
42
+ // plpgsql: parse the full CREATE FUNCTION, then analyze every embedded
43
+ // SQL expression/statement the PL/pgSQL parser hands back.
44
+ const embedded = await plpgsqlStatements(fn);
45
+ if ('opaqueReason' in embedded) {
46
+ return { ...EMPTY, opaque: true, opaqueReason: embedded.opaqueReason };
47
+ }
48
+ const out = { calls: [], tables: [], settings: [], opaque: false, ...embedded.flags };
49
+ for (const sql of embedded.statements)
50
+ mergeBody(out, await extractQuery(sql));
51
+ return finalize(out);
52
+ }
53
+ /**
54
+ * The SQL a PL/pgSQL body embeds, as statements ready to parse, plus whether
55
+ * the body also runs SQL nothing can see (`EXECUTE`).
56
+ *
57
+ * Both body walkers need this and they need it identically — the read/write
58
+ * one for the call graph, the per-privilege one for the reach model — so the
59
+ * PL/pgSQL half is factored out rather than written twice.
60
+ */
61
+ async function plpgsqlStatements(fn) {
62
+ if (!fn.definition)
63
+ return { opaqueReason: 'no function definition available' };
64
+ let parsed;
65
+ try {
66
+ parsed = await (0, libpg_query_1.parsePlPgSQL)(fn.definition);
67
+ }
68
+ catch {
69
+ return { opaqueReason: 'PL/pgSQL body failed to parse' };
70
+ }
71
+ const flags = { calls: [], tables: [], settings: [], opaque: false };
72
+ const exprs = [];
73
+ collectPlpgsql(parsed, exprs, flags);
74
+ const statements = exprs.map((e) => {
75
+ // parseMode 0 = full statement; 3 = assignment (`target := expr` or
76
+ // `target = expr`) — strip the anchored target so the RHS parses. The
77
+ // RHS may itself contain `:=` (named arguments), so only the leading
78
+ // target is removed. Anything else is a bare expression.
79
+ let q = e.query;
80
+ if (e.parseMode === 3) {
81
+ q = q.replace(/^\s*[a-zA-Z_"][\w$".]*(\[[^\]]*\])*\s*:?=\s*/, '');
82
+ }
83
+ return e.parseMode === 0 ? q : `SELECT ${q}`;
84
+ });
85
+ return {
86
+ statements,
87
+ flags: { opaque: flags.opaque, ...(flags.opaqueReason ? { opaqueReason: flags.opaqueReason } : {}) }
88
+ };
89
+ }
90
+ /**
91
+ * {@link extractAccess} asked of a whole function rather than one statement:
92
+ * every relation the body touches, with the privilege the touch exercises.
93
+ *
94
+ * This is what a SECURITY DEFINER function's reach is made of — the body runs
95
+ * as the owner, so each `INSERT INTO t` in it is an INSERT on `t` that the
96
+ * caller never needed a grant for. A body that cannot be read is `opaque`,
97
+ * and an opaque body's access list is a fragment its caller must discard.
98
+ */
99
+ async function extractFunctionAccess(fn) {
100
+ if (!ANALYZABLE.has(fn.language)) {
101
+ if (fn.language === 'internal' || fn.language === 'c')
102
+ return { accesses: [], opaque: false };
103
+ return {
104
+ accesses: [],
105
+ opaque: true,
106
+ opaqueReason: `language "${fn.language}" is not statically analyzable`
107
+ };
108
+ }
109
+ if (fn.language === 'sql') {
110
+ if (!fn.source || fn.source.trim() === '')
111
+ return { accesses: [], opaque: false };
112
+ return extractAccess(fn.source);
113
+ }
114
+ const embedded = await plpgsqlStatements(fn);
115
+ if ('opaqueReason' in embedded) {
116
+ return { accesses: [], opaque: true, opaqueReason: embedded.opaqueReason };
117
+ }
118
+ const accesses = [];
119
+ const seen = new Set();
120
+ let opaque = embedded.flags.opaque;
121
+ let opaqueReason = embedded.flags.opaqueReason;
122
+ for (const sql of embedded.statements) {
123
+ const part = await extractAccess(sql);
124
+ if (part.opaque && !opaque) {
125
+ opaque = true;
126
+ opaqueReason = part.opaqueReason;
127
+ }
128
+ for (const a of part.accesses) {
129
+ const key = `${a.schema ?? ''}.${a.name}::${a.privilege}`;
130
+ if (seen.has(key))
131
+ continue;
132
+ seen.add(key);
133
+ accesses.push(a);
134
+ }
135
+ }
136
+ return { accesses, opaque, ...(opaqueReason ? { opaqueReason } : {}) };
137
+ }
138
+ /** Walk the PL/pgSQL JSON tree: collect embedded SQL, flag dynamic EXECUTE. */
139
+ function collectPlpgsql(node, exprs, out) {
140
+ if (Array.isArray(node)) {
141
+ for (const item of node)
142
+ collectPlpgsql(item, exprs, out);
143
+ return;
144
+ }
145
+ if (!node || typeof node !== 'object')
146
+ return;
147
+ const rec = node;
148
+ if (rec.PLpgSQL_stmt_dynexecute) {
149
+ out.opaque = true;
150
+ out.opaqueReason = 'dynamic SQL (EXECUTE) — cannot follow statically';
151
+ // Still walk it: the format() expression itself may call functions.
152
+ }
153
+ const expr = rec.PLpgSQL_expr;
154
+ if (expr && typeof expr.query === 'string') {
155
+ exprs.push({ query: expr.query, parseMode: typeof expr.parseMode === 'number' ? expr.parseMode : 2 });
156
+ }
157
+ for (const value of Object.values(rec))
158
+ collectPlpgsql(value, exprs, out);
159
+ }
160
+ /**
161
+ * The same extraction over a standalone SQL statement — a view body, a policy
162
+ * predicate, anything that is already plain SQL rather than a function.
163
+ */
164
+ async function extractQuery(sql) {
165
+ let ast;
166
+ try {
167
+ ast = await (0, pgsql_parser_1.parse)(sql);
168
+ }
169
+ catch {
170
+ // Individual fragments can legitimately fail (PL/pgSQL variables in
171
+ // type positions, etc.) — treat as opaque rather than erroring out.
172
+ return { ...EMPTY, opaque: true, opaqueReason: 'SQL fragment failed to parse' };
173
+ }
174
+ const out = { calls: [], tables: [], settings: [], opaque: false };
175
+ for (const call of (0, walk_1.findAll)(ast, 'FuncCall')) {
176
+ const ref = (0, walk_1.funcNameParts)(call);
177
+ if (ref.name === '<unknown>')
178
+ continue;
179
+ out.calls.push(ref);
180
+ if (ref.name === 'set_config' && (!ref.schema || ref.schema === 'pg_catalog')) {
181
+ const setting = firstStringArg(call);
182
+ if (setting)
183
+ out.settings.push(setting);
184
+ }
185
+ // Not opaque: the rest of the body still reads correctly. Tainted: the
186
+ // relations this call reaches are in a string, and we do not follow it.
187
+ if (SQL_EXECUTING.has(ref.name)) {
188
+ out.tainted ??= `\`${ref.name}\` executes SQL this analysis cannot see`;
189
+ }
190
+ }
191
+ // Write targets: the relation of INSERT/UPDATE/DELETE statements.
192
+ const writeOids = new Set();
193
+ for (const tag of ['InsertStmt', 'UpdateStmt', 'DeleteStmt']) {
194
+ for (const stmt of (0, walk_1.findAll)(ast, tag)) {
195
+ const rel = stmt.relation;
196
+ if (rel)
197
+ writeOids.add(rel);
198
+ }
199
+ }
200
+ for (const rv of (0, walk_1.findAll)(ast, 'RangeVar')) {
201
+ const name = typeof rv.relname === 'string' ? rv.relname : undefined;
202
+ if (!name)
203
+ continue;
204
+ const schema = typeof rv.schemaname === 'string' ? rv.schemaname : undefined;
205
+ out.tables.push({ schema, name, write: writeOids.has(rv) });
206
+ }
207
+ // SET role / SET session_authorization / SET request.jwt… inside SQL bodies.
208
+ for (const set of (0, walk_1.findAll)(ast, 'VariableSetStmt')) {
209
+ const name = typeof set.name === 'string' ? set.name : undefined;
210
+ if (name)
211
+ out.settings.push(name.toLowerCase());
212
+ }
213
+ return finalize(out);
214
+ }
215
+ /**
216
+ * The same walk as {@link extractQuery}, but resolving each relation
217
+ * reference to the privilege it exercises instead of a read/write bit.
218
+ *
219
+ * Used for statements whose interesting content is *which grant* a reference
220
+ * needs — a rewrite rule's actions, where `INSERT INTO audit` means the rule
221
+ * needs INSERT on `audit` and nothing else tells you so.
222
+ */
223
+ async function extractAccess(sql) {
224
+ let ast;
225
+ try {
226
+ ast = await (0, pgsql_parser_1.parse)(sql);
227
+ }
228
+ catch {
229
+ return { accesses: [], opaque: true, opaqueReason: 'SQL fragment failed to parse' };
230
+ }
231
+ const byNode = new Map();
232
+ const commands = [
233
+ ['InsertStmt', 'INSERT'],
234
+ ['UpdateStmt', 'UPDATE'],
235
+ ['DeleteStmt', 'DELETE']
236
+ ];
237
+ for (const [tag, privilege] of commands) {
238
+ for (const stmt of (0, walk_1.findAll)(ast, tag)) {
239
+ const rel = stmt.relation;
240
+ if (rel)
241
+ byNode.set(rel, privilege);
242
+ }
243
+ }
244
+ let opaque = false;
245
+ let opaqueReason;
246
+ for (const call of (0, walk_1.findAll)(ast, 'FuncCall')) {
247
+ const ref = (0, walk_1.funcNameParts)(call);
248
+ // A rule action can hide its real target behind a function call; the
249
+ // relations that call touches are not in this AST.
250
+ if (ref.name === 'query_to_xml' || ref.name === 'dblink' || ref.name === 'dblink_exec') {
251
+ opaque = true;
252
+ opaqueReason ??= `\`${ref.name}\` executes SQL this analysis cannot see`;
253
+ }
254
+ }
255
+ const accesses = [];
256
+ const seen = new Set();
257
+ for (const rv of (0, walk_1.findAll)(ast, 'RangeVar')) {
258
+ const name = typeof rv.relname === 'string' ? rv.relname : undefined;
259
+ if (!name)
260
+ continue;
261
+ const schema = typeof rv.schemaname === 'string' ? rv.schemaname : undefined;
262
+ const privilege = byNode.get(rv) ?? 'SELECT';
263
+ const key = `${schema ?? ''}.${name}::${privilege}`;
264
+ if (seen.has(key))
265
+ continue;
266
+ seen.add(key);
267
+ accesses.push({ ...(schema ? { schema } : {}), name, privilege });
268
+ }
269
+ return { accesses, opaque, ...(opaqueReason ? { opaqueReason } : {}) };
270
+ }
271
+ /**
272
+ * Does this body restrict which rows come back?
273
+ *
274
+ * `null` when the SQL could not be parsed — "unknown", never "no". A view that
275
+ * filters is a view someone may be relying on as a row-level boundary, which
276
+ * is the precondition for the `security_barrier` question: without a `WHERE`
277
+ * there are no hidden rows for a leaky qual to reach.
278
+ *
279
+ * Only an explicit `WHERE`/`HAVING` counts. Row-limiting through a join,
280
+ * `DISTINCT` or `LIMIT` is real but is not the pattern that gets written as a
281
+ * security boundary, and treating it as one would fire on ordinary reporting
282
+ * views.
283
+ */
284
+ async function bodyFiltersRows(sql) {
285
+ let ast;
286
+ try {
287
+ ast = await (0, pgsql_parser_1.parse)(sql);
288
+ }
289
+ catch {
290
+ return null;
291
+ }
292
+ return (0, walk_1.findAll)(ast, 'SelectStmt').some((s) => !!s.whereClause || !!s.havingClause);
293
+ }
294
+ function firstStringArg(call) {
295
+ const args = call.args;
296
+ if (!Array.isArray(args) || args.length === 0)
297
+ return null;
298
+ const first = args[0];
299
+ const aconst = first.A_Const;
300
+ const sval = aconst?.sval;
301
+ const v = sval?.sval;
302
+ return typeof v === 'string' ? v : null;
303
+ }
304
+ function mergeBody(into, from) {
305
+ into.calls.push(...from.calls);
306
+ into.tables.push(...from.tables);
307
+ into.settings.push(...from.settings);
308
+ if (from.opaque && !into.opaque) {
309
+ into.opaque = true;
310
+ into.opaqueReason = from.opaqueReason;
311
+ }
312
+ into.tainted ??= from.tainted;
313
+ }
314
+ function finalize(body) {
315
+ const callKeys = new Set();
316
+ const calls = body.calls.filter((c) => {
317
+ const k = `${c.schema ?? ''}.${c.name}`;
318
+ if (callKeys.has(k))
319
+ return false;
320
+ callKeys.add(k);
321
+ return true;
322
+ });
323
+ const tableKeys = new Map();
324
+ for (const t of body.tables) {
325
+ const k = `${t.schema ?? ''}.${t.name}`;
326
+ const existing = tableKeys.get(k);
327
+ if (existing)
328
+ existing.write = existing.write || t.write;
329
+ else
330
+ tableKeys.set(k, { ...t });
331
+ }
332
+ return {
333
+ calls,
334
+ tables: [...tableKeys.values()],
335
+ settings: [...new Set(body.settings)],
336
+ opaque: body.opaque,
337
+ ...(body.opaqueReason ? { opaqueReason: body.opaqueReason } : {}),
338
+ ...(body.tainted ? { tainted: body.tainted } : {})
339
+ };
340
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Call-graph construction and trust-boundary classification.
3
+ *
4
+ * Starting from the exposed entry points (functions the API roles can
5
+ * EXECUTE), walk what those functions transitively call and touch, and emit
6
+ * an unscored audit checklist of trust boundaries for human review:
7
+ *
8
+ * CG1 trust hop — execution crosses into a SECURITY DEFINER
9
+ * CG2 RLS-bypass path — a DEFINER touches a table whose RLS its owner bypasses
10
+ * CG3 auth-context change — a reachable function mutates jwt claims / role
11
+ * CG4 internal reach — a non-exposed table is reached via a DEFINER hop
12
+ * CG5 opaque node — dynamic SQL / unparseable body; audit manually
13
+ *
14
+ * Plus two provable misconfigurations:
15
+ *
16
+ * CF1 SECURITY DEFINER without a pinned search_path (CWE-426)
17
+ * CF2 SECURITY DEFINER executable by anonymous / PUBLIC
18
+ */
19
+ import type { FunctionSnapshot } from '../pg/functions';
20
+ import type { TableSnapshot } from '../pg/introspect';
21
+ export interface CallGraphOptions {
22
+ functions: FunctionSnapshot[];
23
+ tables: TableSnapshot[];
24
+ /** Exposed schemas; when undefined the exposure surface is unknown. */
25
+ exposedSchemas?: string[];
26
+ /** Roles the API connects as. EXECUTE for any of these (or PUBLIC) marks an entry point. */
27
+ apiRoles: string[];
28
+ }
29
+ export interface CallGraphNode {
30
+ id: string;
31
+ schema: string;
32
+ name: string;
33
+ securityDefiner: boolean;
34
+ owner: string;
35
+ ownerBypassesRls: boolean;
36
+ searchPathPinned: boolean;
37
+ language: string;
38
+ opaque: boolean;
39
+ opaqueReason?: string;
40
+ /** Auth-context settings this function writes (jwt claims, role, …). */
41
+ authSettings: string[];
42
+ /** Entry-point roles when this node is an entry (EXECUTE-granted API roles). */
43
+ entryRoles?: string[];
44
+ }
45
+ export interface CallGraphEdge {
46
+ from: string;
47
+ to: string;
48
+ kind: 'call' | 'read' | 'write';
49
+ }
50
+ export type ChecklistCode = 'CG1' | 'CG2' | 'CG3' | 'CG4' | 'CG5' | 'CF1' | 'CF2';
51
+ export interface ChecklistItem {
52
+ code: ChecklistCode;
53
+ /** Entry point this boundary is reachable from. */
54
+ entry: string;
55
+ /** Call path from the entry to the flagged node, entry first. */
56
+ path: string[];
57
+ /** The flagged function. */
58
+ fn: string;
59
+ /** The touched table, for CG2/CG4. */
60
+ table?: string;
61
+ message: string;
62
+ }
63
+ export interface CallGraphReport {
64
+ entries: Array<{
65
+ fn: string;
66
+ roles: string[];
67
+ securityDefiner: boolean;
68
+ }>;
69
+ nodes: CallGraphNode[];
70
+ edges: CallGraphEdge[];
71
+ checklist: ChecklistItem[];
72
+ stats: {
73
+ entryPoints: number;
74
+ reachableFunctions: number;
75
+ trustHops: number;
76
+ rlsBypassPaths: number;
77
+ authContextMutations: number;
78
+ internalReach: number;
79
+ opaqueNodes: number;
80
+ };
81
+ }
82
+ export declare function buildCallGraph(options: CallGraphOptions): Promise<CallGraphReport>;
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+ /**
3
+ * Call-graph construction and trust-boundary classification.
4
+ *
5
+ * Starting from the exposed entry points (functions the API roles can
6
+ * EXECUTE), walk what those functions transitively call and touch, and emit
7
+ * an unscored audit checklist of trust boundaries for human review:
8
+ *
9
+ * CG1 trust hop — execution crosses into a SECURITY DEFINER
10
+ * CG2 RLS-bypass path — a DEFINER touches a table whose RLS its owner bypasses
11
+ * CG3 auth-context change — a reachable function mutates jwt claims / role
12
+ * CG4 internal reach — a non-exposed table is reached via a DEFINER hop
13
+ * CG5 opaque node — dynamic SQL / unparseable body; audit manually
14
+ *
15
+ * Plus two provable misconfigurations:
16
+ *
17
+ * CF1 SECURITY DEFINER without a pinned search_path (CWE-426)
18
+ * CF2 SECURITY DEFINER executable by anonymous / PUBLIC
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.buildCallGraph = buildCallGraph;
22
+ const extract_1 = require("./extract");
23
+ /** GUC names whose mutation means the function changes who the caller "is". */
24
+ const AUTH_SETTING_PATTERN = /jwt|claims|role|session_authorization|user_id|\bauth\b/i;
25
+ async function buildCallGraph(options) {
26
+ const exposureKnown = options.exposedSchemas !== undefined;
27
+ const exposed = new Set(options.exposedSchemas ?? []);
28
+ const apiRoles = new Set([...options.apiRoles, 'PUBLIC']);
29
+ // Overloads collapse into one node per schema.name — we resolve calls by
30
+ // name, not by argument types.
31
+ const nodesById = new Map();
32
+ const fnsById = new Map();
33
+ const fnsByName = new Map();
34
+ for (const fn of options.functions) {
35
+ const id = `${fn.schema}.${fn.name}`;
36
+ const group = fnsById.get(id);
37
+ if (group)
38
+ group.push(fn);
39
+ else {
40
+ fnsById.set(id, [fn]);
41
+ const byName = fnsByName.get(fn.name);
42
+ if (byName)
43
+ byName.push(id);
44
+ else
45
+ fnsByName.set(fn.name, [id]);
46
+ }
47
+ }
48
+ const tablesById = new Map();
49
+ const tablesByName = new Map();
50
+ for (const t of options.tables) {
51
+ const id = `${t.schema}.${t.name}`;
52
+ tablesById.set(id, t);
53
+ const byName = tablesByName.get(t.name);
54
+ if (byName)
55
+ byName.push(id);
56
+ else
57
+ tablesByName.set(t.name, [id]);
58
+ }
59
+ // Entry points: functions in an exposed schema (or anywhere, when exposure
60
+ // is unknown) that an API role can EXECUTE.
61
+ const entries = [];
62
+ for (const [id, group] of fnsById) {
63
+ const fn = group[0];
64
+ if (exposureKnown && !exposed.has(fn.schema))
65
+ continue;
66
+ const roles = [...new Set(group.flatMap((g) => g.grants.map((gr) => gr.role)).filter((r) => apiRoles.has(r)))].sort();
67
+ if (roles.length === 0)
68
+ continue;
69
+ entries.push({ fn: id, roles, securityDefiner: group.some((g) => g.isSecurityDefiner) });
70
+ }
71
+ entries.sort((a, b) => a.fn.localeCompare(b.fn));
72
+ // Extract bodies lazily, memoized per node.
73
+ const bodies = new Map();
74
+ const getBody = async (id) => {
75
+ const cached = bodies.get(id);
76
+ if (cached)
77
+ return cached;
78
+ const group = fnsById.get(id) ?? [];
79
+ const merged = { calls: [], tables: [], settings: [], opaque: false };
80
+ for (const fn of group) {
81
+ const b = await (0, extract_1.extractBody)(fn);
82
+ merged.calls.push(...b.calls);
83
+ merged.tables.push(...b.tables);
84
+ merged.settings.push(...b.settings);
85
+ if (b.opaque && !merged.opaque) {
86
+ merged.opaque = true;
87
+ merged.opaqueReason = b.opaqueReason;
88
+ }
89
+ }
90
+ bodies.set(id, merged);
91
+ return merged;
92
+ };
93
+ const resolveCall = (ref) => {
94
+ if (ref.schema) {
95
+ return fnsById.has(`${ref.schema}.${ref.name}`) ? [`${ref.schema}.${ref.name}`] : [];
96
+ }
97
+ // Unqualified: candidates are every user function with that name. None →
98
+ // assume a builtin. Multiple → follow all (conservative over-approximation).
99
+ return fnsByName.get(ref.name) ?? [];
100
+ };
101
+ const resolveTable = (ref) => {
102
+ if (ref.schema) {
103
+ return tablesById.has(`${ref.schema}.${ref.name}`) ? [`${ref.schema}.${ref.name}`] : [];
104
+ }
105
+ return tablesByName.get(ref.name) ?? [];
106
+ };
107
+ const ensureNode = (id, body) => {
108
+ let node = nodesById.get(id);
109
+ if (node)
110
+ return node;
111
+ const group = fnsById.get(id) ?? [];
112
+ const fn = group[0];
113
+ node = {
114
+ id,
115
+ schema: fn.schema,
116
+ name: fn.name,
117
+ securityDefiner: group.some((g) => g.isSecurityDefiner),
118
+ owner: fn.owner,
119
+ ownerBypassesRls: fn.ownerBypassesRls,
120
+ searchPathPinned: group.every((g) => g.searchPathPinned),
121
+ language: fn.language,
122
+ opaque: body.opaque,
123
+ ...(body.opaqueReason ? { opaqueReason: body.opaqueReason } : {}),
124
+ authSettings: body.settings.filter((s) => AUTH_SETTING_PATTERN.test(s)).sort()
125
+ };
126
+ nodesById.set(id, node);
127
+ return node;
128
+ };
129
+ const edges = [];
130
+ const edgeKeys = new Set();
131
+ const addEdge = (edge) => {
132
+ const key = `${edge.from}→${edge.to}:${edge.kind}`;
133
+ if (edgeKeys.has(key))
134
+ return;
135
+ edgeKeys.add(key);
136
+ edges.push(edge);
137
+ };
138
+ const checklist = [];
139
+ const itemKeys = new Set();
140
+ const addItem = (item) => {
141
+ // Dedupe on (code, fn, table) — the shortest path from the first entry
142
+ // that reaches the boundary is kept as the exemplar.
143
+ const key = `${item.code}::${item.fn}::${item.table ?? ''}`;
144
+ if (itemKeys.has(key))
145
+ return;
146
+ itemKeys.add(key);
147
+ checklist.push(item);
148
+ };
149
+ // BFS from each entry point; nodes are visited once globally for graph
150
+ // construction, but per-entry paths drive the checklist exemplars.
151
+ const globallyVisited = new Set();
152
+ for (const entry of entries) {
153
+ const queue = [{ id: entry.fn, path: [entry.fn] }];
154
+ const visited = new Set([entry.fn]);
155
+ while (queue.length > 0) {
156
+ const { id, path } = queue.shift();
157
+ const body = await getBody(id);
158
+ const node = ensureNode(id, body);
159
+ if (id === entry.fn)
160
+ node.entryRoles = entry.roles;
161
+ const firstVisit = !globallyVisited.has(id);
162
+ globallyVisited.add(id);
163
+ // Does the path so far cross a DEFINER boundary?
164
+ const pathHasDefiner = path.some((p) => nodesById.get(p)?.securityDefiner);
165
+ if (node.opaque) {
166
+ addItem({
167
+ code: 'CG5', entry: entry.fn, path, fn: id,
168
+ message: node.opaqueReason ?? 'body cannot be followed statically — audit manually'
169
+ });
170
+ }
171
+ if (node.authSettings.length > 0) {
172
+ addItem({
173
+ code: 'CG3', entry: entry.fn, path, fn: id,
174
+ message: `mutates auth context: ${node.authSettings.join(', ')}`
175
+ });
176
+ }
177
+ if (node.securityDefiner && firstVisit) {
178
+ if (!node.searchPathPinned) {
179
+ addItem({
180
+ code: 'CF1', entry: entry.fn, path, fn: id,
181
+ message: 'SECURITY DEFINER without a pinned search_path (CWE-426) — set `SET search_path` on the function'
182
+ });
183
+ }
184
+ const group = fnsById.get(id) ?? [];
185
+ const wideRoles = [...new Set(group.flatMap((g) => g.grants.map((gr) => gr.role))
186
+ .filter((r) => r === 'PUBLIC' || r === 'anonymous'))].sort();
187
+ if (wideRoles.length > 0) {
188
+ addItem({
189
+ code: 'CF2', entry: entry.fn, path, fn: id,
190
+ message: `SECURITY DEFINER executable by ${wideRoles.join(', ')} — widest blast radius; confirm this is intended`
191
+ });
192
+ }
193
+ }
194
+ // Table edges.
195
+ for (const t of body.tables) {
196
+ for (const tableId of resolveTable(t)) {
197
+ const table = tablesById.get(tableId);
198
+ addEdge({ from: id, to: tableId, kind: t.write ? 'write' : 'read' });
199
+ const effectiveDefiner = node.securityDefiner || pathHasDefiner;
200
+ if (node.securityDefiner
201
+ && (node.owner === table.owner || node.ownerBypassesRls)
202
+ && !table.rlsForced) {
203
+ addItem({
204
+ code: 'CG2', entry: entry.fn, path, fn: id, table: tableId,
205
+ message: `RLS on ${tableId} does not apply on this path — ${id} is SECURITY DEFINER running as ${node.owner}`
206
+ + (node.ownerBypassesRls ? ' (BYPASSRLS/superuser)' : ` (owner of ${tableId})`)
207
+ });
208
+ }
209
+ if (exposureKnown && !exposed.has(table.schema) && effectiveDefiner) {
210
+ addItem({
211
+ code: 'CG4', entry: entry.fn, path, fn: id, table: tableId,
212
+ message: `${t.write ? 'writes' : 'reads'} internal table ${tableId} via a SECURITY DEFINER path`
213
+ });
214
+ }
215
+ }
216
+ }
217
+ // Call edges.
218
+ for (const c of body.calls) {
219
+ for (const calleeId of resolveCall(c)) {
220
+ if (calleeId === id)
221
+ continue;
222
+ addEdge({ from: id, to: calleeId, kind: 'call' });
223
+ const calleeBody = await getBody(calleeId);
224
+ const callee = ensureNode(calleeId, calleeBody);
225
+ if (callee.securityDefiner) {
226
+ addItem({
227
+ code: 'CG1', entry: entry.fn, path: [...path, calleeId], fn: calleeId,
228
+ message: `trust hop — execution crosses into SECURITY DEFINER ${calleeId} (runs as ${callee.owner})`
229
+ });
230
+ }
231
+ if (!visited.has(calleeId)) {
232
+ visited.add(calleeId);
233
+ queue.push({ id: calleeId, path: [...path, calleeId] });
234
+ }
235
+ }
236
+ }
237
+ }
238
+ // The entry itself being a DEFINER is the first trust hop.
239
+ const entryNode = nodesById.get(entry.fn);
240
+ if (entryNode?.securityDefiner) {
241
+ addItem({
242
+ code: 'CG1', entry: entry.fn, path: [entry.fn], fn: entry.fn,
243
+ message: `trust hop — entry point is SECURITY DEFINER (runs as ${entryNode.owner}; callable by ${entry.roles.join(', ')})`
244
+ });
245
+ }
246
+ }
247
+ checklist.sort(compareItems);
248
+ const nodes = [...nodesById.values()].sort((a, b) => a.id.localeCompare(b.id));
249
+ edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || a.kind.localeCompare(b.kind));
250
+ const count = (code) => checklist.filter((i) => i.code === code).length;
251
+ return {
252
+ entries,
253
+ nodes,
254
+ edges,
255
+ checklist,
256
+ stats: {
257
+ entryPoints: entries.length,
258
+ reachableFunctions: globallyVisited.size,
259
+ trustHops: count('CG1'),
260
+ rlsBypassPaths: count('CG2'),
261
+ authContextMutations: count('CG3'),
262
+ internalReach: count('CG4'),
263
+ opaqueNodes: count('CG5')
264
+ }
265
+ };
266
+ }
267
+ const CODE_ORDER = {
268
+ CF1: 0, CF2: 1, CG2: 2, CG3: 3, CG1: 4, CG4: 5, CG5: 6
269
+ };
270
+ function compareItems(a, b) {
271
+ if (CODE_ORDER[a.code] !== CODE_ORDER[b.code])
272
+ return CODE_ORDER[a.code] - CODE_ORDER[b.code];
273
+ if (a.fn !== b.fn)
274
+ return a.fn.localeCompare(b.fn);
275
+ return (a.table ?? '').localeCompare(b.table ?? '');
276
+ }