@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,377 @@
1
+ "use strict";
2
+ /**
3
+ * L21: `granted − reachable`, the revocable-grant rule.
4
+ *
5
+ * For a role R, {@link analyzeRevocableGrants} computes the set of EXECUTE
6
+ * grants R holds that no path can exercise, so a human never has to guess which
7
+ * of R's grants are safe to drop. It is the reachability proof the rest of the
8
+ * L-series only approximates: L1 asks whether an indirect *relation* grant is
9
+ * admitted by current policy coverage, L3 asks whether an object grant is
10
+ * blocked by missing schema USAGE, and L6 suppresses an unaddressable relation
11
+ * finding whenever a policy predicate merely *names* the relation. L21 asks the
12
+ * broader question — "can this grant be exercised at all?" — across object
13
+ * kinds and every execution path a grant can travel, and only claims a grant
14
+ * revocable when it can prove the answer is no.
15
+ *
16
+ * Reach(R) is closed over, at minimum:
17
+ *
18
+ * 1. **Direct call targets** — functions in an exposed schema R can EXECUTE
19
+ * and reach through schema USAGE.
20
+ * 2. **RLS policy predicates.** For every policy on a relation R can address,
21
+ * the `USING`/`WITH CHECK` expressions and the functions and relations
22
+ * they name, closed over transitively. These evaluate as R.
23
+ * 3. **Trigger bodies.** For every ordinary trigger on a relation R can
24
+ * write, the trigger function's body — when it is not SECURITY DEFINER,
25
+ * it runs as R. (EXECUTE on the trigger function itself is *not* reach:
26
+ * Postgres never checks it when firing a trigger, so such a grant is
27
+ * genuinely revocable, which is the whole point of the constructive-db
28
+ * audit.)
29
+ * 4. **Write-time expressions** — column defaults, generated columns and
30
+ * CHECK constraints on relations R can insert into or update. They run as
31
+ * the writing role.
32
+ * 5. **Views and their bodies**, so a grant exercised only through an
33
+ * `security_invoker` view survives.
34
+ *
35
+ * A SECURITY DEFINER boundary **stops** the closure: inside it the owner's
36
+ * privileges apply, not R's, so R needs EXECUTE on the definer function
37
+ * (retained) but not on anything the body goes on to call.
38
+ *
39
+ * **Fail-closed, bounded by USAGE.** Nothing is reported revocable that the
40
+ * analysis cannot prove unused. If any node reached as R runs SQL this analysis
41
+ * cannot read — dynamic `EXECUTE`, a body that fails to parse, a C/internal
42
+ * function — the role's closure is incomplete: that opaque node, running as R,
43
+ * could construct a call to any function R can *actually name and execute*. But
44
+ * Postgres still enforces schema USAGE at call time, so an opaque node running
45
+ * as R can only reach functions in schemas R holds USAGE on. A candidate whose
46
+ * schema R cannot even name is therefore out of an opaque node's reach: it can
47
+ * only be exercised through a reference this analysis *does* follow (a policy
48
+ * predicate, trigger body, write-time expression or view), so if none landed on
49
+ * it, it is revocable. Only unreached candidates in a USAGE-reachable schema are
50
+ * moved from `revocable` to `suppressed`, with the reason recorded. This is what
51
+ * lets the constructive-db audit prove the blanket EXECUTE on private-schema
52
+ * functions revocable — anonymous cannot name those schemas — while never
53
+ * revoking a grant an opaque node could plausibly exercise.
54
+ */
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.analyzeRevocableGrants = analyzeRevocableGrants;
57
+ exports.revocableGrantFindings = revocableGrantFindings;
58
+ const extract_1 = require("../callgraph/extract");
59
+ const lattice_1 = require("./lattice");
60
+ const WRITE_PRIVILEGES = ['INSERT', 'UPDATE', 'DELETE'];
61
+ const ACCESS_PRIVILEGES = ['SELECT', 'INSERT', 'UPDATE', 'DELETE'];
62
+ /** Languages whose bodies can be read; anything else is opaque for this rule. */
63
+ const READABLE_LANGUAGES = new Set(['sql', 'plpgsql']);
64
+ /**
65
+ * The revocable, retained and suppressed EXECUTE grants for each configured
66
+ * role. One reachability closure is walked per role; the candidate set is R's
67
+ * own EXECUTE grants (direct or inherited — a grant TO PUBLIC is not R's to
68
+ * revoke on its own, so it is never a candidate).
69
+ */
70
+ async function analyzeRevocableGrants(input) {
71
+ const functionsByName = new Map();
72
+ for (const fn of input.functions) {
73
+ const key = `${fn.schema}.${fn.name}`;
74
+ functionsByName.set(key, [...(functionsByName.get(key) ?? []), fn]);
75
+ }
76
+ const tablesByKey = new Map(input.tables.map((t) => [`${t.schema}.${t.name}`, t]));
77
+ const viewsByKey = new Map(input.views.map((v) => [`${v.schema}.${v.name}`, v]));
78
+ const roles = [];
79
+ for (const role of input.roles) {
80
+ roles.push(await analyzeRole(role, input, { functionsByName, tablesByKey, viewsByKey }));
81
+ }
82
+ return { roles };
83
+ }
84
+ async function analyzeRole(role, input, idx) {
85
+ const { graph, schemaAcls, reachInputs, functions, tables, views } = input;
86
+ const fnId = (fn) => `${fn.schema}.${fn.name}(${fn.args})`;
87
+ const reached = new Map();
88
+ const taint = [];
89
+ const framesWalked = new Set();
90
+ const viewsWalked = new Set();
91
+ const relationsClosed = new Set();
92
+ const resolve = (ref, contextSchema) => idx.functionsByName.get(`${ref.schema ?? contextSchema}.${ref.name}`) ?? [];
93
+ const hasUsage = (schema) => {
94
+ const acl = schemaAcls.get(schema);
95
+ if (!acl)
96
+ return true; // schema not introspected — assume reachable, never revoke on a guess
97
+ if (role === acl.owner)
98
+ return true;
99
+ const usage = new Set(acl.grants.filter((g) => g.privilege === 'USAGE').map((g) => g.role));
100
+ if (usage.has('PUBLIC') || usage.has(role))
101
+ return true;
102
+ const ancestors = graph.get(role)?.inheritsFrom ?? [];
103
+ return ancestors.some((a) => usage.has(a) || a === acl.owner);
104
+ };
105
+ const markReached = (fn, reason, proof) => {
106
+ const id = fnId(fn);
107
+ const entry = reached.get(id);
108
+ if (entry)
109
+ entry.reasons.add(reason);
110
+ else
111
+ reached.set(id, { reasons: new Set([reason]), proof });
112
+ };
113
+ // `readBody` re-classifies C/internal (and any other non-SQL) function as
114
+ // opaque. `extractBody` treats those as known-empty for the call graph,
115
+ // which is safe there but would let this rule prove a grant unused off a
116
+ // body it never read — the one thing it must not do.
117
+ const readBody = async (fn) => {
118
+ if (!READABLE_LANGUAGES.has(fn.language)) {
119
+ return {
120
+ calls: [],
121
+ tables: [],
122
+ settings: [],
123
+ opaque: true,
124
+ opaqueReason: `language "${fn.language}" is not statically analyzable`
125
+ };
126
+ }
127
+ return (0, extract_1.extractBody)(fn);
128
+ };
129
+ // A function body running as R: its callees run as R too, so each is a grant
130
+ // R must hold. The function itself is marked reached by its caller, not here.
131
+ const walkFrame = async (fn, reason, proof) => {
132
+ const id = fnId(fn);
133
+ if (framesWalked.has(id))
134
+ return;
135
+ framesWalked.add(id);
136
+ const body = await readBody(fn);
137
+ if (body.opaque) {
138
+ taint.push({ node: id, reason: body.opaqueReason ?? 'body could not be read' });
139
+ return;
140
+ }
141
+ if (body.tainted)
142
+ taint.push({ node: id, reason: body.tainted });
143
+ for (const call of body.calls) {
144
+ for (const callee of resolve(call, fn.schema)) {
145
+ await seedFunction(callee, reason, `${proof} → ${fnId(callee)}`);
146
+ }
147
+ }
148
+ };
149
+ // R invokes `fn` as itself: R needs EXECUTE on it (retained). Follow the body
150
+ // only when it runs as R — a SECURITY DEFINER boundary stops the closure.
151
+ const seedFunction = async (fn, reason, proof) => {
152
+ markReached(fn, reason, proof);
153
+ if (!fn.isSecurityDefiner)
154
+ await walkFrame(fn, reason, proof);
155
+ };
156
+ // A standalone SQL expression evaluated as R: a policy predicate, a column
157
+ // default, a CHECK. Wrapped in `SELECT` so the parser accepts a bare
158
+ // expression; the functions it calls and the relations it reads close over.
159
+ const seedExpr = async (exprSql, reason, contextSchema, proof) => {
160
+ const parsed = await (0, extract_1.extractQuery)(`SELECT ${exprSql}`);
161
+ if (parsed.opaque) {
162
+ taint.push({ node: proof, reason: parsed.opaqueReason ?? 'expression could not be read' });
163
+ return;
164
+ }
165
+ if (parsed.tainted)
166
+ taint.push({ node: proof, reason: parsed.tainted });
167
+ for (const call of parsed.calls) {
168
+ for (const callee of resolve(call, contextSchema)) {
169
+ await seedFunction(callee, reason, `${proof} → ${fnId(callee)}`);
170
+ }
171
+ }
172
+ for (const t of parsed.tables) {
173
+ await closeRelationPolicies(`${t.schema ?? contextSchema}.${t.name}`, reason);
174
+ }
175
+ };
176
+ // The policies of a relation read within a running-as-R context also run as
177
+ // R, so their predicates close over too — this is the load-bearing path the
178
+ // constructive-db audit is about.
179
+ const closeRelationPolicies = async (relKey, reason) => {
180
+ if (relationsClosed.has(relKey))
181
+ return;
182
+ relationsClosed.add(relKey);
183
+ const table = idx.tablesByKey.get(relKey);
184
+ if (!table)
185
+ return;
186
+ for (const p of table.policies) {
187
+ if (p.using)
188
+ await seedExpr(p.using, reason, table.schema, `policy ${p.name} USING on ${relKey}`);
189
+ if (p.withCheck) {
190
+ await seedExpr(p.withCheck, reason, table.schema, `policy ${p.name} WITH CHECK on ${relKey}`);
191
+ }
192
+ }
193
+ };
194
+ const walkView = async (view, reason) => {
195
+ const key = `${view.schema}.${view.name}`;
196
+ if (viewsWalked.has(key))
197
+ return;
198
+ viewsWalked.add(key);
199
+ if (!view.securityInvoker)
200
+ return; // definer view: body runs as the owner
201
+ const parsed = await (0, extract_1.extractQuery)(view.definition);
202
+ if (parsed.opaque) {
203
+ taint.push({ node: key, reason: parsed.opaqueReason ?? 'view body could not be read' });
204
+ return;
205
+ }
206
+ if (parsed.tainted)
207
+ taint.push({ node: key, reason: parsed.tainted });
208
+ for (const call of parsed.calls) {
209
+ for (const callee of resolve(call, view.schema)) {
210
+ await seedFunction(callee, reason, `view ${key} → ${fnId(callee)}`);
211
+ }
212
+ }
213
+ for (const t of parsed.tables) {
214
+ const tk = `${t.schema ?? view.schema}.${t.name}`;
215
+ await closeRelationPolicies(tk, reason);
216
+ const nested = idx.viewsByKey.get(tk);
217
+ if (nested)
218
+ await walkView(nested, reason);
219
+ }
220
+ };
221
+ const reachable = (schema) => !input.exposureKnown || input.exposedSchemas.has(schema);
222
+ // 1. Direct call entrypoints.
223
+ for (const fn of functions) {
224
+ if (fn.returnsTrigger)
225
+ continue; // not directly callable, whatever its ACL
226
+ if (!reachable(fn.schema))
227
+ continue;
228
+ if ((0, lattice_1.effectiveExecute)(fn, role, graph) === null)
229
+ continue;
230
+ if (!hasUsage(fn.schema))
231
+ continue;
232
+ await seedFunction(fn, 'direct-call', `direct EXECUTE entrypoint ${fnId(fn)}`);
233
+ }
234
+ // 2/3/4. Per-relation closures: policy predicates, trigger bodies and
235
+ // write-time expressions, for the relations R can actually address.
236
+ for (const table of tables) {
237
+ const key = `${table.schema}.${table.name}`;
238
+ if (input.exposureKnown && input.unaddressable.has(key))
239
+ continue;
240
+ if (!hasUsage(table.schema))
241
+ continue;
242
+ const held = new Set((0, lattice_1.effectiveGrants)(table, role, graph).map((g) => g.privilege));
243
+ if (!ACCESS_PRIVILEGES.some((p) => held.has(p)))
244
+ continue;
245
+ await closeRelationPolicies(key, 'policy-predicate');
246
+ const writes = WRITE_PRIVILEGES.filter((p) => held.has(p));
247
+ if (writes.length === 0)
248
+ continue;
249
+ for (const trig of reachInputs.triggers.get(key) ?? []) {
250
+ if (trig.instead)
251
+ continue; // INSTEAD OF is a view trigger, reached via walkView
252
+ if (!trig.events.some((e) => writes.includes(e)))
253
+ continue;
254
+ for (const fn of resolve({ schema: trig.functionSchema, name: trig.functionName }, trig.functionSchema)) {
255
+ // EXECUTE on the trigger function is never needed; only what its body
256
+ // calls, and only when the body runs as R (not SECURITY DEFINER).
257
+ if (!fn.isSecurityDefiner) {
258
+ await walkFrame(fn, 'trigger', `trigger ${trig.name} on ${key} → ${fnId(fn)}`);
259
+ }
260
+ }
261
+ }
262
+ for (const expr of reachInputs.expressions.get(key) ?? []) {
263
+ const reason = expr.kind === 'check' ? 'check-constraint'
264
+ : expr.kind === 'generated' ? 'generated-column'
265
+ : 'default-expression';
266
+ await seedExpr(expr.expr, reason, table.schema, `${reason} ${expr.name} on ${key}`);
267
+ }
268
+ }
269
+ // 5. Views R can read (or write): an invoker view's body runs as R.
270
+ for (const view of views) {
271
+ if (view.materialized)
272
+ continue;
273
+ if (!reachable(view.schema))
274
+ continue;
275
+ if (!hasUsage(view.schema))
276
+ continue;
277
+ const held = new Set((0, lattice_1.effectiveGrants)({ grants: view.grants }, role, graph).map((g) => g.privilege));
278
+ if (!ACCESS_PRIVILEGES.some((p) => held.has(p)))
279
+ continue;
280
+ await walkView(view, 'view');
281
+ }
282
+ return classify(role, functions, graph, fnId, reached, taint, hasUsage);
283
+ }
284
+ function classify(role, functions, graph, fnId, reached, taint, hasUsage) {
285
+ const revocable = [];
286
+ const retained = [];
287
+ const suppressed = [];
288
+ // An opaque node reached as R can construct a dynamic call to any function R
289
+ // can name — but only in a schema R holds USAGE on. Candidates outside those
290
+ // schemas are beyond its reach, so opacity does not protect them.
291
+ const opaqueReached = taint.length > 0;
292
+ for (const fn of functions) {
293
+ const via = (0, lattice_1.effectiveExecute)(fn, role, graph);
294
+ if (via === null || via === 'PUBLIC')
295
+ continue; // not a grant R can revoke on its own
296
+ const tuple = {
297
+ role,
298
+ schema: fn.schema,
299
+ object: fn.name,
300
+ args: fn.args,
301
+ kind: 'function',
302
+ privilege: 'EXECUTE',
303
+ via
304
+ };
305
+ const hit = reached.get(fnId(fn));
306
+ if (hit) {
307
+ retained.push({ ...tuple, reasons: [...hit.reasons].sort(), proof: [hit.proof] });
308
+ }
309
+ else if (opaqueReached && hasUsage(fn.schema)) {
310
+ suppressed.push({
311
+ ...tuple,
312
+ reason: 'analysis incomplete — an opaque node reached as this role could construct a dynamic call '
313
+ + 'to any function in a schema the role holds USAGE on, including this one, so the grant '
314
+ + 'cannot be proven unused'
315
+ });
316
+ }
317
+ else {
318
+ revocable.push(tuple);
319
+ }
320
+ }
321
+ const byReason = (r) => retained.filter((g) => g.reasons.includes(r)).length;
322
+ return {
323
+ role,
324
+ summary: {
325
+ candidates: revocable.length + retained.length + suppressed.length,
326
+ revocable: revocable.length,
327
+ retained: retained.length,
328
+ retainedByPolicy: byReason('policy-predicate'),
329
+ retainedByTrigger: byReason('trigger'),
330
+ suppressed: suppressed.length
331
+ },
332
+ revocable: sortTuples(revocable),
333
+ retained: sortTuples(retained),
334
+ suppressed: sortTuples(suppressed),
335
+ taint
336
+ };
337
+ }
338
+ function sortTuples(tuples) {
339
+ return [...tuples].sort((a, b) => `${a.schema}.${a.object}(${a.args})`.localeCompare(`${b.schema}.${b.object}(${b.args})`));
340
+ }
341
+ /**
342
+ * L21 findings: one per revocable grant. The proof of every *retained* grant
343
+ * lives in `report.revocableGrants`, not in the finding stream — a retained
344
+ * grant is not a problem, and the finding stream is problems. The revocable
345
+ * recommendation carries its own tuple in `context` so a caller can act on it
346
+ * without re-deriving it.
347
+ */
348
+ function revocableGrantFindings(report) {
349
+ const out = [];
350
+ for (const role of report.roles) {
351
+ for (const g of role.revocable) {
352
+ const object = `${g.schema}.${g.object}(${g.args})`;
353
+ out.push({
354
+ code: 'L21',
355
+ severity: 'info',
356
+ category: 'coverage',
357
+ schema: g.schema,
358
+ table: g.object,
359
+ role: g.role,
360
+ privilege: 'EXECUTE',
361
+ message: `Role ${g.role} holds EXECUTE on ${object} (${g.via}) but no reachable path exercises it — `
362
+ + 'the grant is revocable',
363
+ hint: 'The reachability closure — direct calls, RLS policy predicates, trigger bodies, write-time '
364
+ + 'expressions and the views this role can reach — never lands on this function, so revoking '
365
+ + 'EXECUTE from the role changes nothing it can do. Review `report.revocableGrants` for the '
366
+ + 'retained grants and their proof paths before applying a batch revoke.',
367
+ context: {
368
+ object,
369
+ kind: g.kind,
370
+ via: g.via,
371
+ revocable: true
372
+ }
373
+ });
374
+ }
375
+ }
376
+ return out;
377
+ }
@@ -0,0 +1,25 @@
1
+ import type { TableSnapshot } from '../pg/introspect';
2
+ import type { Finding } from '../types';
3
+ /**
4
+ * A1: RLS is enabled but there are zero policies on the table.
5
+ *
6
+ * Every non-owner query returns 0 rows. Usually a sign that someone ran
7
+ * `ALTER TABLE … ENABLE ROW LEVEL SECURITY` but never wrote the policies.
8
+ */
9
+ export declare function checkRlsEnabledNoPolicies(table: TableSnapshot): Finding | null;
10
+ /**
11
+ * A2: The table has non-trivial grants but RLS is disabled.
12
+ *
13
+ * Intent: if a real role (not just the owner or PUBLIC) can SELECT/INSERT/UPDATE/DELETE,
14
+ * we'd expect RLS to be on. Tables intentionally meant to be global (e.g. lookup tables)
15
+ * may be false positives; callers can exclude them via `--schemas`.
16
+ */
17
+ export declare function checkGrantsWithoutRls(table: TableSnapshot): Finding | null;
18
+ /**
19
+ * A3: RLS is enabled but FORCE ROW LEVEL SECURITY is not set.
20
+ *
21
+ * Without FORCE, the table owner bypasses all policies. For anything with
22
+ * tenant data, that usually isn't what you want. Medium severity — owners
23
+ * bypassing policies is sometimes intentional (e.g. admin tooling).
24
+ */
25
+ export declare function checkRlsNotForced(table: TableSnapshot): Finding | null;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkRlsEnabledNoPolicies = checkRlsEnabledNoPolicies;
4
+ exports.checkGrantsWithoutRls = checkGrantsWithoutRls;
5
+ exports.checkRlsNotForced = checkRlsNotForced;
6
+ /**
7
+ * A1: RLS is enabled but there are zero policies on the table.
8
+ *
9
+ * Every non-owner query returns 0 rows. Usually a sign that someone ran
10
+ * `ALTER TABLE … ENABLE ROW LEVEL SECURITY` but never wrote the policies.
11
+ */
12
+ function checkRlsEnabledNoPolicies(table) {
13
+ if (!table.rlsEnabled)
14
+ return null;
15
+ if (table.policies.length > 0)
16
+ return null;
17
+ return {
18
+ code: 'A1',
19
+ severity: 'critical',
20
+ category: 'flags',
21
+ schema: table.schema,
22
+ table: table.name,
23
+ message: `RLS is enabled on ${table.schema}.${table.name} but the table has zero policies`,
24
+ hint: 'Either add policies or disable RLS; otherwise non-owner queries return 0 rows (and writes by non-owners fail).'
25
+ };
26
+ }
27
+ /**
28
+ * A2: The table has non-trivial grants but RLS is disabled.
29
+ *
30
+ * Intent: if a real role (not just the owner or PUBLIC) can SELECT/INSERT/UPDATE/DELETE,
31
+ * we'd expect RLS to be on. Tables intentionally meant to be global (e.g. lookup tables)
32
+ * may be false positives; callers can exclude them via `--schemas`.
33
+ */
34
+ function checkGrantsWithoutRls(table) {
35
+ if (table.rlsEnabled)
36
+ return null;
37
+ const nonTrivialGrants = table.grants.filter((g) => g.role !== table.owner
38
+ && g.role !== 'PUBLIC'
39
+ && !g.bypassRls
40
+ && isWriteOrReadPrivilege(g.privilege));
41
+ if (nonTrivialGrants.length === 0)
42
+ return null;
43
+ const roles = Array.from(new Set(nonTrivialGrants.map((g) => g.role))).sort();
44
+ return {
45
+ code: 'A2',
46
+ severity: 'high',
47
+ category: 'flags',
48
+ schema: table.schema,
49
+ table: table.name,
50
+ message: `Roles [${roles.join(', ')}] have grants on ${table.schema}.${table.name} but RLS is disabled`,
51
+ hint: 'Every grantee can read/write every row. Enable RLS and add policies, or document the table as intentionally global.',
52
+ context: { roles }
53
+ };
54
+ }
55
+ /**
56
+ * A3: RLS is enabled but FORCE ROW LEVEL SECURITY is not set.
57
+ *
58
+ * Without FORCE, the table owner bypasses all policies. For anything with
59
+ * tenant data, that usually isn't what you want. Medium severity — owners
60
+ * bypassing policies is sometimes intentional (e.g. admin tooling).
61
+ */
62
+ function checkRlsNotForced(table) {
63
+ if (!table.rlsEnabled)
64
+ return null;
65
+ if (table.rlsForced)
66
+ return null;
67
+ return {
68
+ code: 'A3',
69
+ severity: 'medium',
70
+ category: 'flags',
71
+ schema: table.schema,
72
+ table: table.name,
73
+ message: `RLS enabled on ${table.schema}.${table.name} but FORCE ROW LEVEL SECURITY is not set`,
74
+ hint: 'Without FORCE, the table owner bypasses all policies. Add `ALTER TABLE … FORCE ROW LEVEL SECURITY` unless you deliberately want owner bypass.'
75
+ };
76
+ }
77
+ function isWriteOrReadPrivilege(privilege) {
78
+ return privilege === 'SELECT'
79
+ || privilege === 'INSERT'
80
+ || privilege === 'UPDATE'
81
+ || privilege === 'DELETE';
82
+ }