@safegres/test 0.0.1 → 1.30.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (483) hide show
  1. package/README.md +825 -412
  2. package/adapters.d.ts +8 -0
  3. package/adapters.js +18 -0
  4. package/ast/helpers.d.ts +16 -0
  5. package/ast/helpers.js +40 -0
  6. package/ast/parse.d.ts +31 -0
  7. package/ast/parse.js +87 -0
  8. package/ast/walk.d.ts +27 -0
  9. package/ast/walk.js +79 -0
  10. package/callgraph/baseline.d.ts +28 -0
  11. package/callgraph/baseline.js +55 -0
  12. package/callgraph/extract.d.ts +93 -0
  13. package/callgraph/extract.js +340 -0
  14. package/callgraph/graph.d.ts +82 -0
  15. package/callgraph/graph.js +276 -0
  16. package/checks/anti-patterns.d.ts +53 -0
  17. package/checks/anti-patterns.js +247 -0
  18. package/checks/column-grants.d.ts +20 -0
  19. package/checks/column-grants.js +74 -0
  20. package/checks/coverage.d.ts +43 -0
  21. package/checks/coverage.js +110 -0
  22. package/checks/definer-function.d.ts +91 -0
  23. package/checks/definer-function.js +458 -0
  24. package/checks/definer-view.d.ts +167 -0
  25. package/checks/definer-view.js +392 -0
  26. package/checks/indexes.d.ts +82 -0
  27. package/checks/indexes.js +263 -0
  28. package/checks/lattice.d.ts +179 -0
  29. package/checks/lattice.js +487 -0
  30. package/checks/object-acls.d.ts +32 -0
  31. package/checks/object-acls.js +129 -0
  32. package/checks/policy-index.d.ts +78 -0
  33. package/checks/policy-index.js +349 -0
  34. package/checks/revocable-grants.d.ts +140 -0
  35. package/checks/revocable-grants.js +377 -0
  36. package/checks/rls-flags.d.ts +25 -0
  37. package/checks/rls-flags.js +82 -0
  38. package/checks/role-reach.d.ts +329 -0
  39. package/checks/role-reach.js +277 -0
  40. package/checks/role-trust.d.ts +45 -0
  41. package/checks/role-trust.js +91 -0
  42. package/checks/set-role.d.ts +21 -0
  43. package/checks/set-role.js +66 -0
  44. package/checks/stats.d.ts +52 -0
  45. package/checks/stats.js +192 -0
  46. package/checks/view-exposure.d.ts +68 -0
  47. package/checks/view-exposure.js +236 -0
  48. package/checks/view-writes.d.ts +88 -0
  49. package/checks/view-writes.js +350 -0
  50. package/ci/baseline.d.ts +104 -0
  51. package/ci/baseline.js +111 -0
  52. package/ci/github.d.ts +47 -0
  53. package/ci/github.js +197 -0
  54. package/cli/audit.d.ts +3 -0
  55. package/cli/audit.js +560 -0
  56. package/cli/baseline.d.ts +3 -0
  57. package/cli/baseline.js +104 -0
  58. package/cli/commands.d.ts +2 -0
  59. package/cli/commands.js +60 -0
  60. package/cli/doctor.d.ts +3 -0
  61. package/cli/doctor.js +72 -0
  62. package/cli/eval.d.ts +3 -0
  63. package/cli/eval.js +153 -0
  64. package/cli/print-config.d.ts +3 -0
  65. package/cli/print-config.js +54 -0
  66. package/cli/shared.d.ts +43 -0
  67. package/cli/shared.js +153 -0
  68. package/cli.d.ts +2 -0
  69. package/cli.js +69 -0
  70. package/commands/audit.d.ts +65 -0
  71. package/commands/audit.js +779 -0
  72. package/commands/doctor.d.ts +19 -0
  73. package/commands/doctor.js +181 -0
  74. package/commands/eval.d.ts +67 -0
  75. package/commands/eval.js +93 -0
  76. package/config/fingerprint.d.ts +40 -0
  77. package/config/fingerprint.js +71 -0
  78. package/config/loader.d.ts +44 -0
  79. package/config/loader.js +210 -0
  80. package/config/presets.d.ts +95 -0
  81. package/config/presets.js +327 -0
  82. package/config/resolve.d.ts +47 -0
  83. package/config/resolve.js +192 -0
  84. package/config/schema.d.ts +54 -0
  85. package/config/schema.js +269 -0
  86. package/config/types.d.ts +562 -0
  87. package/config/validate.d.ts +14 -0
  88. package/config/validate.js +125 -0
  89. package/corpus/README.md +83 -0
  90. package/corpus/bootstrap.sql +14 -0
  91. package/corpus/cases/01-anon-write-grant/case.json +26 -0
  92. package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
  93. package/corpus/cases/02-grant-to-public/case.json +25 -0
  94. package/corpus/cases/02-grant-to-public/schema.sql +18 -0
  95. package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
  96. package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
  97. package/corpus/cases/04-permissive-write-policy/case.json +30 -0
  98. package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
  99. package/corpus/cases/05-public-read-policy/case.json +25 -0
  100. package/corpus/cases/05-public-read-policy/schema.sql +16 -0
  101. package/corpus/cases/06-update-without-with-check/case.json +25 -0
  102. package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
  103. package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
  104. package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
  105. package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
  106. package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
  107. package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
  108. package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
  109. package/corpus/cases/10-rls-not-forced/case.json +24 -0
  110. package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
  111. package/corpus/cases/11-session-user-in-policy/case.json +24 -0
  112. package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
  113. package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
  114. package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
  115. package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
  116. package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
  117. package/corpus/cases/14-dead-policy/case.json +24 -0
  118. package/corpus/cases/14-dead-policy/schema.sql +18 -0
  119. package/corpus/cases/15-unreachable-grant/case.json +24 -0
  120. package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
  121. package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
  122. package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
  123. package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
  124. package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
  125. package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
  126. package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
  127. package/corpus/cases/19-policy-column-cast/case.json +24 -0
  128. package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
  129. package/corpus/cases/20-stable-function-per-row/case.json +24 -0
  130. package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
  131. package/corpus/cases/21-table-without-primary-key/case.json +24 -0
  132. package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
  133. package/corpus/cases/22-redundant-index/case.json +24 -0
  134. package/corpus/cases/22-redundant-index/schema.sql +16 -0
  135. package/corpus/cases/23-tsvector-without-index/case.json +24 -0
  136. package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
  137. package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
  138. package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
  139. package/corpus/cases/25-definer-view-bypass/case.json +31 -0
  140. package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
  141. package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
  142. package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
  143. package/corpus/cases/27-definer-view-write/case.json +36 -0
  144. package/corpus/cases/27-definer-view-write/schema.sql +40 -0
  145. package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
  146. package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
  147. package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
  148. package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
  149. package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
  150. package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
  151. package/corpus/cases/31-matview-snapshot/case.json +32 -0
  152. package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
  153. package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
  154. package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
  155. package/corpus/cases/33-leaky-filter-view/case.json +36 -0
  156. package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
  157. package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
  158. package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
  159. package/corpus/cases/35-column-grant-projection/case.json +27 -0
  160. package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
  161. package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
  162. package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
  163. package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
  164. package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
  165. package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
  166. package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
  167. package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
  168. package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
  169. package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
  170. package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
  171. package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
  172. package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
  173. package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
  174. package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
  175. package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
  176. package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
  177. package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
  178. package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
  179. package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
  180. package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
  181. package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
  182. package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
  183. package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
  184. package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
  185. package/corpus/cases/41-unreadable-view-body/case.json +33 -0
  186. package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
  187. package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
  188. package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
  189. package/corpus/cases/42-search-column-indexed/case.json +21 -0
  190. package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
  191. package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
  192. package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
  193. package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
  194. package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
  195. package/corpus/cases/45-anon-foreign-table/case.json +27 -0
  196. package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
  197. package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
  198. package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
  199. package/corpus/cases/47-checked-writable-view/case.json +37 -0
  200. package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
  201. package/corpus/cases/48-definer-function-reach/case.json +38 -0
  202. package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
  203. package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
  204. package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
  205. package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
  206. package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
  207. package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
  208. package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
  209. package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
  210. package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
  211. package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
  212. package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
  213. package/corpus/index.d.ts +79 -0
  214. package/corpus/index.js +125 -0
  215. package/esm/adapters.d.ts +8 -0
  216. package/esm/adapters.js +6 -0
  217. package/esm/ast/helpers.d.ts +16 -0
  218. package/esm/ast/helpers.js +36 -0
  219. package/esm/ast/parse.d.ts +31 -0
  220. package/esm/ast/parse.js +82 -0
  221. package/esm/ast/walk.d.ts +27 -0
  222. package/esm/ast/walk.js +72 -0
  223. package/esm/callgraph/baseline.d.ts +28 -0
  224. package/esm/callgraph/baseline.js +48 -0
  225. package/esm/callgraph/extract.d.ts +93 -0
  226. package/esm/callgraph/extract.js +333 -0
  227. package/esm/callgraph/graph.d.ts +82 -0
  228. package/esm/callgraph/graph.js +273 -0
  229. package/esm/checks/anti-patterns.d.ts +53 -0
  230. package/esm/checks/anti-patterns.js +240 -0
  231. package/esm/checks/column-grants.d.ts +20 -0
  232. package/esm/checks/column-grants.js +71 -0
  233. package/esm/checks/coverage.d.ts +43 -0
  234. package/esm/checks/coverage.js +104 -0
  235. package/esm/checks/definer-function.d.ts +91 -0
  236. package/esm/checks/definer-function.js +452 -0
  237. package/esm/checks/definer-view.d.ts +167 -0
  238. package/esm/checks/definer-view.js +382 -0
  239. package/esm/checks/indexes.d.ts +82 -0
  240. package/esm/checks/indexes.js +256 -0
  241. package/esm/checks/lattice.d.ts +179 -0
  242. package/esm/checks/lattice.js +475 -0
  243. package/esm/checks/object-acls.d.ts +32 -0
  244. package/esm/checks/object-acls.js +125 -0
  245. package/esm/checks/policy-index.d.ts +78 -0
  246. package/esm/checks/policy-index.js +342 -0
  247. package/esm/checks/revocable-grants.d.ts +140 -0
  248. package/esm/checks/revocable-grants.js +373 -0
  249. package/esm/checks/rls-flags.d.ts +25 -0
  250. package/esm/checks/rls-flags.js +77 -0
  251. package/esm/checks/role-reach.d.ts +329 -0
  252. package/esm/checks/role-reach.js +270 -0
  253. package/esm/checks/role-trust.d.ts +45 -0
  254. package/esm/checks/role-trust.js +86 -0
  255. package/esm/checks/set-role.d.ts +21 -0
  256. package/esm/checks/set-role.js +63 -0
  257. package/esm/checks/stats.d.ts +52 -0
  258. package/esm/checks/stats.js +184 -0
  259. package/esm/checks/view-exposure.d.ts +68 -0
  260. package/esm/checks/view-exposure.js +231 -0
  261. package/esm/checks/view-writes.d.ts +88 -0
  262. package/esm/checks/view-writes.js +344 -0
  263. package/esm/ci/baseline.d.ts +104 -0
  264. package/esm/ci/baseline.js +107 -0
  265. package/esm/ci/github.d.ts +47 -0
  266. package/esm/ci/github.js +161 -0
  267. package/esm/cli/audit.d.ts +3 -0
  268. package/esm/cli/audit.js +525 -0
  269. package/esm/cli/baseline.d.ts +3 -0
  270. package/esm/cli/baseline.js +102 -0
  271. package/esm/cli/commands.d.ts +2 -0
  272. package/esm/cli/commands.js +53 -0
  273. package/esm/cli/doctor.d.ts +3 -0
  274. package/esm/cli/doctor.js +67 -0
  275. package/esm/cli/eval.d.ts +3 -0
  276. package/esm/cli/eval.js +115 -0
  277. package/esm/cli/print-config.d.ts +3 -0
  278. package/esm/cli/print-config.js +52 -0
  279. package/esm/cli/shared.d.ts +43 -0
  280. package/esm/cli/shared.js +112 -0
  281. package/esm/cli.d.ts +2 -0
  282. package/esm/cli.js +67 -0
  283. package/esm/commands/audit.d.ts +65 -0
  284. package/esm/commands/audit.js +776 -0
  285. package/esm/commands/doctor.d.ts +19 -0
  286. package/esm/commands/doctor.js +178 -0
  287. package/esm/commands/eval.d.ts +67 -0
  288. package/esm/commands/eval.js +90 -0
  289. package/esm/config/fingerprint.d.ts +40 -0
  290. package/esm/config/fingerprint.js +68 -0
  291. package/esm/config/loader.d.ts +44 -0
  292. package/esm/config/loader.js +172 -0
  293. package/esm/config/presets.d.ts +95 -0
  294. package/esm/config/presets.js +324 -0
  295. package/esm/config/resolve.d.ts +47 -0
  296. package/esm/config/resolve.js +180 -0
  297. package/esm/config/schema.d.ts +54 -0
  298. package/esm/config/schema.js +265 -0
  299. package/esm/config/types.d.ts +562 -0
  300. package/esm/config/validate.d.ts +14 -0
  301. package/esm/config/validate.js +122 -0
  302. package/esm/corpus/index.d.ts +79 -0
  303. package/esm/corpus/index.js +85 -0
  304. package/esm/exposure/adapters.d.ts +152 -0
  305. package/esm/exposure/adapters.js +463 -0
  306. package/esm/exposure/planes.d.ts +59 -0
  307. package/esm/exposure/planes.js +144 -0
  308. package/esm/exposure/reach.d.ts +78 -0
  309. package/esm/exposure/reach.js +105 -0
  310. package/esm/exposure/routines.d.ts +36 -0
  311. package/esm/exposure/routines.js +54 -0
  312. package/esm/index.d.ts +96 -0
  313. package/esm/index.js +57 -6
  314. package/esm/lint/index.d.ts +11 -0
  315. package/esm/lint/index.js +10 -0
  316. package/esm/perf/baseline.d.ts +40 -0
  317. package/esm/perf/baseline.js +83 -0
  318. package/esm/perf/explain.d.ts +46 -0
  319. package/esm/perf/explain.js +201 -0
  320. package/esm/pg/acl.d.ts +72 -0
  321. package/esm/pg/acl.js +186 -0
  322. package/esm/pg/behaviors.d.ts +114 -0
  323. package/esm/pg/behaviors.js +206 -0
  324. package/esm/pg/exposure.d.ts +74 -0
  325. package/esm/pg/exposure.js +198 -0
  326. package/esm/pg/functions.d.ts +52 -0
  327. package/esm/pg/functions.js +128 -0
  328. package/esm/pg/indexes.d.ts +192 -0
  329. package/esm/pg/indexes.js +395 -0
  330. package/esm/pg/introspect.d.ts +134 -0
  331. package/esm/pg/introspect.js +274 -0
  332. package/esm/pg/objects.d.ts +37 -0
  333. package/esm/pg/objects.js +108 -0
  334. package/esm/pg/paths.d.ts +114 -0
  335. package/esm/pg/paths.js +159 -0
  336. package/esm/pg/proc.d.ts +23 -0
  337. package/esm/pg/proc.js +55 -0
  338. package/esm/pg/reach-inputs.d.ts +47 -0
  339. package/esm/pg/reach-inputs.js +133 -0
  340. package/esm/pg/roles.d.ts +28 -0
  341. package/esm/pg/roles.js +50 -0
  342. package/esm/pg/stats.d.ts +65 -0
  343. package/esm/pg/stats.js +129 -0
  344. package/esm/pgpm-test.d.ts +22 -0
  345. package/esm/pgpm-test.js +39 -0
  346. package/esm/report/callgraph.d.ts +9 -0
  347. package/esm/report/callgraph.js +74 -0
  348. package/esm/report/compare.d.ts +143 -0
  349. package/esm/report/compare.js +195 -0
  350. package/esm/report/github.d.ts +74 -0
  351. package/esm/report/github.js +290 -0
  352. package/esm/report/json.d.ts +4 -0
  353. package/esm/report/json.js +3 -0
  354. package/esm/report/markdown.d.ts +25 -0
  355. package/esm/report/markdown.js +352 -0
  356. package/esm/report/pretty.d.ts +14 -0
  357. package/esm/report/pretty.js +318 -0
  358. package/esm/report/sarif.d.ts +40 -0
  359. package/esm/report/sarif.js +189 -0
  360. package/esm/report/view.d.ts +82 -0
  361. package/esm/report/view.js +126 -0
  362. package/esm/rules/registry.d.ts +66 -0
  363. package/esm/rules/registry.js +562 -0
  364. package/esm/score/score.d.ts +84 -0
  365. package/esm/score/score.js +233 -0
  366. package/esm/score/scorecards.d.ts +68 -0
  367. package/esm/score/scorecards.js +157 -0
  368. package/esm/types.d.ts +292 -0
  369. package/esm/types.js +26 -0
  370. package/esm/version.d.ts +8 -0
  371. package/esm/version.js +9 -0
  372. package/exposure/adapters.d.ts +152 -0
  373. package/exposure/adapters.js +468 -0
  374. package/exposure/planes.d.ts +59 -0
  375. package/exposure/planes.js +151 -0
  376. package/exposure/reach.d.ts +78 -0
  377. package/exposure/reach.js +109 -0
  378. package/exposure/routines.d.ts +36 -0
  379. package/exposure/routines.js +57 -0
  380. package/index.d.ts +96 -6
  381. package/index.js +229 -6
  382. package/lint/index.d.ts +11 -0
  383. package/lint/index.js +19 -0
  384. package/package.json +54 -48
  385. package/perf/baseline.d.ts +40 -0
  386. package/perf/baseline.js +92 -0
  387. package/perf/explain.d.ts +46 -0
  388. package/perf/explain.js +204 -0
  389. package/pg/acl.d.ts +72 -0
  390. package/pg/acl.js +191 -0
  391. package/pg/behaviors.d.ts +114 -0
  392. package/pg/behaviors.js +217 -0
  393. package/pg/exposure.d.ts +74 -0
  394. package/pg/exposure.js +205 -0
  395. package/pg/functions.d.ts +52 -0
  396. package/pg/functions.js +131 -0
  397. package/pg/indexes.d.ts +192 -0
  398. package/pg/indexes.js +400 -0
  399. package/pg/introspect.d.ts +134 -0
  400. package/pg/introspect.js +280 -0
  401. package/pg/objects.d.ts +37 -0
  402. package/pg/objects.js +111 -0
  403. package/pg/paths.d.ts +114 -0
  404. package/pg/paths.js +166 -0
  405. package/pg/proc.d.ts +23 -0
  406. package/pg/proc.js +58 -0
  407. package/pg/reach-inputs.d.ts +47 -0
  408. package/pg/reach-inputs.js +136 -0
  409. package/pg/roles.d.ts +28 -0
  410. package/pg/roles.js +54 -0
  411. package/pg/stats.d.ts +65 -0
  412. package/pg/stats.js +133 -0
  413. package/pgpm-test.d.ts +22 -0
  414. package/pgpm-test.js +42 -0
  415. package/report/callgraph.d.ts +9 -0
  416. package/report/callgraph.js +81 -0
  417. package/report/compare.d.ts +143 -0
  418. package/report/compare.js +205 -0
  419. package/report/github.d.ts +74 -0
  420. package/report/github.js +333 -0
  421. package/report/json.d.ts +4 -0
  422. package/report/json.js +6 -0
  423. package/report/markdown.d.ts +25 -0
  424. package/report/markdown.js +355 -0
  425. package/report/pretty.d.ts +14 -0
  426. package/report/pretty.js +324 -0
  427. package/report/sarif.d.ts +40 -0
  428. package/report/sarif.js +193 -0
  429. package/report/view.d.ts +82 -0
  430. package/report/view.js +132 -0
  431. package/rules/registry.d.ts +66 -0
  432. package/rules/registry.js +569 -0
  433. package/schema/safegres.schema.json +1119 -0
  434. package/score/score.d.ts +84 -0
  435. package/score/score.js +238 -0
  436. package/score/scorecards.d.ts +68 -0
  437. package/score/scorecards.js +162 -0
  438. package/types.d.ts +292 -0
  439. package/types.js +32 -0
  440. package/version.d.ts +8 -0
  441. package/version.js +12 -0
  442. package/LICENSE +0 -23
  443. package/admin.d.ts +0 -26
  444. package/admin.js +0 -182
  445. package/connect.d.ts +0 -19
  446. package/connect.js +0 -83
  447. package/dist/README.md +0 -557
  448. package/dist/package.json +0 -72
  449. package/esm/admin.js +0 -178
  450. package/esm/connect.js +0 -78
  451. package/esm/manager.js +0 -136
  452. package/esm/roles.js +0 -32
  453. package/esm/seed/adapters.js +0 -23
  454. package/esm/seed/csv.js +0 -44
  455. package/esm/seed/index.js +0 -16
  456. package/esm/seed/json.js +0 -18
  457. package/esm/seed/launchql.js +0 -19
  458. package/esm/seed/sqitch.js +0 -17
  459. package/esm/stream.js +0 -43
  460. package/esm/test-client.js +0 -150
  461. package/manager.d.ts +0 -25
  462. package/manager.js +0 -140
  463. package/roles.d.ts +0 -17
  464. package/roles.js +0 -38
  465. package/seed/adapters.d.ts +0 -4
  466. package/seed/adapters.js +0 -28
  467. package/seed/csv.d.ts +0 -9
  468. package/seed/csv.js +0 -49
  469. package/seed/index.d.ts +0 -16
  470. package/seed/index.js +0 -33
  471. package/seed/json.d.ts +0 -6
  472. package/seed/json.js +0 -21
  473. package/seed/launchql.d.ts +0 -2
  474. package/seed/launchql.js +0 -22
  475. package/seed/sqitch.d.ts +0 -2
  476. package/seed/sqitch.js +0 -20
  477. package/seed/types.d.ts +0 -13
  478. package/stream.d.ts +0 -2
  479. package/stream.js +0 -46
  480. package/test-client.d.ts +0 -49
  481. package/test-client.js +0 -154
  482. /package/{seed → config}/types.js +0 -0
  483. /package/esm/{seed → config}/types.js +0 -0
