@safegres/test 0.0.2 → 1.30.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (483) hide show
  1. package/README.md +825 -412
  2. package/adapters.d.ts +8 -0
  3. package/adapters.js +18 -0
  4. package/ast/helpers.d.ts +16 -0
  5. package/ast/helpers.js +40 -0
  6. package/ast/parse.d.ts +31 -0
  7. package/ast/parse.js +87 -0
  8. package/ast/walk.d.ts +27 -0
  9. package/ast/walk.js +79 -0
  10. package/callgraph/baseline.d.ts +28 -0
  11. package/callgraph/baseline.js +55 -0
  12. package/callgraph/extract.d.ts +93 -0
  13. package/callgraph/extract.js +340 -0
  14. package/callgraph/graph.d.ts +82 -0
  15. package/callgraph/graph.js +276 -0
  16. package/checks/anti-patterns.d.ts +53 -0
  17. package/checks/anti-patterns.js +247 -0
  18. package/checks/column-grants.d.ts +20 -0
  19. package/checks/column-grants.js +74 -0
  20. package/checks/coverage.d.ts +43 -0
  21. package/checks/coverage.js +110 -0
  22. package/checks/definer-function.d.ts +91 -0
  23. package/checks/definer-function.js +458 -0
  24. package/checks/definer-view.d.ts +167 -0
  25. package/checks/definer-view.js +392 -0
  26. package/checks/indexes.d.ts +82 -0
  27. package/checks/indexes.js +263 -0
  28. package/checks/lattice.d.ts +179 -0
  29. package/checks/lattice.js +487 -0
  30. package/checks/object-acls.d.ts +32 -0
  31. package/checks/object-acls.js +129 -0
  32. package/checks/policy-index.d.ts +78 -0
  33. package/checks/policy-index.js +349 -0
  34. package/checks/revocable-grants.d.ts +140 -0
  35. package/checks/revocable-grants.js +377 -0
  36. package/checks/rls-flags.d.ts +25 -0
  37. package/checks/rls-flags.js +82 -0
  38. package/checks/role-reach.d.ts +329 -0
  39. package/checks/role-reach.js +277 -0
  40. package/checks/role-trust.d.ts +45 -0
  41. package/checks/role-trust.js +91 -0
  42. package/checks/set-role.d.ts +21 -0
  43. package/checks/set-role.js +66 -0
  44. package/checks/stats.d.ts +52 -0
  45. package/checks/stats.js +192 -0
  46. package/checks/view-exposure.d.ts +68 -0
  47. package/checks/view-exposure.js +236 -0
  48. package/checks/view-writes.d.ts +88 -0
  49. package/checks/view-writes.js +350 -0
  50. package/ci/baseline.d.ts +104 -0
  51. package/ci/baseline.js +111 -0
  52. package/ci/github.d.ts +47 -0
  53. package/ci/github.js +197 -0
  54. package/cli/audit.d.ts +3 -0
  55. package/cli/audit.js +560 -0
  56. package/cli/baseline.d.ts +3 -0
  57. package/cli/baseline.js +104 -0
  58. package/cli/commands.d.ts +2 -0
  59. package/cli/commands.js +60 -0
  60. package/cli/doctor.d.ts +3 -0
  61. package/cli/doctor.js +72 -0
  62. package/cli/eval.d.ts +3 -0
  63. package/cli/eval.js +153 -0
  64. package/cli/print-config.d.ts +3 -0
  65. package/cli/print-config.js +54 -0
  66. package/cli/shared.d.ts +43 -0
  67. package/cli/shared.js +153 -0
  68. package/cli.d.ts +2 -0
  69. package/cli.js +69 -0
  70. package/commands/audit.d.ts +65 -0
  71. package/commands/audit.js +779 -0
  72. package/commands/doctor.d.ts +19 -0
  73. package/commands/doctor.js +181 -0
  74. package/commands/eval.d.ts +67 -0
  75. package/commands/eval.js +93 -0
  76. package/config/fingerprint.d.ts +40 -0
  77. package/config/fingerprint.js +71 -0
  78. package/config/loader.d.ts +44 -0
  79. package/config/loader.js +210 -0
  80. package/config/presets.d.ts +95 -0
  81. package/config/presets.js +327 -0
  82. package/config/resolve.d.ts +47 -0
  83. package/config/resolve.js +192 -0
  84. package/config/schema.d.ts +54 -0
  85. package/config/schema.js +269 -0
  86. package/config/types.d.ts +562 -0
  87. package/config/validate.d.ts +14 -0
  88. package/config/validate.js +125 -0
  89. package/corpus/README.md +83 -0
  90. package/corpus/bootstrap.sql +14 -0
  91. package/corpus/cases/01-anon-write-grant/case.json +26 -0
  92. package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
  93. package/corpus/cases/02-grant-to-public/case.json +25 -0
  94. package/corpus/cases/02-grant-to-public/schema.sql +18 -0
  95. package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
  96. package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
  97. package/corpus/cases/04-permissive-write-policy/case.json +30 -0
  98. package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
  99. package/corpus/cases/05-public-read-policy/case.json +25 -0
  100. package/corpus/cases/05-public-read-policy/schema.sql +16 -0
  101. package/corpus/cases/06-update-without-with-check/case.json +25 -0
  102. package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
  103. package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
  104. package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
  105. package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
  106. package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
  107. package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
  108. package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
  109. package/corpus/cases/10-rls-not-forced/case.json +24 -0
  110. package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
  111. package/corpus/cases/11-session-user-in-policy/case.json +24 -0
  112. package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
  113. package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
  114. package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
  115. package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
  116. package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
  117. package/corpus/cases/14-dead-policy/case.json +24 -0
  118. package/corpus/cases/14-dead-policy/schema.sql +18 -0
  119. package/corpus/cases/15-unreachable-grant/case.json +24 -0
  120. package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
  121. package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
  122. package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
  123. package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
  124. package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
  125. package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
  126. package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
  127. package/corpus/cases/19-policy-column-cast/case.json +24 -0
  128. package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
  129. package/corpus/cases/20-stable-function-per-row/case.json +24 -0
  130. package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
  131. package/corpus/cases/21-table-without-primary-key/case.json +24 -0
  132. package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
  133. package/corpus/cases/22-redundant-index/case.json +24 -0
  134. package/corpus/cases/22-redundant-index/schema.sql +16 -0
  135. package/corpus/cases/23-tsvector-without-index/case.json +24 -0
  136. package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
  137. package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
  138. package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
  139. package/corpus/cases/25-definer-view-bypass/case.json +31 -0
  140. package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
  141. package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
  142. package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
  143. package/corpus/cases/27-definer-view-write/case.json +36 -0
  144. package/corpus/cases/27-definer-view-write/schema.sql +40 -0
  145. package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
  146. package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
  147. package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
  148. package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
  149. package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
  150. package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
  151. package/corpus/cases/31-matview-snapshot/case.json +32 -0
  152. package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
  153. package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
  154. package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
  155. package/corpus/cases/33-leaky-filter-view/case.json +36 -0
  156. package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
  157. package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
  158. package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
  159. package/corpus/cases/35-column-grant-projection/case.json +27 -0
  160. package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
  161. package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
  162. package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
  163. package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
  164. package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
  165. package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
  166. package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
  167. package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
  168. package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
  169. package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
  170. package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
  171. package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
  172. package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
  173. package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
  174. package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
  175. package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
  176. package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
  177. package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
  178. package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
  179. package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
  180. package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
  181. package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
  182. package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
  183. package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
  184. package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
  185. package/corpus/cases/41-unreadable-view-body/case.json +33 -0
  186. package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
  187. package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
  188. package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
  189. package/corpus/cases/42-search-column-indexed/case.json +21 -0
  190. package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
  191. package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
  192. package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
  193. package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
  194. package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
  195. package/corpus/cases/45-anon-foreign-table/case.json +27 -0
  196. package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
  197. package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
  198. package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
  199. package/corpus/cases/47-checked-writable-view/case.json +37 -0
  200. package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
  201. package/corpus/cases/48-definer-function-reach/case.json +38 -0
  202. package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
  203. package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
  204. package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
  205. package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
  206. package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
  207. package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
  208. package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
  209. package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
  210. package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
  211. package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
  212. package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
  213. package/corpus/index.d.ts +79 -0
  214. package/corpus/index.js +125 -0
  215. package/esm/adapters.d.ts +8 -0
  216. package/esm/adapters.js +6 -0
  217. package/esm/ast/helpers.d.ts +16 -0
  218. package/esm/ast/helpers.js +36 -0
  219. package/esm/ast/parse.d.ts +31 -0
  220. package/esm/ast/parse.js +82 -0
  221. package/esm/ast/walk.d.ts +27 -0
  222. package/esm/ast/walk.js +72 -0
  223. package/esm/callgraph/baseline.d.ts +28 -0
  224. package/esm/callgraph/baseline.js +48 -0
  225. package/esm/callgraph/extract.d.ts +93 -0
  226. package/esm/callgraph/extract.js +333 -0
  227. package/esm/callgraph/graph.d.ts +82 -0
  228. package/esm/callgraph/graph.js +273 -0
  229. package/esm/checks/anti-patterns.d.ts +53 -0
  230. package/esm/checks/anti-patterns.js +240 -0
  231. package/esm/checks/column-grants.d.ts +20 -0
  232. package/esm/checks/column-grants.js +71 -0
  233. package/esm/checks/coverage.d.ts +43 -0
  234. package/esm/checks/coverage.js +104 -0
  235. package/esm/checks/definer-function.d.ts +91 -0
  236. package/esm/checks/definer-function.js +452 -0
  237. package/esm/checks/definer-view.d.ts +167 -0
  238. package/esm/checks/definer-view.js +382 -0
  239. package/esm/checks/indexes.d.ts +82 -0
  240. package/esm/checks/indexes.js +256 -0
  241. package/esm/checks/lattice.d.ts +179 -0
  242. package/esm/checks/lattice.js +475 -0
  243. package/esm/checks/object-acls.d.ts +32 -0
  244. package/esm/checks/object-acls.js +125 -0
  245. package/esm/checks/policy-index.d.ts +78 -0
  246. package/esm/checks/policy-index.js +342 -0
  247. package/esm/checks/revocable-grants.d.ts +140 -0
  248. package/esm/checks/revocable-grants.js +373 -0
  249. package/esm/checks/rls-flags.d.ts +25 -0
  250. package/esm/checks/rls-flags.js +77 -0
  251. package/esm/checks/role-reach.d.ts +329 -0
  252. package/esm/checks/role-reach.js +270 -0
  253. package/esm/checks/role-trust.d.ts +45 -0
  254. package/esm/checks/role-trust.js +86 -0
  255. package/esm/checks/set-role.d.ts +21 -0
  256. package/esm/checks/set-role.js +63 -0
  257. package/esm/checks/stats.d.ts +52 -0
  258. package/esm/checks/stats.js +184 -0
  259. package/esm/checks/view-exposure.d.ts +68 -0
  260. package/esm/checks/view-exposure.js +231 -0
  261. package/esm/checks/view-writes.d.ts +88 -0
  262. package/esm/checks/view-writes.js +344 -0
  263. package/esm/ci/baseline.d.ts +104 -0
  264. package/esm/ci/baseline.js +107 -0
  265. package/esm/ci/github.d.ts +47 -0
  266. package/esm/ci/github.js +161 -0
  267. package/esm/cli/audit.d.ts +3 -0
  268. package/esm/cli/audit.js +525 -0
  269. package/esm/cli/baseline.d.ts +3 -0
  270. package/esm/cli/baseline.js +102 -0
  271. package/esm/cli/commands.d.ts +2 -0
  272. package/esm/cli/commands.js +53 -0
  273. package/esm/cli/doctor.d.ts +3 -0
  274. package/esm/cli/doctor.js +67 -0
  275. package/esm/cli/eval.d.ts +3 -0
  276. package/esm/cli/eval.js +115 -0
  277. package/esm/cli/print-config.d.ts +3 -0
  278. package/esm/cli/print-config.js +52 -0
  279. package/esm/cli/shared.d.ts +43 -0
  280. package/esm/cli/shared.js +112 -0
  281. package/esm/cli.d.ts +2 -0
  282. package/esm/cli.js +67 -0
  283. package/esm/commands/audit.d.ts +65 -0
  284. package/esm/commands/audit.js +776 -0
  285. package/esm/commands/doctor.d.ts +19 -0
  286. package/esm/commands/doctor.js +178 -0
  287. package/esm/commands/eval.d.ts +67 -0
  288. package/esm/commands/eval.js +90 -0
  289. package/esm/config/fingerprint.d.ts +40 -0
  290. package/esm/config/fingerprint.js +68 -0
  291. package/esm/config/loader.d.ts +44 -0
  292. package/esm/config/loader.js +172 -0
  293. package/esm/config/presets.d.ts +95 -0
  294. package/esm/config/presets.js +324 -0
  295. package/esm/config/resolve.d.ts +47 -0
  296. package/esm/config/resolve.js +180 -0
  297. package/esm/config/schema.d.ts +54 -0
  298. package/esm/config/schema.js +265 -0
  299. package/esm/config/types.d.ts +562 -0
  300. package/esm/config/validate.d.ts +14 -0
  301. package/esm/config/validate.js +122 -0
  302. package/esm/corpus/index.d.ts +79 -0
  303. package/esm/corpus/index.js +85 -0
  304. package/esm/exposure/adapters.d.ts +152 -0
  305. package/esm/exposure/adapters.js +463 -0
  306. package/esm/exposure/planes.d.ts +59 -0
  307. package/esm/exposure/planes.js +144 -0
  308. package/esm/exposure/reach.d.ts +78 -0
  309. package/esm/exposure/reach.js +105 -0
  310. package/esm/exposure/routines.d.ts +36 -0
  311. package/esm/exposure/routines.js +54 -0
  312. package/esm/index.d.ts +96 -0
  313. package/esm/index.js +57 -6
  314. package/esm/lint/index.d.ts +11 -0
  315. package/esm/lint/index.js +10 -0
  316. package/esm/perf/baseline.d.ts +40 -0
  317. package/esm/perf/baseline.js +83 -0
  318. package/esm/perf/explain.d.ts +46 -0
  319. package/esm/perf/explain.js +201 -0
  320. package/esm/pg/acl.d.ts +72 -0
  321. package/esm/pg/acl.js +186 -0
  322. package/esm/pg/behaviors.d.ts +114 -0
  323. package/esm/pg/behaviors.js +206 -0
  324. package/esm/pg/exposure.d.ts +74 -0
  325. package/esm/pg/exposure.js +198 -0
  326. package/esm/pg/functions.d.ts +52 -0
  327. package/esm/pg/functions.js +128 -0
  328. package/esm/pg/indexes.d.ts +192 -0
  329. package/esm/pg/indexes.js +395 -0
  330. package/esm/pg/introspect.d.ts +134 -0
  331. package/esm/pg/introspect.js +274 -0
  332. package/esm/pg/objects.d.ts +37 -0
  333. package/esm/pg/objects.js +108 -0
  334. package/esm/pg/paths.d.ts +114 -0
  335. package/esm/pg/paths.js +159 -0
  336. package/esm/pg/proc.d.ts +23 -0
  337. package/esm/pg/proc.js +55 -0
  338. package/esm/pg/reach-inputs.d.ts +47 -0
  339. package/esm/pg/reach-inputs.js +133 -0
  340. package/esm/pg/roles.d.ts +28 -0
  341. package/esm/pg/roles.js +50 -0
  342. package/esm/pg/stats.d.ts +65 -0
  343. package/esm/pg/stats.js +129 -0
  344. package/esm/pgpm-test.d.ts +22 -0
  345. package/esm/pgpm-test.js +39 -0
  346. package/esm/report/callgraph.d.ts +9 -0
  347. package/esm/report/callgraph.js +74 -0
  348. package/esm/report/compare.d.ts +143 -0
  349. package/esm/report/compare.js +195 -0
  350. package/esm/report/github.d.ts +74 -0
  351. package/esm/report/github.js +290 -0
  352. package/esm/report/json.d.ts +4 -0
  353. package/esm/report/json.js +3 -0
  354. package/esm/report/markdown.d.ts +25 -0
  355. package/esm/report/markdown.js +352 -0
  356. package/esm/report/pretty.d.ts +14 -0
  357. package/esm/report/pretty.js +318 -0
  358. package/esm/report/sarif.d.ts +40 -0
  359. package/esm/report/sarif.js +189 -0
  360. package/esm/report/view.d.ts +82 -0
  361. package/esm/report/view.js +126 -0
  362. package/esm/rules/registry.d.ts +66 -0
  363. package/esm/rules/registry.js +562 -0
  364. package/esm/score/score.d.ts +84 -0
  365. package/esm/score/score.js +233 -0
  366. package/esm/score/scorecards.d.ts +68 -0
  367. package/esm/score/scorecards.js +157 -0
  368. package/esm/types.d.ts +292 -0
  369. package/esm/types.js +26 -0
  370. package/esm/version.d.ts +8 -0
  371. package/esm/version.js +9 -0
  372. package/exposure/adapters.d.ts +152 -0
  373. package/exposure/adapters.js +468 -0
  374. package/exposure/planes.d.ts +59 -0
  375. package/exposure/planes.js +151 -0
  376. package/exposure/reach.d.ts +78 -0
  377. package/exposure/reach.js +109 -0
  378. package/exposure/routines.d.ts +36 -0
  379. package/exposure/routines.js +57 -0
  380. package/index.d.ts +96 -6
  381. package/index.js +229 -6
  382. package/lint/index.d.ts +11 -0
  383. package/lint/index.js +19 -0
  384. package/package.json +54 -48
  385. package/perf/baseline.d.ts +40 -0
  386. package/perf/baseline.js +92 -0
  387. package/perf/explain.d.ts +46 -0
  388. package/perf/explain.js +204 -0
  389. package/pg/acl.d.ts +72 -0
  390. package/pg/acl.js +191 -0
  391. package/pg/behaviors.d.ts +114 -0
  392. package/pg/behaviors.js +217 -0
  393. package/pg/exposure.d.ts +74 -0
  394. package/pg/exposure.js +205 -0
  395. package/pg/functions.d.ts +52 -0
  396. package/pg/functions.js +131 -0
  397. package/pg/indexes.d.ts +192 -0
  398. package/pg/indexes.js +400 -0
  399. package/pg/introspect.d.ts +134 -0
  400. package/pg/introspect.js +280 -0
  401. package/pg/objects.d.ts +37 -0
  402. package/pg/objects.js +111 -0
  403. package/pg/paths.d.ts +114 -0
  404. package/pg/paths.js +166 -0
  405. package/pg/proc.d.ts +23 -0
  406. package/pg/proc.js +58 -0
  407. package/pg/reach-inputs.d.ts +47 -0
  408. package/pg/reach-inputs.js +136 -0
  409. package/pg/roles.d.ts +28 -0
  410. package/pg/roles.js +54 -0
  411. package/pg/stats.d.ts +65 -0
  412. package/pg/stats.js +133 -0
  413. package/pgpm-test.d.ts +22 -0
  414. package/pgpm-test.js +42 -0
  415. package/report/callgraph.d.ts +9 -0
  416. package/report/callgraph.js +81 -0
  417. package/report/compare.d.ts +143 -0
  418. package/report/compare.js +205 -0
  419. package/report/github.d.ts +74 -0
  420. package/report/github.js +333 -0
  421. package/report/json.d.ts +4 -0
  422. package/report/json.js +6 -0
  423. package/report/markdown.d.ts +25 -0
  424. package/report/markdown.js +355 -0
  425. package/report/pretty.d.ts +14 -0
  426. package/report/pretty.js +324 -0
  427. package/report/sarif.d.ts +40 -0
  428. package/report/sarif.js +193 -0
  429. package/report/view.d.ts +82 -0
  430. package/report/view.js +132 -0
  431. package/rules/registry.d.ts +66 -0
  432. package/rules/registry.js +569 -0
  433. package/schema/safegres.schema.json +1119 -0
  434. package/score/score.d.ts +84 -0
  435. package/score/score.js +238 -0
  436. package/score/scorecards.d.ts +68 -0
  437. package/score/scorecards.js +162 -0
  438. package/types.d.ts +292 -0
  439. package/types.js +32 -0
  440. package/version.d.ts +8 -0
  441. package/version.js +12 -0
  442. package/LICENSE +0 -23
  443. package/admin.d.ts +0 -26
  444. package/admin.js +0 -182
  445. package/connect.d.ts +0 -19
  446. package/connect.js +0 -83
  447. package/dist/README.md +0 -557
  448. package/dist/package.json +0 -72
  449. package/esm/admin.js +0 -178
  450. package/esm/connect.js +0 -78
  451. package/esm/manager.js +0 -136
  452. package/esm/roles.js +0 -32
  453. package/esm/seed/adapters.js +0 -23
  454. package/esm/seed/csv.js +0 -44
  455. package/esm/seed/index.js +0 -16
  456. package/esm/seed/json.js +0 -18
  457. package/esm/seed/launchql.js +0 -19
  458. package/esm/seed/sqitch.js +0 -17
  459. package/esm/stream.js +0 -43
  460. package/esm/test-client.js +0 -150
  461. package/manager.d.ts +0 -25
  462. package/manager.js +0 -140
  463. package/roles.d.ts +0 -17
  464. package/roles.js +0 -38
  465. package/seed/adapters.d.ts +0 -4
  466. package/seed/adapters.js +0 -28
  467. package/seed/csv.d.ts +0 -9
  468. package/seed/csv.js +0 -49
  469. package/seed/index.d.ts +0 -16
  470. package/seed/index.js +0 -33
  471. package/seed/json.d.ts +0 -6
  472. package/seed/json.js +0 -21
  473. package/seed/launchql.d.ts +0 -2
  474. package/seed/launchql.js +0 -22
  475. package/seed/sqitch.d.ts +0 -2
  476. package/seed/sqitch.js +0 -20
  477. package/seed/types.d.ts +0 -13
  478. package/stream.d.ts +0 -2
  479. package/stream.js +0 -46
  480. package/test-client.d.ts +0 -49
  481. package/test-client.js +0 -154
  482. /package/{seed → config}/types.js +0 -0
  483. /package/esm/{seed → config}/types.js +0 -0
