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