@safegres/test 0.0.2 → 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,206 @@
1
+ /**
2
+ * PostGraphile *behaviors*, read from the object comments they are declared in.
3
+ *
4
+ * A behavior string is the schema author stating which parts of the generated
5
+ * API an object participates in — `@behavior -list -connection` on a foreign
6
+ * key constraint says the reverse relation is not exposed. That is a different
7
+ * kind of evidence from anything else safegres collects: `pg_class.reltuples`
8
+ * and `pg_stat_user_indexes.idx_scan` are *measurements*, and safegres grades
9
+ * an ephemeral CI database that has never held data, so both read zero at
10
+ * exactly the moment they would have to mean something. A declaration reads the
11
+ * same in CI as in production.
12
+ *
13
+ * Read from the comment rather than from a running Graphile instance, or from
14
+ * whatever tables the author generated the comment out of:
15
+ *
16
+ * - `@behavior` in a comment is the PostGraphile v5 convention, so this works
17
+ * on any Graphile database rather than one project's metadata schema.
18
+ * - The comment is the composed value — one tag per object — so it is already
19
+ * the author's resolved intent.
20
+ * - A live instance would additionally resolve preset defaults, but requires an
21
+ * API to be running. safegres grades a database.
22
+ *
23
+ * The consequence of that last point is the rule this module exists to enforce:
24
+ * **only an explicit negative fragment is evidence.** Presets grant most
25
+ * behaviors by default, so the *absence* of `+list` says nothing whatsoever. A
26
+ * scanner that reads silence as denial is a scanner that tells you to drop an
27
+ * index a live API is using.
28
+ */
29
+ export function emptyBehaviorSnapshot() {
30
+ return {
31
+ tables: new Map(),
32
+ columns: new Map(),
33
+ constraints: new Map(),
34
+ constraintDirections: new Map()
35
+ };
36
+ }
37
+ /**
38
+ * The behavior governing one direction of a relation: the directional tag when
39
+ * present, else the undirected one. They do not merge — Graphile resolves the
40
+ * specific tag against the preset, and treating `@behavior -list` as an extra
41
+ * fragment on top of `@backwardBehavior +list` would invert the author's
42
+ * override.
43
+ */
44
+ export function directionalBehavior(behaviors, direction) {
45
+ if (!behaviors)
46
+ return undefined;
47
+ return behaviors[direction] ?? behaviors.both;
48
+ }
49
+ /** Catalog schemas no API surface ever contains. */
50
+ export const SYSTEM_SCHEMAS = ['pg_catalog', 'information_schema', 'pg_toast'];
51
+ const DEFAULT_EXCLUDES = SYSTEM_SCHEMAS;
52
+ /**
53
+ * Extract the `@behavior` smart tag from an object comment.
54
+ *
55
+ * Smart tags occupy the leading lines of the comment, `@tag [value]`, and stop
56
+ * at the first line that is not one; everything after is the description. Only
57
+ * `@behavior` is returned — `@omit` is deliberately not translated here, since
58
+ * its v4 semantics ("remove from the schema") are not the same question as a
59
+ * behavior fragment and conflating them would put a guess in the evidence.
60
+ */
61
+ export function parseBehaviorTag(comment) {
62
+ return parseBehaviorTags(comment).both ?? null;
63
+ }
64
+ /** Every behavior tag on a comment, by direction. */
65
+ export function parseBehaviorTags(comment) {
66
+ const out = {};
67
+ if (!comment)
68
+ return out;
69
+ for (const line of comment.split('\n')) {
70
+ const trimmed = line.trim();
71
+ if (!trimmed.startsWith('@'))
72
+ break;
73
+ const match = /^@(behavior|forwardBehavior|backwardBehavior)\s+(.+)$/.exec(trimmed);
74
+ if (!match)
75
+ continue;
76
+ const value = match[2].trim();
77
+ if (match[1] === 'behavior')
78
+ out.both = value;
79
+ else if (match[1] === 'forwardBehavior')
80
+ out.forward = value;
81
+ else
82
+ out.backward = value;
83
+ }
84
+ return out;
85
+ }
86
+ /** Split a behavior string into fragments. A bare scope is a grant, as in PostGraphile. */
87
+ export function parseFragments(behavior) {
88
+ if (!behavior)
89
+ return [];
90
+ const out = [];
91
+ for (const term of behavior.trim().split(/\s+/)) {
92
+ if (!term)
93
+ continue;
94
+ const modifier = term[0] === '-' ? '-' : '+';
95
+ const scope = term[0] === '+' || term[0] === '-' ? term.slice(1) : term;
96
+ if (scope)
97
+ out.push({ modifier, scope });
98
+ }
99
+ return out;
100
+ }
101
+ /**
102
+ * Does this fragment's scope speak to `ability`?
103
+ *
104
+ * Scope paths are `:`-separated and get more specific left to right
105
+ * (`resource:connection`), so the ability is the final segment. `*` matches
106
+ * anything, which is how `-*` denies wholesale.
107
+ */
108
+ function scopeMatches(scope, ability) {
109
+ const segments = scope.split(':');
110
+ const last = segments[segments.length - 1];
111
+ return last === ability || last === '*';
112
+ }
113
+ /**
114
+ * Resolve one ability against a behavior string.
115
+ *
116
+ * `undefined` means *undeclared*, and is the answer that matters: it is not
117
+ * `false`. Later fragments win, matching PostGraphile — `-* +list` grants
118
+ * `list` and denies the rest.
119
+ */
120
+ export function resolveAbility(behavior, ability) {
121
+ let verdict;
122
+ for (const fragment of parseFragments(behavior)) {
123
+ if (scopeMatches(fragment.scope, ability))
124
+ verdict = fragment.modifier === '+';
125
+ }
126
+ return verdict;
127
+ }
128
+ /**
129
+ * True when *every* one of `abilities` is explicitly denied.
130
+ *
131
+ * Every, not some: a relation reachable as a single record is still reachable,
132
+ * and one undeclared ability is enough to leave the question open.
133
+ */
134
+ export function deniesAll(behavior, abilities) {
135
+ if (abilities.length === 0)
136
+ return false;
137
+ return abilities.every((ability) => resolveAbility(behavior, ability) === false);
138
+ }
139
+ /**
140
+ * Behavior tags on every table, column and constraint in scope.
141
+ *
142
+ * Extension-owned objects are not filtered here: the maps are only ever
143
+ * consulted for relations that survived the scan's own filtering, so an extra
144
+ * entry costs a map slot and changes nothing.
145
+ */
146
+ export async function introspectBehaviors(exec, options = {}) {
147
+ const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
148
+ const schemaFilter = options.schemas && options.schemas.length > 0
149
+ ? `n.nspname = ANY($1::text[])`
150
+ : `NOT (n.nspname = ANY($2::text[]))`;
151
+ // Both parameters are referenced (even when only one filters) so Postgres can
152
+ // infer their types — an unused $N errors out at bind time.
153
+ const sql = `
154
+ WITH _params AS (
155
+ SELECT $1::text[] AS include_schemas, $2::text[] AS exclude_schemas
156
+ ),
157
+ rels AS (
158
+ SELECT c.oid, n.nspname AS schema_name, c.relname AS table_name
159
+ FROM pg_class c
160
+ JOIN pg_namespace n ON n.oid = c.relnamespace
161
+ WHERE c.relkind IN ('r', 'p', 'v', 'm')
162
+ AND ${schemaFilter}
163
+ )
164
+ SELECT 'table'::text AS kind, r.schema_name, r.table_name, NULL::text AS member, d.description
165
+ FROM rels r
166
+ JOIN pg_description d
167
+ ON d.objoid = r.oid AND d.classoid = 'pg_class'::regclass AND d.objsubid = 0
168
+ UNION ALL
169
+ SELECT 'column', r.schema_name, r.table_name, a.attname, d.description
170
+ FROM rels r
171
+ JOIN pg_attribute a ON a.attrelid = r.oid AND a.attnum > 0 AND NOT a.attisdropped
172
+ JOIN pg_description d
173
+ ON d.objoid = r.oid AND d.classoid = 'pg_class'::regclass AND d.objsubid = a.attnum
174
+ UNION ALL
175
+ SELECT 'constraint', r.schema_name, r.table_name, co.conname, d.description
176
+ FROM rels r
177
+ JOIN pg_constraint co ON co.conrelid = r.oid
178
+ JOIN pg_description d
179
+ ON d.objoid = co.oid AND d.classoid = 'pg_constraint'::regclass
180
+ `;
181
+ const { rows } = await exec.query(sql, [options.schemas ?? [], excludes]);
182
+ const snapshot = emptyBehaviorSnapshot();
183
+ for (const row of rows) {
184
+ const tags = parseBehaviorTags(row.description);
185
+ const relation = `${row.schema_name}.${row.table_name}`;
186
+ if (row.kind === 'table') {
187
+ if (tags.both)
188
+ snapshot.tables.set(relation, tags.both);
189
+ continue;
190
+ }
191
+ if (!row.member)
192
+ continue;
193
+ const key = `${relation}.${row.member}`;
194
+ if (row.kind === 'column') {
195
+ if (tags.both)
196
+ snapshot.columns.set(key, tags.both);
197
+ continue;
198
+ }
199
+ if (tags.both)
200
+ snapshot.constraints.set(key, tags.both);
201
+ if (tags.both || tags.forward || tags.backward) {
202
+ snapshot.constraintDirections.set(key, tags);
203
+ }
204
+ }
205
+ return snapshot;
206
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Exposure-surface resolution: what is actually reachable, and by whom.
3
+ *
4
+ * A database has more than one way in. The declared API is the one the score
5
+ * is *about* — `resolveExposure` returns it, unchanged, and it stays the
6
+ * headline. `resolvePlanes` returns it alongside every other access path the
7
+ * config declares or an adapter discovers, so a direct connection as some
8
+ * role can be graded too, without moving the number that describes the API.
9
+ */
10
+ import type { ExposureConfig, PlaneKind } from '../config/types';
11
+ import type { ApiReach } from '../exposure/reach';
12
+ import type { QueryExecutor } from './introspect';
13
+ export interface ResolvedExposure {
14
+ /** True when a surface was configured or auto-resolved. */
15
+ known: boolean;
16
+ source: string;
17
+ schemas: string[];
18
+ roles?: string[];
19
+ /** The subset of `roles` reachable without authenticating. */
20
+ anonRoles?: string[];
21
+ }
22
+ /** A plane before its reach is computed against the table snapshot. */
23
+ export interface ResolvedPlane {
24
+ name: string;
25
+ kind: PlaneKind;
26
+ /** The headline plane: its score is `report.score`. Exactly one is primary. */
27
+ primary: boolean;
28
+ /** Where the plane came from: an adapter name, `config`, or `none`. */
29
+ source: string;
30
+ schemas: string[];
31
+ roles: string[];
32
+ anonRoles: string[];
33
+ }
34
+ export declare const UNKNOWN_EXPOSURE: ResolvedExposure;
35
+ /**
36
+ * Resolve the primary exposure surface: the declared API. Adapters run first
37
+ * (static `schemas`/`roles` extend, never replace, what they find), then the
38
+ * static surface, then `UNKNOWN_EXPOSURE`.
39
+ */
40
+ export declare function resolveExposure(exec: QueryExecutor, config?: ExposureConfig): Promise<ResolvedExposure>;
41
+ /**
42
+ * Every plane to grade, primary first.
43
+ *
44
+ * The primary plane is the resolved exposure surface — the same set of
45
+ * schemas the score has always been computed against — so adding planes
46
+ * cannot move the headline number. Secondary planes come from adapters (one
47
+ * per API, where a stack has several) and from `exposure.planes`.
48
+ */
49
+ export declare function resolvePlanes(exec: QueryExecutor, config: ExposureConfig | undefined, primary: ResolvedExposure): Promise<ResolvedPlane[]>;
50
+ /**
51
+ * Relation-level reach for the exposed schemas, from whichever adapters can
52
+ * compute it.
53
+ *
54
+ * Adapters *intersect*: a relation is subtracted from a plane only when every
55
+ * adapter that has an opinion agrees it is unaddressable. One adapter's
56
+ * silence is not agreement, so an adapter without `reach` is not consulted,
57
+ * but an adapter that ran and did not name a relation is a positive vote to
58
+ * keep it.
59
+ */
60
+ export declare function resolveReach(exec: QueryExecutor, config: ExposureConfig | undefined, context: {
61
+ schemas: string[];
62
+ excludeSchemas?: string[];
63
+ }): Promise<ApiReach | undefined>;
64
+ /**
65
+ * Constructive routing-plane introspection.
66
+ *
67
+ * @deprecated Use `constructiveAdapter` from `safegres/adapters`; this wrapper
68
+ * preserves the pre-adapter shape (one union of every API) for callers that
69
+ * still import it.
70
+ */
71
+ export declare function resolveConstructiveExposure(exec: QueryExecutor): Promise<{
72
+ schemas: string[];
73
+ roles: string[];
74
+ } | null>;
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Exposure-surface resolution: what is actually reachable, and by whom.
3
+ *
4
+ * A database has more than one way in. The declared API is the one the score
5
+ * is *about* — `resolveExposure` returns it, unchanged, and it stays the
6
+ * headline. `resolvePlanes` returns it alongside every other access path the
7
+ * config declares or an adapter discovers, so a direct connection as some
8
+ * role can be graded too, without moving the number that describes the API.
9
+ */
10
+ import { BUILTIN_ADAPTERS, resolveAdapters } from '../exposure/adapters';
11
+ export const UNKNOWN_EXPOSURE = {
12
+ known: false,
13
+ source: 'none',
14
+ schemas: []
15
+ };
16
+ /**
17
+ * Resolve the primary exposure surface: the declared API. Adapters run first
18
+ * (static `schemas`/`roles` extend, never replace, what they find), then the
19
+ * static surface, then `UNKNOWN_EXPOSURE`.
20
+ */
21
+ export async function resolveExposure(exec, config) {
22
+ if (!config)
23
+ return UNKNOWN_EXPOSURE;
24
+ for (const adapter of adaptersFor(config)) {
25
+ const planes = await runAdapter(exec, adapter);
26
+ if (planes.length === 0)
27
+ continue;
28
+ const primary = planes.find((p) => p.primary) ?? planes[0];
29
+ return {
30
+ known: true,
31
+ source: adapter.name,
32
+ schemas: union(primary.schemas ?? [], config.schemas),
33
+ roles: union(primary.roles ?? [], config.roles),
34
+ anonRoles: union(primary.anonRoles ?? [], config.anonRoles)
35
+ };
36
+ }
37
+ if (config.schemas && config.schemas.length > 0) {
38
+ return {
39
+ known: true,
40
+ source: 'config',
41
+ schemas: [...config.schemas].sort(),
42
+ roles: config.roles,
43
+ anonRoles: config.anonRoles
44
+ };
45
+ }
46
+ return UNKNOWN_EXPOSURE;
47
+ }
48
+ /**
49
+ * Every plane to grade, primary first.
50
+ *
51
+ * The primary plane is the resolved exposure surface — the same set of
52
+ * schemas the score has always been computed against — so adding planes
53
+ * cannot move the headline number. Secondary planes come from adapters (one
54
+ * per API, where a stack has several) and from `exposure.planes`.
55
+ */
56
+ export async function resolvePlanes(exec, config, primary) {
57
+ const primaryName = config?.name ?? 'api';
58
+ const planes = [
59
+ {
60
+ name: primaryName,
61
+ kind: 'api',
62
+ primary: true,
63
+ source: primary.source,
64
+ schemas: primary.schemas,
65
+ roles: primary.roles ?? [],
66
+ anonRoles: primary.anonRoles ?? []
67
+ }
68
+ ];
69
+ const seen = new Set([primaryName]);
70
+ const push = (input, source) => {
71
+ if (input.primary || seen.has(input.name))
72
+ return;
73
+ seen.add(input.name);
74
+ planes.push({
75
+ name: input.name,
76
+ kind: input.kind ?? defaultKind(input),
77
+ primary: false,
78
+ source,
79
+ schemas: [...(input.schemas ?? [])].sort(),
80
+ roles: [...(input.roles ?? [])].sort(),
81
+ anonRoles: [...(input.anonRoles ?? [])].sort()
82
+ });
83
+ };
84
+ if (config && primary.known) {
85
+ for (const adapter of adaptersFor(config)) {
86
+ for (const plane of await runAdapter(exec, adapter))
87
+ push(plane, adapter.name);
88
+ }
89
+ }
90
+ const declared = config?.planes ?? [];
91
+ const claimingPrimary = declared.filter((p) => p.primary);
92
+ if (claimingPrimary.length > 1) {
93
+ throw new Error('more than one exposure plane declares `primary: true` '
94
+ + `(${claimingPrimary.map((p) => p.name).join(', ')}) — exactly one plane is the headline score`);
95
+ }
96
+ for (const plane of declared)
97
+ push(plane, 'config');
98
+ // A declared plane may take the headline over from the API surface: a
99
+ // database whose product *is* direct SQL access has no API to grade.
100
+ const override = claimingPrimary[0];
101
+ if (override) {
102
+ planes[0] = {
103
+ name: override.name,
104
+ kind: override.kind ?? defaultKind(override),
105
+ primary: true,
106
+ source: 'config',
107
+ schemas: [...(override.schemas ?? [])].sort(),
108
+ roles: [...(override.roles ?? [])].sort(),
109
+ anonRoles: [...(override.anonRoles ?? [])].sort()
110
+ };
111
+ }
112
+ return planes;
113
+ }
114
+ /**
115
+ * Relation-level reach for the exposed schemas, from whichever adapters can
116
+ * compute it.
117
+ *
118
+ * Adapters *intersect*: a relation is subtracted from a plane only when every
119
+ * adapter that has an opinion agrees it is unaddressable. One adapter's
120
+ * silence is not agreement, so an adapter without `reach` is not consulted,
121
+ * but an adapter that ran and did not name a relation is a positive vote to
122
+ * keep it.
123
+ */
124
+ export async function resolveReach(exec, config, context) {
125
+ if (!config || config.reach === false)
126
+ return undefined;
127
+ let agreed;
128
+ const hidden = new Set();
129
+ let ran = false;
130
+ for (const adapter of adaptersFor(config)) {
131
+ if (!adapter.reach)
132
+ continue;
133
+ if (!(await adapter.detect(exec)))
134
+ continue;
135
+ const reach = await adapter.reach(exec, context);
136
+ ran = true;
137
+ for (const key of reach.hiddenBackwardRelations)
138
+ hidden.add(key);
139
+ const named = new Map(reach.unreachable.map((r) => [`${r.schema}.${r.table}`, r]));
140
+ if (agreed === undefined)
141
+ agreed = named;
142
+ else
143
+ for (const key of [...agreed.keys()])
144
+ if (!named.has(key))
145
+ agreed.delete(key);
146
+ }
147
+ if (!ran)
148
+ return undefined;
149
+ return {
150
+ unreachable: [...(agreed?.values() ?? [])].sort((a, b) => `${a.schema}.${a.table}`.localeCompare(`${b.schema}.${b.table}`)),
151
+ hiddenBackwardRelations: [...hidden].sort()
152
+ };
153
+ }
154
+ function defaultKind(plane) {
155
+ if (plane.kind)
156
+ return plane.kind;
157
+ return plane.roles && plane.roles.length > 0 && !(plane.schemas && plane.schemas.length > 0)
158
+ ? 'role'
159
+ : 'schema';
160
+ }
161
+ /** Adapters named by the config, plus the `resolver` alias for the built-in. */
162
+ function adaptersFor(config) {
163
+ const adapters = resolveAdapters(config.adapters);
164
+ if (config.resolver && config.resolver !== 'static') {
165
+ const builtin = BUILTIN_ADAPTERS[config.resolver];
166
+ if (builtin && !adapters.some((a) => a.name === builtin.name))
167
+ adapters.unshift(builtin);
168
+ }
169
+ return adapters;
170
+ }
171
+ /** An adapter that isn't present in this database contributes nothing. */
172
+ async function runAdapter(exec, adapter) {
173
+ if (!(await adapter.detect(exec)))
174
+ return [];
175
+ return adapter.resolve(exec);
176
+ }
177
+ /**
178
+ * Constructive routing-plane introspection.
179
+ *
180
+ * @deprecated Use `constructiveAdapter` from `safegres/adapters`; this wrapper
181
+ * preserves the pre-adapter shape (one union of every API) for callers that
182
+ * still import it.
183
+ */
184
+ export async function resolveConstructiveExposure(exec) {
185
+ const adapter = BUILTIN_ADAPTERS.constructive;
186
+ if (!(await adapter.detect(exec)))
187
+ return null;
188
+ const planes = await adapter.resolve(exec);
189
+ const primary = planes.find((p) => p.primary) ?? planes[0];
190
+ if (!primary || (primary.schemas ?? []).length === 0)
191
+ return null;
192
+ return { schemas: primary.schemas ?? [], roles: primary.roles ?? [] };
193
+ }
194
+ function union(base, extra) {
195
+ if (!extra || extra.length === 0)
196
+ return base;
197
+ return [...new Set([...base, ...extra])].sort();
198
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Function catalog introspection for the call-graph audit.
3
+ *
4
+ * One query over `pg_proc` returns every user-defined function with the
5
+ * attributes the trust-boundary analysis needs: SECURITY DEFINER vs INVOKER,
6
+ * the owner role (what a DEFINER actually runs as), whether `search_path`
7
+ * is pinned, the language, the body source, and EXECUTE grants per role.
8
+ */
9
+ import type { ExtensionScopeOptions, QueryExecutor } from './introspect';
10
+ export interface FunctionGrant {
11
+ role: string;
12
+ /** Was the grant delegated (`WITH GRANT OPTION`). */
13
+ grantable: boolean;
14
+ }
15
+ export interface FunctionSnapshot {
16
+ oid: number;
17
+ schema: string;
18
+ name: string;
19
+ /** Argument type signature, e.g. `text, uuid` — disambiguates overloads. */
20
+ args: string;
21
+ owner: string;
22
+ /** Owner is a superuser or has BYPASSRLS — RLS never applies to it. */
23
+ ownerBypassesRls: boolean;
24
+ isSecurityDefiner: boolean;
25
+ /** True when `proconfig` pins `search_path` (CWE-426 mitigation for DEFINERs). */
26
+ searchPathPinned: boolean;
27
+ language: string;
28
+ /** Raw body (`prosrc`); null for C/internal functions. */
29
+ source: string | null;
30
+ /** Full `CREATE FUNCTION` statement — what `parsePlPgSQL` consumes. */
31
+ definition: string | null;
32
+ /**
33
+ * EXECUTE grants. `PUBLIC` appears as the role name `PUBLIC`.
34
+ * When `proacl` is NULL Postgres applies the default function ACL, which
35
+ * includes EXECUTE to PUBLIC — represented here as a synthetic PUBLIC grant.
36
+ */
37
+ grants: FunctionGrant[];
38
+ /** True when the grants came from the default ACL (no explicit GRANT/REVOKE). */
39
+ defaultAcl: boolean;
40
+ /**
41
+ * The function returns `trigger`, so Postgres refuses to call it directly
42
+ * however wide its EXECUTE ACL is. EXECUTE on one confers nothing on its
43
+ * own: the body is only reachable by firing the trigger it is attached to.
44
+ */
45
+ returnsTrigger: boolean;
46
+ }
47
+ export interface IntrospectFunctionOptions {
48
+ schemas?: string[];
49
+ excludeSchemas?: string[];
50
+ extensions?: ExtensionScopeOptions;
51
+ }
52
+ export declare function introspectFunctions(exec: QueryExecutor, options?: IntrospectFunctionOptions): Promise<FunctionSnapshot[]>;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Function catalog introspection for the call-graph audit.
3
+ *
4
+ * One query over `pg_proc` returns every user-defined function with the
5
+ * attributes the trust-boundary analysis needs: SECURITY DEFINER vs INVOKER,
6
+ * the owner role (what a DEFINER actually runs as), whether `search_path`
7
+ * is pinned, the language, the body source, and EXECUTE grants per role.
8
+ */
9
+ const DEFAULT_EXCLUDES = ['pg_catalog', 'information_schema', 'pg_toast'];
10
+ /**
11
+ * Extension scoping for routines. The table-side filter reads ownership off
12
+ * `pg_class`; a function's is on `pg_proc`, and nothing about a routine is
13
+ * inherited the way a partition inherits its parent's dependency — so this is
14
+ * the same rule expressed against the other catalog, not a shared helper.
15
+ *
16
+ * Without it `extensions.ignore` narrowed the relation rules and left the
17
+ * routine rules reading the extension's own bodies: `pg_partman` alone
18
+ * accounted for every C1 finding and 60% of C4 on a real audit.
19
+ */
20
+ function routineExtensionFilter(options, paramIndex) {
21
+ const clauses = [];
22
+ if (options?.skipOwned ?? true) {
23
+ clauses.push(`
24
+ AND NOT EXISTS (
25
+ SELECT 1 FROM pg_depend d
26
+ WHERE d.classid = 'pg_proc'::regclass
27
+ AND d.refclassid = 'pg_extension'::regclass
28
+ AND d.deptype = 'e'
29
+ AND d.objid = p.oid
30
+ )`);
31
+ }
32
+ clauses.push(`
33
+ AND NOT (n.oid = ANY (
34
+ SELECT e.extnamespace FROM pg_extension e WHERE e.extname = ANY($${paramIndex}::text[])
35
+ ))`);
36
+ return clauses.join('');
37
+ }
38
+ export async function introspectFunctions(exec, options = {}) {
39
+ const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
40
+ const schemaFilter = options.schemas && options.schemas.length > 0
41
+ ? `AND n.nspname = ANY($1::text[])`
42
+ : `AND NOT (n.nspname = ANY($2::text[]))`;
43
+ const extFilter = routineExtensionFilter(options.extensions, 3);
44
+ const sql = `
45
+ WITH _params AS (
46
+ SELECT $1::text[] AS include_schemas, $2::text[] AS exclude_schemas,
47
+ $3::text[] AS ignore_extensions
48
+ ),
49
+ procs AS (
50
+ SELECT
51
+ p.oid,
52
+ n.nspname AS schema_name,
53
+ p.proname AS name,
54
+ pg_get_function_identity_arguments(p.oid) AS args,
55
+ pg_get_userbyid(p.proowner) AS owner,
56
+ COALESCE(o.rolsuper OR o.rolbypassrls, false) AS owner_bypasses_rls,
57
+ p.prosecdef AS security_definer,
58
+ EXISTS (
59
+ SELECT 1 FROM unnest(COALESCE(p.proconfig, ARRAY[]::text[])) cfg
60
+ WHERE cfg LIKE 'search_path=%'
61
+ ) AS search_path_pinned,
62
+ l.lanname AS language,
63
+ p.prorettype = 'pg_catalog.trigger'::regtype AS returns_trigger,
64
+ p.prosrc AS source,
65
+ CASE WHEN l.lanname IN ('sql', 'plpgsql')
66
+ THEN pg_get_functiondef(p.oid)
67
+ ELSE NULL END AS definition,
68
+ p.proacl AS proacl
69
+ FROM pg_proc p
70
+ JOIN pg_namespace n ON n.oid = p.pronamespace
71
+ JOIN pg_language l ON l.oid = p.prolang
72
+ LEFT JOIN pg_roles o ON o.oid = p.proowner
73
+ WHERE p.prokind IN ('f', 'p')
74
+ ${schemaFilter}${extFilter}
75
+ ),
76
+ grants AS (
77
+ SELECT
78
+ pr.oid,
79
+ CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE rol.rolname END AS grantee,
80
+ a.is_grantable
81
+ FROM procs pr,
82
+ LATERAL aclexplode(pr.proacl) a
83
+ LEFT JOIN pg_roles rol ON rol.oid = a.grantee
84
+ WHERE pr.proacl IS NOT NULL
85
+ AND a.privilege_type = 'EXECUTE'
86
+ )
87
+ SELECT
88
+ p.oid::int AS oid,
89
+ p.schema_name,
90
+ p.name,
91
+ p.args,
92
+ p.owner,
93
+ p.owner_bypasses_rls,
94
+ p.security_definer,
95
+ p.search_path_pinned,
96
+ p.returns_trigger,
97
+ p.language,
98
+ p.source,
99
+ p.definition,
100
+ COALESCE(
101
+ (SELECT jsonb_agg(jsonb_build_object('role', g.grantee, 'grantable', g.is_grantable))
102
+ FROM grants g WHERE g.oid = p.oid),
103
+ '[]'::jsonb
104
+ ) AS grants,
105
+ (p.proacl IS NULL) AS default_acl
106
+ FROM procs p
107
+ ORDER BY p.schema_name, p.name, p.args
108
+ `;
109
+ const { rows } = await exec.query(sql, [options.schemas ?? [], excludes, options.extensions?.ignore ?? []]);
110
+ return rows.map((r) => ({
111
+ oid: r.oid,
112
+ schema: r.schema_name,
113
+ name: r.name,
114
+ args: r.args,
115
+ owner: r.owner,
116
+ ownerBypassesRls: r.owner_bypasses_rls,
117
+ isSecurityDefiner: r.security_definer,
118
+ searchPathPinned: r.search_path_pinned,
119
+ returnsTrigger: r.returns_trigger,
120
+ language: r.language,
121
+ source: r.source,
122
+ definition: r.definition,
123
+ grants: r.default_acl
124
+ ? [{ role: 'PUBLIC', grantable: false }]
125
+ : r.grants.map((g) => ({ role: g.role, grantable: g.grantable })),
126
+ defaultAcl: r.default_acl
127
+ }));
128
+ }