@@ -0,0 +1,30 @@
1
+ DROP SCHEMA IF EXISTS c_anon_set_role_escalation CASCADE;
2
+ CREATE SCHEMA c_anon_set_role_escalation;
3
+
4
+ -- A privileged role the anonymous role is not meant to *be*, but can *become*.
5
+ DO $$ BEGIN
6
+ CREATE ROLE c_priv_role NOLOGIN;
7
+ EXCEPTION WHEN duplicate_object THEN NULL;
8
+ END $$;
9
+
10
+ GRANT USAGE ON SCHEMA c_anon_set_role_escalation TO corpus_anon, corpus_user, c_priv_role;
11
+
12
+ -- The flaw: corpus_anon inherits *nothing* from c_priv_role (INHERIT FALSE), so
13
+ -- every passive-grant check sees an anonymous role that holds no privileges on
14
+ -- the table below. But SET TRUE lets it `SET ROLE c_priv_role` on demand and
15
+ -- execute with that role's grants — an escalation invisible to A2/R1/L5.
16
+ GRANT c_priv_role TO corpus_anon WITH INHERIT FALSE, SET TRUE;
17
+
18
+ CREATE TABLE c_anon_set_role_escalation.secrets (
19
+ id bigserial PRIMARY KEY,
20
+ is_public boolean NOT NULL DEFAULT false,
21
+ detail text
22
+ );
23
+ ALTER TABLE c_anon_set_role_escalation.secrets ENABLE ROW LEVEL SECURITY;
24
+ ALTER TABLE c_anon_set_role_escalation.secrets FORCE ROW LEVEL SECURITY;
25
+
26
+ -- A properly scoped table for the role that owns the access: c_priv_role can
27
+ -- read, corpus_anon cannot — unless it assumes c_priv_role.
28
+ CREATE POLICY secrets_read ON c_anon_set_role_escalation.secrets
29
+ FOR SELECT TO c_priv_role USING (is_public);
30
+ GRANT SELECT ON c_anon_set_role_escalation.secrets TO c_priv_role;
@@ -0,0 +1,31 @@
1
+ {
2
+ "title": "Anonymous role reads a locked table through a DEFINER view",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_definer_view_bypass"
7
+ ],
8
+ "roles": [
9
+ "corpus_anon",
10
+ "corpus_user"
11
+ ],
12
+ "anonRoles": [
13
+ "corpus_anon"
14
+ ]
15
+ },
16
+ "expect": [
17
+ {
18
+ "code": "L8",
19
+ "relation": "c_definer_view_bypass.orders",
20
+ "note": "the view is not security_invoker, so corpus_anon's SELECT on it reads `orders` as c_definer_view_owner — a grant no ACL row on `orders` gives it"
21
+ }
22
+ ],
23
+ "forbid": [
24
+ "A2",
25
+ "R1",
26
+ "L4"
27
+ ],
28
+ "worstSeverity": "info",
29
+ "fix": "Recreate c_definer_view_bypass.order_totals WITH (security_invoker = true) so the caller's own grants and policies apply, or give it an owner whose reach matches what the view is meant to expose. Revoking corpus_anon's SELECT on the view is not the fix — that grant is what the API serves.",
30
+ "id": "25-definer-view-bypass"
31
+ }
@@ -0,0 +1,36 @@
1
+ DROP SCHEMA IF EXISTS c_definer_view_bypass CASCADE;
2
+ CREATE SCHEMA c_definer_view_bypass;
3
+
4
+ -- The role the view executes as. It owns the base table, so its reach is
5
+ -- everything the table's policies were written to constrain.
6
+ DO $$ BEGIN
7
+ CREATE ROLE c_definer_view_owner NOLOGIN;
8
+ EXCEPTION WHEN duplicate_object THEN NULL;
9
+ END $$;
10
+
11
+ GRANT USAGE ON SCHEMA c_definer_view_bypass TO corpus_anon, corpus_user, c_definer_view_owner;
12
+
13
+ CREATE TABLE c_definer_view_bypass.orders (
14
+ id bigserial PRIMARY KEY,
15
+ customer text NOT NULL,
16
+ total numeric NOT NULL
17
+ );
18
+ ALTER TABLE c_definer_view_bypass.orders OWNER TO c_definer_view_owner;
19
+ ALTER TABLE c_definer_view_bypass.orders ENABLE ROW LEVEL SECURITY;
20
+ ALTER TABLE c_definer_view_bypass.orders FORCE ROW LEVEL SECURITY;
21
+
22
+ -- The table is correctly locked down: only the signed-in role can read it, and
23
+ -- only its own rows. corpus_anon holds no grant on it at all.
24
+ CREATE POLICY orders_own_rows ON c_definer_view_bypass.orders
25
+ FOR SELECT TO corpus_user USING (customer = (SELECT current_setting('app.customer', true)));
26
+ CREATE INDEX orders_customer_idx ON c_definer_view_bypass.orders (customer);
27
+ GRANT SELECT ON c_definer_view_bypass.orders TO corpus_user;
28
+
29
+ -- The flaw: the view is not `security_invoker`, so it executes as its owner —
30
+ -- who owns the base table. corpus_anon holds nothing but SELECT on the view,
31
+ -- and reads every order through it. No ACL row on `orders` names corpus_anon,
32
+ -- so every catalog-only check sees a table the anonymous role cannot reach.
33
+ CREATE VIEW c_definer_view_bypass.order_totals AS
34
+ SELECT id, customer, total FROM c_definer_view_bypass.orders;
35
+ ALTER VIEW c_definer_view_bypass.order_totals OWNER TO c_definer_view_owner;
36
+ GRANT SELECT ON c_definer_view_bypass.order_totals TO corpus_anon;
@@ -0,0 +1,31 @@
1
+ {
2
+ "title": "A security_invoker view over the same shape is not a bypass",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_invoker_view_no_bypass"
7
+ ],
8
+ "roles": [
9
+ "corpus_anon",
10
+ "corpus_user"
11
+ ],
12
+ "anonRoles": [
13
+ "corpus_anon"
14
+ ]
15
+ },
16
+ "expect": [
17
+ {
18
+ "code": "A3",
19
+ "relation": "c_invoker_view_no_bypass.orders",
20
+ "note": "the only real finding left: RLS is not FORCEd, so the table owner still reads every row"
21
+ }
22
+ ],
23
+ "forbid": [
24
+ "L8",
25
+ "L5",
26
+ "A2"
27
+ ],
28
+ "worstSeverity": "low",
29
+ "fix": "ALTER TABLE c_invoker_view_no_bypass.orders FORCE ROW LEVEL SECURITY. The view needs no fix — `security_invoker` is what the definer-view bypass (case 25) is fixed *into*, and a rule that reported L8 here would be flagging a correct schema.",
30
+ "id": "26-invoker-view-no-bypass"
31
+ }
@@ -0,0 +1,44 @@
1
+ DROP SCHEMA IF EXISTS c_invoker_view_no_bypass CASCADE;
2
+ CREATE SCHEMA c_invoker_view_no_bypass;
3
+
4
+ -- The same shape as 25-definer-view-bypass, with the one difference that
5
+ -- decides it: the view is `security_invoker`, so it executes as the caller.
6
+ DO $$ BEGIN
7
+ CREATE ROLE c_invoker_view_owner NOLOGIN;
8
+ EXCEPTION WHEN duplicate_object THEN NULL;
9
+ END $$;
10
+
11
+ GRANT USAGE ON SCHEMA c_invoker_view_no_bypass TO corpus_anon, corpus_user, c_invoker_view_owner;
12
+
13
+ CREATE TABLE c_invoker_view_no_bypass.orders (
14
+ id bigserial PRIMARY KEY,
15
+ customer text NOT NULL,
16
+ total numeric NOT NULL
17
+ );
18
+ ALTER TABLE c_invoker_view_no_bypass.orders OWNER TO c_invoker_view_owner;
19
+ ALTER TABLE c_invoker_view_no_bypass.orders ENABLE ROW LEVEL SECURITY;
20
+
21
+ CREATE POLICY orders_own_rows ON c_invoker_view_no_bypass.orders
22
+ FOR SELECT TO corpus_user USING (customer = (SELECT current_setting('app.customer', true)));
23
+ CREATE INDEX orders_customer_idx ON c_invoker_view_no_bypass.orders (customer);
24
+ GRANT SELECT ON c_invoker_view_no_bypass.orders TO corpus_user;
25
+
26
+ -- corpus_anon can SELECT the view and nothing else, exactly as in case 25 —
27
+ -- but `security_invoker` means Postgres checks *corpus_anon's* privileges on
28
+ -- `orders`, which are none, so the read is denied. There is no bypass here and
29
+ -- a rule that reported one would be recommending a change to a correct schema.
30
+ CREATE VIEW c_invoker_view_no_bypass.order_totals
31
+ WITH (security_invoker = true) AS
32
+ SELECT id, customer, total FROM c_invoker_view_no_bypass.orders;
33
+ ALTER VIEW c_invoker_view_no_bypass.order_totals OWNER TO c_invoker_view_owner;
34
+ GRANT SELECT ON c_invoker_view_no_bypass.order_totals TO corpus_anon;
35
+
36
+ -- `security_invoker` is a boolean reloption, and Postgres stores whichever
37
+ -- spelling was written: `on` here is exactly as invoker as `true` above. A
38
+ -- reader that string-matches 'true' calls this view a definer view and tells
39
+ -- the author to fix a schema that is already correct.
40
+ CREATE VIEW c_invoker_view_no_bypass.order_totals_on
41
+ WITH (security_invoker = on) AS
42
+ SELECT id, customer, total FROM c_invoker_view_no_bypass.orders;
43
+ ALTER VIEW c_invoker_view_no_bypass.order_totals_on OWNER TO c_invoker_view_owner;
44
+ GRANT SELECT ON c_invoker_view_no_bypass.order_totals_on TO corpus_anon;
@@ -0,0 +1,36 @@
1
+ {
2
+ "title": "Anonymous role writes a locked table through an auto-updatable DEFINER view",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_definer_view_write"
7
+ ],
8
+ "roles": [
9
+ "corpus_anon",
10
+ "corpus_user"
11
+ ],
12
+ "anonRoles": [
13
+ "corpus_anon"
14
+ ]
15
+ },
16
+ "expect": [
17
+ {
18
+ "code": "L9",
19
+ "relation": "c_definer_view_write.submissions",
20
+ "note": "the view is auto-updatable and not security_invoker, so corpus_anon's INSERT on it inserts into `submissions` as c_view_write_owner — a write no ACL row on `submissions` gives it"
21
+ },
22
+ {
23
+ "code": "A3",
24
+ "relation": "c_definer_view_write.submissions",
25
+ "note": "RLS is not FORCEd, which is exactly why the owner's rewritten insert is not filtered by the table's own policy"
26
+ }
27
+ ],
28
+ "forbid": [
29
+ "L8",
30
+ "L10",
31
+ "L4"
32
+ ],
33
+ "worstSeverity": "low",
34
+ "fix": "Recreate c_definer_view_write.submission_inbox WITH (security_invoker = true) so the insert is checked against the caller, or give the view an owner whose reach matches what it is meant to expose. Revoking corpus_anon's INSERT on the view is not the fix — that grant is what the API serves.",
35
+ "id": "27-definer-view-write"
36
+ }
@@ -0,0 +1,40 @@
1
+ DROP SCHEMA IF EXISTS c_definer_view_write CASCADE;
2
+ CREATE SCHEMA c_definer_view_write;
3
+
4
+ -- The role the view executes as, and the owner of the table behind it.
5
+ DO $$ BEGIN
6
+ CREATE ROLE c_view_write_owner NOLOGIN;
7
+ EXCEPTION WHEN duplicate_object THEN NULL;
8
+ END $$;
9
+
10
+ GRANT USAGE ON SCHEMA c_definer_view_write TO corpus_anon, corpus_user, c_view_write_owner;
11
+
12
+ CREATE TABLE c_definer_view_write.submissions (
13
+ id bigserial PRIMARY KEY,
14
+ author text NOT NULL,
15
+ body text NOT NULL
16
+ );
17
+ ALTER TABLE c_definer_view_write.submissions OWNER TO c_view_write_owner;
18
+ ALTER TABLE c_definer_view_write.submissions ENABLE ROW LEVEL SECURITY;
19
+
20
+ -- Signed-in users write their own rows and read them back. corpus_anon holds
21
+ -- no grant on the table at all.
22
+ CREATE POLICY submissions_own_rows ON c_definer_view_write.submissions
23
+ FOR ALL TO corpus_user
24
+ USING (author = (SELECT current_setting('app.author', true)))
25
+ WITH CHECK (author = (SELECT current_setting('app.author', true)));
26
+ CREATE INDEX submissions_author_idx ON c_definer_view_write.submissions (author);
27
+ GRANT SELECT, INSERT ON c_definer_view_write.submissions TO corpus_user;
28
+ GRANT USAGE ON SEQUENCE c_definer_view_write.submissions_id_seq TO corpus_user;
29
+
30
+ -- The flaw: a simple view over one table is *auto-updatable*, so Postgres
31
+ -- rewrites an INSERT on the view into an INSERT on `submissions` — and because
32
+ -- the view is not `security_invoker`, that insert is permission-checked
33
+ -- against the view's owner, who owns the table. corpus_anon holds nothing but
34
+ -- INSERT on the view, and writes rows no policy of `submissions` would have
35
+ -- admitted from it. Nothing in the body says so: the write path is
36
+ -- `pg_relation_is_updatable`, not `pg_get_viewdef`.
37
+ CREATE VIEW c_definer_view_write.submission_inbox AS
38
+ SELECT id, author, body FROM c_definer_view_write.submissions;
39
+ ALTER VIEW c_definer_view_write.submission_inbox OWNER TO c_view_write_owner;
40
+ GRANT INSERT ON c_definer_view_write.submission_inbox TO corpus_anon;
@@ -0,0 +1,31 @@
1
+ {
2
+ "title": "A security_invoker view over the same auto-updatable shape is not a write bypass",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_invoker_view_no_write"
7
+ ],
8
+ "roles": [
9
+ "corpus_anon",
10
+ "corpus_user"
11
+ ],
12
+ "anonRoles": [
13
+ "corpus_anon"
14
+ ]
15
+ },
16
+ "expect": [
17
+ {
18
+ "code": "A3",
19
+ "relation": "c_invoker_view_no_write.submissions",
20
+ "note": "the only real finding left: RLS is not FORCEd, so the table owner still sees and writes every row"
21
+ }
22
+ ],
23
+ "forbid": [
24
+ "L8",
25
+ "L9",
26
+ "L10"
27
+ ],
28
+ "worstSeverity": "low",
29
+ "fix": "ALTER TABLE c_invoker_view_no_write.submissions FORCE ROW LEVEL SECURITY. The view needs no fix — `security_invoker` is what the definer-view write path (case 27) is fixed *into*.",
30
+ "id": "28-invoker-view-no-write"
31
+ }
@@ -0,0 +1,37 @@
1
+ DROP SCHEMA IF EXISTS c_invoker_view_no_write CASCADE;
2
+ CREATE SCHEMA c_invoker_view_no_write;
3
+
4
+ DO $$ BEGIN
5
+ CREATE ROLE c_invoker_write_owner NOLOGIN;
6
+ EXCEPTION WHEN duplicate_object THEN NULL;
7
+ END $$;
8
+
9
+ GRANT USAGE ON SCHEMA c_invoker_view_no_write
10
+ TO corpus_anon, corpus_user, c_invoker_write_owner;
11
+
12
+ CREATE TABLE c_invoker_view_no_write.submissions (
13
+ id bigserial PRIMARY KEY,
14
+ author text NOT NULL,
15
+ body text NOT NULL
16
+ );
17
+ ALTER TABLE c_invoker_view_no_write.submissions OWNER TO c_invoker_write_owner;
18
+ ALTER TABLE c_invoker_view_no_write.submissions ENABLE ROW LEVEL SECURITY;
19
+
20
+ CREATE POLICY submissions_own_rows ON c_invoker_view_no_write.submissions
21
+ FOR ALL TO corpus_user
22
+ USING (author = (SELECT current_setting('app.author', true)))
23
+ WITH CHECK (author = (SELECT current_setting('app.author', true)));
24
+ CREATE INDEX submissions_author_idx ON c_invoker_view_no_write.submissions (author);
25
+ GRANT SELECT, INSERT ON c_invoker_view_no_write.submissions TO corpus_user;
26
+ GRANT USAGE ON SEQUENCE c_invoker_view_no_write.submissions_id_seq TO corpus_user;
27
+
28
+ -- The same auto-updatable shape as case 27, with the one difference that
29
+ -- matters: `security_invoker` sends the rewritten insert through the caller's
30
+ -- own privileges, so corpus_anon's INSERT on the view is denied on
31
+ -- `submissions` exactly as a direct insert would be. There is no write edge
32
+ -- here, and a rule that reported one would be flagging a correct schema.
33
+ CREATE VIEW c_invoker_view_no_write.submission_inbox
34
+ WITH (security_invoker = true) AS
35
+ SELECT id, author, body FROM c_invoker_view_no_write.submissions;
36
+ ALTER VIEW c_invoker_view_no_write.submission_inbox OWNER TO c_invoker_write_owner;
37
+ GRANT INSERT ON c_invoker_view_no_write.submission_inbox TO corpus_anon;
@@ -0,0 +1,36 @@
1
+ {
2
+ "title": "Anonymous role writes an audit table through a rewrite rule on a view",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_rewrite_rule_bypass"
7
+ ],
8
+ "roles": [
9
+ "corpus_anon",
10
+ "corpus_user"
11
+ ],
12
+ "anonRoles": [
13
+ "corpus_anon"
14
+ ]
15
+ },
16
+ "expect": [
17
+ {
18
+ "code": "L10",
19
+ "relation": "c_rewrite_rule_bypass.audit_log",
20
+ "note": "the rule's action runs as the view owner c_rule_owner, so corpus_anon's INSERT on the view appends to `audit_log` — and `security_invoker` on the view does not govern it"
21
+ },
22
+ {
23
+ "code": "A1",
24
+ "relation": "c_rewrite_rule_bypass.audit_log",
25
+ "note": "the audit table has RLS on and no policies, which is the correct posture for a table only its owner writes — and exactly why the rule's owner-privileged write is worth naming"
26
+ }
27
+ ],
28
+ "forbid": [
29
+ "L8",
30
+ "L9",
31
+ "L4"
32
+ ],
33
+ "worstSeverity": "low",
34
+ "fix": "Move the audit write into a function corpus_anon must be granted EXECUTE on, or give c_rewrite_rule_bypass.message_inbox an owner whose reach matches what the rule is meant to do. Revoking corpus_anon's INSERT on the view is not the fix — that grant is what the API serves.",
35
+ "id": "29-rewrite-rule-bypass"
36
+ }
@@ -0,0 +1,52 @@
1
+ DROP SCHEMA IF EXISTS c_rewrite_rule_bypass CASCADE;
2
+ CREATE SCHEMA c_rewrite_rule_bypass;
3
+
4
+ DO $$ BEGIN
5
+ CREATE ROLE c_rule_owner NOLOGIN;
6
+ EXCEPTION WHEN duplicate_object THEN NULL;
7
+ END $$;
8
+
9
+ GRANT USAGE ON SCHEMA c_rewrite_rule_bypass TO corpus_anon, corpus_user, c_rule_owner;
10
+
11
+ CREATE TABLE c_rewrite_rule_bypass.messages (
12
+ id bigserial PRIMARY KEY,
13
+ author text NOT NULL,
14
+ body text NOT NULL
15
+ );
16
+ ALTER TABLE c_rewrite_rule_bypass.messages OWNER TO c_rule_owner;
17
+ ALTER TABLE c_rewrite_rule_bypass.messages ENABLE ROW LEVEL SECURITY;
18
+ ALTER TABLE c_rewrite_rule_bypass.messages FORCE ROW LEVEL SECURITY;
19
+
20
+ CREATE POLICY messages_own_rows ON c_rewrite_rule_bypass.messages
21
+ FOR ALL TO corpus_user
22
+ USING (author = (SELECT current_setting('app.author', true)))
23
+ WITH CHECK (author = (SELECT current_setting('app.author', true)));
24
+ CREATE INDEX messages_author_idx ON c_rewrite_rule_bypass.messages (author);
25
+ GRANT SELECT, INSERT ON c_rewrite_rule_bypass.messages TO corpus_user;
26
+ GRANT USAGE ON SEQUENCE c_rewrite_rule_bypass.messages_id_seq TO corpus_user;
27
+
28
+ -- An append-only audit trail nobody but the owner is meant to write.
29
+ CREATE TABLE c_rewrite_rule_bypass.audit_log (
30
+ id bigserial PRIMARY KEY,
31
+ note text NOT NULL
32
+ );
33
+ ALTER TABLE c_rewrite_rule_bypass.audit_log OWNER TO c_rule_owner;
34
+ ALTER TABLE c_rewrite_rule_bypass.audit_log ENABLE ROW LEVEL SECURITY;
35
+ ALTER TABLE c_rewrite_rule_bypass.audit_log FORCE ROW LEVEL SECURITY;
36
+
37
+ -- The flaw, and note what it survives: the view *is* `security_invoker`, so
38
+ -- its own base relation is read and written as the caller. That setting does
39
+ -- not reach the rule. A rewrite rule's actions are permission-checked against
40
+ -- the owner of the relation the rule is on, so corpus_anon's INSERT on the
41
+ -- view writes `audit_log` as c_rule_owner. `pg_get_viewdef` never names
42
+ -- `audit_log`: no reading of the view's definition can find this edge.
43
+ CREATE VIEW c_rewrite_rule_bypass.message_inbox
44
+ WITH (security_invoker = true) AS
45
+ SELECT id, author, body FROM c_rewrite_rule_bypass.messages;
46
+ ALTER VIEW c_rewrite_rule_bypass.message_inbox OWNER TO c_rule_owner;
47
+
48
+ CREATE RULE message_inbox_insert AS
49
+ ON INSERT TO c_rewrite_rule_bypass.message_inbox
50
+ DO INSTEAD INSERT INTO c_rewrite_rule_bypass.audit_log (note) VALUES (new.body);
51
+
52
+ GRANT INSERT ON c_rewrite_rule_bypass.message_inbox TO corpus_anon;
@@ -0,0 +1,31 @@
1
+ {
2
+ "title": "A DO INSTEAD NOTHING rule reaches nothing and is not a write bypass",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_instead_nothing_rule"
7
+ ],
8
+ "roles": [
9
+ "corpus_anon",
10
+ "corpus_user"
11
+ ],
12
+ "anonRoles": [
13
+ "corpus_anon"
14
+ ]
15
+ },
16
+ "expect": [
17
+ {
18
+ "code": "A3",
19
+ "relation": "c_instead_nothing_rule.messages",
20
+ "note": "the only real finding: RLS is not FORCEd, so the table owner still reads every row — the rules themselves confer nothing"
21
+ }
22
+ ],
23
+ "forbid": [
24
+ "L8",
25
+ "L9",
26
+ "L10"
27
+ ],
28
+ "worstSeverity": "low",
29
+ "fix": "ALTER TABLE c_instead_nothing_rule.messages FORCE ROW LEVEL SECURITY. The view and its rules need no fix — `DO INSTEAD NOTHING` is what a read-only view is *built* from, and this case exists to pin that the write rules stay silent on it.",
30
+ "id": "30-instead-nothing-rule"
31
+ }
@@ -0,0 +1,44 @@
1
+ DROP SCHEMA IF EXISTS c_instead_nothing_rule CASCADE;
2
+ CREATE SCHEMA c_instead_nothing_rule;
3
+
4
+ DO $$ BEGIN
5
+ CREATE ROLE c_readonly_view_owner NOLOGIN;
6
+ EXCEPTION WHEN duplicate_object THEN NULL;
7
+ END $$;
8
+
9
+ GRANT USAGE ON SCHEMA c_instead_nothing_rule
10
+ TO corpus_anon, corpus_user, c_readonly_view_owner;
11
+
12
+ CREATE TABLE c_instead_nothing_rule.messages (
13
+ id bigserial PRIMARY KEY,
14
+ author text NOT NULL,
15
+ body text NOT NULL
16
+ );
17
+ ALTER TABLE c_instead_nothing_rule.messages OWNER TO c_readonly_view_owner;
18
+ ALTER TABLE c_instead_nothing_rule.messages ENABLE ROW LEVEL SECURITY;
19
+
20
+ CREATE POLICY messages_own_rows ON c_instead_nothing_rule.messages
21
+ FOR ALL TO corpus_user
22
+ USING (author = (SELECT current_setting('app.author', true)))
23
+ WITH CHECK (author = (SELECT current_setting('app.author', true)));
24
+ CREATE INDEX messages_author_idx ON c_instead_nothing_rule.messages (author);
25
+ GRANT SELECT, INSERT ON c_instead_nothing_rule.messages TO corpus_user;
26
+ GRANT USAGE ON SEQUENCE c_instead_nothing_rule.messages_id_seq TO corpus_user;
27
+
28
+ -- The commonest rule in the wild, and a correct one: `DO INSTEAD NOTHING`
29
+ -- makes the view read-only by swallowing the write. It reaches no relation at
30
+ -- all, so it confers nothing, and a rule that reported a write edge here would
31
+ -- be flagging the very construct that closes the write path.
32
+ CREATE VIEW c_instead_nothing_rule.message_feed
33
+ WITH (security_invoker = true) AS
34
+ SELECT id, author, body FROM c_instead_nothing_rule.messages;
35
+ ALTER VIEW c_instead_nothing_rule.message_feed OWNER TO c_readonly_view_owner;
36
+
37
+ CREATE RULE message_feed_no_insert AS
38
+ ON INSERT TO c_instead_nothing_rule.message_feed DO INSTEAD NOTHING;
39
+ CREATE RULE message_feed_no_update AS
40
+ ON UPDATE TO c_instead_nothing_rule.message_feed DO INSTEAD NOTHING;
41
+ CREATE RULE message_feed_no_delete AS
42
+ ON DELETE TO c_instead_nothing_rule.message_feed DO INSTEAD NOTHING;
43
+
44
+ GRANT SELECT, INSERT ON c_instead_nothing_rule.message_feed TO corpus_anon;
@@ -0,0 +1,32 @@
1
+ {
2
+ "title": "Anonymous role reads an RLS-protected table's rows out of a materialized view",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_matview_snapshot"
7
+ ],
8
+ "roles": [
9
+ "corpus_anon",
10
+ "corpus_user"
11
+ ],
12
+ "anonRoles": [
13
+ "corpus_anon"
14
+ ]
15
+ },
16
+ "expect": [
17
+ {
18
+ "code": "L11",
19
+ "relation": "c_matview_snapshot.readings",
20
+ "note": "the matview stores rows computed as c_matview_owner, so corpus_anon reads every tenant's readings without holding a grant on the table and without its policies ever running"
21
+ }
22
+ ],
23
+ "forbid": [
24
+ "L8",
25
+ "L9",
26
+ "L10",
27
+ "L12"
28
+ ],
29
+ "worstSeverity": "info",
30
+ "fix": "Serve the rollup from a `security_invoker` view, or materialize only the columns and rows that are safe to hand out unconditionally and keep the full snapshot in a schema the API does not expose. A materialized view can carry neither RLS policies nor `security_invoker`, so there is no option on the matview itself that reinstates the filter — and revoking corpus_anon's SELECT on it is not the fix, that grant is what the API serves.",
31
+ "id": "31-matview-snapshot"
32
+ }
@@ -0,0 +1,36 @@
1
+ DROP SCHEMA IF EXISTS c_matview_snapshot CASCADE;
2
+ CREATE SCHEMA c_matview_snapshot;
3
+
4
+ -- The role that owns the table and runs REFRESH.
5
+ DO $$ BEGIN
6
+ CREATE ROLE c_matview_owner NOLOGIN;
7
+ EXCEPTION WHEN duplicate_object THEN NULL;
8
+ END $$;
9
+
10
+ GRANT USAGE ON SCHEMA c_matview_snapshot TO corpus_anon, corpus_user, c_matview_owner;
11
+
12
+ CREATE TABLE c_matview_snapshot.readings (
13
+ id bigserial PRIMARY KEY,
14
+ tenant text NOT NULL,
15
+ value numeric NOT NULL
16
+ );
17
+ ALTER TABLE c_matview_snapshot.readings OWNER TO c_matview_owner;
18
+ ALTER TABLE c_matview_snapshot.readings ENABLE ROW LEVEL SECURITY;
19
+ ALTER TABLE c_matview_snapshot.readings FORCE ROW LEVEL SECURITY;
20
+
21
+ -- Signed-in users see only their own tenant's rows.
22
+ CREATE POLICY readings_own_tenant ON c_matview_snapshot.readings
23
+ FOR SELECT TO corpus_user
24
+ USING (tenant = (SELECT current_setting('app.tenant', true)));
25
+ CREATE INDEX readings_tenant_idx ON c_matview_snapshot.readings (tenant);
26
+ GRANT SELECT ON c_matview_snapshot.readings TO corpus_user;
27
+
28
+ -- The flaw: a materialized view is a stored copy. Its rows were computed once,
29
+ -- as c_matview_owner, and reading it never consults `readings` — so neither
30
+ -- the table's ACL nor its policies apply, and the matview can carry neither
31
+ -- policies nor `security_invoker` of its own. corpus_anon holds nothing on
32
+ -- `readings` and reads every tenant's rows out of the snapshot.
33
+ CREATE MATERIALIZED VIEW c_matview_snapshot.readings_rollup AS
34
+ SELECT id, tenant, value FROM c_matview_snapshot.readings;
35
+ ALTER MATERIALIZED VIEW c_matview_snapshot.readings_rollup OWNER TO c_matview_owner;
36
+ GRANT SELECT ON c_matview_snapshot.readings_rollup TO corpus_anon, corpus_user;
@@ -0,0 +1,30 @@
1
+ {
2
+ "title": "A materialized view over a table the role can already read is a cache, not a bypass",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_matview_no_bypass"
7
+ ],
8
+ "roles": [
9
+ "corpus_anon",
10
+ "corpus_user"
11
+ ],
12
+ "anonRoles": [
13
+ "corpus_anon"
14
+ ]
15
+ },
16
+ "expect": [
17
+ {
18
+ "code": "A2",
19
+ "relation": "c_matview_no_bypass.regions",
20
+ "note": "the only real finding left: the table is granted without RLS, which is the intent here — it is public reference data"
21
+ }
22
+ ],
23
+ "forbid": [
24
+ "L11",
25
+ "L12"
26
+ ],
27
+ "worstSeverity": "high",
28
+ "fix": "Nothing on the matview. The snapshot shows corpus_anon exactly what `regions` would, and no policy was skipped because there is none — this case exists to pin that L11 stays silent when materialization changes nothing about reach.",
29
+ "id": "32-matview-no-rls-no-bypass"
30
+ }
@@ -0,0 +1,18 @@
1
+ DROP SCHEMA IF EXISTS c_matview_no_bypass CASCADE;
2
+ CREATE SCHEMA c_matview_no_bypass;
3
+
4
+ GRANT USAGE ON SCHEMA c_matview_no_bypass TO corpus_anon, corpus_user;
5
+
6
+ -- Reference data: public by intent, no RLS, readable directly.
7
+ CREATE TABLE c_matview_no_bypass.regions (
8
+ code text PRIMARY KEY,
9
+ label text NOT NULL
10
+ );
11
+ GRANT SELECT ON c_matview_no_bypass.regions TO corpus_anon, corpus_user;
12
+
13
+ -- A matview over it is a cache, not a bypass: the snapshot shows corpus_anon
14
+ -- nothing it could not select from `regions` itself, and there are no policies
15
+ -- for the stored rows to have skipped. L11 must stay silent.
16
+ CREATE MATERIALIZED VIEW c_matview_no_bypass.regions_cached AS
17
+ SELECT code, label FROM c_matview_no_bypass.regions;
18
+ GRANT SELECT ON c_matview_no_bypass.regions_cached TO corpus_anon, corpus_user;
@@ -0,0 +1,36 @@
1
+ {
2
+ "title": "A view's WHERE is the only boundary an anonymous role faces, and it is not a security barrier",
3
+ "dimension": "security",
4
+ "exposure": {
5
+ "schemas": [
6
+ "c_leaky_filter_view"
7
+ ],
8
+ "roles": [
9
+ "corpus_anon",
10
+ "corpus_user"
11
+ ],
12
+ "anonRoles": [
13
+ "corpus_anon"
14
+ ]
15
+ },
16
+ "expect": [
17
+ {
18
+ "code": "L12",
19
+ "relation": "c_leaky_filter_view.documents",
20
+ "note": "corpus_anon reaches `documents` only through the view, so the view's `visibility = 'public'` is a security boundary — and without `security_barrier` a caller-supplied leaky qual is evaluated against the private rows too"
21
+ },
22
+ {
23
+ "code": "L8",
24
+ "relation": "c_leaky_filter_view.documents",
25
+ "note": "the same view also executes as its owner, which is what puts corpus_anon in reach of the table at all"
26
+ }
27
+ ],
28
+ "forbid": [
29
+ "L9",
30
+ "L10",
31
+ "L11"
32
+ ],
33
+ "worstSeverity": "info",
34
+ "fix": "Recreate c_leaky_filter_view.public_documents WITH (security_barrier = true) so the planner cannot evaluate a caller's qual below the view's own. Where the filter is per-caller rather than a constant, an RLS policy on `documents` is stronger still: policy quals already get barrier treatment. Do not revoke corpus_anon's SELECT on the view.",
35
+ "id": "33-leaky-filter-view"
36
+ }