@safegres/test 0.0.1 → 1.30.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (483) hide show
  1. package/README.md +825 -412
  2. package/adapters.d.ts +8 -0
  3. package/adapters.js +18 -0
  4. package/ast/helpers.d.ts +16 -0
  5. package/ast/helpers.js +40 -0
  6. package/ast/parse.d.ts +31 -0
  7. package/ast/parse.js +87 -0
  8. package/ast/walk.d.ts +27 -0
  9. package/ast/walk.js +79 -0
  10. package/callgraph/baseline.d.ts +28 -0
  11. package/callgraph/baseline.js +55 -0
  12. package/callgraph/extract.d.ts +93 -0
  13. package/callgraph/extract.js +340 -0
  14. package/callgraph/graph.d.ts +82 -0
  15. package/callgraph/graph.js +276 -0
  16. package/checks/anti-patterns.d.ts +53 -0
  17. package/checks/anti-patterns.js +247 -0
  18. package/checks/column-grants.d.ts +20 -0
  19. package/checks/column-grants.js +74 -0
  20. package/checks/coverage.d.ts +43 -0
  21. package/checks/coverage.js +110 -0
  22. package/checks/definer-function.d.ts +91 -0
  23. package/checks/definer-function.js +458 -0
  24. package/checks/definer-view.d.ts +167 -0
  25. package/checks/definer-view.js +392 -0
  26. package/checks/indexes.d.ts +82 -0
  27. package/checks/indexes.js +263 -0
  28. package/checks/lattice.d.ts +179 -0
  29. package/checks/lattice.js +487 -0
  30. package/checks/object-acls.d.ts +32 -0
  31. package/checks/object-acls.js +129 -0
  32. package/checks/policy-index.d.ts +78 -0
  33. package/checks/policy-index.js +349 -0
  34. package/checks/revocable-grants.d.ts +140 -0
  35. package/checks/revocable-grants.js +377 -0
  36. package/checks/rls-flags.d.ts +25 -0
  37. package/checks/rls-flags.js +82 -0
  38. package/checks/role-reach.d.ts +329 -0
  39. package/checks/role-reach.js +277 -0
  40. package/checks/role-trust.d.ts +45 -0
  41. package/checks/role-trust.js +91 -0
  42. package/checks/set-role.d.ts +21 -0
  43. package/checks/set-role.js +66 -0
  44. package/checks/stats.d.ts +52 -0
  45. package/checks/stats.js +192 -0
  46. package/checks/view-exposure.d.ts +68 -0
  47. package/checks/view-exposure.js +236 -0
  48. package/checks/view-writes.d.ts +88 -0
  49. package/checks/view-writes.js +350 -0
  50. package/ci/baseline.d.ts +104 -0
  51. package/ci/baseline.js +111 -0
  52. package/ci/github.d.ts +47 -0
  53. package/ci/github.js +197 -0
  54. package/cli/audit.d.ts +3 -0
  55. package/cli/audit.js +560 -0
  56. package/cli/baseline.d.ts +3 -0
  57. package/cli/baseline.js +104 -0
  58. package/cli/commands.d.ts +2 -0
  59. package/cli/commands.js +60 -0
  60. package/cli/doctor.d.ts +3 -0
  61. package/cli/doctor.js +72 -0
  62. package/cli/eval.d.ts +3 -0
  63. package/cli/eval.js +153 -0
  64. package/cli/print-config.d.ts +3 -0
  65. package/cli/print-config.js +54 -0
  66. package/cli/shared.d.ts +43 -0
  67. package/cli/shared.js +153 -0
  68. package/cli.d.ts +2 -0
  69. package/cli.js +69 -0
  70. package/commands/audit.d.ts +65 -0
  71. package/commands/audit.js +779 -0
  72. package/commands/doctor.d.ts +19 -0
  73. package/commands/doctor.js +181 -0
  74. package/commands/eval.d.ts +67 -0
  75. package/commands/eval.js +93 -0
  76. package/config/fingerprint.d.ts +40 -0
  77. package/config/fingerprint.js +71 -0
  78. package/config/loader.d.ts +44 -0
  79. package/config/loader.js +210 -0
  80. package/config/presets.d.ts +95 -0
  81. package/config/presets.js +327 -0
  82. package/config/resolve.d.ts +47 -0
  83. package/config/resolve.js +192 -0
  84. package/config/schema.d.ts +54 -0
  85. package/config/schema.js +269 -0
  86. package/config/types.d.ts +562 -0
  87. package/config/validate.d.ts +14 -0
  88. package/config/validate.js +125 -0
  89. package/corpus/README.md +83 -0
  90. package/corpus/bootstrap.sql +14 -0
  91. package/corpus/cases/01-anon-write-grant/case.json +26 -0
  92. package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
  93. package/corpus/cases/02-grant-to-public/case.json +25 -0
  94. package/corpus/cases/02-grant-to-public/schema.sql +18 -0
  95. package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
  96. package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
  97. package/corpus/cases/04-permissive-write-policy/case.json +30 -0
  98. package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
  99. package/corpus/cases/05-public-read-policy/case.json +25 -0
  100. package/corpus/cases/05-public-read-policy/schema.sql +16 -0
  101. package/corpus/cases/06-update-without-with-check/case.json +25 -0
  102. package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
  103. package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
  104. package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
  105. package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
  106. package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
  107. package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
  108. package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
  109. package/corpus/cases/10-rls-not-forced/case.json +24 -0
  110. package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
  111. package/corpus/cases/11-session-user-in-policy/case.json +24 -0
  112. package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
  113. package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
  114. package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
  115. package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
  116. package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
  117. package/corpus/cases/14-dead-policy/case.json +24 -0
  118. package/corpus/cases/14-dead-policy/schema.sql +18 -0
  119. package/corpus/cases/15-unreachable-grant/case.json +24 -0
  120. package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
  121. package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
  122. package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
  123. package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
  124. package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
  125. package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
  126. package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
  127. package/corpus/cases/19-policy-column-cast/case.json +24 -0
  128. package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
  129. package/corpus/cases/20-stable-function-per-row/case.json +24 -0
  130. package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
  131. package/corpus/cases/21-table-without-primary-key/case.json +24 -0
  132. package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
  133. package/corpus/cases/22-redundant-index/case.json +24 -0
  134. package/corpus/cases/22-redundant-index/schema.sql +16 -0
  135. package/corpus/cases/23-tsvector-without-index/case.json +24 -0
  136. package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
  137. package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
  138. package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
  139. package/corpus/cases/25-definer-view-bypass/case.json +31 -0
  140. package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
  141. package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
  142. package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
  143. package/corpus/cases/27-definer-view-write/case.json +36 -0
  144. package/corpus/cases/27-definer-view-write/schema.sql +40 -0
  145. package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
  146. package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
  147. package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
  148. package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
  149. package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
  150. package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
  151. package/corpus/cases/31-matview-snapshot/case.json +32 -0
  152. package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
  153. package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
  154. package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
  155. package/corpus/cases/33-leaky-filter-view/case.json +36 -0
  156. package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
  157. package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
  158. package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
  159. package/corpus/cases/35-column-grant-projection/case.json +27 -0
  160. package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
  161. package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
  162. package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
  163. package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
  164. package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
  165. package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
  166. package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
  167. package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
  168. package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
  169. package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
  170. package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
  171. package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
  172. package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
  173. package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
  174. package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
  175. package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
  176. package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
  177. package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
  178. package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
  179. package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
  180. package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
  181. package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
  182. package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
  183. package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
  184. package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
  185. package/corpus/cases/41-unreadable-view-body/case.json +33 -0
  186. package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
  187. package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
  188. package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
  189. package/corpus/cases/42-search-column-indexed/case.json +21 -0
  190. package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
  191. package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
  192. package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
  193. package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
  194. package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
  195. package/corpus/cases/45-anon-foreign-table/case.json +27 -0
  196. package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
  197. package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
  198. package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
  199. package/corpus/cases/47-checked-writable-view/case.json +37 -0
  200. package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
  201. package/corpus/cases/48-definer-function-reach/case.json +38 -0
  202. package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
  203. package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
  204. package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
  205. package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
  206. package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
  207. package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
  208. package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
  209. package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
  210. package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
  211. package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
  212. package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
  213. package/corpus/index.d.ts +79 -0
  214. package/corpus/index.js +125 -0
  215. package/esm/adapters.d.ts +8 -0
  216. package/esm/adapters.js +6 -0
  217. package/esm/ast/helpers.d.ts +16 -0
  218. package/esm/ast/helpers.js +36 -0
  219. package/esm/ast/parse.d.ts +31 -0
  220. package/esm/ast/parse.js +82 -0
  221. package/esm/ast/walk.d.ts +27 -0
  222. package/esm/ast/walk.js +72 -0
  223. package/esm/callgraph/baseline.d.ts +28 -0
  224. package/esm/callgraph/baseline.js +48 -0
  225. package/esm/callgraph/extract.d.ts +93 -0
  226. package/esm/callgraph/extract.js +333 -0
  227. package/esm/callgraph/graph.d.ts +82 -0
  228. package/esm/callgraph/graph.js +273 -0
  229. package/esm/checks/anti-patterns.d.ts +53 -0
  230. package/esm/checks/anti-patterns.js +240 -0
  231. package/esm/checks/column-grants.d.ts +20 -0
  232. package/esm/checks/column-grants.js +71 -0
  233. package/esm/checks/coverage.d.ts +43 -0
  234. package/esm/checks/coverage.js +104 -0
  235. package/esm/checks/definer-function.d.ts +91 -0
  236. package/esm/checks/definer-function.js +452 -0
  237. package/esm/checks/definer-view.d.ts +167 -0
  238. package/esm/checks/definer-view.js +382 -0
  239. package/esm/checks/indexes.d.ts +82 -0
  240. package/esm/checks/indexes.js +256 -0
  241. package/esm/checks/lattice.d.ts +179 -0
  242. package/esm/checks/lattice.js +475 -0
  243. package/esm/checks/object-acls.d.ts +32 -0
  244. package/esm/checks/object-acls.js +125 -0
  245. package/esm/checks/policy-index.d.ts +78 -0
  246. package/esm/checks/policy-index.js +342 -0
  247. package/esm/checks/revocable-grants.d.ts +140 -0
  248. package/esm/checks/revocable-grants.js +373 -0
  249. package/esm/checks/rls-flags.d.ts +25 -0
  250. package/esm/checks/rls-flags.js +77 -0
  251. package/esm/checks/role-reach.d.ts +329 -0
  252. package/esm/checks/role-reach.js +270 -0
  253. package/esm/checks/role-trust.d.ts +45 -0
  254. package/esm/checks/role-trust.js +86 -0
  255. package/esm/checks/set-role.d.ts +21 -0
  256. package/esm/checks/set-role.js +63 -0
  257. package/esm/checks/stats.d.ts +52 -0
  258. package/esm/checks/stats.js +184 -0
  259. package/esm/checks/view-exposure.d.ts +68 -0
  260. package/esm/checks/view-exposure.js +231 -0
  261. package/esm/checks/view-writes.d.ts +88 -0
  262. package/esm/checks/view-writes.js +344 -0
  263. package/esm/ci/baseline.d.ts +104 -0
  264. package/esm/ci/baseline.js +107 -0
  265. package/esm/ci/github.d.ts +47 -0
  266. package/esm/ci/github.js +161 -0
  267. package/esm/cli/audit.d.ts +3 -0
  268. package/esm/cli/audit.js +525 -0
  269. package/esm/cli/baseline.d.ts +3 -0
  270. package/esm/cli/baseline.js +102 -0
  271. package/esm/cli/commands.d.ts +2 -0
  272. package/esm/cli/commands.js +53 -0
  273. package/esm/cli/doctor.d.ts +3 -0
  274. package/esm/cli/doctor.js +67 -0
  275. package/esm/cli/eval.d.ts +3 -0
  276. package/esm/cli/eval.js +115 -0
  277. package/esm/cli/print-config.d.ts +3 -0
  278. package/esm/cli/print-config.js +52 -0
  279. package/esm/cli/shared.d.ts +43 -0
  280. package/esm/cli/shared.js +112 -0
  281. package/esm/cli.d.ts +2 -0
  282. package/esm/cli.js +67 -0
  283. package/esm/commands/audit.d.ts +65 -0
  284. package/esm/commands/audit.js +776 -0
  285. package/esm/commands/doctor.d.ts +19 -0
  286. package/esm/commands/doctor.js +178 -0
  287. package/esm/commands/eval.d.ts +67 -0
  288. package/esm/commands/eval.js +90 -0
  289. package/esm/config/fingerprint.d.ts +40 -0
  290. package/esm/config/fingerprint.js +68 -0
  291. package/esm/config/loader.d.ts +44 -0
  292. package/esm/config/loader.js +172 -0
  293. package/esm/config/presets.d.ts +95 -0
  294. package/esm/config/presets.js +324 -0
  295. package/esm/config/resolve.d.ts +47 -0
  296. package/esm/config/resolve.js +180 -0
  297. package/esm/config/schema.d.ts +54 -0
  298. package/esm/config/schema.js +265 -0
  299. package/esm/config/types.d.ts +562 -0
  300. package/esm/config/validate.d.ts +14 -0
  301. package/esm/config/validate.js +122 -0
  302. package/esm/corpus/index.d.ts +79 -0
  303. package/esm/corpus/index.js +85 -0
  304. package/esm/exposure/adapters.d.ts +152 -0
  305. package/esm/exposure/adapters.js +463 -0
  306. package/esm/exposure/planes.d.ts +59 -0
  307. package/esm/exposure/planes.js +144 -0
  308. package/esm/exposure/reach.d.ts +78 -0
  309. package/esm/exposure/reach.js +105 -0
  310. package/esm/exposure/routines.d.ts +36 -0
  311. package/esm/exposure/routines.js +54 -0
  312. package/esm/index.d.ts +96 -0
  313. package/esm/index.js +57 -6
  314. package/esm/lint/index.d.ts +11 -0
  315. package/esm/lint/index.js +10 -0
  316. package/esm/perf/baseline.d.ts +40 -0
  317. package/esm/perf/baseline.js +83 -0
  318. package/esm/perf/explain.d.ts +46 -0
  319. package/esm/perf/explain.js +201 -0
  320. package/esm/pg/acl.d.ts +72 -0
  321. package/esm/pg/acl.js +186 -0
  322. package/esm/pg/behaviors.d.ts +114 -0
  323. package/esm/pg/behaviors.js +206 -0
  324. package/esm/pg/exposure.d.ts +74 -0
  325. package/esm/pg/exposure.js +198 -0
  326. package/esm/pg/functions.d.ts +52 -0
  327. package/esm/pg/functions.js +128 -0
  328. package/esm/pg/indexes.d.ts +192 -0
  329. package/esm/pg/indexes.js +395 -0
  330. package/esm/pg/introspect.d.ts +134 -0
  331. package/esm/pg/introspect.js +274 -0
  332. package/esm/pg/objects.d.ts +37 -0
  333. package/esm/pg/objects.js +108 -0
  334. package/esm/pg/paths.d.ts +114 -0
  335. package/esm/pg/paths.js +159 -0
  336. package/esm/pg/proc.d.ts +23 -0
  337. package/esm/pg/proc.js +55 -0
  338. package/esm/pg/reach-inputs.d.ts +47 -0
  339. package/esm/pg/reach-inputs.js +133 -0
  340. package/esm/pg/roles.d.ts +28 -0
  341. package/esm/pg/roles.js +50 -0
  342. package/esm/pg/stats.d.ts +65 -0
  343. package/esm/pg/stats.js +129 -0
  344. package/esm/pgpm-test.d.ts +22 -0
  345. package/esm/pgpm-test.js +39 -0
  346. package/esm/report/callgraph.d.ts +9 -0
  347. package/esm/report/callgraph.js +74 -0
  348. package/esm/report/compare.d.ts +143 -0
  349. package/esm/report/compare.js +195 -0
  350. package/esm/report/github.d.ts +74 -0
  351. package/esm/report/github.js +290 -0
  352. package/esm/report/json.d.ts +4 -0
  353. package/esm/report/json.js +3 -0
  354. package/esm/report/markdown.d.ts +25 -0
  355. package/esm/report/markdown.js +352 -0
  356. package/esm/report/pretty.d.ts +14 -0
  357. package/esm/report/pretty.js +318 -0
  358. package/esm/report/sarif.d.ts +40 -0
  359. package/esm/report/sarif.js +189 -0
  360. package/esm/report/view.d.ts +82 -0
  361. package/esm/report/view.js +126 -0
  362. package/esm/rules/registry.d.ts +66 -0
  363. package/esm/rules/registry.js +562 -0
  364. package/esm/score/score.d.ts +84 -0
  365. package/esm/score/score.js +233 -0
  366. package/esm/score/scorecards.d.ts +68 -0
  367. package/esm/score/scorecards.js +157 -0
  368. package/esm/types.d.ts +292 -0
  369. package/esm/types.js +26 -0
  370. package/esm/version.d.ts +8 -0
  371. package/esm/version.js +9 -0
  372. package/exposure/adapters.d.ts +152 -0
  373. package/exposure/adapters.js +468 -0
  374. package/exposure/planes.d.ts +59 -0
  375. package/exposure/planes.js +151 -0
  376. package/exposure/reach.d.ts +78 -0
  377. package/exposure/reach.js +109 -0
  378. package/exposure/routines.d.ts +36 -0
  379. package/exposure/routines.js +57 -0
  380. package/index.d.ts +96 -6
  381. package/index.js +229 -6
  382. package/lint/index.d.ts +11 -0
  383. package/lint/index.js +19 -0
  384. package/package.json +54 -48
  385. package/perf/baseline.d.ts +40 -0
  386. package/perf/baseline.js +92 -0
  387. package/perf/explain.d.ts +46 -0
  388. package/perf/explain.js +204 -0
  389. package/pg/acl.d.ts +72 -0
  390. package/pg/acl.js +191 -0
  391. package/pg/behaviors.d.ts +114 -0
  392. package/pg/behaviors.js +217 -0
  393. package/pg/exposure.d.ts +74 -0
  394. package/pg/exposure.js +205 -0
  395. package/pg/functions.d.ts +52 -0
  396. package/pg/functions.js +131 -0
  397. package/pg/indexes.d.ts +192 -0
  398. package/pg/indexes.js +400 -0
  399. package/pg/introspect.d.ts +134 -0
  400. package/pg/introspect.js +280 -0
  401. package/pg/objects.d.ts +37 -0
  402. package/pg/objects.js +111 -0
  403. package/pg/paths.d.ts +114 -0
  404. package/pg/paths.js +166 -0
  405. package/pg/proc.d.ts +23 -0
  406. package/pg/proc.js +58 -0
  407. package/pg/reach-inputs.d.ts +47 -0
  408. package/pg/reach-inputs.js +136 -0
  409. package/pg/roles.d.ts +28 -0
  410. package/pg/roles.js +54 -0
  411. package/pg/stats.d.ts +65 -0
  412. package/pg/stats.js +133 -0
  413. package/pgpm-test.d.ts +22 -0
  414. package/pgpm-test.js +42 -0
  415. package/report/callgraph.d.ts +9 -0
  416. package/report/callgraph.js +81 -0
  417. package/report/compare.d.ts +143 -0
  418. package/report/compare.js +205 -0
  419. package/report/github.d.ts +74 -0
  420. package/report/github.js +333 -0
  421. package/report/json.d.ts +4 -0
  422. package/report/json.js +6 -0
  423. package/report/markdown.d.ts +25 -0
  424. package/report/markdown.js +355 -0
  425. package/report/pretty.d.ts +14 -0
  426. package/report/pretty.js +324 -0
  427. package/report/sarif.d.ts +40 -0
  428. package/report/sarif.js +193 -0
  429. package/report/view.d.ts +82 -0
  430. package/report/view.js +132 -0
  431. package/rules/registry.d.ts +66 -0
  432. package/rules/registry.js +569 -0
  433. package/schema/safegres.schema.json +1119 -0
  434. package/score/score.d.ts +84 -0
  435. package/score/score.js +238 -0
  436. package/score/scorecards.d.ts +68 -0
  437. package/score/scorecards.js +162 -0
  438. package/types.d.ts +292 -0
  439. package/types.js +32 -0
  440. package/version.d.ts +8 -0
  441. package/version.js +12 -0
  442. package/LICENSE +0 -23
  443. package/admin.d.ts +0 -26
  444. package/admin.js +0 -182
  445. package/connect.d.ts +0 -19
  446. package/connect.js +0 -83
  447. package/dist/README.md +0 -557
  448. package/dist/package.json +0 -72
  449. package/esm/admin.js +0 -178
  450. package/esm/connect.js +0 -78
  451. package/esm/manager.js +0 -136
  452. package/esm/roles.js +0 -32
  453. package/esm/seed/adapters.js +0 -23
  454. package/esm/seed/csv.js +0 -44
  455. package/esm/seed/index.js +0 -16
  456. package/esm/seed/json.js +0 -18
  457. package/esm/seed/launchql.js +0 -19
  458. package/esm/seed/sqitch.js +0 -17
  459. package/esm/stream.js +0 -43
  460. package/esm/test-client.js +0 -150
  461. package/manager.d.ts +0 -25
  462. package/manager.js +0 -140
  463. package/roles.d.ts +0 -17
  464. package/roles.js +0 -38
  465. package/seed/adapters.d.ts +0 -4
  466. package/seed/adapters.js +0 -28
  467. package/seed/csv.d.ts +0 -9
  468. package/seed/csv.js +0 -49
  469. package/seed/index.d.ts +0 -16
  470. package/seed/index.js +0 -33
  471. package/seed/json.d.ts +0 -6
  472. package/seed/json.js +0 -21
  473. package/seed/launchql.d.ts +0 -2
  474. package/seed/launchql.js +0 -22
  475. package/seed/sqitch.d.ts +0 -2
  476. package/seed/sqitch.js +0 -20
  477. package/seed/types.d.ts +0 -13
  478. package/stream.d.ts +0 -2
  479. package/stream.js +0 -46
  480. package/test-client.d.ts +0 -49
  481. package/test-client.js +0 -154
  482. /package/{seed → config}/types.js +0 -0
  483. /package/esm/{seed → config}/types.js +0 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Policy-aware performance checks (X2/X3/X4).
