@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,776 @@
1
+ /**
2
+ * Script A driver: pure-Postgres RLS audit.
3
+ *
4
+ * Ingests a catalog snapshot, runs every check, and returns a structured report.
5
+ */
6
+ import { buildCallGraph } from '../callgraph/graph';
7
+ import { checkSessionUserGating, checkTriviallyPermissive, checkVolatileFunctions, collectFunctionNames, parseOrNull } from '../checks/anti-patterns';
8
+ import { checkUntrustedColumnGrants } from '../checks/column-grants';
9
+ import { checkCoverageGaps, checkUpdateWithCheckCoverage } from '../checks/coverage';
10
+ import { analyzeFunctionBodies, checkDefinerFunctionReach, checkInsteadOfTriggerWrite, checkUnreadableFunctionReach } from '../checks/definer-function';
11
+ import { analyzeViewBodies, checkDefinerViewBypass, checkUnauditedViewReach, checkUnreadableViewReach } from '../checks/definer-view';
12
+ import { checkMissingPrimaryKey, checkRedundantIndexes, checkUnindexedForeignKeys, checkUnindexedSearchColumns, checkUnindexedSortColumns } from '../checks/indexes';
13
+ import { checkDeadPolicies, checkDeadSchemaUsage, checkIndirectCoverageGaps, checkUnaddressableGrant, checkUnreachableGrants, checkUntrustedIndirectAccess, computeRoleAccess } from '../checks/lattice';
14
+ import { checkUntrustedForeignTableGrants, checkUntrustedSequenceGrants } from '../checks/object-acls';
15
+ import { checkNonLeakproofPolicyFunctions, checkPolicyColumnCasts, checkUnhoistedPolicyFunctions, checkUnindexedPolicyColumns, collectPredicateColumns } from '../checks/policy-index';
16
+ import { analyzeRevocableGrants, revocableGrantFindings } from '../checks/revocable-grants';
17
+ import { checkGrantsWithoutRls, checkRlsEnabledNoPolicies, checkRlsNotForced } from '../checks/rls-flags';
18
+ import { checkPublicGrants, checkUntrustedRolePolicies, checkUntrustedRoleWrites } from '../checks/role-trust';
19
+ import { checkSetRoleEscalation } from '../checks/set-role';
20
+ import { checkStats, DEFAULT_STATS_THRESHOLDS } from '../checks/stats';
21
+ import { analyzeViewExposure, checkLeakyFilterView, checkMatviewSnapshot } from '../checks/view-exposure';
22
+ import { analyzeViewWrites, checkDefinerViewWrite, checkUncheckedViewWrite, checkViewRuleBypass } from '../checks/view-writes';
23
+ import { configFingerprint } from '../config/fingerprint';
24
+ import { allAstRulesDisabled, applyRulesToFindings, matchTablePattern, resolveRules, rulesForTable } from '../config/resolve';
25
+ import { resolvePlaneReach, scorePlane, stampPlanes } from '../exposure/planes';
26
+ import { resolveRoutineReach } from '../exposure/routines';
27
+ import { LINT_RULES, LINT_RULES_BY_ID, lintDefinition } from '../lint';
28
+ import { proveFindings } from '../perf/explain';
29
+ import { introspectRoleGraph, introspectSchemaAcls } from '../pg/acl';
30
+ import { resolveExposure, resolvePlanes, resolveReach } from '../pg/exposure';
31
+ import { introspectFunctions } from '../pg/functions';
32
+ import { introspectIndexes, introspectViews } from '../pg/indexes';
33
+ import { asExecutor, introspectTables } from '../pg/introspect';
34
+ import { introspectObjectAcls } from '../pg/objects';
35
+ import { classifyPaths } from '../pg/paths';
36
+ import { lookupVolatility } from '../pg/proc';
37
+ import { introspectReachInputs } from '../pg/reach-inputs';
38
+ import { listAuditableRoles, resolveRoles } from '../pg/roles';
39
+ import { introspectStats } from '../pg/stats';
40
+ import { dimensionOf, RULES_BY_CODE, subjectOf } from '../rules/registry';
41
+ import { computeScore } from '../score/score';
42
+ import { computeScorecards, RESERVED_SCORECARDS } from '../score/scorecards';
43
+ import { summarize } from '../types';
44
+ import { version as PKG_VERSION } from '../version';
45
+ export async function audit(client, options = {}) {
46
+ const exec = asExecutor(client);
47
+ const config = options.config ?? {};
48
+ const resolved = resolveRules(config);
49
+ const extensions = options.extensions ?? config.extensions;
50
+ // Function definitions are read by two independent features (the convention
51
+ // linter and the call graph); introspect them at most once.
52
+ let functionsCache;
53
+ const getFunctions = async () => {
54
+ if (!functionsCache) {
55
+ functionsCache = await introspectFunctions(exec, {
56
+ schemas: options.schemas ?? config.schemas,
57
+ excludeSchemas: options.excludeSchemas ?? config.excludeSchemas,
58
+ extensions
59
+ });
60
+ }
61
+ return functionsCache;
62
+ };
63
+ const statsEnabled = options.stats ?? config.perf?.stats?.enabled ?? false;
64
+ const explainEnabled = options.explain ?? config.perf?.explain?.enabled ?? false;
65
+ // Both tiers are refinements of the perf dimension: asking for either turns
66
+ // it on, but neither is reachable without opting into perf in the first place.
67
+ const perfEnabled = (options.perf ?? config.perf?.enabled ?? false) || statsEnabled || explainEnabled;
68
+ const skipAst = options.skipAstChecks || allAstRulesDisabled(resolved, { perf: perfEnabled });
69
+ // Resolve role set.
70
+ const allRoles = await listAuditableRoles(exec);
71
+ const resolution = resolveRoles(allRoles, options.includeRoles ?? config.roles, options.excludeRoles ?? config.excludeRoles);
72
+ const exposureConfig = options.exposure ?? config.exposure;
73
+ const exposure = await resolveExposure(exec, exposureConfig);
74
+ const exposedSchemas = new Set(exposure.schemas);
75
+ const planes = await resolvePlanes(exec, exposureConfig, exposure);
76
+ // Relation-level precision within those schemas: what the generated API can
77
+ // actually name. Schema membership is the coarse answer; this is the fine
78
+ // one, and only an adapter that can prove it contributes.
79
+ const apiReach = exposure.known
80
+ ? await resolveReach(exec, exposureConfig, {
81
+ schemas: exposure.schemas,
82
+ excludeSchemas: options.excludeSchemas ?? config.excludeSchemas
83
+ })
84
+ : undefined;
85
+ const unaddressable = new Set((apiReach?.unreachable ?? []).map((r) => `${r.schema}.${r.table}`));
86
+ const snapshot = await introspectTables(exec, {
87
+ schemas: options.schemas ?? config.schemas,
88
+ excludeSchemas: options.excludeSchemas ?? config.excludeSchemas,
89
+ roles: resolution.roles,
90
+ extensions
91
+ });
92
+ const isExposed = (schema, table) => {
93
+ if (!exposedSchemas.has(schema))
94
+ return false;
95
+ return table === undefined || !unaddressable.has(`${schema}.${table}`);
96
+ };
97
+ const exposedTables = exposure.known
98
+ ? snapshot.filter((t) => isExposed(t.schema, t.name)).length
99
+ : snapshot.length;
100
+ // Effective-access inputs for the lattice rules: the INHERIT-following
101
+ // role graph and the schema USAGE ACLs. Both are single cheap queries.
102
+ const roleGraph = await introspectRoleGraph(exec);
103
+ const schemaAcls = await introspectSchemaAcls(exec, {
104
+ schemas: options.schemas ?? config.schemas,
105
+ excludeSchemas: options.excludeSchemas ?? config.excludeSchemas
106
+ });
107
+ const schemaAclsByName = new Map(schemaAcls.map((a) => [a.schema, a]));
108
+ // Which schemas the audit actually looked at — the same include/exclude
109
+ // filters the table snapshot used. A view body naming anything outside this
110
+ // set reaches past the audit rather than reaching nothing (L14).
111
+ const auditedSchemas = new Set(schemaAcls.map((a) => a.schema));
112
+ // Roles requests arrive as, and the relations some policy predicate names.
113
+ // L6 needs both: the first is whose grants it is talking about, the second
114
+ // is the veto that stops it recommending a revoke of a load-bearing grant.
115
+ const apiRoles = exposure.roles ?? [];
116
+ const policyReferenced = policyReferencedRelations(snapshot);
117
+ let findings = [];
118
+ // --- Performance dimension (opt-in): index hygiene ---
119
+ const indexSnapshot = perfEnabled
120
+ ? await introspectIndexes(exec, {
121
+ schemas: options.schemas ?? config.schemas,
122
+ excludeSchemas: options.excludeSchemas ?? config.excludeSchemas,
123
+ extensions
124
+ })
125
+ : [];
126
+ const indexesByTable = new Map(indexSnapshot.map((t) => [`${t.schema}.${t.name}`, t]));
127
+ // Views are read for three independent reasons: their bodies refute "nothing
128
+ // queries this column" for the perf paths, their owners carry the L8 reach
129
+ // edge, and their ACLs keep L4 from calling schema USAGE dead when a view is
130
+ // the only thing in the schema the role can reach. One introspection serves
131
+ // all three.
132
+ const definerViewRoles = withExposedRoles(resolved.rules.get('L8')?.options, exposure)?.roles ?? [];
133
+ const viewWriteRoles = withExposedRoles(resolved.rules.get('L9')?.options, exposure)?.roles ?? [];
134
+ const ruleBypassRoles = withExposedRoles(resolved.rules.get('L10')?.options, exposure)?.roles ?? [];
135
+ const uncheckedWriteRoles = withExposedRoles(resolved.rules.get('L18')?.options, exposure)?.roles ?? [];
136
+ const matviewRoles = withExposedRoles(resolved.rules.get('L11')?.options, exposure)?.roles ?? [];
137
+ const leakyViewRoles = withExposedRoles(resolved.rules.get('L12')?.options, exposure)?.roles ?? [];
138
+ const unauditedReachRoles = withExposedRoles(resolved.rules.get('L14')?.options, exposure)?.roles ?? [];
139
+ const unreadableViewRoles = withExposedRoles(resolved.rules.get('L15')?.options, exposure)?.roles ?? [];
140
+ // Function bodies: L19 is the reach a definer function's body confers, L20
141
+ // the write an INSTEAD OF trigger's definer function performs, and L15
142
+ // reports the bodies neither could read.
143
+ const definerFunctionRoles = withExposedRoles(resolved.rules.get('L19')?.options, exposure)?.roles ?? [];
144
+ const triggerWriteRoles = withExposedRoles(resolved.rules.get('L20')?.options, exposure)?.roles ?? [];
145
+ // Sequences and foreign tables: one introspection, two rules, and no reason
146
+ // to run it when neither is configured with a role list.
147
+ const sequenceRoles = withExposedRoles(resolved.rules.get('L16')?.options, exposure)?.roles ?? [];
148
+ const foreignTableRoles = withExposedRoles(resolved.rules.get('L17')?.options, exposure)?.roles ?? [];
149
+ // L21 (`granted − reachable`) runs on the untrusted roles by default — its
150
+ // question is precisely "which of anonymous's grants are load-bearing?" — so
151
+ // it reads its role list from `exposure.anonRoles` unless the config overrides
152
+ // it. It reads function/view bodies, so it is gated on `skipAst` like the
153
+ // other body rules.
154
+ const revocableRoles = withExposedRoles({ rolesFrom: 'anon', ...resolved.rules.get('L21')?.options }, exposure)?.roles ?? [];
155
+ const revocableEnabled = !skipAst && revocableRoles.length > 0 && resolved.rules.get('L21')?.enabled !== false;
156
+ // L8 and L14 are two answers from one body pass: the base relations that
157
+ // were graded, and the ones that could not be because they are out of scope.
158
+ const viewBodiesEnabled = !skipAst
159
+ && ((definerViewRoles.length > 0 && resolved.rules.get('L8')?.enabled !== false)
160
+ || (unauditedReachRoles.length > 0 && resolved.rules.get('L14')?.enabled !== false)
161
+ || (unreadableViewRoles.length > 0 && resolved.rules.get('L15')?.enabled !== false));
162
+ const functionBodiesEnabled = !skipAst
163
+ && ((definerFunctionRoles.length > 0 && resolved.rules.get('L19')?.enabled !== false)
164
+ || (triggerWriteRoles.length > 0 && resolved.rules.get('L20')?.enabled !== false)
165
+ || (unreadableViewRoles.length > 0 && resolved.rules.get('L15')?.enabled !== false));
166
+ const viewExposureEnabled = !skipAst
167
+ && ((matviewRoles.length > 0 && resolved.rules.get('L11')?.enabled !== false)
168
+ || (leakyViewRoles.length > 0 && resolved.rules.get('L12')?.enabled !== false));
169
+ const viewWritesEnabled = !skipAst
170
+ && ((viewWriteRoles.length > 0 && resolved.rules.get('L9')?.enabled !== false)
171
+ || (ruleBypassRoles.length > 0 && resolved.rules.get('L10')?.enabled !== false)
172
+ || (uncheckedWriteRoles.length > 0 && resolved.rules.get('L18')?.enabled !== false));
173
+ const needsViews = (perfEnabled && config.perf?.paths?.infer !== false)
174
+ || resolved.rules.get('L4')?.enabled !== false
175
+ || viewBodiesEnabled
176
+ || viewWritesEnabled
177
+ || viewExposureEnabled
178
+ || functionBodiesEnabled
179
+ || revocableEnabled;
180
+ const viewSnapshot = needsViews
181
+ ? await introspectViews(exec, {
182
+ schemas: options.schemas ?? config.schemas,
183
+ excludeSchemas: options.excludeSchemas ?? config.excludeSchemas
184
+ })
185
+ : [];
186
+ // Evidence about which foreign keys anything reads. Reported alongside the
187
+ // findings; by default it changes neither, because no signal available on an
188
+ // empty database proves a path is unreachable.
189
+ const onWriteOncePointer = config.perf?.paths?.onWriteOncePointer ?? 'report';
190
+ const paths = perfEnabled && config.perf?.paths?.infer !== false
191
+ ? classifyPaths(indexSnapshot, snapshot, viewSnapshot.map((v) => v.definition), {
192
+ minPointers: config.perf?.paths?.minPointers,
193
+ hiddenBackwardRelations: new Set(apiReach?.hiddenBackwardRelations ?? [])
194
+ })
195
+ : new Map();
196
+ for (const table of snapshot) {
197
+ // --- RLS flags (structural) ---
198
+ const a1 = checkRlsEnabledNoPolicies(table);
199
+ if (a1)
200
+ findings.push(a1);
201
+ const a2 = checkGrantsWithoutRls(table);
202
+ if (a2)
203
+ findings.push(a2);
204
+ const a3 = checkRlsNotForced(table);
205
+ if (a3)
206
+ findings.push(a3);
207
+ // --- Grant-vs-policy coverage ---
208
+ findings.push(...checkCoverageGaps(table));
209
+ findings.push(...checkUpdateWithCheckCoverage(table));
210
+ // --- Role-trust (options-driven; per-table overrides can retune roles) ---
211
+ const tableRules = rulesForTable(resolved, table.schema, table.name);
212
+ findings.push(...checkUntrustedRoleWrites(table, withExposedRoles(tableRules.get('R1')?.options, exposure)));
213
+ findings.push(...checkUntrustedRolePolicies(table, withExposedRoles(tableRules.get('R2')?.options, exposure)));
214
+ findings.push(...checkPublicGrants(table));
215
+ // --- Grant/RLS/policy lattice (effective access: PUBLIC + inheritance) ---
216
+ findings.push(...checkIndirectCoverageGaps(table, roleGraph));
217
+ findings.push(...checkDeadPolicies(table, roleGraph));
218
+ findings.push(...checkUnreachableGrants(table, schemaAclsByName, roleGraph));
219
+ findings.push(...checkUntrustedIndirectAccess(table, roleGraph, withExposedRoles(tableRules.get('L5')?.options, exposure)));
220
+ if (unaddressable.size > 0 && apiRoles.length > 0) {
221
+ findings.push(...checkUnaddressableGrant(table, roleGraph, unaddressable, {
222
+ roles: apiRoles,
223
+ policyReferenced
224
+ }));
225
+ }
226
+ findings.push(...checkSetRoleEscalation(table, roleGraph, withExposedRoles(tableRules.get('L7')?.options, exposure)));
227
+ findings.push(...checkUntrustedColumnGrants(table, roleGraph, withExposedRoles(tableRules.get('L13')?.options, exposure)));
228
+ // --- AST-level anti-patterns (and, with perf on, policy-aware index rules) ---
229
+ if (!skipAst) {
230
+ findings.push(...(await auditTableAst(exec, table, perfEnabled ? indexesByTable.get(`${table.schema}.${table.name}`) : undefined)));
231
+ }
232
+ }
233
+ findings.push(...checkDeadSchemaUsage(schemaAcls, snapshot, roleGraph, viewSnapshot));
234
+ // L16/L17 grade relations that are not tables, so they sit outside the
235
+ // per-table loop entirely.
236
+ const objectAclsEnabled = (sequenceRoles.length > 0 && resolved.rules.get('L16')?.enabled !== false)
237
+ || (foreignTableRoles.length > 0 && resolved.rules.get('L17')?.enabled !== false);
238
+ if (objectAclsEnabled) {
239
+ const objects = await introspectObjectAcls(exec, {
240
+ schemas: options.schemas ?? config.schemas,
241
+ excludeSchemas: options.excludeSchemas ?? config.excludeSchemas
242
+ });
243
+ if (sequenceRoles.length > 0 && resolved.rules.get('L16')?.enabled !== false) {
244
+ findings.push(...checkUntrustedSequenceGrants(objects, roleGraph, { roles: sequenceRoles }));
245
+ }
246
+ if (foreignTableRoles.length > 0 && resolved.rules.get('L17')?.enabled !== false) {
247
+ findings.push(...checkUntrustedForeignTableGrants(objects, roleGraph, { roles: foreignTableRoles }));
248
+ }
249
+ }
250
+ // L8 is per-view, not per-table: a view body names its own base relations.
251
+ if (viewBodiesEnabled) {
252
+ const viewBodies = await analyzeViewBodies(viewSnapshot, snapshot, auditedSchemas);
253
+ if (definerViewRoles.length > 0 && resolved.rules.get('L8')?.enabled !== false) {
254
+ findings.push(...checkDefinerViewBypass(viewBodies.views, snapshot, roleGraph, { roles: definerViewRoles }));
255
+ }
256
+ if (unauditedReachRoles.length > 0 && resolved.rules.get('L14')?.enabled !== false) {
257
+ findings.push(...checkUnauditedViewReach(viewBodies.views, roleGraph, { roles: unauditedReachRoles }));
258
+ }
259
+ if (unreadableViewRoles.length > 0 && resolved.rules.get('L15')?.enabled !== false) {
260
+ findings.push(...checkUnreadableViewReach(viewBodies.views, roleGraph, { roles: unreadableViewRoles }));
261
+ }
262
+ }
263
+ // L9/L10/L18 are the write half of the same question, and share one analysis.
264
+ if (viewWritesEnabled) {
265
+ const writes = await analyzeViewWrites(viewSnapshot, snapshot);
266
+ if (viewWriteRoles.length > 0 && resolved.rules.get('L9')?.enabled !== false) {
267
+ findings.push(...checkDefinerViewWrite(writes.autoUpdatable, snapshot, roleGraph, { roles: viewWriteRoles }));
268
+ }
269
+ if (ruleBypassRoles.length > 0 && resolved.rules.get('L10')?.enabled !== false) {
270
+ findings.push(...checkViewRuleBypass(writes.ruleDriven, snapshot, roleGraph, { roles: ruleBypassRoles }));
271
+ }
272
+ if (uncheckedWriteRoles.length > 0 && resolved.rules.get('L18')?.enabled !== false) {
273
+ findings.push(...checkUncheckedViewWrite(writes.unchecked, snapshot, roleGraph, {
274
+ roles: uncheckedWriteRoles
275
+ }));
276
+ }
277
+ }
278
+ // L19/L20 are the function half of "SQL bodies confer privilege": what a
279
+ // definer function's body reaches, and where an INSTEAD OF trigger's write
280
+ // actually lands. One body pass serves both, plus L15's coverage report.
281
+ if (functionBodiesEnabled) {
282
+ const bodies = await analyzeFunctionBodies(await getFunctions(), viewSnapshot, snapshot, auditedSchemas);
283
+ if (definerFunctionRoles.length > 0 && resolved.rules.get('L19')?.enabled !== false) {
284
+ findings.push(...checkDefinerFunctionReach(bodies.functions, snapshot, roleGraph, schemaAclsByName, {
285
+ roles: definerFunctionRoles
286
+ }));
287
+ }
288
+ if (triggerWriteRoles.length > 0 && resolved.rules.get('L20')?.enabled !== false) {
289
+ findings.push(...checkInsteadOfTriggerWrite(bodies.triggers, snapshot, roleGraph, {
290
+ roles: triggerWriteRoles
291
+ }));
292
+ }
293
+ if (unreadableViewRoles.length > 0 && resolved.rules.get('L15')?.enabled !== false) {
294
+ findings.push(...checkUnreadableFunctionReach(bodies.functions, roleGraph, schemaAclsByName, {
295
+ roles: unreadableViewRoles
296
+ }));
297
+ }
298
+ }
299
+ // L11/L12 are the other two things a readable view does: store rows, and
300
+ // filter them without being a boundary. One body pass serves both.
301
+ if (viewExposureEnabled) {
302
+ const exposureViews = await analyzeViewExposure(viewSnapshot, snapshot);
303
+ if (matviewRoles.length > 0 && resolved.rules.get('L11')?.enabled !== false) {
304
+ findings.push(...checkMatviewSnapshot(exposureViews.matviews, snapshot, roleGraph, { roles: matviewRoles }));
305
+ }
306
+ if (leakyViewRoles.length > 0 && resolved.rules.get('L12')?.enabled !== false) {
307
+ findings.push(...checkLeakyFilterView(exposureViews.leaky, snapshot, roleGraph, { roles: leakyViewRoles }));
308
+ }
309
+ }
310
+ // L21: the EXECUTE grants each untrusted role holds that no reachable path
311
+ // exercises. Its own reachability closure — policy predicates, trigger
312
+ // bodies, write-time expressions and views — needs the trigger/expression
313
+ // inputs no other rule reads. The revocable recommendations enter the finding
314
+ // stream; the retained-and-suppressed proof lists ride on the report.
315
+ let revocableGrants;
316
+ if (revocableEnabled) {
317
+ const reachInputs = await introspectReachInputs(exec, {
318
+ schemas: options.schemas ?? config.schemas,
319
+ excludeSchemas: options.excludeSchemas ?? config.excludeSchemas
320
+ });
321
+ revocableGrants = await analyzeRevocableGrants({
322
+ roles: revocableRoles,
323
+ functions: await getFunctions(),
324
+ tables: snapshot,
325
+ views: viewSnapshot,
326
+ graph: roleGraph,
327
+ schemaAcls: schemaAclsByName,
328
+ reachInputs,
329
+ exposedSchemas,
330
+ exposureKnown: exposure.known,
331
+ unaddressable
332
+ });
333
+ findings.push(...revocableGrantFindings(revocableGrants));
334
+ }
335
+ const statsSnapshot = statsEnabled
336
+ ? await introspectStats(exec, {
337
+ schemas: options.schemas ?? config.schemas,
338
+ excludeSchemas: options.excludeSchemas ?? config.excludeSchemas,
339
+ extensions,
340
+ statementLimit: config.perf?.stats?.topStatements
341
+ })
342
+ : null;
343
+ if (perfEnabled) {
344
+ for (const table of indexSnapshot) {
345
+ findings.push(...checkUnindexedForeignKeys(table, paths, onWriteOncePointer));
346
+ findings.push(...checkRedundantIndexes(table));
347
+ findings.push(...checkUnindexedSearchColumns(table));
348
+ findings.push(...checkUnindexedSortColumns(table));
349
+ const x6 = checkMissingPrimaryKey(table);
350
+ if (x6)
351
+ findings.push(x6);
352
+ }
353
+ }
354
+ if (statsSnapshot) {
355
+ findings.push(...checkStats(statsSnapshot, statsThresholds(config)));
356
+ }
357
+ // --- Convention linter (C*): source-level rules over function definitions ---
358
+ const enabledLintRules = LINT_RULES.filter((r) => resolved.rules.get(r.code)?.enabled !== false);
359
+ if (enabledLintRules.length > 0) {
360
+ const lintRuleIds = enabledLintRules.map((r) => r.id);
361
+ for (const fn of await getFunctions()) {
362
+ if (!fn.definition)
363
+ continue;
364
+ const subject = `${fn.schema}.${fn.name}(${fn.args})`;
365
+ const { problems, suppressed } = await lintDefinition(fn.definition, fn.language, subject, { rules: lintRuleIds });
366
+ for (const p of problems)
367
+ findings.push(lintFinding(fn, subject, p));
368
+ for (const s of suppressed)
369
+ findings.push(lintFinding(fn, subject, s, true));
370
+ }
371
+ }
372
+ findings = applyRulesToFindings(resolved, findings);
373
+ // Routine findings are graded by whether an untrusted role can *call the
374
+ // function*, not by whether its schema is on the API surface — see
375
+ // `RuleMeta.subject`. Resolved only when such a finding exists, so an audit
376
+ // with the routine rules off still costs one query less.
377
+ const routineRoles = [...new Set([...apiRoles, ...(exposure.anonRoles ?? [])])];
378
+ let routineReach;
379
+ const isRoutineExposed = async (schema, name) => {
380
+ if (!routineReach) {
381
+ routineReach = resolveRoutineReach(await getFunctions(), {
382
+ roles: routineRoles,
383
+ graph: roleGraph,
384
+ schemaAcls: schemaAclsByName,
385
+ exposedSchemas
386
+ });
387
+ }
388
+ return routineReach.has(`${schema}.${name}`);
389
+ };
390
+ // Stamp direction (from the registry) and exposure on every finding.
391
+ const publicRead = config.public?.read ?? [];
392
+ const perfIgnore = config.perf?.ignore ?? [];
393
+ for (const f of findings) {
394
+ const meta = RULES_BY_CODE.get(f.code);
395
+ if (meta && f.direction === undefined)
396
+ f.direction = meta.direction;
397
+ if (meta && f.dimension === undefined)
398
+ f.dimension = dimensionOf(meta);
399
+ if (exposure.known && f.schema) {
400
+ // L14 names a relation outside the audited schemas by construction, so
401
+ // grading it by its own schema would file every finding as an internal
402
+ // advisory. What is exposed is the view an API role reads it through.
403
+ const viewRef = f.code === 'L14'
404
+ ? f.context?.view
405
+ : undefined;
406
+ // A routine finding names a function somewhere in the finding — in
407
+ // `context.function` for the reach rules, in `schema`/`table` for the
408
+ // convention linter, which files a function under the fields a relation
409
+ // finding uses.
410
+ const routineRef = meta && subjectOf(meta) === 'routine'
411
+ ? f.context?.function?.replace(/\(.*$/, '')
412
+ ?? (f.table ? `${f.schema}.${f.table}` : undefined)
413
+ : undefined;
414
+ if (routineRef) {
415
+ const dot = routineRef.lastIndexOf('.');
416
+ f.exposed = await isRoutineExposed(routineRef.slice(0, dot), routineRef.slice(dot + 1));
417
+ }
418
+ else if (viewRef) {
419
+ f.exposed = isExposed(viewRef.slice(0, viewRef.lastIndexOf('.')), viewRef.slice(viewRef.lastIndexOf('.') + 1));
420
+ }
421
+ else {
422
+ f.exposed = isExposed(f.schema, f.table);
423
+ }
424
+ }
425
+ // A key that looks like a write-once provisioning pointer, where the
426
+ // reviewer has chosen to read the finding rather than gate on it. Applied
427
+ // here because severities are restamped from the rule registry above.
428
+ if (onWriteOncePointer === 'demote' && f.code === 'X1'
429
+ && f.context?.pathAssessment === 'write-once-shaped') {
430
+ f.acknowledged = true;
431
+ f.severity = 'info';
432
+ f.message += ' — write-once-shaped (perf.paths.onWriteOncePointer)';
433
+ }
434
+ // Declared-intentional perf debt: cold tables, tiny lookups, anything the
435
+ // planner will seq-scan regardless. Reported, but off the perf score.
436
+ if (f.dimension === 'perf' && f.schema && f.table
437
+ && perfIgnore.some((p) => matchTablePattern(p, `${f.schema}.${f.table}`))) {
438
+ f.acknowledged = true;
439
+ f.severity = 'info';
440
+ f.message += ' — acknowledged (perf.ignore)';
441
+ f.hint = 'This table is declared in `perf.ignore`, so the finding is treated as intentional and does not affect the perf score.';
442
+ }
443
+ // Declared-public reads: an open SELECT on a table listed in
444
+ // `public.read` is intent, not a finding — acknowledge it (info,
445
+ // excluded from the score). Undeclared open reads stay scored.
446
+ if (f.code === 'A8' && f.schema && f.table
447
+ && publicRead.some((p) => matchTablePattern(p, `${f.schema}.${f.table}`))) {
448
+ f.acknowledged = true;
449
+ f.severity = 'info';
450
+ f.message += ' — declared public read (public.read)';
451
+ f.hint = 'This table is declared in `public.read`, so the open read is treated as intentional and does not affect the score.';
452
+ }
453
+ }
454
+ // W1: no exposure surface — the whole database is assumed reachable.
455
+ if (!exposure.known && resolved.rules.get('W1')?.enabled !== false) {
456
+ findings.push({
457
+ code: 'W1',
458
+ severity: resolved.rules.get('W1')?.severity ?? 'medium',
459
+ category: 'meta',
460
+ direction: 'neutral',
461
+ message: 'No exposure surface configured — the audit assumes the entire database is reachable and the score is capped',
462
+ hint: 'Declare `exposure.schemas` (or use `exposure.resolver: "constructive"` on a Constructive database) so the score reflects what the exposed APIs can actually reach.'
463
+ });
464
+ }
465
+ // Planner proof runs last: it reads the findings the rules produced and
466
+ // acknowledges the ones the planner disagrees with, before scoring.
467
+ let explainReport;
468
+ if (explainEnabled) {
469
+ explainReport = await proveFindings(exec, findings.filter((f) => f.dimension === 'perf' && !f.acknowledged), indexesByTable, { minRows: config.perf?.explain?.minRows });
470
+ }
471
+ findings.sort(compareFindings);
472
+ const exposureReport = {
473
+ known: exposure.known,
474
+ source: exposure.source,
475
+ plane: planes[0].name,
476
+ schemas: exposure.schemas,
477
+ ...(exposure.roles ? { roles: exposure.roles } : {}),
478
+ ...(exposure.anonRoles && exposure.anonRoles.length > 0
479
+ ? { anonRoles: exposure.anonRoles }
480
+ : {}),
481
+ exposedTables,
482
+ totalTables: snapshot.length,
483
+ ...(apiReach && apiReach.unreachable.length > 0
484
+ ? { unaddressable: apiReach.unreachable.filter((r) => exposedSchemas.has(r.schema)) }
485
+ : {})
486
+ };
487
+ const securityFindings = findings.filter((f) => f.dimension !== 'perf');
488
+ const perfFindings = findings.filter((f) => f.dimension === 'perf');
489
+ const report = {
490
+ version: PKG_VERSION,
491
+ generatedAt: new Date().toISOString(),
492
+ provenance: {
493
+ version: PKG_VERSION,
494
+ fingerprint: configFingerprint(config, PKG_VERSION),
495
+ sealed: options.sealed === true,
496
+ ...(options.preset ? { preset: options.preset } : {})
497
+ },
498
+ summary: summarize(findings),
499
+ findings,
500
+ score: computeScore(securityFindings, config.scoring, {
501
+ exposedTables,
502
+ exposureKnown: exposure.known
503
+ }),
504
+ exposure: exposureReport
505
+ };
506
+ // Access planes. The primary plane's score is `report.score` — computed
507
+ // above, against the exposure surface — so declaring planes can never move
508
+ // the headline number; the secondaries answer what the headline cannot.
509
+ const reaches = resolvePlaneReach(planes, snapshot, roleGraph, apiReach);
510
+ stampPlanes(findings, reaches);
511
+ if (reaches.length > 1) {
512
+ report.planes = reaches.map((reach) => {
513
+ if (reach.skipped) {
514
+ return {
515
+ ...scorePlane(reach, [], config.scoring, exposure.known),
516
+ skipped: reach.skipped
517
+ };
518
+ }
519
+ if (reach.plane.primary) {
520
+ return {
521
+ ...scorePlane(reach, securityFindings, config.scoring, exposure.known),
522
+ exposedTables,
523
+ score: report.score,
524
+ summary: summarize(securityFindings.filter((f) => f.exposed !== false))
525
+ };
526
+ }
527
+ return scorePlane(reach, securityFindings, config.scoring, exposure.known);
528
+ });
529
+ }
530
+ // Per-role exposure: computed for the configured untrusted roles (L5/R1
531
+ // options), so the report answers "what can anonymous access?" directly
532
+ // even when no finding fires.
533
+ const untrustedRoles = [...new Set([
534
+ ...(withExposedRoles(resolved.rules.get('L5')?.options, exposure)?.roles ?? []),
535
+ ...(withExposedRoles(resolved.rules.get('R1')?.options, exposure)?.roles ?? [])
536
+ ])].sort();
537
+ if (untrustedRoles.length > 0) {
538
+ const roleAccess = {
539
+ roles: computeRoleAccess(snapshot, roleGraph, untrustedRoles)
540
+ };
541
+ report.roleAccess = roleAccess;
542
+ }
543
+ if (revocableGrants)
544
+ report.revocableGrants = revocableGrants;
545
+ if (perfEnabled) {
546
+ // `S*` findings are scored by default — opting into `--stats` is the
547
+ // opt-in — but can be demoted to advisories for a deterministic grade.
548
+ const includeStats = config.perf?.scoring?.includeStats ?? true;
549
+ const scorable = includeStats
550
+ ? perfFindings
551
+ : perfFindings.filter((f) => !f.code.startsWith('S'));
552
+ const perf = {
553
+ findings: perfFindings,
554
+ summary: summarize(perfFindings),
555
+ score: computeScore(scorable, config.perf?.scoring, {
556
+ exposedTables,
557
+ exposureKnown: exposure.known
558
+ })
559
+ };
560
+ if (paths.size > 0) {
561
+ const all = [...paths.values()];
562
+ const shaped = all.filter((p) => p.assessment === 'write-once-shaped');
563
+ const declaredHidden = all.filter((p) => p.signals.some((s) => s.name === 'behavior-hidden')).length;
564
+ perf.paths = {
565
+ total: paths.size,
566
+ read: all.filter((p) => p.assessment === 'read').length,
567
+ writeOnceShaped: shaped.length,
568
+ tables: new Set(shaped.map((p) => `${p.schema}.${p.table}`)).size,
569
+ onWriteOncePointer,
570
+ ...(declaredHidden > 0 ? { declaredHidden } : {})
571
+ };
572
+ }
573
+ if (statsSnapshot) {
574
+ const stats = {
575
+ source: 'live',
576
+ tables: statsSnapshot.tables.length,
577
+ statsReset: statsSnapshot.statsReset,
578
+ scored: includeStats
579
+ };
580
+ if (statsSnapshot.statementsUnavailable) {
581
+ stats.notes = [statsSnapshot.statementsUnavailable];
582
+ }
583
+ perf.stats = stats;
584
+ }
585
+ if (explainReport)
586
+ perf.explain = explainReport;
587
+ report.perf = perf;
588
+ }
589
+ // Scorecards last: they are named questions asked of the finished finding
590
+ // set, so every stamp a scorecard can select on (exposure, planes, perf
591
+ // dimension) is already on the findings by now.
592
+ report.scorecards = computeScorecards(findings, report.score, { ...RESERVED_SCORECARDS, ...(config.scorecards ?? {}) }, { exposedTables, totalTables: snapshot.length, exposureKnown: exposure.known });
593
+ if (options.callGraph) {
594
+ const functions = await getFunctions();
595
+ report.callGraph = await buildCallGraph({
596
+ functions,
597
+ tables: snapshot,
598
+ exposedSchemas: exposure.known ? exposure.schemas : undefined,
599
+ apiRoles: exposure.roles && exposure.roles.length > 0
600
+ ? exposure.roles
601
+ : ['anonymous', 'authenticated']
602
+ });
603
+ }
604
+ return report;
605
+ }
606
+ /**
607
+ * Materializes `rolesFrom: 'exposure'` into concrete role names. An adapter
608
+ * resolves the API-edge roles from the catalog (a Constructive API's
609
+ * `anon_role`, PostgREST's `pgrst.db_anon_role`, the role a graphile
610
+ * authenticator can `SET ROLE` to); those roles are exactly the untrusted
611
+ * ones, and their names are per-deployment, so a preset names the *source*
612
+ * instead of the roles. Explicit `roles` still apply — the two union.
613
+ */
614
+ function withExposedRoles(options, exposure) {
615
+ if (!options?.rolesFrom)
616
+ return options;
617
+ const resolved = options.rolesFrom === 'anon' ? exposure.anonRoles : exposure.roles;
618
+ const roles = [...new Set([...(options.roles ?? []), ...(resolved ?? [])])].sort();
619
+ return { ...options, roles };
620
+ }
621
+ /** Stats floors, config over defaults. */
622
+ function statsThresholds(config) {
623
+ const stats = config.perf?.stats ?? {};
624
+ return {
625
+ minRows: stats.minRows ?? DEFAULT_STATS_THRESHOLDS.minRows,
626
+ seqScanRatio: stats.seqScanRatio ?? DEFAULT_STATS_THRESHOLDS.seqScanRatio,
627
+ minIndexBytes: stats.minIndexBytes ?? DEFAULT_STATS_THRESHOLDS.minIndexBytes,
628
+ deadTupleRatio: stats.deadTupleRatio ?? DEFAULT_STATS_THRESHOLDS.deadTupleRatio,
629
+ minTimeShare: stats.minTimeShare ?? DEFAULT_STATS_THRESHOLDS.minTimeShare,
630
+ topStatements: stats.topStatements ?? DEFAULT_STATS_THRESHOLDS.topStatements
631
+ };
632
+ }
633
+ async function auditTableAst(exec, table, indexes) {
634
+ if (table.policies.length === 0)
635
+ return [];
636
+ // Collect all function names referenced across this table's policies so we
637
+ // can resolve volatility in one round-trip.
638
+ const funcNames = [];
639
+ const parsed = [];
640
+ for (const p of table.policies) {
641
+ const using = await parseOrNull(p.using, `${table.schema}.${table.name}.${p.name} USING`);
642
+ const withCheck = await parseOrNull(p.withCheck, `${table.schema}.${table.name}.${p.name} WITH CHECK`);
643
+ parsed.push({ policy: p, using, withCheck });
644
+ if (using)
645
+ funcNames.push(...collectFunctionNames(using));
646
+ if (withCheck)
647
+ funcNames.push(...collectFunctionNames(withCheck));
648
+ }
649
+ let volatility;
650
+ try {
651
+ volatility = await lookupVolatility(exec, funcNames);
652
+ }
653
+ catch {
654
+ volatility = new Map();
655
+ }
656
+ const findings = [];
657
+ const predicateColumns = new Map();
658
+ for (const { policy, using, withCheck } of parsed) {
659
+ const trivial = checkTriviallyPermissive(table, policy, using, withCheck);
660
+ if (trivial)
661
+ findings.push(trivial);
662
+ if (using) {
663
+ findings.push(...checkVolatileFunctions(table, using, volatility, policy.name));
664
+ findings.push(...checkSessionUserGating(table, using, policy.name));
665
+ }
666
+ if (withCheck) {
667
+ findings.push(...checkVolatileFunctions(table, withCheck, volatility, policy.name));
668
+ findings.push(...checkSessionUserGating(table, withCheck, policy.name));
669
+ }
670
+ if (!indexes)
671
+ continue;
672
+ // --- Policy-aware perf rules (X2/X3/X4/X9) ---
673
+ const cols = [];
674
+ if (using) {
675
+ cols.push(...collectPredicateColumns(using, 'USING', table.name));
676
+ findings.push(...checkNonLeakproofPolicyFunctions(table, using, volatility, policy.name));
677
+ findings.push(...checkUnhoistedPolicyFunctions(table, using, volatility, policy.name, 'USING'));
678
+ }
679
+ if (withCheck) {
680
+ cols.push(...collectPredicateColumns(withCheck, 'WITH CHECK', table.name));
681
+ findings.push(...checkNonLeakproofPolicyFunctions(table, withCheck, volatility, policy.name));
682
+ findings.push(...checkUnhoistedPolicyFunctions(table, withCheck, volatility, policy.name, 'WITH CHECK'));
683
+ }
684
+ if (cols.length > 0)
685
+ predicateColumns.set(policy.name, cols);
686
+ }
687
+ if (indexes && predicateColumns.size > 0) {
688
+ findings.push(...checkUnindexedPolicyColumns(table, indexes, predicateColumns));
689
+ findings.push(...checkPolicyColumnCasts(table, indexes, predicateColumns));
690
+ }
691
+ return dedupe(findings);
692
+ }
693
+ /**
694
+ * Relations named by any RLS policy predicate in the snapshot.
695
+ *
696
+ * Whole-word token matching over the predicate text, matching both the bare
697
+ * and the schema-qualified name. Deliberately over-eager: a spurious match
698
+ * costs one unreported L6, a missed one costs a recommendation to revoke a
699
+ * grant that authorization depends on.
700
+ */
701
+ function policyReferencedRelations(tables) {
702
+ const tokens = new Set();
703
+ for (const table of tables) {
704
+ for (const policy of table.policies) {
705
+ for (const clause of [policy.using, policy.withCheck]) {
706
+ if (!clause)
707
+ continue;
708
+ for (const match of clause.matchAll(/[A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)?/g)) {
709
+ tokens.add(match[0].toLowerCase());
710
+ }
711
+ }
712
+ }
713
+ }
714
+ const referenced = new Set();
715
+ for (const table of tables) {
716
+ const key = `${table.schema}.${table.name}`;
717
+ if (tokens.has(key.toLowerCase()) || tokens.has(table.name.toLowerCase())) {
718
+ referenced.add(key);
719
+ }
720
+ }
721
+ return referenced;
722
+ }
723
+ /**
724
+ * Map a source-level lint problem onto a `Finding`. The function's name rides
725
+ * in the `table` slot so overrides, exposure and sorting key on it the same
726
+ * way they do for table findings. Suppressed problems are emitted as
727
+ * acknowledged findings — visible as accepted risk, off the score — carrying
728
+ * their waiver reason and scope in `context`.
729
+ */
730
+ function lintFinding(fn, subject, problem, suppressed = false) {
731
+ const meta = LINT_RULES_BY_ID.get(problem.ruleId);
732
+ return {
733
+ code: meta.code,
734
+ severity: RULES_BY_CODE.get(meta.code).defaultSeverity,
735
+ category: 'convention',
736
+ schema: fn.schema,
737
+ table: fn.name,
738
+ message: `${problem.message} in ${subject}`,
739
+ ...(problem.hint ? { hint: problem.hint } : {}),
740
+ ...(suppressed ? { acknowledged: true } : {}),
741
+ context: {
742
+ ...problem.context,
743
+ function: subject,
744
+ line: problem.line,
745
+ ...(suppressed
746
+ ? {
747
+ suppressed: true,
748
+ reason: problem.reason,
749
+ suppressionScope: problem.scope
750
+ }
751
+ : {})
752
+ }
753
+ };
754
+ }
755
+ function compareFindings(a, b) {
756
+ const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
757
+ if (order[a.severity] !== order[b.severity])
758
+ return order[a.severity] - order[b.severity];
759
+ if (a.schema !== b.schema)
760
+ return (a.schema ?? '').localeCompare(b.schema ?? '');
761
+ if (a.table !== b.table)
762
+ return (a.table ?? '').localeCompare(b.table ?? '');
763
+ return a.code.localeCompare(b.code);
764
+ }
765
+ function dedupe(findings) {
766
+ const seen = new Set();
767
+ const out = [];
768
+ for (const f of findings) {
769
+ const key = [f.code, f.schema, f.table, f.policy, f.message].join('::');
770
+ if (seen.has(key))
771
+ continue;
772
+ seen.add(key);
773
+ out.push(f);
774
+ }
775
+ return out;
776
+ }