@@ -0,0 +1,487 @@
1
+ "use strict";
2
+ /**
3
+ * The grant/RLS/policy lattice (L-series).
4
+ *
5
+ * The A/R rules reason from grants exactly as the catalog stores them, which
6
+ * misses two ways access actually arrives: `GRANT TO PUBLIC` (one ACL row
7
+ * that applies to every role) and role inheritance (zero ACL rows on the
8
+ * member). The lattice rules evaluate the *effective* cell each
9
+ * (relation, role, privilege) lands in:
10
+ *
11
+ * | grant | RLS | policy | verdict |
12
+ * |-------|-----|--------|-------------------------------------------|
13
+ * | yes | on | yes | normal — policy-mediated access |
14
+ * | yes | on | no | dead grant (L1, indirect; A4/A5, direct) |
15
+ * | yes | off | — | unmediated (L5 for untrusted, indirect) |
16
+ * | no | — | yes | dead policy (L2) |
17
+ *
18
+ * plus schema-USAGE composition: an object grant the grantee cannot reach
19
+ * because USAGE is missing (L3), and USAGE that reaches no object (L4).
20
+ *
21
+ * Every L rule ships signal-only: L1–L3 are fail-closed (zero score weight by
22
+ * default), L4/L5 default to `info`. Escalate via config once the findings
23
+ * prove themselves.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.effectiveGrants = effectiveGrants;
27
+ exports.effectiveExecute = effectiveExecute;
28
+ exports.effectiveColumnGrants = effectiveColumnGrants;
29
+ exports.checkIndirectCoverageGaps = checkIndirectCoverageGaps;
30
+ exports.checkDeadPolicies = checkDeadPolicies;
31
+ exports.checkUnreachableGrants = checkUnreachableGrants;
32
+ exports.checkDeadSchemaUsage = checkDeadSchemaUsage;
33
+ exports.checkUnaddressableGrant = checkUnaddressableGrant;
34
+ exports.checkUntrustedIndirectAccess = checkUntrustedIndirectAccess;
35
+ exports.computeRoleAccess = computeRoleAccess;
36
+ const coverage_1 = require("./coverage");
37
+ const RLS_PRIVILEGES = ['SELECT', 'INSERT', 'UPDATE', 'DELETE'];
38
+ /**
39
+ * Every privilege `role` effectively holds on `table`: direct grants, grants
40
+ * TO PUBLIC, and grants to roles it inherits from. Deduplicated per privilege
41
+ * with the most direct provenance winning (direct > PUBLIC > inherited).
42
+ *
43
+ * Takes anything carrying an ACL, not just a table: a view's own grants
44
+ * compose the same way, and the view-ownership reach edge asks this same
45
+ * question of them.
46
+ */
47
+ function effectiveGrants(table, role, graph) {
48
+ const byPrivilege = new Map();
49
+ const rank = (via) => via === 'direct' ? 0 : via === 'PUBLIC' ? 1 : 2;
50
+ const record = (privilege, via) => {
51
+ const existing = byPrivilege.get(privilege);
52
+ if (existing === undefined || rank(via) < rank(existing)) {
53
+ byPrivilege.set(privilege, via);
54
+ }
55
+ };
56
+ const ancestors = new Set(graph.get(role)?.inheritsFrom ?? []);
57
+ for (const g of table.grants) {
58
+ if (g.role === role)
59
+ record(g.privilege, 'direct');
60
+ else if (g.role === 'PUBLIC')
61
+ record(g.privilege, 'PUBLIC');
62
+ else if (ancestors.has(g.role))
63
+ record(g.privilege, `member of ${g.role}`);
64
+ }
65
+ return [...byPrivilege.entries()]
66
+ .map(([privilege, via]) => ({ privilege, via }))
67
+ .sort((a, b) => a.privilege.localeCompare(b.privilege));
68
+ }
69
+ /**
70
+ * How `role` comes to hold EXECUTE on a function, or `null` when it does not.
71
+ *
72
+ * A function ACL carries one privilege, so the closure {@link effectiveGrants}
73
+ * performs per privilege collapses to a single provenance — but it is the same
74
+ * closure: a grant to PUBLIC and a grant to a role the caller INHERITs from
75
+ * both confer EXECUTE without naming the caller anywhere.
76
+ */
77
+ function effectiveExecute(fn, role, graph) {
78
+ const ancestors = new Set(graph.get(role)?.inheritsFrom ?? []);
79
+ let best = null;
80
+ const rank = (via) => (via === 'direct' ? 0 : via === 'PUBLIC' ? 1 : 2);
81
+ for (const g of fn.grants) {
82
+ const via = g.role === role ? 'direct'
83
+ : g.role === 'PUBLIC' ? 'PUBLIC'
84
+ : ancestors.has(g.role) ? `member of ${g.role}`
85
+ : null;
86
+ if (via && (best === null || rank(via) < rank(best)))
87
+ best = via;
88
+ }
89
+ return best;
90
+ }
91
+ /**
92
+ * Every privilege `role` effectively holds on individual *columns* of `table`,
93
+ * composed exactly like {@link effectiveGrants} — direct, PUBLIC, inherited.
94
+ *
95
+ * A column grant lives in `pg_attribute.attacl` and never appears in the
96
+ * relation's ACL, so a role can hold nothing at all by `effectiveGrants` and
97
+ * still read the columns that matter. Privileges the role already holds on the
98
+ * whole relation are dropped: the column grant then adds no reach, and
99
+ * reporting it would double-count the same access.
100
+ */
101
+ function effectiveColumnGrants(table, role, graph) {
102
+ if (table.columnGrants.length === 0)
103
+ return [];
104
+ const wholeRelation = new Set(effectiveGrants(table, role, graph).map((g) => g.privilege));
105
+ const rank = (via) => (via === 'direct' ? 0 : via === 'PUBLIC' ? 1 : 2);
106
+ const ancestors = new Set(graph.get(role)?.inheritsFrom ?? []);
107
+ const byPrivilege = new Map();
108
+ for (const g of table.columnGrants) {
109
+ if (wholeRelation.has(g.privilege))
110
+ continue;
111
+ const via = g.role === role ? 'direct'
112
+ : g.role === 'PUBLIC' ? 'PUBLIC'
113
+ : ancestors.has(g.role) ? `member of ${g.role}`
114
+ : null;
115
+ if (!via)
116
+ continue;
117
+ const entry = byPrivilege.get(g.privilege);
118
+ if (!entry)
119
+ byPrivilege.set(g.privilege, { via, columns: new Set([g.column]) });
120
+ else {
121
+ entry.columns.add(g.column);
122
+ if (rank(via) < rank(entry.via))
123
+ entry.via = via;
124
+ }
125
+ }
126
+ return [...byPrivilege.entries()]
127
+ .map(([privilege, e]) => ({ privilege, via: e.via, columns: [...e.columns].sort() }))
128
+ .sort((a, b) => a.privilege.localeCompare(b.privilege));
129
+ }
130
+ /**
131
+ * Whether a policy applies to `role` at runtime: named directly, via PUBLIC,
132
+ * or via a role it inherits from (`pg_has_role(..., 'member')` semantics).
133
+ */
134
+ function policyAppliesToRole(p, role, graph) {
135
+ if (p.roles.includes('PUBLIC') || p.roles.includes(role))
136
+ return true;
137
+ const ancestors = graph.get(role)?.inheritsFrom ?? [];
138
+ return ancestors.some((a) => p.roles.includes(a));
139
+ }
140
+ function coveredForRole(table, role, privilege, graph) {
141
+ const clause = coverage_1.CLAUSE_REQUIRED[privilege];
142
+ if (!clause)
143
+ return true;
144
+ return table.policies.some((p) => p.permissive
145
+ && coverage_1.POLICY_CMDS[p.cmd].includes(privilege)
146
+ && policyAppliesToRole(p, role, graph)
147
+ && (clause === 'USING' ? p.using != null : p.withCheck != null));
148
+ }
149
+ /**
150
+ * L1: an *indirect* grant (TO PUBLIC, or inherited from a granted role) on an
151
+ * RLS-enabled table with no applicable permissive policy — a dead grant that
152
+ * A4/A5 cannot see because no direct ACL row names the role. Default-deny
153
+ * admits nothing today; the risk is the grant silently activating the day a
154
+ * broad policy is added.
155
+ */
156
+ function checkIndirectCoverageGaps(table, graph) {
157
+ if (!table.rlsEnabled)
158
+ return [];
159
+ const out = [];
160
+ // PUBLIC grants: dead if no permissive policy provides the clause for anyone.
161
+ for (const g of table.grants) {
162
+ if (g.role !== 'PUBLIC')
163
+ continue;
164
+ const clause = coverage_1.CLAUSE_REQUIRED[g.privilege];
165
+ if (!clause)
166
+ continue;
167
+ const covered = table.policies.some((p) => p.permissive
168
+ && coverage_1.POLICY_CMDS[p.cmd].includes(g.privilege)
169
+ && (clause === 'USING' ? p.using != null : p.withCheck != null));
170
+ if (covered)
171
+ continue;
172
+ out.push({
173
+ code: 'L1',
174
+ severity: 'low',
175
+ category: 'coverage',
176
+ schema: table.schema,
177
+ table: table.name,
178
+ role: 'PUBLIC',
179
+ privilege: g.privilege,
180
+ message: `${g.privilege} is granted TO PUBLIC on ${table.schema}.${table.name} but no permissive policy can admit it — a dead grant under RLS default-deny`,
181
+ hint: 'Revoke the PUBLIC grant: it admits nothing today, and it silently activates for every role the day a broad policy is added.'
182
+ });
183
+ }
184
+ // Inherited grants: evaluate roles that inherit from anything, skipping
185
+ // bypass roles (policies don't apply) and privileges the role also holds
186
+ // directly (A4/A5 own that report).
187
+ for (const [role, attrs] of graph) {
188
+ if (attrs.inheritsFrom.length === 0 || attrs.bypassRls)
189
+ continue;
190
+ if (role === table.owner)
191
+ continue;
192
+ const direct = new Set(table.grants.filter((g) => g.role === role).map((g) => g.privilege));
193
+ for (const eff of effectiveGrants(table, role, graph)) {
194
+ if (eff.via === 'direct' || eff.via === 'PUBLIC')
195
+ continue;
196
+ if (direct.has(eff.privilege))
197
+ continue;
198
+ if (!coverage_1.CLAUSE_REQUIRED[eff.privilege])
199
+ continue;
200
+ if (coveredForRole(table, role, eff.privilege, graph))
201
+ continue;
202
+ out.push({
203
+ code: 'L1',
204
+ severity: 'low',
205
+ category: 'coverage',
206
+ schema: table.schema,
207
+ table: table.name,
208
+ role,
209
+ privilege: eff.privilege,
210
+ message: `Role ${role} holds ${eff.privilege} on ${table.schema}.${table.name} (${eff.via}) but no applicable permissive policy can admit it — a dead inherited grant`,
211
+ hint: `Either add a policy covering ${role} (or the granted role) or revoke ${eff.privilege} from the granted role — today the grant admits nothing.`
212
+ });
213
+ }
214
+ }
215
+ return out;
216
+ }
217
+ /**
218
+ * L2: a permissive policy names a role that holds no grant the policy could
219
+ * mediate — authorization written for a role that cannot reach the table.
220
+ * Either the grant was forgotten (a broken feature) or the policy is stale.
221
+ */
222
+ function checkDeadPolicies(table, graph) {
223
+ if (!table.rlsEnabled)
224
+ return [];
225
+ const out = [];
226
+ const anyGrantee = table.grants.some((g) => g.role !== table.owner)
227
+ || table.columnGrants.some((g) => g.role !== table.owner);
228
+ for (const policy of table.policies) {
229
+ if (!policy.permissive)
230
+ continue;
231
+ const privileges = coverage_1.POLICY_CMDS[policy.cmd];
232
+ if (policy.roles.includes('PUBLIC')) {
233
+ if (!anyGrantee) {
234
+ out.push(deadPolicyFinding(table, policy, 'PUBLIC', `Permissive policy ${policy.name} on ${table.schema}.${table.name} applies to PUBLIC but no role holds any grant on the table`));
235
+ }
236
+ continue;
237
+ }
238
+ for (const role of policy.roles) {
239
+ const attrs = graph.get(role);
240
+ if (attrs?.bypassRls)
241
+ continue; // policies never apply to it anyway
242
+ // A column grant is a grant: the policy it mediates is not dead just
243
+ // because the privilege is scoped to a projection of the relation.
244
+ const held = [
245
+ ...effectiveGrants(table, role, graph),
246
+ ...effectiveColumnGrants(table, role, graph)
247
+ ].map((e) => e.privilege);
248
+ if (privileges.some((p) => held.includes(p)))
249
+ continue;
250
+ out.push(deadPolicyFinding(table, policy, role, `Permissive ${policy.cmd} policy ${policy.name} on ${table.schema}.${table.name} applies to ${role}, but ${role} holds no ${privileges.join('/')} grant (directly, via PUBLIC, or by inheritance)`));
251
+ }
252
+ }
253
+ return out;
254
+ }
255
+ function deadPolicyFinding(table, policy, role, message) {
256
+ return {
257
+ code: 'L2',
258
+ severity: 'low',
259
+ category: 'coverage',
260
+ schema: table.schema,
261
+ table: table.name,
262
+ policy: policy.name,
263
+ role,
264
+ message,
265
+ hint: 'A policy is not a grant: without the underlying privilege the role gets nothing. Grant the privilege if the access is intended, or drop the stale policy.'
266
+ };
267
+ }
268
+ /**
269
+ * L3: an object grant the grantee cannot use because it lacks USAGE on the
270
+ * schema — direct, via PUBLIC, or inherited. The grant is unreachable today
271
+ * and activates silently the day USAGE is granted.
272
+ */
273
+ function checkUnreachableGrants(table, schemaAcls, graph) {
274
+ const acl = schemaAcls.get(table.schema);
275
+ if (!acl)
276
+ return [];
277
+ const usageRoles = new Set(acl.grants.filter((g) => g.privilege === 'USAGE').map((g) => g.role));
278
+ const byRole = new Map();
279
+ for (const g of table.grants) {
280
+ if (g.role === 'PUBLIC')
281
+ continue; // per-role USAGE cannot be asserted for PUBLIC grantees
282
+ const attrs = graph.get(g.role);
283
+ if (!attrs || attrs.isSuper)
284
+ continue; // superusers skip ACL checks entirely
285
+ if (g.role === acl.owner)
286
+ continue;
287
+ if (usageRoles.has('PUBLIC') || usageRoles.has(g.role))
288
+ continue;
289
+ if (attrs.inheritsFrom.some((a) => usageRoles.has(a) || a === acl.owner))
290
+ continue;
291
+ byRole.set(g.role, [...(byRole.get(g.role) ?? []), g.privilege]);
292
+ }
293
+ return [...byRole.entries()].map(([role, privileges]) => ({
294
+ code: 'L3',
295
+ severity: 'low',
296
+ category: 'coverage',
297
+ schema: table.schema,
298
+ table: table.name,
299
+ role,
300
+ privilege: [...new Set(privileges)].sort().join(', '),
301
+ message: `Role ${role} is granted ${[...new Set(privileges)].sort().join(', ')} on ${table.schema}.${table.name} but has no USAGE on schema ${table.schema} — the grant is unreachable`,
302
+ hint: 'Grant USAGE on the schema if the access is intended, or revoke the unreachable object grant so it cannot activate silently later.'
303
+ }));
304
+ }
305
+ /**
306
+ * L4: schema USAGE that opens the door to nothing — the role reaches no
307
+ * relation (directly, via PUBLIC, or by inheritance) and can EXECUTE no
308
+ * function in the schema. Advisory only: sequences, types and future objects
309
+ * are not modeled, so this is a review candidate, not a proof.
310
+ *
311
+ * `views` matters more than its optionality suggests: a role whose only
312
+ * reachable object in the schema is a view holds USAGE that is load-bearing,
313
+ * and L8 exists precisely because that view can be the role's entire read
314
+ * path. Called without them, L4 sees a table-only schema and reports a grant
315
+ * the API depends on as revokable — so pass them wherever they are available.
316
+ */
317
+ function checkDeadSchemaUsage(schemaAcls, tables, graph, views = []) {
318
+ const tablesBySchema = new Map();
319
+ for (const t of tables) {
320
+ tablesBySchema.set(t.schema, [...(tablesBySchema.get(t.schema) ?? []), t]);
321
+ }
322
+ const viewsBySchema = new Map();
323
+ for (const v of views) {
324
+ viewsBySchema.set(v.schema, [...(viewsBySchema.get(v.schema) ?? []), v]);
325
+ }
326
+ const out = [];
327
+ for (const acl of schemaAcls) {
328
+ const schemaTables = tablesBySchema.get(acl.schema) ?? [];
329
+ const executes = new Set(acl.executeRoles);
330
+ for (const g of acl.grants) {
331
+ if (g.privilege !== 'USAGE' || g.role === 'PUBLIC')
332
+ continue;
333
+ const attrs = graph.get(g.role);
334
+ if (!attrs || attrs.isSuper)
335
+ continue;
336
+ if (g.role === acl.owner)
337
+ continue;
338
+ const reachesRelation = [...schemaTables, ...(viewsBySchema.get(acl.schema) ?? [])].some((r) => effectiveGrants(r, g.role, graph).length > 0)
339
+ // A column grant is reach the relation's own ACL does not show, and
340
+ // the USAGE it needs is every bit as load-bearing.
341
+ || schemaTables.some((t) => effectiveColumnGrants(t, g.role, graph).length > 0);
342
+ if (reachesRelation)
343
+ continue;
344
+ const reachesFunction = executes.has('PUBLIC')
345
+ || executes.has(g.role)
346
+ || attrs.inheritsFrom.some((a) => executes.has(a));
347
+ if (reachesFunction)
348
+ continue;
349
+ out.push({
350
+ code: 'L4',
351
+ severity: 'info',
352
+ category: 'coverage',
353
+ schema: acl.schema,
354
+ role: g.role,
355
+ privilege: 'USAGE',
356
+ message: `Role ${g.role} has USAGE on schema ${acl.schema} but reaches no relation and can execute no function in it — dead schema USAGE`,
357
+ hint: 'Advisory: sequences, types and default privileges are not modeled. If nothing in the schema is meant for this role, revoke USAGE to keep the surface airtight.'
358
+ });
359
+ }
360
+ }
361
+ return out;
362
+ }
363
+ /**
364
+ * L6: an API-edge role holds privileges on a relation its own API cannot
365
+ * address.
366
+ *
367
+ * The composition the two halves of safegres were built for: the lattice knows
368
+ * what a role effectively holds, exposure reach knows what the generated API
369
+ * can name, and the difference is grant surface that exists for nothing. It is
370
+ * not a leak — the role would have to connect directly to use it — which is
371
+ * exactly why it is the kind of finding that survives for years.
372
+ */
373
+ function checkUnaddressableGrant(table, graph, unaddressable, options) {
374
+ const key = `${table.schema}.${table.name}`;
375
+ if (!unaddressable.has(key) || options.policyReferenced.has(key))
376
+ return [];
377
+ const out = [];
378
+ for (const role of options.roles) {
379
+ const grants = effectiveGrants(table, role, graph).filter((e) => RLS_PRIVILEGES.includes(e.privilege));
380
+ if (grants.length === 0)
381
+ continue;
382
+ const privileges = grants.map((e) => e.privilege).join(', ');
383
+ out.push({
384
+ code: 'L6',
385
+ severity: 'info',
386
+ category: 'coverage',
387
+ schema: table.schema,
388
+ table: table.name,
389
+ role,
390
+ privilege: privileges,
391
+ message: `API role ${role} holds ${privileges} on ${key}, which the generated API declares it cannot address — the grant serves no request`,
392
+ hint: 'Revoke the grant, or drop the behavior denial if the relation is meant to be exposed. No policy predicate references this relation, so nothing else depends on the grant.'
393
+ });
394
+ }
395
+ return out;
396
+ }
397
+ /**
398
+ * L5: an untrusted role reaches an RLS-disabled table *indirectly* — through
399
+ * a grant TO PUBLIC or by inheriting from a granted role. Direct grants are
400
+ * A2/R1 territory; the indirect paths are exactly the ones that slip past
401
+ * exact-name matching (e.g. a world-readable table reached by `anonymous`
402
+ * through PUBLIC).
403
+ */
404
+ function checkUntrustedIndirectAccess(table, graph, options = {}) {
405
+ const untrusted = options.roles ?? [];
406
+ if (untrusted.length === 0 || table.rlsEnabled)
407
+ return [];
408
+ const out = [];
409
+ for (const role of untrusted) {
410
+ const indirect = effectiveGrants(table, role, graph).filter((e) => e.via !== 'direct' && RLS_PRIVILEGES.includes(e.privilege));
411
+ if (indirect.length === 0)
412
+ continue;
413
+ const privileges = indirect.map((e) => e.privilege).join(', ');
414
+ const vias = [...new Set(indirect.map((e) => e.via))].join(', ');
415
+ out.push({
416
+ code: 'L5',
417
+ severity: 'info',
418
+ category: 'anti-pattern',
419
+ schema: table.schema,
420
+ table: table.name,
421
+ role,
422
+ privilege: privileges,
423
+ message: `Untrusted role ${role} reaches ${table.schema}.${table.name} (${privileges}) via ${vias} — RLS is disabled, so every row is visible`,
424
+ hint: 'Narrow the grant to the trusted roles that need it, or enable RLS. PUBLIC and inherited grants apply to untrusted roles even though no ACL row names them.'
425
+ });
426
+ }
427
+ return out;
428
+ }
429
+ /**
430
+ * The direct answer to "what can role X see?": every relation the role
431
+ * effectively reaches, classified by whether RLS mediates the access.
432
+ */
433
+ function computeRoleAccess(tables, graph, roles) {
434
+ const out = [];
435
+ for (const role of roles) {
436
+ const attrs = graph.get(role);
437
+ const entry = {
438
+ role,
439
+ accessibleTables: 0,
440
+ unmediated: [],
441
+ mediated: 0,
442
+ dead: 0
443
+ };
444
+ for (const table of tables) {
445
+ const grants = effectiveGrants(table, role, graph).filter((e) => RLS_PRIVILEGES.includes(e.privilege));
446
+ // Column grants are reported only when the relation is reachable by
447
+ // nothing else — otherwise the whole-relation row already covers them.
448
+ const columnGrants = grants.length > 0
449
+ ? []
450
+ : effectiveColumnGrants(table, role, graph).filter((e) => RLS_PRIVILEGES.includes(e.privilege));
451
+ if (grants.length === 0 && columnGrants.length === 0)
452
+ continue;
453
+ const columns = columnGrants.length > 0
454
+ ? [...new Set(columnGrants.flatMap((g) => g.columns))].sort()
455
+ : undefined;
456
+ const effective = grants.length > 0 ? grants : columnGrants;
457
+ const privileges = effective.map((g) => g.privilege);
458
+ const via = effective.reduce((best, g) => (rankVia(g.via) < rankVia(best) ? g.via : best), effective[0].via);
459
+ if (!table.rlsEnabled || attrs?.bypassRls || (role === table.owner && !table.rlsForced)) {
460
+ entry.accessibleTables += 1;
461
+ entry.unmediated.push({
462
+ schema: table.schema,
463
+ table: table.name,
464
+ privileges,
465
+ via,
466
+ ...(columns ? { columns } : {}),
467
+ access: 'unmediated'
468
+ });
469
+ continue;
470
+ }
471
+ const usable = privileges.some((p) => coveredForRole(table, role, p, graph));
472
+ if (usable) {
473
+ entry.accessibleTables += 1;
474
+ entry.mediated += 1;
475
+ }
476
+ else {
477
+ entry.dead += 1;
478
+ }
479
+ }
480
+ entry.unmediated.sort((a, b) => a.schema === b.schema ? a.table.localeCompare(b.table) : a.schema.localeCompare(b.schema));
481
+ out.push(entry);
482
+ }
483
+ return out;
484
+ }
485
+ function rankVia(via) {
486
+ return via === 'direct' ? 0 : via === 'PUBLIC' ? 1 : 2;
487
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * L16/L17: privileges on the relations that are not tables.
3
+ *
4
+ * Every rule in this package grades a table, a view or a schema, so two kinds
5
+ * of object have carried real privileges past the audit untouched.
6
+ *
7
+ * **L16 — sequences.** `USAGE` or `UPDATE` on a sequence is the right to call
8
+ * `nextval` and `setval`: an untrusted role can burn the identifier space of
9
+ * whatever the sequence feeds, or reset the counter so the next insert
10
+ * collides with an existing row. `SELECT` is the right to read `last_value`,
11
+ * which is a live row-count estimate for the owning table and the classic way
12
+ * an "how many customers do they have" question gets answered through an API
13
+ * that exposes none of them. None of it is row-filterable: RLS does not apply
14
+ * to sequences.
15
+ *
16
+ * **L17 — foreign tables.** A foreign table cannot carry RLS *at all* —
17
+ * Postgres rejects `ALTER FOREIGN TABLE ... ENABLE ROW LEVEL SECURITY` (18) —
18
+ * so a grant on one is unconditionally unfiltered. That makes it strictly
19
+ * worse than the A2 shape it resembles: A2 says "grants and no RLS", and its
20
+ * remedy is to add RLS, which here does not exist as an option.
21
+ *
22
+ * Both are `info` and score-neutral to start, and neither recommends a revoke
23
+ * it cannot justify. That constraint has teeth on sequences in particular: a
24
+ * role holding `INSERT` on a table with a `serial` column *must* hold `USAGE`
25
+ * on its sequence, so the `OWNED BY` link is reported alongside the grant and
26
+ * the remedy leads with identity columns rather than with `REVOKE`.
27
+ */
28
+ import type { ObjectAclSnapshot } from '../pg/objects';
29
+ import type { Finding } from '../types';
30
+ import { type LatticeRoleOptions, type RoleGraph } from './lattice';
31
+ export declare function checkUntrustedSequenceGrants(objects: ObjectAclSnapshot[], graph: RoleGraph, options?: LatticeRoleOptions): Finding[];
32
+ export declare function checkUntrustedForeignTableGrants(objects: ObjectAclSnapshot[], graph: RoleGraph, options?: LatticeRoleOptions): Finding[];
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ /**
3
+ * L16/L17: privileges on the relations that are not tables.
4
+ *
5
+ * Every rule in this package grades a table, a view or a schema, so two kinds
6
+ * of object have carried real privileges past the audit untouched.
7
+ *
8
+ * **L16 — sequences.** `USAGE` or `UPDATE` on a sequence is the right to call
9
+ * `nextval` and `setval`: an untrusted role can burn the identifier space of
10
+ * whatever the sequence feeds, or reset the counter so the next insert
11
+ * collides with an existing row. `SELECT` is the right to read `last_value`,
12
+ * which is a live row-count estimate for the owning table and the classic way
13
+ * an "how many customers do they have" question gets answered through an API
14
+ * that exposes none of them. None of it is row-filterable: RLS does not apply
15
+ * to sequences.
16
+ *
17
+ * **L17 — foreign tables.** A foreign table cannot carry RLS *at all* —
18
+ * Postgres rejects `ALTER FOREIGN TABLE ... ENABLE ROW LEVEL SECURITY` (18) —
19
+ * so a grant on one is unconditionally unfiltered. That makes it strictly
20
+ * worse than the A2 shape it resembles: A2 says "grants and no RLS", and its
21
+ * remedy is to add RLS, which here does not exist as an option.
22
+ *
23
+ * Both are `info` and score-neutral to start, and neither recommends a revoke
24
+ * it cannot justify. That constraint has teeth on sequences in particular: a
25
+ * role holding `INSERT` on a table with a `serial` column *must* hold `USAGE`
26
+ * on its sequence, so the `OWNED BY` link is reported alongside the grant and
27
+ * the remedy leads with identity columns rather than with `REVOKE`.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.checkUntrustedSequenceGrants = checkUntrustedSequenceGrants;
31
+ exports.checkUntrustedForeignTableGrants = checkUntrustedForeignTableGrants;
32
+ const lattice_1 = require("./lattice");
33
+ /** Advancing the counter, versus reading where it has got to. */
34
+ const WRITE_PRIVILEGES = ['USAGE', 'UPDATE'];
35
+ function checkUntrustedSequenceGrants(objects, graph, options = {}) {
36
+ const roles = options.roles ?? [];
37
+ const sequences = objects.filter((o) => o.kind === 'sequence');
38
+ if (roles.length === 0 || sequences.length === 0)
39
+ return [];
40
+ const out = [];
41
+ for (const seq of sequences) {
42
+ for (const role of roles) {
43
+ const grants = (0, lattice_1.effectiveGrants)(seq, role, graph);
44
+ if (grants.length === 0)
45
+ continue;
46
+ const privileges = [...new Set(grants.map((g) => g.privilege))].sort();
47
+ const advances = privileges.some((p) => WRITE_PRIVILEGES.includes(p));
48
+ const reads = privileges.includes('SELECT');
49
+ const effects = [];
50
+ if (advances) {
51
+ effects.push('call `nextval`/`setval` — consuming the identifier space, or resetting the '
52
+ + 'counter so the next insert collides');
53
+ }
54
+ if (reads) {
55
+ effects.push('read `last_value`, a live estimate of how many rows the owning table has '
56
+ + 'taken');
57
+ }
58
+ out.push({
59
+ code: 'L16',
60
+ severity: 'info',
61
+ category: 'anti-pattern',
62
+ schema: seq.schema,
63
+ table: seq.name,
64
+ role,
65
+ privilege: privileges[0],
66
+ message: `Untrusted role ${role} holds ${privileges.join(', ')} on sequence `
67
+ + `${seq.schema}.${seq.name} (${grants[0].via}), so it can ${effects.join(', and ')}`
68
+ + (seq.ownedBy ? ` — the sequence feeds ${seq.ownedBy}` : ''),
69
+ hint: seq.ownedBy
70
+ ? `Check whether ${role} inserts into ${seq.ownedBy.slice(0, seq.ownedBy.lastIndexOf('.'))} `
71
+ + 'before touching this: a role writing a `serial` column needs USAGE, and revoking it '
72
+ + 'breaks the insert. `GENERATED ... AS IDENTITY` needs no grant on the sequence at '
73
+ + 'all, which is the fix that does not trade one break for another. Drop SELECT '
74
+ + 'separately — reading `last_value` is never needed to insert.'
75
+ : 'Nothing row-filters a sequence: RLS does not apply to one. If the role does not '
76
+ + 'insert into a table this sequence feeds, the grant confers only the ability to '
77
+ + 'consume or read the counter — confirm that before revoking, since an `OWNED BY` '
78
+ + 'link is not the only way an insert can depend on it (a DEFAULT can name it).',
79
+ context: {
80
+ objectKind: 'sequence',
81
+ privileges,
82
+ via: grants[0].via,
83
+ ...(seq.ownedBy ? { ownedBy: seq.ownedBy } : {})
84
+ }
85
+ });
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+ function checkUntrustedForeignTableGrants(objects, graph, options = {}) {
91
+ const roles = options.roles ?? [];
92
+ const foreign = objects.filter((o) => o.kind === 'foreign table');
93
+ if (roles.length === 0 || foreign.length === 0)
94
+ return [];
95
+ const out = [];
96
+ for (const ft of foreign) {
97
+ for (const role of roles) {
98
+ const grants = (0, lattice_1.effectiveGrants)(ft, role, graph);
99
+ if (grants.length === 0)
100
+ continue;
101
+ const privileges = [...new Set(grants.map((g) => g.privilege))].sort();
102
+ out.push({
103
+ code: 'L17',
104
+ severity: 'info',
105
+ category: 'anti-pattern',
106
+ schema: ft.schema,
107
+ table: ft.name,
108
+ role,
109
+ privilege: privileges[0],
110
+ message: `Untrusted role ${role} holds ${privileges.join(', ')} on foreign table `
111
+ + `${ft.schema}.${ft.name}${ft.server ? ` (server ${ft.server})` : ''} `
112
+ + `(${grants[0].via}) — a foreign table cannot carry RLS, so every row the remote side `
113
+ + 'returns is returned',
114
+ hint: 'Postgres rejects `ENABLE ROW LEVEL SECURITY` on a foreign table, so unlike an ordinary '
115
+ + 'grant-without-RLS this one cannot be fixed by adding a policy. Expose it through a '
116
+ + 'view that carries the filter (and grant on the view instead), or move the filter to '
117
+ + 'the foreign table\'s own options/remote side. Confirm the API does not serve this '
118
+ + 'relation before revoking.',
119
+ context: {
120
+ objectKind: 'foreign table',
121
+ privileges,
122
+ via: grants[0].via,
123
+ ...(ft.server ? { server: ft.server } : {})
124
+ }
125
+ });
126
+ }
127
+ }
128
+ return out;
129
+ }