3
+ *
4
+ * These are the checks a generic index linter can't make: they read the RLS
5
+ * policy predicate and ask whether the *security* qual — the one Postgres
6
+ * evaluates first, on every candidate row, for every query against the table —
7
+ * can actually use an index.
8
+ *
9
+ * All three are static (catalog + AST only); no runtime statistics, no EXPLAIN.
10
+ */
11
+ import type { PgAstNode } from '../ast/parse';
12
+ import type { TableIndexSnapshot } from '../pg/indexes';
13
+ import type { TableSnapshot } from '../pg/introspect';
14
+ import type { ProcVolatility } from '../pg/proc';
15
+ import type { Finding } from '../types';
16
+ export type PolicyClause = 'USING' | 'WITH CHECK';
17
+ /** A column of the policy's own table, as referenced by the policy predicate. */
18
+ export interface PredicateColumn {
19
+ column: string;
20
+ clause: PolicyClause;
21
+ /**
22
+ * The cast type or function name wrapping the column reference, if any.
23
+ * `undefined` means the column is compared directly (index-usable).
24
+ */
25
+ wrappedBy?: string;
26
+ /** True when `wrappedBy` is a cast rather than a function call. */
27
+ isCast?: boolean;
28
+ }
29
+ /**
30
+ * Collect the policy's own columns that participate in a comparison, along
31
+ * with any cast/function wrapping them.
32
+ *
33
+ * Only single-part column references (`tenant_id`) and references qualified
34
+ * with the table's own name/alias (`posts.tenant_id`) count — a column from a
35
+ * subquery's table belongs to that table's policies, not this one.
36
+ */
37
+ export declare function collectPredicateColumns(expr: PgAstNode, clause: PolicyClause, tableName: string): PredicateColumn[];
38
+ /**
39
+ * X2: a policy predicate compares a column that has no index with that column
40
+ * in the leading position, so the security qual degrades to a sequential scan
41
+ * on every query touching the table.
42
+ */
43
+ export declare function checkUnindexedPolicyColumns(table: TableSnapshot, indexes: TableIndexSnapshot, columns: Map<string, PredicateColumn[]>): Finding[];
44
+ /**
45
+ * X3: the policy wraps its own column in a cast or function call
46
+ * (`tenant_id::text = ...`, `lower(email) = ...`). A plain b-tree index on the
47
+ * bare column can't serve that comparison — it needs a matching expression
48
+ * index, or the cast should move to the other side of the operator.
49
+ */
50
+ export declare function checkPolicyColumnCasts(table: TableSnapshot, indexes: TableIndexSnapshot, columns: Map<string, PredicateColumn[]>): Finding[];
51
+ /**
52
+ * X4: the policy predicate calls a non-LEAKPROOF function.
53
+ *
54
+ * Postgres assigns security quals a lower security level than user quals and
55
+ * will not push a non-leakproof qual below a join or subquery scan. The
56
+ * practical effect is that the policy filter is applied later and to more rows
57
+ * than necessary, and index scans on the underlying relation are given up.
58
+ *
59
+ * System functions are exempt: their leakproofness is a fixed property of the
60
+ * server, not something the schema author chose.
61
+ */
62
+ export declare function checkNonLeakproofPolicyFunctions(table: TableSnapshot, expr: PgAstNode, volatility: Map<string, ProcVolatility>, policyName: string): Finding[];
63
+ /**
64
+ * X9: the policy calls a STABLE function whose arguments don't depend on the
65
+ * row, but the call isn't wrapped in a scalar sub-select.
66
+ *
67
+ * `STABLE` promises the result won't change within the statement; it does *not*
68
+ * make the planner evaluate the call once. A bare `current_principal_id()` in a
69
+ * qual is executed for every row the scan considers. Wrapped as
70
+ * `(SELECT current_principal_id())` the expression references no column, so the
71
+ * planner hoists it into an InitPlan: one call per query, and the result is a
72
+ * constant the index can be probed with.
73
+ *
74
+ * VOLATILE calls are deliberately excluded — per-row evaluation is their
75
+ * defined behaviour, and hoisting one would change semantics (P1 covers those).
76
+ * IMMUTABLE calls with constant arguments are folded at plan time already.
77
+ */
78
+ export declare function checkUnhoistedPolicyFunctions(table: TableSnapshot, expr: PgAstNode, volatility: Map<string, ProcVolatility>, policyName: string, clause: PolicyClause): Finding[];
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Policy-aware performance checks (X2/X3/X4).
3
+ *
4
+ * These are the checks a generic index linter can't make: they read the RLS
5
+ * policy predicate and ask whether the *security* qual — the one Postgres
6
+ * evaluates first, on every candidate row, for every query against the table —
7
+ * can actually use an index.
8
+ *
9
+ * All three are static (catalog + AST only); no runtime statistics, no EXPLAIN.
10
+ */
11
+ import { walk } from '@pgsql/traverse';
12
+ import { columnRefPath, funcNameQualified } from '../ast/walk';
13
+ /**
14
+ * Comparison-ish nodes worth indexing for. We deliberately ignore `IS NULL`,
15
+ * boolean tests and the like: an index rarely helps there, so flagging them
16
+ * would be noise.
17
+ */
18
+ const COMPARISON_TAGS = new Set(['A_Expr', 'SubLink', 'NullTest', 'ScalarArrayOpExpr']);
19
+ /**
20
+ * Collect the policy's own columns that participate in a comparison, along
21
+ * with any cast/function wrapping them.
22
+ *
23
+ * Only single-part column references (`tenant_id`) and references qualified
24
+ * with the table's own name/alias (`posts.tenant_id`) count — a column from a
25
+ * subquery's table belongs to that table's policies, not this one.
26
+ */
27
+ export function collectPredicateColumns(expr, clause, tableName) {
28
+ const out = [];
29
+ const seen = new Set();
30
+ walk(expr, (path) => {
31
+ if (path.tag !== 'ColumnRef')
32
+ return;
33
+ const parts = columnRefPath(path.node);
34
+ const column = ownColumnName(parts, tableName);
35
+ if (!column)
36
+ return;
37
+ const { wrappedBy, isCast } = wrappingOf(path);
38
+ if (!inComparison(path))
39
+ return;
40
+ const key = `${clause}:${column}:${wrappedBy ?? ''}`;
41
+ if (seen.has(key))
42
+ return;
43
+ seen.add(key);
44
+ out.push({ column, clause, wrappedBy, isCast });
45
+ });
46
+ return out;
47
+ }
48
+ /**
49
+ * X2: a policy predicate compares a column that has no index with that column
50
+ * in the leading position, so the security qual degrades to a sequential scan
51
+ * on every query touching the table.
52
+ */
53
+ export function checkUnindexedPolicyColumns(table, indexes, columns) {
54
+ const out = [];
55
+ const leading = leadingColumns(indexes);
56
+ for (const [policyName, cols] of columns) {
57
+ const reported = new Set();
58
+ for (const col of cols) {
59
+ // A wrapped column is X3's business — the raw column being indexed
60
+ // wouldn't help the wrapped comparison anyway.
61
+ if (col.wrappedBy)
62
+ continue;
63
+ if (leading.has(col.column))
64
+ continue;
65
+ if (reported.has(col.column))
66
+ continue;
67
+ reported.add(col.column);
68
+ out.push({
69
+ code: 'X2',
70
+ severity: 'medium',
71
+ category: 'index',
72
+ schema: table.schema,
73
+ table: table.name,
74
+ policy: policyName,
75
+ message: `Policy "${policyName}" on ${table.schema}.${table.name} filters by ${col.column}, which is not the leading column of any index`,
76
+ hint: `RLS quals run before user quals on every row the planner considers, so an unindexed policy column forces a sequential scan for all callers. CREATE INDEX ON ${table.schema}.${table.name} (${col.column}).`,
77
+ context: { column: col.column, clause: col.clause }
78
+ });
79
+ }
80
+ }
81
+ return out;
82
+ }
83
+ /**
84
+ * X3: the policy wraps its own column in a cast or function call
85
+ * (`tenant_id::text = ...`, `lower(email) = ...`). A plain b-tree index on the
86
+ * bare column can't serve that comparison — it needs a matching expression
87
+ * index, or the cast should move to the other side of the operator.
88
+ */
89
+ export function checkPolicyColumnCasts(table, indexes, columns) {
90
+ const out = [];
91
+ const expressionDefs = indexes.indexes
92
+ .filter((i) => i.expression)
93
+ .map((i) => i.definition.toLowerCase());
94
+ for (const [policyName, cols] of columns) {
95
+ const reported = new Set();
96
+ for (const col of cols) {
97
+ if (!col.wrappedBy)
98
+ continue;
99
+ const key = `${col.column}:${col.wrappedBy}`;
100
+ if (reported.has(key))
101
+ continue;
102
+ reported.add(key);
103
+ if (expressionDefs.some((def) => defCovers(def, col)))
104
+ continue;
105
+ const shape = col.isCast
106
+ ? `${col.column}::${col.wrappedBy}`
107
+ : `${col.wrappedBy}(${col.column})`;
108
+ out.push({
109
+ code: 'X3',
110
+ severity: 'medium',
111
+ category: 'index',
112
+ schema: table.schema,
113
+ table: table.name,
114
+ policy: policyName,
115
+ message: `Policy "${policyName}" on ${table.schema}.${table.name} compares ${shape} — a plain index on ${col.column} cannot serve this`,
116
+ hint: col.isCast
117
+ ? `Cast the other side of the comparison instead, or add a matching expression index: CREATE INDEX ON ${table.schema}.${table.name} ((${shape})).`
118
+ : `Add a matching expression index: CREATE INDEX ON ${table.schema}.${table.name} ((${shape})).`,
119
+ context: { column: col.column, expression: shape, clause: col.clause }
120
+ });
121
+ }
122
+ }
123
+ return out;
124
+ }
125
+ /**
126
+ * X4: the policy predicate calls a non-LEAKPROOF function.
127
+ *
128
+ * Postgres assigns security quals a lower security level than user quals and
129
+ * will not push a non-leakproof qual below a join or subquery scan. The
130
+ * practical effect is that the policy filter is applied later and to more rows
131
+ * than necessary, and index scans on the underlying relation are given up.
132
+ *
133
+ * System functions are exempt: their leakproofness is a fixed property of the
134
+ * server, not something the schema author chose.
135
+ */
136
+ export function checkNonLeakproofPolicyFunctions(table, expr, volatility, policyName) {
137
+ const out = [];
138
+ const seen = new Set();
139
+ walk(expr, (path) => {
140
+ if (path.tag !== 'FuncCall')
141
+ return;
142
+ const qualified = funcNameQualified(path.node);
143
+ const bare = qualified.split('.').pop() ?? qualified;
144
+ const info = volatility.get(qualified) ?? volatility.get(bare);
145
+ if (!info)
146
+ return;
147
+ if (info.isSystem)
148
+ return;
149
+ if (info.isLeakproof)
150
+ return;
151
+ if (seen.has(info.name))
152
+ return;
153
+ seen.add(info.name);
154
+ out.push({
155
+ code: 'X4',
156
+ severity: 'low',
157
+ category: 'index',
158
+ schema: table.schema,
159
+ table: table.name,
160
+ policy: policyName,
161
+ message: `Policy "${policyName}" on ${table.schema}.${table.name} calls non-LEAKPROOF function ${info.name}`,
162
+ hint: 'Non-leakproof quals cannot be pushed below joins or subquery scans, so the policy filter is applied late and to more rows than necessary. If the function provably leaks nothing about its arguments (no error messages containing values), mark it LEAKPROOF.',
163
+ context: { function: info.name }
164
+ });
165
+ });
166
+ return out;
167
+ }
168
+ /**
169
+ * System functions worth flagging for X9 despite being built in: reading a GUC
170
+ * per row is the exact pattern the rule exists to catch, whether it goes
171
+ * through a wrapper or not.
172
+ */
173
+ const HOISTABLE_SYSTEM_FUNCTIONS = new Set(['current_setting', 'pg_catalog.current_setting']);
174
+ /**
175
+ * X9: the policy calls a STABLE function whose arguments don't depend on the
176
+ * row, but the call isn't wrapped in a scalar sub-select.
177
+ *
178
+ * `STABLE` promises the result won't change within the statement; it does *not*
179
+ * make the planner evaluate the call once. A bare `current_principal_id()` in a
180
+ * qual is executed for every row the scan considers. Wrapped as
181
+ * `(SELECT current_principal_id())` the expression references no column, so the
182
+ * planner hoists it into an InitPlan: one call per query, and the result is a
183
+ * constant the index can be probed with.
184
+ *
185
+ * VOLATILE calls are deliberately excluded — per-row evaluation is their
186
+ * defined behaviour, and hoisting one would change semantics (P1 covers those).
187
+ * IMMUTABLE calls with constant arguments are folded at plan time already.
188
+ */
189
+ export function checkUnhoistedPolicyFunctions(table, expr, volatility, policyName, clause) {
190
+ const out = [];
191
+ const seen = new Set();
192
+ walk(expr, (path) => {
193
+ if (path.tag !== 'FuncCall')
194
+ return;
195
+ const node = path.node;
196
+ const qualified = funcNameQualified(node);
197
+ const bare = qualified.split('.').pop() ?? qualified;
198
+ const info = volatility.get(qualified) ?? volatility.get(bare);
199
+ if (!info)
200
+ return;
201
+ if (info.volatility !== 's')
202
+ return;
203
+ if (info.isSystem && !HOISTABLE_SYSTEM_FUNCTIONS.has(info.name))
204
+ return;
205
+ if (referencesColumn(node))
206
+ return;
207
+ if (isHoisted(path))
208
+ return;
209
+ if (seen.has(info.name))
210
+ return;
211
+ seen.add(info.name);
212
+ out.push({
213
+ code: 'X9',
214
+ severity: 'medium',
215
+ category: 'index',
216
+ schema: table.schema,
217
+ table: table.name,
218
+ policy: policyName,
219
+ message: `Policy "${policyName}" on ${table.schema}.${table.name} calls ${info.name}() per row — the call is not wrapped in a scalar sub-select`,
220
+ hint: `STABLE does not mean "evaluated once". Whenever this qual lands in a Filter rather than an index `
221
+ + `condition — an unindexed policy column, a join, an OR branch — ${info.name}() is executed for every `
222
+ + `row the scan considers. Wrap it as (SELECT ${info.name}()) so the expression references no column and `
223
+ + 'the planner hoists it into an InitPlan: one call per query, whatever plan it picks.',
224
+ context: { function: info.name, clause }
225
+ });
226
+ });
227
+ return out;
228
+ }
229
+ /** True when any argument of the call references a column (so it can't be hoisted). */
230
+ function referencesColumn(funcCall) {
231
+ const args = funcCall.args;
232
+ if (!Array.isArray(args) || args.length === 0)
233
+ return false;
234
+ let found = false;
235
+ for (const arg of args) {
236
+ walk(arg, (path) => {
237
+ if (path.tag === 'ColumnRef')
238
+ found = true;
239
+ });
240
+ if (found)
241
+ return true;
242
+ }
243
+ return false;
244
+ }
245
+ /**
246
+ * True when the call already sits inside an uncorrelated scalar sub-select —
247
+ * `(SELECT fn())` — which is what makes the planner hoist it.
248
+ *
249
+ * Being inside an `EXISTS` sub-select is *not* enough: that subquery is
250
+ * correlated with the outer row, so it runs per row and takes the function
251
+ * call with it.
252
+ */
253
+ function isHoisted(path) {
254
+ for (let cursor = path.parent; cursor; cursor = cursor.parent) {
255
+ if (cursor.tag !== 'SubLink')
256
+ continue;
257
+ const sublink = cursor.node;
258
+ if (sublink.subLinkType !== 'EXPR_SUBLINK')
259
+ return false;
260
+ const subselect = sublink.subselect;
261
+ const stmt = (subselect?.SelectStmt ?? subselect);
262
+ const from = stmt?.fromClause;
263
+ return !Array.isArray(from) || from.length === 0;
264
+ }
265
+ return false;
266
+ }
267
+ /** Columns that sit in the leading position of at least one index. */
268
+ function leadingColumns(indexes) {
269
+ const out = new Set();
270
+ for (const index of indexes.indexes) {
271
+ const first = index.columnNames[0];
272
+ if (first)
273
+ out.add(first);
274
+ }
275
+ return out;
276
+ }
277
+ /**
278
+ * Resolve a column reference to a bare column name on `tableName`, or null if
279
+ * it belongs to another relation (or is `*`).
280
+ */
281
+ function ownColumnName(parts, tableName) {
282
+ if (parts.length === 1)
283
+ return parts[0] === '*' ? null : parts[0];
284
+ if (parts.length === 2 && parts[0] === tableName)
285
+ return parts[1] === '*' ? null : parts[1];
286
+ return null;
287
+ }
288
+ /** Nearest enclosing cast or function call, if the column is wrapped by one. */
289
+ function wrappingOf(path) {
290
+ const parent = path.parent;
291
+ if (!parent)
292
+ return {};
293
+ if (parent.tag === 'TypeCast') {
294
+ const typeName = castTypeName(parent.node);
295
+ return typeName ? { wrappedBy: typeName, isCast: true } : {};
296
+ }
297
+ if (parent.tag === 'FuncCall') {
298
+ return { wrappedBy: funcNameQualified(parent.node), isCast: false };
299
+ }
300
+ return {};
301
+ }
302
+ /** True when the column participates in a comparison rather than, say, a projection. */
303
+ function inComparison(path) {
304
+ let cursor = path.parent;
305
+ // Allow one or two wrapping levels (cast, function call) between the column
306
+ // and the comparison it feeds.
307
+ for (let depth = 0; cursor && depth < 3; depth++) {
308
+ if (COMPARISON_TAGS.has(cursor.tag))
309
+ return true;
310
+ cursor = cursor.parent;
311
+ }
312
+ return false;
313
+ }
314
+ function castTypeName(typeCast) {
315
+ const typeName = typeCast.typeName;
316
+ if (!typeName)
317
+ return null;
318
+ const names = typeName.names;
319
+ if (!Array.isArray(names) || names.length === 0)
320
+ return null;
321
+ const last = names[names.length - 1];
322
+ const str = (last.String ?? last.string);
323
+ if (!str)
324
+ return null;
325
+ const value = String((str.sval ?? str.str) ?? '');
326
+ return value.length > 0 ? value : null;
327
+ }
328
+ /**
329
+ * Heuristic match of an expression index definition against a wrapped column.
330
+ * `pg_get_indexdef` renders casts as `((col)::text)` and calls as `lower(col)`,
331
+ * so requiring both tokens is enough to avoid the common false positive
332
+ * without parsing the definition back into an AST.
333
+ */
334
+ function defCovers(definitionLower, col) {
335
+ const wrapper = (col.wrappedBy ?? '').toLowerCase();
336
+ const column = col.column.toLowerCase();
337
+ if (!wrapper)
338
+ return false;
339
+ if (col.isCast)
340
+ return definitionLower.includes(`::${wrapper}`) && definitionLower.includes(column);
341
+ return definitionLower.includes(`${wrapper}(`) && definitionLower.includes(column);
342
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * L21: `granted − reachable`, the revocable-grant rule.
3
+ *
4
+ * For a role R, {@link analyzeRevocableGrants} computes the set of EXECUTE
5
+ * grants R holds that no path can exercise, so a human never has to guess which
6
+ * of R's grants are safe to drop. It is the reachability proof the rest of the
7
+ * L-series only approximates: L1 asks whether an indirect *relation* grant is
8
+ * admitted by current policy coverage, L3 asks whether an object grant is
9
+ * blocked by missing schema USAGE, and L6 suppresses an unaddressable relation
10
+ * finding whenever a policy predicate merely *names* the relation. L21 asks the
11
+ * broader question — "can this grant be exercised at all?" — across object
12
+ * kinds and every execution path a grant can travel, and only claims a grant
13
+ * revocable when it can prove the answer is no.
14
+ *
15
+ * Reach(R) is closed over, at minimum:
16
+ *
17
+ * 1. **Direct call targets** — functions in an exposed schema R can EXECUTE
18
+ * and reach through schema USAGE.
19
+ * 2. **RLS policy predicates.** For every policy on a relation R can address,
20
+ * the `USING`/`WITH CHECK` expressions and the functions and relations
21
+ * they name, closed over transitively. These evaluate as R.
22
+ * 3. **Trigger bodies.** For every ordinary trigger on a relation R can
23
+ * write, the trigger function's body — when it is not SECURITY DEFINER,
24
+ * it runs as R. (EXECUTE on the trigger function itself is *not* reach:
25
+ * Postgres never checks it when firing a trigger, so such a grant is
26
+ * genuinely revocable, which is the whole point of the constructive-db
27
+ * audit.)
28
+ * 4. **Write-time expressions** — column defaults, generated columns and
29
+ * CHECK constraints on relations R can insert into or update. They run as
30
+ * the writing role.
31
+ * 5. **Views and their bodies**, so a grant exercised only through an
32
+ * `security_invoker` view survives.
33
+ *
34
+ * A SECURITY DEFINER boundary **stops** the closure: inside it the owner's
35
+ * privileges apply, not R's, so R needs EXECUTE on the definer function
36
+ * (retained) but not on anything the body goes on to call.
37
+ *
38
+ * **Fail-closed, bounded by USAGE.** Nothing is reported revocable that the
39
+ * analysis cannot prove unused. If any node reached as R runs SQL this analysis
40
+ * cannot read — dynamic `EXECUTE`, a body that fails to parse, a C/internal
41
+ * function — the role's closure is incomplete: that opaque node, running as R,
42
+ * could construct a call to any function R can *actually name and execute*. But
43
+ * Postgres still enforces schema USAGE at call time, so an opaque node running
44
+ * as R can only reach functions in schemas R holds USAGE on. A candidate whose
45
+ * schema R cannot even name is therefore out of an opaque node's reach: it can
46
+ * only be exercised through a reference this analysis *does* follow (a policy
47
+ * predicate, trigger body, write-time expression or view), so if none landed on
48
+ * it, it is revocable. Only unreached candidates in a USAGE-reachable schema are
49
+ * moved from `revocable` to `suppressed`, with the reason recorded. This is what
50
+ * lets the constructive-db audit prove the blanket EXECUTE on private-schema
51
+ * functions revocable — anonymous cannot name those schemas — while never
52
+ * revoking a grant an opaque node could plausibly exercise.
53
+ */
54
+ import type { SchemaAclInfo } from '../pg/acl';
55
+ import type { FunctionSnapshot } from '../pg/functions';
56
+ import type { ViewSnapshot } from '../pg/indexes';
57
+ import type { TableSnapshot } from '../pg/introspect';
58
+ import type { ReachInputs } from '../pg/reach-inputs';
59
+ import type { Finding } from '../types';
60
+ import { type GrantVia, type RoleGraph } from './lattice';
61
+ /** Which execution path retained a grant — the proof a human reviews. */
62
+ export type RetainReason = 'direct-call' | 'policy-predicate' | 'trigger' | 'default-expression' | 'generated-column' | 'check-constraint' | 'view';
63
+ /** A `(role, object, kind)` grant, the machine-consumable unit of the rule. */
64
+ export interface GrantTuple {
65
+ role: string;
66
+ schema: string;
67
+ object: string;
68
+ /** Argument signature, for the function overload the grant is on. */
69
+ args: string;
70
+ kind: 'function';
71
+ privilege: 'EXECUTE';
72
+ /** How R comes to hold it — `direct` or `member of <role>` (never PUBLIC). */
73
+ via: GrantVia;
74
+ }
75
+ /** A grant the analysis proved reachable, with the path(s) that retained it. */
76
+ export interface RetainedGrant extends GrantTuple {
77
+ reasons: RetainReason[];
78
+ proof: string[];
79
+ }
80
+ /** A grant the analysis could not prove unused, and why. */
81
+ export interface SuppressedGrant extends GrantTuple {
82
+ reason: string;
83
+ }
84
+ /** A node in R's closure that ran SQL the analysis could not read. */
85
+ export interface TaintNode {
86
+ node: string;
87
+ reason: string;
88
+ }
89
+ export interface RoleRevocable {
90
+ role: string;
91
+ summary: {
92
+ candidates: number;
93
+ revocable: number;
94
+ retained: number;
95
+ retainedByPolicy: number;
96
+ retainedByTrigger: number;
97
+ suppressed: number;
98
+ };
99
+ /** Grants that can be dropped without changing what R can do. */
100
+ revocable: GrantTuple[];
101
+ /** Grants that must be kept, each with the path(s) proving it load-bearing. */
102
+ retained: RetainedGrant[];
103
+ /** Grants left retained only because the closure was incomplete. */
104
+ suppressed: SuppressedGrant[];
105
+ /** The opaque nodes that forced the suppressions above. */
106
+ taint: TaintNode[];
107
+ }
108
+ export interface RevocableGrantsReport {
109
+ roles: RoleRevocable[];
110
+ }
111
+ export interface RevocableGrantsInput {
112
+ roles: string[];
113
+ functions: FunctionSnapshot[];
114
+ tables: TableSnapshot[];
115
+ views: ViewSnapshot[];
116
+ graph: RoleGraph;
117
+ schemaAcls: Map<string, SchemaAclInfo>;
118
+ reachInputs: ReachInputs;
119
+ /** Schemas the exposed API surface can reach; empty when the surface is unknown. */
120
+ exposedSchemas: Set<string>;
121
+ /** True when an exposure surface was resolved — otherwise all schemas are reachable. */
122
+ exposureKnown: boolean;
123
+ /** Relations the generated API declares it cannot address (`schema.table`). */
124
+ unaddressable: Set<string>;
125
+ }
126
+ /**
127
+ * The revocable, retained and suppressed EXECUTE grants for each configured
128
+ * role. One reachability closure is walked per role; the candidate set is R's
129
+ * own EXECUTE grants (direct or inherited — a grant TO PUBLIC is not R's to
130
+ * revoke on its own, so it is never a candidate).
131
+ */
132
+ export declare function analyzeRevocableGrants(input: RevocableGrantsInput): Promise<RevocableGrantsReport>;
133
+ /**
134
+ * L21 findings: one per revocable grant. The proof of every *retained* grant
135
+ * lives in `report.revocableGrants`, not in the finding stream — a retained
136
+ * grant is not a problem, and the finding stream is problems. The revocable
137
+ * recommendation carries its own tuple in `context` so a caller can act on it
138
+ * without re-deriving it.
139
+ */
140
+ export declare function revocableGrantFindings(report: RevocableGrantsReport): Finding[];