@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,452 @@
1
+ /**
2
+ * L19 and L20: privilege that arrives through a *function body*.
3
+ *
4
+ * L8 modelled the view half of "SQL bodies confer privilege". This is the
5
+ * other half, and the larger one:
6
+ *
7
+ * - **L19, SECURITY DEFINER functions.** A definer function executes as its
8
+ * owner, so every relation its body touches is touched with the owner's
9
+ * privileges — including tables the caller holds nothing on, and, when
10
+ * the owner owns the table or bypasses RLS, without the row filter the
11
+ * table's policies would have applied. EXECUTE on the function is the
12
+ * only grant the caller needs, and no ACL on the base relation names it.
13
+ * Verified on PostgreSQL 18: an anonymous role with EXECUTE read every
14
+ * row of an RLS-protected table it had no grant on, while the invoker
15
+ * twin of the same function was denied.
16
+ *
17
+ * - **L20, `INSTEAD OF` triggers.** A write against a view carrying one
18
+ * never reaches a base relation: it *becomes* the trigger function's
19
+ * body. L9 suppressed those views because nothing followed that body —
20
+ * this rule follows it. The body is permission-checked against the
21
+ * function's effective user, so the escalation exists only when the
22
+ * trigger function is SECURITY DEFINER; the view's own owner and its
23
+ * `security_invoker` setting do not govern it. Both halves were probed on
24
+ * PG 18: the invoker trigger function was denied on the relation its body
25
+ * wrote, the definer one wrote it as its owner.
26
+ *
27
+ * The conservatism is L8's, unchanged. A body that cannot be read is unknown,
28
+ * not empty: its fragmentary access list is discarded and the function is
29
+ * reported as a coverage gap (L15) rather than scanned clean. And the fix is
30
+ * never a revoke — EXECUTE on the function is what the API serves; the defect
31
+ * is what the body does with the owner's rights.
32
+ */
33
+ import { extractBody, extractFunctionAccess } from '../callgraph/extract';
34
+ import { canEnterSchema } from '../pg/acl';
35
+ import { buildRelationIndex, readBodies, resolveRelation, resolveViewBases } from './definer-view';
36
+ import { effectiveGrants } from './lattice';
37
+ import { computeFunctionReach, computeTriggerWriteReach } from './role-reach';
38
+ /** How deep a chain of calls is followed before the walk gives up. */
39
+ const MAX_CALL_DEPTH = 8;
40
+ /** The commands an `INSTEAD OF` trigger can be defined for. */
41
+ const TRIGGER_EVENTS = ['INSERT', 'UPDATE', 'DELETE'];
42
+ /**
43
+ * Read every SECURITY DEFINER function's body and resolve what it reaches.
44
+ *
45
+ * Calls are followed: an invoker function called from a definer still runs
46
+ * with the definer's owner in force, while an inner definer switches the
47
+ * executing role again — the same rule as nested views, because it is the
48
+ * same rule. Views read from a body are followed too, through the existing
49
+ * view walk, so a definer function selecting from a definer view reaches that
50
+ * view's bases as well.
51
+ */
52
+ export async function analyzeFunctionBodies(functions, views, tables, auditedSchemas) {
53
+ const queryableViews = views.filter((v) => !v.materialized);
54
+ const index = buildRelationIndex(queryableViews, tables, auditedSchemas);
55
+ const viewBodies = await readBodies(queryableViews);
56
+ const byName = new Map();
57
+ for (const fn of functions) {
58
+ const key = `${fn.schema}.${fn.name}`;
59
+ byName.set(key, [...(byName.get(key) ?? []), fn]);
60
+ }
61
+ const out = [];
62
+ const suppressed = [];
63
+ for (const fn of functions) {
64
+ if (!fn.isSecurityDefiner)
65
+ continue;
66
+ // A trigger function is not callable: Postgres refuses a direct call
67
+ // however wide its EXECUTE ACL is, so EXECUTE on one is not reach. It is
68
+ // reachable only by firing the trigger, which is what L20 grades.
69
+ if (fn.returnsTrigger)
70
+ continue;
71
+ const id = `${fn.schema}.${fn.name}(${fn.args})`;
72
+ const walk = await walkFunction(fn, byName, index, viewBodies);
73
+ if (walk.opaque)
74
+ suppressed.push({ view: id, reason: walk.opaque });
75
+ const unreadable = walk.opaque ?? walk.tainted;
76
+ if (!unreadable && walk.accesses.length === 0)
77
+ continue;
78
+ out.push({
79
+ schema: fn.schema,
80
+ name: fn.name,
81
+ args: fn.args,
82
+ owner: fn.owner,
83
+ grants: fn.grants,
84
+ defaultAcl: fn.defaultAcl,
85
+ accesses: walk.opaque ? [] : walk.accesses,
86
+ ...(unreadable ? { unreadable } : {})
87
+ });
88
+ }
89
+ const triggers = await analyzeInsteadOfTriggers(queryableViews, byName, index, viewBodies, suppressed);
90
+ return { functions: out, triggers, suppressed };
91
+ }
92
+ /**
93
+ * The relations a write against a view carrying `INSTEAD OF` triggers lands
94
+ * on, for the triggers whose function re-owns the write.
95
+ *
96
+ * An invoker trigger function is not an edge: its body runs as the caller and
97
+ * Postgres denies exactly what it would have denied on a direct write. A
98
+ * trigger function that is missing from the snapshot, or whose body cannot be
99
+ * read, is suppressed — the write goes *somewhere*, and guessing where is the
100
+ * one thing this analysis must not do.
101
+ */
102
+ async function analyzeInsteadOfTriggers(views, byName, index, viewBodies, suppressed) {
103
+ const out = [];
104
+ for (const view of views) {
105
+ for (const trigger of view.insteadOf) {
106
+ const name = `${view.schema}.${view.name}`;
107
+ const fnId = `${trigger.functionSchema}.${trigger.functionName}`;
108
+ const group = byName.get(fnId);
109
+ if (!group || group.length === 0) {
110
+ suppressed.push({ view: name, reason: `trigger function ${fnId} is outside the audited schemas` });
111
+ continue;
112
+ }
113
+ for (const fn of group) {
114
+ if (!fn.isSecurityDefiner)
115
+ continue; // the body runs as the caller: no edge
116
+ const walk = await walkFunction(fn, byName, index, viewBodies);
117
+ if (walk.opaque) {
118
+ suppressed.push({
119
+ view: name,
120
+ reason: `trigger ${trigger.name} runs ${fnId}, whose body could not be read: ${walk.opaque}`
121
+ });
122
+ continue;
123
+ }
124
+ if (walk.accesses.length === 0)
125
+ continue;
126
+ out.push({
127
+ schema: view.schema,
128
+ name: view.name,
129
+ grants: view.grants,
130
+ trigger: trigger.name,
131
+ events: trigger.events.filter((e) => TRIGGER_EVENTS.includes(e)),
132
+ fn: fnId,
133
+ fnOwner: fn.owner,
134
+ accesses: walk.accesses.map((a) => ({
135
+ schema: a.schema,
136
+ table: a.table,
137
+ privilege: a.privilege,
138
+ ...(a.external ? { external: true } : {})
139
+ }))
140
+ });
141
+ }
142
+ }
143
+ }
144
+ return out;
145
+ }
146
+ /**
147
+ * The relations `root`'s body reaches, with the role in force at each one.
148
+ *
149
+ * Overloads collapse when resolving a call, as they do in the call graph:
150
+ * a body naming `f(...)` is followed into every `f` in scope, because the
151
+ * argument types the call actually binds are not in the reference.
152
+ */
153
+ async function walkFunction(root, byName, index, viewBodies) {
154
+ const accesses = [];
155
+ const seen = new Set();
156
+ let opaque;
157
+ let tainted;
158
+ const visit = async (fn, hops, stack) => {
159
+ const id = `${fn.schema}.${fn.name}`;
160
+ if (hops.length > MAX_CALL_DEPTH) {
161
+ opaque ??= `call chain deeper than ${MAX_CALL_DEPTH} hops`;
162
+ return;
163
+ }
164
+ const body = await extractFunctionAccess(fn);
165
+ if (body.opaque) {
166
+ // Only the entry function's opacity discards the walk; a callee we
167
+ // cannot read leaves what we *did* read standing, and taints the rest.
168
+ if (hops.length === 1)
169
+ opaque ??= body.opaqueReason ?? 'body could not be read';
170
+ else
171
+ tainted ??= `${id}: ${body.opaqueReason ?? 'body could not be read'}`;
172
+ return;
173
+ }
174
+ const owner = hops[hops.length - 1].owner;
175
+ for (const access of body.accesses) {
176
+ const relation = resolveRelation(access, fn.schema, index);
177
+ if (!relation)
178
+ continue; // a CTE, an alias, or a name we cannot pin down
179
+ if (relation.kind === 'view') {
180
+ // Only reads through a view are placeable: where a *write* against a
181
+ // view lands is the view-writes question, not this one.
182
+ if (access.privilege !== 'SELECT')
183
+ continue;
184
+ const view = relation.view;
185
+ const { bases, opaque: viewOpaque, tainted: viewTainted } = resolveViewBases(view, index, viewBodies);
186
+ if (viewOpaque) {
187
+ tainted ??= `${view.schema}.${view.name}: ${viewOpaque}`;
188
+ continue;
189
+ }
190
+ if (viewTainted)
191
+ tainted ??= viewTainted;
192
+ for (const base of bases) {
193
+ // The view's own hops re-own the read only where the view is a
194
+ // definer; an invoker view keeps whichever owner is already in
195
+ // force, which here is the function's.
196
+ const viewHops = base.hops.map((h) => ({
197
+ view: h.view,
198
+ owner: h.view === `${view.schema}.${view.name}` && view.securityInvoker ? owner : h.owner
199
+ }));
200
+ push({
201
+ schema: base.schema,
202
+ table: base.table,
203
+ privilege: 'SELECT',
204
+ hops: [...hops],
205
+ viewHops,
206
+ ...(base.external ? { external: true } : {})
207
+ });
208
+ }
209
+ continue;
210
+ }
211
+ push({
212
+ schema: relation.schema,
213
+ table: relation.name,
214
+ privilege: access.privilege,
215
+ hops: [...hops],
216
+ ...(relation.kind === 'external' ? { external: true } : {})
217
+ });
218
+ }
219
+ // Calls. `extractFunctionAccess` answers "which relation, which
220
+ // privilege"; the call list is the read/write walk's answer, and both
221
+ // read the same body.
222
+ const called = await extractBody(fn);
223
+ if (called.tainted)
224
+ tainted ??= `${id}: ${called.tainted}`;
225
+ for (const ref of called.calls) {
226
+ const candidates = ref.schema
227
+ ? byName.get(`${ref.schema}.${ref.name}`) ?? []
228
+ : byName.get(`${fn.schema}.${ref.name}`) ?? [];
229
+ for (const callee of candidates) {
230
+ const calleeId = `${callee.schema}.${callee.name}(${callee.args})`;
231
+ if (stack.has(calleeId))
232
+ continue; // recursion: the reach is already counted
233
+ // An inner definer re-owns the execution; an invoker runs under
234
+ // whichever owner is already in force.
235
+ const nextOwner = callee.isSecurityDefiner ? callee.owner : owner;
236
+ await visit(callee, [...hops, { fn: `${callee.schema}.${callee.name}`, owner: nextOwner }], new Set([...stack, calleeId]));
237
+ }
238
+ }
239
+ };
240
+ const push = (access) => {
241
+ const last = access.viewHops?.[access.viewHops.length - 1]?.owner
242
+ ?? access.hops[access.hops.length - 1].owner;
243
+ const key = `${access.schema}.${access.table}::${access.privilege}::${last}`;
244
+ if (seen.has(key))
245
+ return;
246
+ seen.add(key);
247
+ accesses.push(access);
248
+ };
249
+ const rootId = `${root.schema}.${root.name}(${root.args})`;
250
+ await visit(root, [{ fn: `${root.schema}.${root.name}`, owner: root.owner }], new Set([rootId]));
251
+ return { accesses, ...(opaque ? { opaque } : {}), ...(tainted ? { tainted } : {}) };
252
+ }
253
+ /**
254
+ * L19: an untrusted role reaches a relation by executing a SECURITY DEFINER
255
+ * function.
256
+ *
257
+ * Fires once per (role, function, relation, privilege) where the role can
258
+ * EXECUTE the function, the body touches the relation as someone else, and
259
+ * the role holds no such privilege on the relation itself. The invoker twin
260
+ * of the same function produces nothing: its body runs as the caller, so the
261
+ * relation's own ACL and policies apply and there is no edge to report.
262
+ */
263
+ export function checkDefinerFunctionReach(functions, tables, graph, schemaAcls, options = {}) {
264
+ const untrusted = options.roles ?? [];
265
+ if (untrusted.length === 0 || functions.length === 0)
266
+ return [];
267
+ const byKey = new Map(tables.map((t) => [`${t.schema}.${t.name}`, t]));
268
+ const byFn = new Map(functions.map((f) => [`${f.schema}.${f.name}`, f]));
269
+ const out = [];
270
+ for (const { role, cells } of computeFunctionReach(functions, graph, untrusted)) {
271
+ for (const cell of cells) {
272
+ if (cell.effectiveRole === role)
273
+ continue;
274
+ // A tainted cell names the function, not a relation: L15 reports it.
275
+ if (cell.proof === 'opaque-tainted')
276
+ continue;
277
+ const base = byKey.get(`${cell.schema}.${cell.table}`);
278
+ if (!base)
279
+ continue;
280
+ const privilege = cell.privileges[0];
281
+ // Already reachable in its own right: the function launders nothing.
282
+ if (effectiveGrants(base, role, graph).some((g) => g.privilege === privilege))
283
+ continue;
284
+ const hops = functionHops(cell);
285
+ const entry = hops[0];
286
+ if (!entry)
287
+ continue;
288
+ if (!canEnterSchema(entry.fn.split('.')[0], role, graph, schemaAcls))
289
+ continue;
290
+ const owner = cell.effectiveRole;
291
+ const rlsBypassed = ownerBypassesRls(base, owner, graph);
292
+ const target = `${base.schema}.${base.name}`;
293
+ const viewHops = cell.path.filter((e) => e.kind === 'view');
294
+ const defaultAcl = byFn.get(entry.fn)?.defaultAcl === true;
295
+ out.push({
296
+ code: 'L19',
297
+ severity: 'info',
298
+ category: 'anti-pattern',
299
+ schema: base.schema,
300
+ table: base.name,
301
+ role,
302
+ privilege,
303
+ message: `Untrusted role ${role} can ${privilege} ${target} by executing SECURITY DEFINER function `
304
+ + `${entry.fn}, which runs as its owner ${owner} — ${role} holds no ${privilege} on the `
305
+ + 'relation'
306
+ + (rlsBypassed ? `, and ${owner} is not subject to its RLS policies` : ''),
307
+ hint: `A SECURITY DEFINER function is a deliberate hand-off of ${owner}'s privileges: everything `
308
+ + `its body touches, it touches as ${owner}, and EXECUTE is the only grant ${role} needed. `
309
+ + `Narrow the body to what the function is meant to expose, authorize the caller inside it, `
310
+ + `or give the function an owner whose reach matches its job. Make it \`SECURITY INVOKER\` `
311
+ + `if the hand-off was not intended. Do not revoke EXECUTE — that grant is what the API `
312
+ + 'serves'
313
+ + (defaultAcl
314
+ ? `, though note the EXECUTE here is Postgres's default function ACL (EXECUTE TO PUBLIC), `
315
+ + 'not a grant anyone wrote: confirm it was intended.'
316
+ : '.'),
317
+ context: {
318
+ function: entry.fn,
319
+ effectiveRole: owner,
320
+ viaFunctions: hops.map((h) => h.fn),
321
+ ...(viewHops.length > 0 ? { viaViews: viewHops.map((h) => h.view) } : {}),
322
+ baseRlsEnabled: base.rlsEnabled,
323
+ rlsBypassed,
324
+ defaultAcl,
325
+ proof: cell.proof
326
+ }
327
+ });
328
+ }
329
+ }
330
+ return out;
331
+ }
332
+ /**
333
+ * L20: an untrusted role writes a relation through an `INSTEAD OF` trigger
334
+ * whose function is SECURITY DEFINER.
335
+ *
336
+ * This is the suppression L9 has carried since it shipped. A write against a
337
+ * view with `INSTEAD OF` triggers never reaches a base relation — Postgres
338
+ * runs the trigger function instead — and where that write lands is in the
339
+ * body. Following it makes the edge provable; where the body is unreadable,
340
+ * or the trigger function runs as the caller, the suppression stands.
341
+ */
342
+ export function checkInsteadOfTriggerWrite(triggers, tables, graph, options = {}) {
343
+ const untrusted = options.roles ?? [];
344
+ if (untrusted.length === 0 || triggers.length === 0)
345
+ return [];
346
+ const byKey = new Map(tables.map((t) => [`${t.schema}.${t.name}`, t]));
347
+ const out = [];
348
+ for (const { role, cells } of computeTriggerWriteReach(triggers, graph, untrusted)) {
349
+ for (const cell of cells) {
350
+ if (cell.effectiveRole === role)
351
+ continue;
352
+ const base = byKey.get(`${cell.schema}.${cell.table}`);
353
+ if (!base)
354
+ continue;
355
+ const privilege = cell.privileges[0];
356
+ if (effectiveGrants(base, role, graph).some((g) => g.privilege === privilege))
357
+ continue;
358
+ const edge = cell.path.find((e) => e.kind === 'trigger');
359
+ if (!edge)
360
+ continue;
361
+ const grantEdge = cell.path.find((e) => e.kind === 'grant');
362
+ const owner = cell.effectiveRole;
363
+ const rlsBypassed = ownerBypassesRls(base, owner, graph);
364
+ const target = `${base.schema}.${base.name}`;
365
+ out.push({
366
+ code: 'L20',
367
+ severity: 'info',
368
+ category: 'anti-pattern',
369
+ schema: base.schema,
370
+ table: base.name,
371
+ role,
372
+ privilege,
373
+ message: `Untrusted role ${role} can ${privilege} ${target} by writing to view ${edge.view}, whose `
374
+ + `INSTEAD OF trigger ${edge.trigger} runs SECURITY DEFINER function ${edge.fn} as its `
375
+ + `owner ${owner} — ${role} holds no ${privilege} on the relation`
376
+ + (rlsBypassed ? `, whose RLS policies ${owner} is not subject to` : ''),
377
+ hint: `The write never reaches a base relation directly: Postgres replaces it with the trigger `
378
+ + `function's body, which is permission-checked against ${owner} because the function is `
379
+ + `SECURITY DEFINER. Make the trigger function \`SECURITY INVOKER\` so the caller's own `
380
+ + `grants apply, authorize the caller inside the body, or give the function an owner whose `
381
+ + `reach matches what the view is meant to accept. Do not revoke the grant on the view — `
382
+ + 'that grant is what the API serves.',
383
+ context: {
384
+ view: edge.view,
385
+ trigger: edge.trigger,
386
+ function: edge.fn,
387
+ effectiveRole: owner,
388
+ viewPrivilege: grantEdge?.privilege,
389
+ baseRlsEnabled: base.rlsEnabled,
390
+ rlsBypassed,
391
+ proof: cell.proof
392
+ }
393
+ });
394
+ }
395
+ }
396
+ return out;
397
+ }
398
+ /**
399
+ * L15, asked of a function instead of a view: an untrusted role can execute a
400
+ * definer function whose body this analysis could not fully read.
401
+ *
402
+ * The same coverage statement the view producer makes, for the same reason —
403
+ * the execution is proven and the far end is unknown, so no grading rule saw
404
+ * it. Reporting the gap is the alternative to a silent clean bill; it is
405
+ * `info`, score-neutral, and recommends no revoke.
406
+ */
407
+ export function checkUnreadableFunctionReach(functions, graph, schemaAcls, options = {}) {
408
+ const untrusted = options.roles ?? [];
409
+ if (untrusted.length === 0 || functions.length === 0)
410
+ return [];
411
+ const out = [];
412
+ for (const { role, cells } of computeFunctionReach(functions, graph, untrusted)) {
413
+ for (const cell of cells) {
414
+ if (cell.proof !== 'opaque-tainted')
415
+ continue;
416
+ if (!canEnterSchema(cell.schema, role, graph, schemaAcls))
417
+ continue;
418
+ out.push({
419
+ code: 'L15',
420
+ severity: 'info',
421
+ category: 'coverage',
422
+ schema: cell.schema,
423
+ table: cell.table,
424
+ role,
425
+ privilege: 'EXECUTE',
426
+ message: `Untrusted role ${role} can execute ${cell.schema}.${cell.table}, which runs as its owner `
427
+ + `${cell.effectiveRole} and whose body this analysis could not fully read (${cell.taint}) `
428
+ + '— what it reaches under that owner was never graded',
429
+ hint: 'The execution is proven; what the body reaches is not, so no function rule graded this '
430
+ + 'path. Rewrite the body so the relations it touches are visible statically, or satisfy '
431
+ + 'yourself that what it does is safe to expose to this role. Reporting stayed silent '
432
+ + 'rather than guessing, and nothing here justifies a revoke.',
433
+ context: {
434
+ function: `${cell.schema}.${cell.table}`,
435
+ effectiveRole: cell.effectiveRole,
436
+ taint: cell.taint,
437
+ proof: cell.proof
438
+ }
439
+ });
440
+ }
441
+ }
442
+ return out;
443
+ }
444
+ /** The owner is exempt from the relation's policies on this path. */
445
+ function ownerBypassesRls(base, owner, graph) {
446
+ if (!base.rlsEnabled)
447
+ return false;
448
+ return !!graph.get(owner)?.bypassRls || (base.owner === owner && !base.rlsForced);
449
+ }
450
+ function functionHops(cell) {
451
+ return cell.path.filter((e) => e.kind === 'function');
452
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * L8: a view that is not `security_invoker` hands its readers the owner's
3
+ * privileges on every relation its body touches.
4
+ *
5
+ * A view executes as its owner unless it was created `WITH (security_invoker
6
+ * = true)`. So a role holding nothing but SELECT on the view reads the base
7
+ * relations under the *owner's* rights — including tables it has no grant on,
8
+ * and, when the owner owns the base table or bypasses RLS, without the row
9
+ * filter the base table's policies would have applied.
10
+ *
11
+ * Nothing in the catalog-only effective-access layer can see this:
12
+ * `effectiveGrants` never names the calling role on the base relation, and
13
+ * the base relation's ACL never names it either. The edge only exists in the
14
+ * view's *body*, which makes this the first rule that reads SQL rather than
15
+ * pure catalog — the `proof: 'ast'` edge of the reach model in `role-reach.ts`.
16
+ *
17
+ * Two conservatism rules the rest of the analysis depends on:
18
+ *
19
+ * - **A body we cannot read is unknown, not empty.** Dynamic SQL or an
20
+ * unparseable body suppresses the view entirely rather than producing a
21
+ * partial (and therefore misleading) base-relation set.
22
+ * - **The fix is never a revoke.** The grant on the view is what the API
23
+ * serves; the defect is the view executing as its owner. L8 recommends
24
+ * `security_invoker = true` or a different owner, and nothing else.
25
+ */
26
+ import { type ExtractedBody } from '../callgraph/extract';
27
+ import type { ViewSnapshot } from '../pg/indexes';
28
+ import type { TableSnapshot } from '../pg/introspect';
29
+ import type { Finding } from '../types';
30
+ import { type LatticeRoleOptions, type RoleGraph } from './lattice';
31
+ import { type ViewBaseRelation, type ViewReachInput } from './role-reach';
32
+ /** A view whose body did not yield a usable base-relation set. */
33
+ export interface SuppressedView {
34
+ view: string;
35
+ reason: string;
36
+ }
37
+ export interface ViewBodyAnalysis {
38
+ /** Definer views whose bodies were read, as reach inputs. */
39
+ views: ViewReachInput[];
40
+ /** Views deliberately left out, with why — an unread body is not a clean bill. */
41
+ suppressed: SuppressedView[];
42
+ }
43
+ /**
44
+ * Read every definer view's body and resolve the base relations it reaches.
45
+ *
46
+ * Nested views are followed: a definer view over an invoker view still
47
+ * executes the inner body as the *outer* owner, while an inner definer view
48
+ * switches the executing role again. Each resolved relation therefore carries
49
+ * the hops it passed through and the owner of the last one.
50
+ */
51
+ export declare function analyzeViewBodies(views: ViewSnapshot[], tables: TableSnapshot[], auditedSchemas?: Iterable<string>): Promise<ViewBodyAnalysis>;
52
+ /** Parsed bodies, keyed `schema.name`, as {@link resolveViewBases} expects. */
53
+ export type ViewBodies = Map<string, ExtractedBody>;
54
+ export declare function readBodies(views: ViewSnapshot[]): Promise<ViewBodies>;
55
+ /**
56
+ * The base relations `root` reads, following nested views, with the owner in
57
+ * force at each hop.
58
+ *
59
+ * `opaque` is set to the first reason the walk had to stop, and when it is set
60
+ * the relation list is a fragment: the caller must discard it, because a body
61
+ * we could only partly read under-reports what the view reaches.
62
+ *
63
+ * `root` itself need not be in `index` — a materialized view is not a
64
+ * queryable relation for the purposes of resolving *other* bodies, but its own
65
+ * body reads the same way.
66
+ */
67
+ export declare function resolveViewBases(root: ViewSnapshot, index: RelationIndex, bodies: ViewBodies): {
68
+ bases: ViewBaseRelation[];
69
+ opaque?: string;
70
+ tainted?: string;
71
+ };
72
+ export type Resolved = {
73
+ kind: 'table';
74
+ schema: string;
75
+ name: string;
76
+ } | {
77
+ kind: 'view';
78
+ view: ViewSnapshot;
79
+ }
80
+ /**
81
+ * A schema-qualified relation in a schema the audit did not introspect —
82
+ * excluded by config, owned by an extension, or a system catalog. The
83
+ * reference is unambiguous, so dropping it would under-report the view's
84
+ * reach; nothing about the relation itself is known.
85
+ */
86
+ | {
87
+ kind: 'external';
88
+ schema: string;
89
+ name: string;
90
+ };
91
+ /** The lookup tables {@link resolveRelation} needs, built once per snapshot. */
92
+ export interface RelationIndex {
93
+ tableKeys: Set<string>;
94
+ viewKeys: Map<string, ViewSnapshot>;
95
+ viewsByName: Map<string, ViewSnapshot[]>;
96
+ /**
97
+ * The schemas the snapshot covers. A qualified reference into a schema
98
+ * *not* in this set is external rather than unresolvable; a miss inside it
99
+ * is a name the audit genuinely could not pin down.
100
+ */
101
+ auditedSchemas: Set<string>;
102
+ }
103
+ export declare function buildRelationIndex(views: ViewSnapshot[], tables: TableSnapshot[], auditedSchemas?: Iterable<string>): RelationIndex;
104
+ /**
105
+ * Pin a body reference to a relation in the snapshot.
106
+ *
107
+ * `pg_get_viewdef` qualifies anything outside the creating `search_path` but
108
+ * leaves the rest bare, so an unqualified name is resolved against the view's
109
+ * own schema first and then against the snapshot as a whole — and only when
110
+ * exactly one relation answers to it. An ambiguous or unknown name (a CTE, a
111
+ * table alias, a relation outside the audited schemas) resolves to nothing:
112
+ * over-approximating here would attribute a read to a relation nobody named.
113
+ */
114
+ export declare function resolveRelation(ref: {
115
+ schema?: string;
116
+ name: string;
117
+ }, viewSchema: string, { tableKeys, viewKeys, viewsByName, auditedSchemas }: RelationIndex): Resolved | null;
118
+ /**
119
+ * L8: an untrusted role reads a base relation through a definer view.
120
+ *
121
+ * Fires once per (role, view, base relation) where the role can SELECT the
122
+ * view, the view executes as someone else, and the role holds no SELECT of
123
+ * its own on the relation the view reads. A `security_invoker` view over the
124
+ * same shape produces nothing — it executes as the caller, so the base
125
+ * relation's own ACL and policies apply and there is no edge to report.
126
+ */
127
+ export declare function checkDefinerViewBypass(views: ViewReachInput[], tables: TableSnapshot[], graph: RoleGraph, options?: LatticeRoleOptions): Finding[];
128
+ /**
129
+ * L14: a definer view hands an untrusted role a relation the audit never
130
+ * looked at.
131
+ *
132
+ * Excluding a schema is a statement about what is *graded*, not about what is
133
+ * *reachable*. A view in an audited schema can read `information_schema`,
134
+ * `pg_catalog`, an extension's tables, or any schema left out of
135
+ * `exposure.schemas` — and because that relation is not in the snapshot,
136
+ * every rule that grades a base relation silently drops the edge. The view
137
+ * looked clean because the audit could not see the far end.
138
+ *
139
+ * This is the one L-rule that reports an *absence of knowledge*. It says the
140
+ * read happens and that nothing was graded on the other side; it does not
141
+ * claim the relation is sensitive, and it recommends no revoke — the remedy
142
+ * is to bring the schema into scope, or to satisfy yourself that the
143
+ * projection is safe and leave it.
144
+ */
145
+ export declare function checkUnauditedViewReach(views: ViewReachInput[], graph: RoleGraph, options?: LatticeRoleOptions): Finding[];
146
+ /**
147
+ * **L15 — an untrusted role reads a view whose body this analysis cannot
148
+ * read.** The coverage sibling of L14: there, the far end was out of scope;
149
+ * here, the body itself is.
150
+ *
151
+ * Two shapes reach this rule, and the distinction is the point of the
152
+ * `tainted` bit in {@link ExtractedBody}. A body that could not be read at all
153
+ * — an unparseable definition, a chain deeper than the hop limit — yields no
154
+ * relations, and every rule that grades a base relation had nothing to grade.
155
+ * A body that *was* read but calls something executing SQL of its own
156
+ * (`dblink`, `query_to_xml`) yields relations that are all real and not all of
157
+ * them: the graded ones still grade, and the gap is what is reported here.
158
+ *
159
+ * Either way the view executes as its owner and an untrusted role can read it,
160
+ * so the reach is proven and only its far end is unknown — which is exactly
161
+ * `proof: 'opaque-tainted'`, the state the reach model has carried since it
162
+ * was written and nothing could produce until now.
163
+ *
164
+ * Like L14 it is `info`/`neutral` and recommends no revoke: an unknown is not
165
+ * a leak, and scoring one would let an unreadable body move the number.
166
+ */
167
+ export declare function checkUnreadableViewReach(views: ViewReachInput[], graph: RoleGraph, options?: LatticeRoleOptions): Finding[];