@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,53 @@
1
+ import type { PgAstNode } from '../ast/parse';
2
+ import type { PolicyInfo, TableSnapshot } from '../pg/introspect';
3
+ import type { ProcVolatility } from '../pg/proc';
4
+ import type { Finding } from '../types';
5
+ /**
6
+ * Anti-pattern P1: policy predicate invokes a VOLATILE function (per-row
7
+ * evaluation, breaks the planner's ability to hoist or cache).
8
+ *
9
+ * Also flags SECURITY DEFINER wrappers here (P1b): they can't be inlined and
10
+ * become per-row function calls.
11
+ *
12
+ * Expected input: a volatility map keyed by `schema.name` (see pg/proc.ts).
13
+ */
14
+ export declare function checkVolatileFunctions(table: TableSnapshot, expr: PgAstNode, volatility: Map<string, ProcVolatility>, policyName: string): Finding[];
15
+ /**
16
+ * Anti-pattern P5: policy references `session_user`, `current_user`, or
17
+ * `pg_has_role(...)` for tenant gating. These bypass the role-and-JWT layer
18
+ * that the rest of the auth model is built on, and they silently evaluate
19
+ * against the *login* role instead of the `SET LOCAL role` role, which
20
+ * produces confusing RLS behavior across pooled connections.
21
+ */
22
+ export declare function checkSessionUserGating(table: TableSnapshot, expr: PgAstNode, policyName: string): Finding[];
23
+ /**
24
+ * Anti-patterns A7 / A8: trivially-permissive policy body.
25
+ *
26
+ * A permissive policy whose body is the literal `true` adds zero security —
27
+ * it's equivalent to not having RLS at all for the covered command. This is
28
+ * different from an intentional *restrictive* `true` (which would require
29
+ * all rows to satisfy it). We only flag `permissive = true` here because
30
+ * Postgres defaults to PERMISSIVE and `USING (true)` is the most common
31
+ * accidental "fail-open" shape.
32
+ *
33
+ * The verb matters enormously:
34
+ * - A7 (critical): a WRITE verb (INSERT/UPDATE/DELETE/ALL) with literal
35
+ * `true` lets every applicable role mutate every row — and, because
36
+ * permissive policies OR together, it silently defeats any carefully
37
+ * scoped policies on the same table.
38
+ * - A8 (low): a SELECT-only `USING (true)` is the standard public-read
39
+ * reference-table pattern; flag it for confirmation, not alarm.
40
+ *
41
+ * Input: the parsed USING and WITH CHECK ASTs (may be null if empty).
42
+ */
43
+ export declare function checkTriviallyPermissive(table: TableSnapshot, policy: PolicyInfo, usingAst: PgAstNode | null, withCheckAst: PgAstNode | null): Finding | null;
44
+ /**
45
+ * Walk a policy expression, collecting unique function `(schema, name)` tuples
46
+ * so we can resolve volatility in one batch query.
47
+ */
48
+ export declare function collectFunctionNames(expr: PgAstNode): Array<{
49
+ schema?: string;
50
+ name: string;
51
+ }>;
52
+ /** Parse a policy expression or return `null` on empty input. Logs parse errors to stderr. */
53
+ export declare function parseOrNull(expr: string | null, context: string): Promise<PgAstNode | null>;
@@ -0,0 +1,240 @@
1
+ import { boolConst } from '../ast/helpers';
2
+ import { parsePolicyExpression } from '../ast/parse';
3
+ import { columnRefPath, findAll, funcNameParts, funcNameQualified, visitAll } from '../ast/walk';
4
+ /**
5
+ * Function names we consider "safe" (stable) for policy predicates, even when
6
+ * pg_proc marks them volatile. These are the well-known Postgres session
7
+ * primitives that the planner treats as constant for the duration of a query.
8
+ *
9
+ * This is intentionally narrow — everything else that's volatile gets flagged.
10
+ */
11
+ const SAFE_VOLATILE_ALLOWLIST = new Set([
12
+ 'current_setting', // only because PostgreSQL optimises this specially
13
+ 'txid_current',
14
+ 'clock_timestamp', // still flagged elsewhere — listed here to document intent
15
+ 'now',
16
+ 'statement_timestamp',
17
+ 'transaction_timestamp'
18
+ ]);
19
+ /**
20
+ * Anti-pattern P1: policy predicate invokes a VOLATILE function (per-row
21
+ * evaluation, breaks the planner's ability to hoist or cache).
22
+ *
23
+ * Also flags SECURITY DEFINER wrappers here (P1b): they can't be inlined and
24
+ * become per-row function calls.
25
+ *
26
+ * Expected input: a volatility map keyed by `schema.name` (see pg/proc.ts).
27
+ */
28
+ export function checkVolatileFunctions(table, expr, volatility, policyName) {
29
+ const out = [];
30
+ for (const fc of findAll(expr, 'FuncCall')) {
31
+ const { schema, name } = funcNameParts(fc);
32
+ const qualified = funcNameQualified(fc);
33
+ const lookupKey = schema ? `${schema}.${name}` : name;
34
+ const info = volatility.get(qualified) ?? volatility.get(lookupKey) ?? volatility.get(name);
35
+ if (!info)
36
+ continue; // unknown function (e.g. operator expansion) — skip silently
37
+ // A SECURITY DEFINER call is a plan fence whatever its volatility: the
38
+ // planner cannot inline it, so the qual never reaches an index. Checked
39
+ // before the volatility filter below, which would otherwise let every
40
+ // STABLE wrapper — the common shape — through unreported.
41
+ if (info.isSecurityDefiner) {
42
+ out.push({
43
+ code: 'P1b',
44
+ severity: 'medium',
45
+ category: 'anti-pattern',
46
+ schema: table.schema,
47
+ table: table.name,
48
+ policy: policyName,
49
+ message: `Policy "${policyName}" on ${table.schema}.${table.name} calls SECURITY DEFINER function ${info.name}`,
50
+ hint: 'SECURITY DEFINER wrappers can\'t be inlined by the planner — every row forces a function call. Convert to a STABLE SQL function or replace with a direct expression.',
51
+ context: { function: info.name }
52
+ });
53
+ }
54
+ if (info.volatility !== 'v')
55
+ continue;
56
+ if (SAFE_VOLATILE_ALLOWLIST.has(info.name.split('.').pop() ?? ''))
57
+ continue;
58
+ if (info.isSystem && SAFE_VOLATILE_ALLOWLIST.has(name))
59
+ continue;
60
+ out.push({
61
+ code: 'P1',
62
+ severity: 'high',
63
+ category: 'anti-pattern',
64
+ schema: table.schema,
65
+ table: table.name,
66
+ policy: policyName,
67
+ message: `Policy "${policyName}" on ${table.schema}.${table.name} calls VOLATILE function ${info.name}`,
68
+ hint: 'Volatile functions re-execute for every row considered by the planner. Mark the function STABLE if safe, or precompute the result in a CTE / subquery.',
69
+ context: { function: info.name, securityDefiner: info.isSecurityDefiner }
70
+ });
71
+ }
72
+ return out;
73
+ }
74
+ /**
75
+ * Anti-pattern P5: policy references `session_user`, `current_user`, or
76
+ * `pg_has_role(...)` for tenant gating. These bypass the role-and-JWT layer
77
+ * that the rest of the auth model is built on, and they silently evaluate
78
+ * against the *login* role instead of the `SET LOCAL role` role, which
79
+ * produces confusing RLS behavior across pooled connections.
80
+ */
81
+ export function checkSessionUserGating(table, expr, policyName) {
82
+ const out = [];
83
+ // session_user / current_user appear as CurrentOfExpr-adjacent nodes in some
84
+ // parser versions, but the common form is `SQLValueFunction` with `op:
85
+ // SVFOP_CURRENT_USER` (4) or SVFOP_SESSION_USER (6).
86
+ visitAll(expr, (node, tag) => {
87
+ if (tag === 'SQLValueFunction') {
88
+ const op = node.op;
89
+ const opStr = typeof op === 'string' ? op : String(op);
90
+ if (/CURRENT_USER|SESSION_USER/i.test(opStr)) {
91
+ out.push({
92
+ code: 'P5',
93
+ severity: 'high',
94
+ category: 'anti-pattern',
95
+ schema: table.schema,
96
+ table: table.name,
97
+ policy: policyName,
98
+ message: `Policy "${policyName}" on ${table.schema}.${table.name} references ${opStr.replace('SVFOP_', '').toLowerCase()}`,
99
+ hint: 'Use a JWT-backed helper (`current_setting(\'jwt.claims...\')`) or a dedicated STABLE auth function. `current_user` / `session_user` bypass the app auth layer.'
100
+ });
101
+ }
102
+ }
103
+ });
104
+ // pg_has_role(text, text, text)
105
+ for (const fc of findAll(expr, 'FuncCall')) {
106
+ const { name } = funcNameParts(fc);
107
+ if (name === 'pg_has_role') {
108
+ out.push({
109
+ code: 'P5',
110
+ severity: 'high',
111
+ category: 'anti-pattern',
112
+ schema: table.schema,
113
+ table: table.name,
114
+ policy: policyName,
115
+ message: `Policy "${policyName}" on ${table.schema}.${table.name} uses pg_has_role() for tenant gating`,
116
+ hint: 'pg_has_role() checks Postgres role grants — not application-level authorization. Consider a dedicated auth helper instead.'
117
+ });
118
+ }
119
+ }
120
+ // Also flag raw ColumnRef `current_user` — parser versions vary.
121
+ for (const cr of findAll(expr, 'ColumnRef')) {
122
+ const path = columnRefPath(cr);
123
+ if (path.length === 1 && (path[0] === 'current_user' || path[0] === 'session_user')) {
124
+ out.push({
125
+ code: 'P5',
126
+ severity: 'high',
127
+ category: 'anti-pattern',
128
+ schema: table.schema,
129
+ table: table.name,
130
+ policy: policyName,
131
+ message: `Policy "${policyName}" on ${table.schema}.${table.name} references ${path[0]}`,
132
+ hint: 'Use a JWT-backed helper or STABLE auth function instead of raw session/current role.'
133
+ });
134
+ }
135
+ }
136
+ return out;
137
+ }
138
+ /**
139
+ * Anti-patterns A7 / A8: trivially-permissive policy body.
140
+ *
141
+ * A permissive policy whose body is the literal `true` adds zero security —
142
+ * it's equivalent to not having RLS at all for the covered command. This is
143
+ * different from an intentional *restrictive* `true` (which would require
144
+ * all rows to satisfy it). We only flag `permissive = true` here because
145
+ * Postgres defaults to PERMISSIVE and `USING (true)` is the most common
146
+ * accidental "fail-open" shape.
147
+ *
148
+ * The verb matters enormously:
149
+ * - A7 (critical): a WRITE verb (INSERT/UPDATE/DELETE/ALL) with literal
150
+ * `true` lets every applicable role mutate every row — and, because
151
+ * permissive policies OR together, it silently defeats any carefully
152
+ * scoped policies on the same table.
153
+ * - A8 (low): a SELECT-only `USING (true)` is the standard public-read
154
+ * reference-table pattern; flag it for confirmation, not alarm.
155
+ *
156
+ * Input: the parsed USING and WITH CHECK ASTs (may be null if empty).
157
+ */
158
+ export function checkTriviallyPermissive(table, policy, usingAst, withCheckAst) {
159
+ if (!policy.permissive)
160
+ return null;
161
+ const usingIsTrue = usingAst ? boolConst(usingAst) === true : false;
162
+ const withCheckIsTrue = withCheckAst ? boolConst(withCheckAst) === true : false;
163
+ // Figure out which clauses are "open".
164
+ // - If USING exists, it must be literal true to be "open" on the read side.
165
+ // - If WITH CHECK exists, it must also be literal true.
166
+ // - If a clause is absent, the command doesn't care about it (e.g. INSERT
167
+ // has no USING; SELECT has no WITH CHECK) — that's a separate concern
168
+ // covered by the coverage checks.
169
+ // We flag when *every* present clause is literal `true`.
170
+ const presentClauses = [];
171
+ const trivialClauses = [];
172
+ if (policy.using !== null) {
173
+ presentClauses.push('USING');
174
+ if (usingIsTrue)
175
+ trivialClauses.push('USING');
176
+ }
177
+ if (policy.withCheck !== null) {
178
+ presentClauses.push('WITH CHECK');
179
+ if (withCheckIsTrue)
180
+ trivialClauses.push('WITH CHECK');
181
+ }
182
+ if (presentClauses.length === 0)
183
+ return null;
184
+ if (trivialClauses.length !== presentClauses.length)
185
+ return null;
186
+ const clauseList = trivialClauses.join(' and ');
187
+ const isWrite = policy.cmd !== 'SELECT';
188
+ return isWrite
189
+ ? {
190
+ code: 'A7',
191
+ severity: 'critical',
192
+ category: 'anti-pattern',
193
+ schema: table.schema,
194
+ table: table.name,
195
+ policy: policy.name,
196
+ message: `Policy "${policy.name}" (FOR ${policy.cmd}) on ${table.schema}.${table.name} is trivially permissive (${clauseList} = true) — every applicable role can write every row`,
197
+ hint: 'Permissive policies OR together, so a literal-true write policy defeats every scoped policy on the table. Tighten the predicate or drop the policy.',
198
+ context: { cmd: policy.cmd, clauses: trivialClauses }
199
+ }
200
+ : {
201
+ code: 'A8',
202
+ severity: 'low',
203
+ category: 'anti-pattern',
204
+ schema: table.schema,
205
+ table: table.name,
206
+ policy: policy.name,
207
+ message: `Policy "${policy.name}" (FOR SELECT) on ${table.schema}.${table.name} is trivially permissive (${clauseList} = true) — all rows readable by applicable roles`,
208
+ hint: 'This is the standard public-read reference-table pattern. Confirm the table holds no per-tenant data; otherwise tighten the predicate.',
209
+ context: { cmd: policy.cmd, clauses: trivialClauses }
210
+ };
211
+ }
212
+ /**
213
+ * Walk a policy expression, collecting unique function `(schema, name)` tuples
214
+ * so we can resolve volatility in one batch query.
215
+ */
216
+ export function collectFunctionNames(expr) {
217
+ const out = [];
218
+ const seen = new Set();
219
+ for (const fc of findAll(expr, 'FuncCall')) {
220
+ const parts = funcNameParts(fc);
221
+ const key = `${parts.schema ?? ''}.${parts.name}`;
222
+ if (seen.has(key))
223
+ continue;
224
+ seen.add(key);
225
+ out.push(parts);
226
+ }
227
+ return out;
228
+ }
229
+ /** Parse a policy expression or return `null` on empty input. Logs parse errors to stderr. */
230
+ export async function parseOrNull(expr, context) {
231
+ try {
232
+ const parsed = await parsePolicyExpression(expr);
233
+ return parsed?.ast ?? null;
234
+ }
235
+ catch (err) {
236
+ // eslint-disable-next-line no-console
237
+ console.warn(`[safegres-audit] could not parse ${context}: ${err.message}`);
238
+ return null;
239
+ }
240
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * L13: reach that exists only in `pg_attribute.attacl`.
3
+ *
4
+ * `GRANT SELECT (secret) ON t TO anonymous` writes nothing to the relation's
5
+ * ACL. Every grant query in this package read `relacl` alone, so a role whose
6
+ * entire access to a table is column-scoped was reported as reaching *nothing*
7
+ * — no A2, no R1, no L-series, and "0 relation(s) accessible" in the role
8
+ * access report. The privilege is real: the role selects those columns, and
9
+ * when the table has no RLS it selects every row of them.
10
+ *
11
+ * The finding is deliberately about the *invisibility* as much as the access.
12
+ * A column grant is a legitimate and often good way to expose a projection;
13
+ * what is not fine is that nothing else in the audit grades it. So L13 reports
14
+ * the reach, states whether RLS mediates it, and — as everywhere else — never
15
+ * recommends revoking the grant it cannot prove unused.
16
+ */
17
+ import type { TableSnapshot } from '../pg/introspect';
18
+ import type { Finding } from '../types';
19
+ import { type LatticeRoleOptions, type RoleGraph } from './lattice';
20
+ export declare function checkUntrustedColumnGrants(table: TableSnapshot, graph: RoleGraph, options?: LatticeRoleOptions): Finding[];
@@ -0,0 +1,71 @@
1
+ /**
2
+ * L13: reach that exists only in `pg_attribute.attacl`.
3
+ *
4
+ * `GRANT SELECT (secret) ON t TO anonymous` writes nothing to the relation's
5
+ * ACL. Every grant query in this package read `relacl` alone, so a role whose
6
+ * entire access to a table is column-scoped was reported as reaching *nothing*
7
+ * — no A2, no R1, no L-series, and "0 relation(s) accessible" in the role
8
+ * access report. The privilege is real: the role selects those columns, and
9
+ * when the table has no RLS it selects every row of them.
10
+ *
11
+ * The finding is deliberately about the *invisibility* as much as the access.
12
+ * A column grant is a legitimate and often good way to expose a projection;
13
+ * what is not fine is that nothing else in the audit grades it. So L13 reports
14
+ * the reach, states whether RLS mediates it, and — as everywhere else — never
15
+ * recommends revoking the grant it cannot prove unused.
16
+ */
17
+ import { effectiveColumnGrants } from './lattice';
18
+ /** SELECT/INSERT/UPDATE are the column privileges RLS has anything to say about. */
19
+ const MEDIATED = ['SELECT', 'INSERT', 'UPDATE'];
20
+ export function checkUntrustedColumnGrants(table, graph, options = {}) {
21
+ const roles = options.roles ?? [];
22
+ if (roles.length === 0 || table.columnGrants.length === 0)
23
+ return [];
24
+ const out = [];
25
+ for (const role of roles) {
26
+ const grants = effectiveColumnGrants(table, role, graph).filter((g) => MEDIATED.includes(g.privilege));
27
+ if (grants.length === 0)
28
+ continue;
29
+ const attrs = graph.get(role);
30
+ // Same exemption test the rest of the lattice uses: a role RLS does not
31
+ // apply to reads every row of the columns it is granted.
32
+ const rlsApplies = table.rlsEnabled
33
+ && !attrs?.bypassRls
34
+ && !attrs?.isSuper
35
+ && !(role === table.owner && !table.rlsForced);
36
+ const columns = [...new Set(grants.flatMap((g) => g.columns))].sort();
37
+ const privileges = grants.map((g) => g.privilege).sort();
38
+ const via = grants[0].via;
39
+ out.push({
40
+ code: 'L13',
41
+ severity: 'info',
42
+ category: 'anti-pattern',
43
+ schema: table.schema,
44
+ table: table.name,
45
+ role,
46
+ privilege: privileges.join(', '),
47
+ message: `Untrusted role ${role} holds column-level ${privileges.join(', ')} on `
48
+ + `${table.schema}.${table.name} (${columns.join(', ')})`
49
+ + (via === 'direct' ? '' : ` via ${via}`)
50
+ + (rlsApplies
51
+ ? ' — mediated by RLS, but invisible to every rule that reads the relation ACL'
52
+ : ' — with no RLS to mediate it, so every row of those columns is readable'),
53
+ hint: `Column grants live in \`pg_attribute.attacl\`, not \`relacl\`: \`\\dp\` shows them in `
54
+ + `the "Column privileges" column and nothing else in this audit graded them until now. `
55
+ + (rlsApplies
56
+ ? 'Confirm the policies that mediate this relation are the ones you would want applied '
57
+ + 'to a projection of it.'
58
+ : 'If the projection is meant to be public this is correct as written; if not, enable '
59
+ + 'RLS on the relation — the column grant restricts *which columns*, never which rows.')
60
+ + ' Do not revoke the grant on the strength of this finding alone: nothing here proves it '
61
+ + 'unused.',
62
+ context: {
63
+ columns,
64
+ via,
65
+ rlsMediated: rlsApplies,
66
+ rlsEnabled: table.rlsEnabled
67
+ }
68
+ });
69
+ }
70
+ return out;
71
+ }
@@ -0,0 +1,43 @@
1
+ import type { PgPrivilege, PolicyCmd, PolicyInfo, TableSnapshot } from '../pg/introspect';
2
+ import type { Finding } from '../types';
3
+ /**
4
+ * Coverage rules for row-level security.
5
+ *
6
+ * The security posture of a table is determined per `(role, privilege)` — not
7
+ * per individual policy. A grant is "covered" iff at least one applicable
8
+ * *permissive* policy supplies the clause that privilege requires:
9
+ *
10
+ * - INSERT → a `WITH CHECK` clause (USING is not valid on FOR INSERT).
11
+ * - SELECT → a `USING` clause.
12
+ * - UPDATE → a `USING` clause (the FOR UPDATE visibility gate). If absent,
13
+ * writes against the row are impossible and the grant is dead.
14
+ * - DELETE → a `USING` clause.
15
+ *
16
+ * Postgres fails writes at runtime when the required clause is missing across
17
+ * *every* applicable permissive policy, so that's the only bar this check
18
+ * holds us to. Whether an UPDATE policy also pairs a WITH CHECK (defense in
19
+ * depth against row smuggling) is a separate, weaker concern that is
20
+ * covered by `checkUpdateWithCheckCoverage` below as an INFO-level finding.
21
+ */
22
+ /** What clause kind satisfies coverage for a given privilege. */
23
+ export type ClauseKind = 'USING' | 'WITH CHECK';
24
+ export declare const CLAUSE_REQUIRED: Partial<Record<PgPrivilege, ClauseKind>>;
25
+ /** `polcmd` → the single verb the policy can satisfy (`ALL` satisfies every verb). */
26
+ export declare const POLICY_CMDS: Record<PolicyCmd, PgPrivilege[]>;
27
+ /**
28
+ * A4 / A5: Grant of INSERT/UPDATE/DELETE (A4, high) or SELECT (A5, medium) to
29
+ * a role but no applicable permissive policy supplies the clause that verb
30
+ * needs. A4 means "writes silently fail at runtime". A5 means "queries
31
+ * silently return 0 rows".
32
+ */
33
+ export declare function checkCoverageGaps(table: TableSnapshot): Finding[];
34
+ /**
35
+ * A6 (informational): UPDATE coverage exists for a role but no applicable
36
+ * permissive UPDATE policy pairs a `WITH CHECK`. This is defense-in-depth —
37
+ * without WITH CHECK, updates can move rows out of the visible scope (a
38
+ * "row smuggling" pattern). Low severity because it does not cause runtime
39
+ * failures and in many designs is by construction safe (e.g. the columns
40
+ * that define scope are not writable, or a trigger re-asserts scope).
41
+ */
42
+ export declare function checkUpdateWithCheckCoverage(table: TableSnapshot): Finding[];
43
+ export declare function policyProvidesClause(p: PolicyInfo, role: string, privilege: PgPrivilege, clause: ClauseKind): boolean;
@@ -0,0 +1,104 @@
1
+ export const CLAUSE_REQUIRED = {
2
+ SELECT: 'USING',
3
+ INSERT: 'WITH CHECK',
4
+ UPDATE: 'USING',
5
+ DELETE: 'USING'
6
+ };
7
+ /** `polcmd` → the single verb the policy can satisfy (`ALL` satisfies every verb). */
8
+ export const POLICY_CMDS = {
9
+ SELECT: ['SELECT'],
10
+ INSERT: ['INSERT'],
11
+ UPDATE: ['UPDATE'],
12
+ DELETE: ['DELETE'],
13
+ ALL: ['SELECT', 'INSERT', 'UPDATE', 'DELETE']
14
+ };
15
+ /**
16
+ * A4 / A5: Grant of INSERT/UPDATE/DELETE (A4, high) or SELECT (A5, medium) to
17
+ * a role but no applicable permissive policy supplies the clause that verb
18
+ * needs. A4 means "writes silently fail at runtime". A5 means "queries
19
+ * silently return 0 rows".
20
+ */
21
+ export function checkCoverageGaps(table) {
22
+ if (!table.rlsEnabled)
23
+ return []; // A2 covers the "RLS off entirely" case.
24
+ const out = [];
25
+ for (const grant of table.grants) {
26
+ if (grant.role === table.owner)
27
+ continue;
28
+ if (grant.role === 'PUBLIC')
29
+ continue;
30
+ if (grant.bypassRls)
31
+ continue; // Superusers and BYPASSRLS roles aren't subject to policies.
32
+ const requiredClause = CLAUSE_REQUIRED[grant.privilege];
33
+ if (!requiredClause)
34
+ continue; // TRUNCATE / REFERENCES / TRIGGER don't participate in RLS.
35
+ const covered = table.policies.some((p) => policyProvidesClause(p, grant.role, grant.privilege, requiredClause));
36
+ if (covered)
37
+ continue;
38
+ const [code, severity] = grant.privilege === 'SELECT'
39
+ ? ['A5', 'medium']
40
+ : ['A4', 'high'];
41
+ out.push({
42
+ code,
43
+ severity,
44
+ category: 'coverage',
45
+ schema: table.schema,
46
+ table: table.name,
47
+ role: grant.role,
48
+ privilege: grant.privilege,
49
+ message: `Role ${grant.role} has ${grant.privilege} grant on ${table.schema}.${table.name} but no permissive policy supplies ${requiredClause} for ${grant.privilege}`,
50
+ hint: grant.privilege === 'SELECT'
51
+ ? 'Queries by this role will silently return 0 rows. Add a permissive policy FOR SELECT TO that role, or revoke the grant.'
52
+ : `${grant.privilege} by this role will fail at runtime. Add a permissive policy FOR ${grant.privilege} TO that role supplying ${requiredClause}, or revoke the grant.`
53
+ });
54
+ }
55
+ return out;
56
+ }
57
+ /**
58
+ * A6 (informational): UPDATE coverage exists for a role but no applicable
59
+ * permissive UPDATE policy pairs a `WITH CHECK`. This is defense-in-depth —
60
+ * without WITH CHECK, updates can move rows out of the visible scope (a
61
+ * "row smuggling" pattern). Low severity because it does not cause runtime
62
+ * failures and in many designs is by construction safe (e.g. the columns
63
+ * that define scope are not writable, or a trigger re-asserts scope).
64
+ */
65
+ export function checkUpdateWithCheckCoverage(table) {
66
+ if (!table.rlsEnabled)
67
+ return [];
68
+ const out = [];
69
+ const updateGrants = table.grants.filter((g) => g.privilege === 'UPDATE'
70
+ && g.role !== table.owner
71
+ && g.role !== 'PUBLIC'
72
+ && !g.bypassRls);
73
+ if (updateGrants.length === 0)
74
+ return [];
75
+ for (const grant of updateGrants) {
76
+ const usingCovered = table.policies.some((p) => policyProvidesClause(p, grant.role, 'UPDATE', 'USING'));
77
+ if (!usingCovered)
78
+ continue; // already reported as A4.
79
+ const withCheckCovered = table.policies.some((p) => policyProvidesClause(p, grant.role, 'UPDATE', 'WITH CHECK'));
80
+ if (withCheckCovered)
81
+ continue;
82
+ out.push({
83
+ code: 'A6',
84
+ severity: 'info',
85
+ category: 'coverage',
86
+ schema: table.schema,
87
+ table: table.name,
88
+ role: grant.role,
89
+ privilege: 'UPDATE',
90
+ message: `Role ${grant.role} has UPDATE on ${table.schema}.${table.name}; USING is supplied but no applicable permissive policy provides WITH CHECK`,
91
+ hint: 'Without a WITH CHECK on UPDATE, rows can be updated to values that move them outside the role\'s visible set. Add WITH CHECK if the scoped columns are writable by this role.'
92
+ });
93
+ }
94
+ return out;
95
+ }
96
+ export function policyProvidesClause(p, role, privilege, clause) {
97
+ if (!p.permissive)
98
+ return false;
99
+ if (!POLICY_CMDS[p.cmd].includes(privilege))
100
+ return false;
101
+ if (!p.roles.includes('PUBLIC') && !p.roles.includes(role))
102
+ return false;
103
+ return clause === 'USING' ? p.using != null : p.withCheck != null;
104
+ }
@@ -0,0 +1,91 @@
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 { type SchemaAclInfo } from '../pg/acl';
34
+ import type { FunctionSnapshot } from '../pg/functions';
35
+ import type { ViewSnapshot } from '../pg/indexes';
36
+ import type { TableSnapshot } from '../pg/introspect';
37
+ import type { Finding } from '../types';
38
+ import { type SuppressedView } from './definer-view';
39
+ import { type LatticeRoleOptions, type RoleGraph } from './lattice';
40
+ import { type FunctionReachInput, type TriggerWriteInput } from './role-reach';
41
+ export interface FunctionBodyAnalysis {
42
+ /** Functions that execute as someone other than the caller, as reach inputs. */
43
+ functions: FunctionReachInput[];
44
+ /** `INSTEAD OF` triggers whose function re-owns the write. */
45
+ triggers: TriggerWriteInput[];
46
+ /** Bodies deliberately left out, with why — an unread body is not a clean bill. */
47
+ suppressed: SuppressedView[];
48
+ }
49
+ /**
50
+ * Read every SECURITY DEFINER function's body and resolve what it reaches.
51
+ *
52
+ * Calls are followed: an invoker function called from a definer still runs
53
+ * with the definer's owner in force, while an inner definer switches the
54
+ * executing role again — the same rule as nested views, because it is the
55
+ * same rule. Views read from a body are followed too, through the existing
56
+ * view walk, so a definer function selecting from a definer view reaches that
57
+ * view's bases as well.
58
+ */
59
+ export declare function analyzeFunctionBodies(functions: FunctionSnapshot[], views: ViewSnapshot[], tables: TableSnapshot[], auditedSchemas?: Iterable<string>): Promise<FunctionBodyAnalysis>;
60
+ /**
61
+ * L19: an untrusted role reaches a relation by executing a SECURITY DEFINER
62
+ * function.
63
+ *
64
+ * Fires once per (role, function, relation, privilege) where the role can
65
+ * EXECUTE the function, the body touches the relation as someone else, and
66
+ * the role holds no such privilege on the relation itself. The invoker twin
67
+ * of the same function produces nothing: its body runs as the caller, so the
68
+ * relation's own ACL and policies apply and there is no edge to report.
69
+ */
70
+ export declare function checkDefinerFunctionReach(functions: FunctionReachInput[], tables: TableSnapshot[], graph: RoleGraph, schemaAcls: Map<string, SchemaAclInfo>, options?: LatticeRoleOptions): Finding[];
71
+ /**
72
+ * L20: an untrusted role writes a relation through an `INSTEAD OF` trigger
73
+ * whose function is SECURITY DEFINER.
74
+ *
75
+ * This is the suppression L9 has carried since it shipped. A write against a
76
+ * view with `INSTEAD OF` triggers never reaches a base relation — Postgres
77
+ * runs the trigger function instead — and where that write lands is in the
78
+ * body. Following it makes the edge provable; where the body is unreadable,
79
+ * or the trigger function runs as the caller, the suppression stands.
80
+ */
81
+ export declare function checkInsteadOfTriggerWrite(triggers: TriggerWriteInput[], tables: TableSnapshot[], graph: RoleGraph, options?: LatticeRoleOptions): Finding[];
82
+ /**
83
+ * L15, asked of a function instead of a view: an untrusted role can execute a
84
+ * definer function whose body this analysis could not fully read.
85
+ *
86
+ * The same coverage statement the view producer makes, for the same reason —
87
+ * the execution is proven and the far end is unknown, so no grading rule saw
88
+ * it. Reporting the gap is the alternative to a silent clean bill; it is
89
+ * `info`, score-neutral, and recommends no revoke.
90
+ */
91
+ export declare function checkUnreadableFunctionReach(functions: FunctionReachInput[], graph: RoleGraph, schemaAcls: Map<string, SchemaAclInfo>, options?: LatticeRoleOptions): Finding[];