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