@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,382 @@
1
+ /**
2
+ * L8: a view that is not `security_invoker` hands its readers the owner's
3
+ * privileges on every relation its body touches.
4
+ *
5
+ * A view executes as its owner unless it was created `WITH (security_invoker
6
+ * = true)`. So a role holding nothing but SELECT on the view reads the base
7
+ * relations under the *owner's* rights — including tables it has no grant on,
8
+ * and, when the owner owns the base table or bypasses RLS, without the row
9
+ * filter the base table's policies would have applied.
10
+ *
11
+ * Nothing in the catalog-only effective-access layer can see this:
12
+ * `effectiveGrants` never names the calling role on the base relation, and
13
+ * the base relation's ACL never names it either. The edge only exists in the
14
+ * view's *body*, which makes this the first rule that reads SQL rather than
15
+ * pure catalog — the `proof: 'ast'` edge of the reach model in `role-reach.ts`.
16
+ *
17
+ * Two conservatism rules the rest of the analysis depends on:
18
+ *
19
+ * - **A body we cannot read is unknown, not empty.** Dynamic SQL or an
20
+ * unparseable body suppresses the view entirely rather than producing a
21
+ * partial (and therefore misleading) base-relation set.
22
+ * - **The fix is never a revoke.** The grant on the view is what the API
23
+ * serves; the defect is the view executing as its owner. L8 recommends
24
+ * `security_invoker = true` or a different owner, and nothing else.
25
+ */
26
+ import { extractQuery } from '../callgraph/extract';
27
+ import { effectiveColumnGrants, effectiveGrants } from './lattice';
28
+ import { computeViewReach } from './role-reach';
29
+ /** How deep a chain of views on views is followed before giving up. */
30
+ const MAX_VIEW_DEPTH = 8;
31
+ /**
32
+ * Read every definer view's body and resolve the base relations it reaches.
33
+ *
34
+ * Nested views are followed: a definer view over an invoker view still
35
+ * executes the inner body as the *outer* owner, while an inner definer view
36
+ * switches the executing role again. Each resolved relation therefore carries
37
+ * the hops it passed through and the owner of the last one.
38
+ */
39
+ export async function analyzeViewBodies(views, tables, auditedSchemas) {
40
+ // A materialized view stores its rows: reading it touches no base relation,
41
+ // so it is a leaf here, never an edge.
42
+ const queryable = views.filter((v) => !v.materialized);
43
+ const index = buildRelationIndex(queryable, tables, auditedSchemas);
44
+ const bodies = await readBodies(queryable);
45
+ const out = [];
46
+ const suppressed = [];
47
+ for (const view of queryable) {
48
+ if (view.securityInvoker)
49
+ continue;
50
+ const { bases, opaque, tainted } = resolveViewBases(view, index, bodies);
51
+ // An unreadable body is unknown, not empty: its fragmentary relation list
52
+ // is discarded, but the view stays in the model so the gap is reportable
53
+ // rather than a silent clean bill (L15).
54
+ if (opaque)
55
+ suppressed.push({ view: `${view.schema}.${view.name}`, reason: opaque });
56
+ const unreadable = opaque ?? tainted;
57
+ if (!unreadable && bases.length === 0)
58
+ continue;
59
+ out.push({
60
+ schema: view.schema,
61
+ name: view.name,
62
+ owner: view.owner,
63
+ grants: view.grants,
64
+ baseRelations: opaque ? [] : bases,
65
+ ...(unreadable ? { unreadable } : {})
66
+ });
67
+ }
68
+ return { views: out, suppressed };
69
+ }
70
+ export async function readBodies(views) {
71
+ const bodies = new Map();
72
+ for (const v of views)
73
+ bodies.set(`${v.schema}.${v.name}`, await extractQuery(v.definition));
74
+ return bodies;
75
+ }
76
+ /**
77
+ * The base relations `root` reads, following nested views, with the owner in
78
+ * force at each hop.
79
+ *
80
+ * `opaque` is set to the first reason the walk had to stop, and when it is set
81
+ * the relation list is a fragment: the caller must discard it, because a body
82
+ * we could only partly read under-reports what the view reaches.
83
+ *
84
+ * `root` itself need not be in `index` — a materialized view is not a
85
+ * queryable relation for the purposes of resolving *other* bodies, but its own
86
+ * body reads the same way.
87
+ */
88
+ export function resolveViewBases(root, index, bodies) {
89
+ const bases = [];
90
+ const seen = new Set();
91
+ let opaque;
92
+ let tainted;
93
+ const walk = (current, hops) => {
94
+ const key = `${current.schema}.${current.name}`;
95
+ if (hops.length > MAX_VIEW_DEPTH) {
96
+ opaque ??= `view chain deeper than ${MAX_VIEW_DEPTH} hops`;
97
+ return;
98
+ }
99
+ const body = bodies.get(key);
100
+ if (!body)
101
+ return;
102
+ if (body.opaque) {
103
+ opaque ??= body.opaqueReason ?? 'body could not be read';
104
+ return;
105
+ }
106
+ // Readable, but it runs SQL of its own: the relations below are real and
107
+ // the list of them is a lower bound.
108
+ if (body.tainted)
109
+ tainted ??= `${key}: ${body.tainted}`;
110
+ for (const ref of body.tables) {
111
+ const relation = resolveRelation(ref, current.schema, index);
112
+ if (!relation)
113
+ continue; // a CTE, an alias, or a name we cannot pin down
114
+ if (relation.kind === 'view') {
115
+ const nested = relation.view;
116
+ if (`${nested.schema}.${nested.name}` === key)
117
+ continue;
118
+ // An inner definer view re-owns the read; an inner invoker view runs
119
+ // under whichever owner is already in force.
120
+ const owner = nested.securityInvoker ? hops[hops.length - 1].owner : nested.owner;
121
+ walk(nested, [...hops, { view: `${nested.schema}.${nested.name}`, owner }]);
122
+ continue;
123
+ }
124
+ const relKey = `${relation.schema}.${relation.name}`;
125
+ const dedupe = `${relKey}::${hops[hops.length - 1].owner}`;
126
+ if (seen.has(dedupe))
127
+ continue;
128
+ seen.add(dedupe);
129
+ // Which columns escape is the catalog's answer, not the body's: the
130
+ // rewriter recorded one dependency row per column the view reads.
131
+ const columns = current.columnDeps?.find((d) => d.schema === relation.schema && d.table === relation.name)?.columns;
132
+ bases.push({
133
+ schema: relation.schema,
134
+ table: relation.name,
135
+ hops: [...hops],
136
+ ...(relation.kind === 'external' ? { external: true } : {}),
137
+ ...(columns ? { columns } : {})
138
+ });
139
+ }
140
+ };
141
+ walk(root, [{ view: `${root.schema}.${root.name}`, owner: root.owner }]);
142
+ return { bases, ...(opaque ? { opaque } : {}), ...(tainted ? { tainted } : {}) };
143
+ }
144
+ export function buildRelationIndex(views, tables, auditedSchemas) {
145
+ const viewsByName = new Map();
146
+ for (const v of views)
147
+ viewsByName.set(v.name, [...(viewsByName.get(v.name) ?? []), v]);
148
+ return {
149
+ tableKeys: new Set(tables.map((t) => `${t.schema}.${t.name}`)),
150
+ viewKeys: new Map(views.map((v) => [`${v.schema}.${v.name}`, v])),
151
+ viewsByName,
152
+ auditedSchemas: new Set(auditedSchemas ?? [...tables.map((t) => t.schema), ...views.map((v) => v.schema)])
153
+ };
154
+ }
155
+ /**
156
+ * Pin a body reference to a relation in the snapshot.
157
+ *
158
+ * `pg_get_viewdef` qualifies anything outside the creating `search_path` but
159
+ * leaves the rest bare, so an unqualified name is resolved against the view's
160
+ * own schema first and then against the snapshot as a whole — and only when
161
+ * exactly one relation answers to it. An ambiguous or unknown name (a CTE, a
162
+ * table alias, a relation outside the audited schemas) resolves to nothing:
163
+ * over-approximating here would attribute a read to a relation nobody named.
164
+ */
165
+ export function resolveRelation(ref, viewSchema, { tableKeys, viewKeys, viewsByName, auditedSchemas }) {
166
+ const candidates = ref.schema ? [ref.schema] : [viewSchema];
167
+ for (const schema of candidates) {
168
+ const key = `${schema}.${ref.name}`;
169
+ const view = viewKeys.get(key);
170
+ if (view)
171
+ return { kind: 'view', view };
172
+ if (tableKeys.has(key))
173
+ return { kind: 'table', schema, name: ref.name };
174
+ }
175
+ // Qualified and unknown: either a relation the audit skipped (external, and
176
+ // the reach is still real) or a genuine miss inside a schema it did read.
177
+ if (ref.schema) {
178
+ return auditedSchemas.has(ref.schema)
179
+ ? null
180
+ : { kind: 'external', schema: ref.schema, name: ref.name };
181
+ }
182
+ const matches = [
183
+ ...[...tableKeys]
184
+ .filter((k) => k.endsWith(`.${ref.name}`))
185
+ .map((k) => ({ kind: 'table', schema: k.slice(0, -(ref.name.length + 1)), name: ref.name })),
186
+ ...(viewsByName.get(ref.name) ?? []).map((v) => ({ kind: 'view', view: v }))
187
+ ];
188
+ return matches.length === 1 ? matches[0] : null;
189
+ }
190
+ /**
191
+ * L8: an untrusted role reads a base relation through a definer view.
192
+ *
193
+ * Fires once per (role, view, base relation) where the role can SELECT the
194
+ * view, the view executes as someone else, and the role holds no SELECT of
195
+ * its own on the relation the view reads. A `security_invoker` view over the
196
+ * same shape produces nothing — it executes as the caller, so the base
197
+ * relation's own ACL and policies apply and there is no edge to report.
198
+ */
199
+ export function checkDefinerViewBypass(views, tables, graph, options = {}) {
200
+ const untrusted = options.roles ?? [];
201
+ if (untrusted.length === 0 || views.length === 0)
202
+ return [];
203
+ const byKey = new Map(tables.map((t) => [`${t.schema}.${t.name}`, t]));
204
+ const out = [];
205
+ for (const { role, cells } of computeViewReach(views, graph, untrusted)) {
206
+ for (const cell of cells) {
207
+ if (cell.effectiveRole === role)
208
+ continue;
209
+ // A tainted cell names the view, not a base relation: there is nothing
210
+ // here to grade, and L15 reports it instead.
211
+ if (cell.proof === 'opaque-tainted')
212
+ continue;
213
+ const base = byKey.get(`${cell.schema}.${cell.table}`);
214
+ if (!base)
215
+ continue;
216
+ // Already reachable in its own right: the view launders nothing.
217
+ if (effectiveGrants(base, role, graph).some((g) => g.privilege === 'SELECT'))
218
+ continue;
219
+ // Nor does it when the columns that escape are ones the role could
220
+ // already read column-by-column (L13's closure) and the base has no RLS
221
+ // for the owner's read to skip. With RLS on, the projection matching is
222
+ // beside the point: the owner sees rows the caller's policies hide.
223
+ const granted = effectiveColumnGrants(base, role, graph)
224
+ .find((g) => g.privilege === 'SELECT');
225
+ if (cell.columns && granted && !base.rlsEnabled
226
+ && cell.columns.every((c) => granted.columns.includes(c)))
227
+ continue;
228
+ const hops = cell.path.filter((e) => e.kind === 'view');
229
+ const outermost = hops[0];
230
+ const owner = cell.effectiveRole;
231
+ const ownerAttrs = graph.get(owner);
232
+ // The owner is exempt from the base table's policies — either because
233
+ // it owns the table and FORCE is off, or because it bypasses RLS
234
+ // outright — so the rows the policies would have filtered come back.
235
+ const rlsBypassed = base.rlsEnabled
236
+ && (!!ownerAttrs?.bypassRls || (base.owner === owner && !base.rlsForced));
237
+ out.push({
238
+ code: 'L8',
239
+ severity: 'info',
240
+ category: 'anti-pattern',
241
+ schema: base.schema,
242
+ table: base.name,
243
+ role,
244
+ privilege: 'SELECT',
245
+ message: `Untrusted role ${role} reads ${base.schema}.${base.name}`
246
+ + (cell.columns ? ` (${cell.columns.join(', ')})` : '')
247
+ + ` through view ${outermost.view}, `
248
+ + `which executes as its owner ${owner} — ${role} holds no SELECT on the base relation`
249
+ + (rlsBypassed ? `, and ${owner} is not subject to its RLS policies` : ''),
250
+ hint: `The view is not \`security_invoker\`, so Postgres checks ${owner}'s privileges on the base `
251
+ + `relation, not ${role}'s. Recreate it \`WITH (security_invoker = true)\` so the caller's `
252
+ + `own grants and policies apply, or give it an owner whose reach matches what the view is `
253
+ + `meant to expose. Do not revoke the SELECT on the view — that grant is what the API serves.`,
254
+ context: {
255
+ view: outermost.view,
256
+ effectiveRole: owner,
257
+ viaViews: hops.map((h) => h.view),
258
+ baseRlsEnabled: base.rlsEnabled,
259
+ rlsBypassed,
260
+ ...(cell.columns ? { columns: cell.columns } : {}),
261
+ ...(granted ? { grantedColumns: granted.columns } : {}),
262
+ proof: cell.proof
263
+ }
264
+ });
265
+ }
266
+ }
267
+ return out;
268
+ }
269
+ /**
270
+ * L14: a definer view hands an untrusted role a relation the audit never
271
+ * looked at.
272
+ *
273
+ * Excluding a schema is a statement about what is *graded*, not about what is
274
+ * *reachable*. A view in an audited schema can read `information_schema`,
275
+ * `pg_catalog`, an extension's tables, or any schema left out of
276
+ * `exposure.schemas` — and because that relation is not in the snapshot,
277
+ * every rule that grades a base relation silently drops the edge. The view
278
+ * looked clean because the audit could not see the far end.
279
+ *
280
+ * This is the one L-rule that reports an *absence of knowledge*. It says the
281
+ * read happens and that nothing was graded on the other side; it does not
282
+ * claim the relation is sensitive, and it recommends no revoke — the remedy
283
+ * is to bring the schema into scope, or to satisfy yourself that the
284
+ * projection is safe and leave it.
285
+ */
286
+ export function checkUnauditedViewReach(views, graph, options = {}) {
287
+ const untrusted = options.roles ?? [];
288
+ if (untrusted.length === 0 || views.length === 0)
289
+ return [];
290
+ const out = [];
291
+ for (const { role, cells } of computeViewReach(views, graph, untrusted)) {
292
+ for (const cell of cells) {
293
+ if (!cell.external || cell.proof === 'opaque-tainted')
294
+ continue;
295
+ const hops = cell.path.filter((e) => e.kind === 'view');
296
+ const outermost = hops[0];
297
+ if (!outermost)
298
+ continue;
299
+ const owner = cell.effectiveRole;
300
+ out.push({
301
+ code: 'L14',
302
+ severity: 'info',
303
+ category: 'coverage',
304
+ schema: cell.schema,
305
+ table: cell.table,
306
+ role,
307
+ privilege: 'SELECT',
308
+ message: `Untrusted role ${role} reads ${cell.schema}.${cell.table} through view ${outermost.view} `
309
+ + `as its owner ${owner}, and ${cell.schema} is outside the audited schemas — nothing `
310
+ + 'graded what that relation exposes',
311
+ hint: `The read is proven from the view body; its consequences are not, because ${cell.schema} `
312
+ + 'was never introspected. Add the schema to the audit (`schemas`, or remove it from '
313
+ + '`excludeSchemas`) to grade the far end, or confirm the projection is safe to serve. '
314
+ + 'This is a gap in coverage, not a proven leak: do not revoke anything on its strength.',
315
+ context: {
316
+ view: outermost.view,
317
+ effectiveRole: owner,
318
+ viaViews: hops.map((h) => h.view),
319
+ unauditedSchema: cell.schema,
320
+ proof: cell.proof
321
+ }
322
+ });
323
+ }
324
+ }
325
+ return out;
326
+ }
327
+ /**
328
+ * **L15 — an untrusted role reads a view whose body this analysis cannot
329
+ * read.** The coverage sibling of L14: there, the far end was out of scope;
330
+ * here, the body itself is.
331
+ *
332
+ * Two shapes reach this rule, and the distinction is the point of the
333
+ * `tainted` bit in {@link ExtractedBody}. A body that could not be read at all
334
+ * — an unparseable definition, a chain deeper than the hop limit — yields no
335
+ * relations, and every rule that grades a base relation had nothing to grade.
336
+ * A body that *was* read but calls something executing SQL of its own
337
+ * (`dblink`, `query_to_xml`) yields relations that are all real and not all of
338
+ * them: the graded ones still grade, and the gap is what is reported here.
339
+ *
340
+ * Either way the view executes as its owner and an untrusted role can read it,
341
+ * so the reach is proven and only its far end is unknown — which is exactly
342
+ * `proof: 'opaque-tainted'`, the state the reach model has carried since it
343
+ * was written and nothing could produce until now.
344
+ *
345
+ * Like L14 it is `info`/`neutral` and recommends no revoke: an unknown is not
346
+ * a leak, and scoring one would let an unreadable body move the number.
347
+ */
348
+ export function checkUnreadableViewReach(views, graph, options = {}) {
349
+ const untrusted = options.roles ?? [];
350
+ if (untrusted.length === 0 || views.length === 0)
351
+ return [];
352
+ const out = [];
353
+ for (const { role, cells } of computeViewReach(views, graph, untrusted)) {
354
+ for (const cell of cells) {
355
+ if (cell.proof !== 'opaque-tainted')
356
+ continue;
357
+ out.push({
358
+ code: 'L15',
359
+ severity: 'info',
360
+ category: 'coverage',
361
+ schema: cell.schema,
362
+ table: cell.table,
363
+ role,
364
+ privilege: 'SELECT',
365
+ message: `Untrusted role ${role} reads view ${cell.schema}.${cell.table}, which executes as its `
366
+ + `owner ${cell.effectiveRole} and whose body this analysis could not fully read `
367
+ + `(${cell.taint}) — what it reaches under that owner was never graded`,
368
+ hint: 'The read is proven; what it reaches is not, so no view rule graded this path. Rewrite '
369
+ + 'the body so the relations it reads are visible statically, or satisfy yourself that '
370
+ + 'what it returns is safe to serve this role. Reporting stayed silent rather than '
371
+ + 'guessing, and nothing here justifies a revoke.',
372
+ context: {
373
+ view: `${cell.schema}.${cell.table}`,
374
+ effectiveRole: cell.effectiveRole,
375
+ taint: cell.taint,
376
+ proof: cell.proof
377
+ }
378
+ });
379
+ }
380
+ }
381
+ return out;
382
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Structural index-hygiene checks (perf dimension, `X*`).
3
+ *
4
+ * Everything here is pure catalog analysis — deterministic, workload-free,
5
+ * and safe to run in CI against an empty database.
6
+ */
7
+ import type { TableIndexSnapshot } from '../pg/indexes';
8
+ import { type AccessPath } from '../pg/paths';
9
+ import type { Finding } from '../types';
10
+ /**
11
+ * What X1 does with a foreign key whose only evidence is shape — it looks like
12
+ * a write-once provisioning pointer, but nothing has proven the path is
13
+ * unreachable.
14
+ *
15
+ * `report` is the default and the only honest one until a signal exists that
16
+ * can observe the generated API surface: the finding stands, and the shape is
17
+ * attached to it as context so a reviewer can act on it. The other two exist
18
+ * so the decision is a line of config rather than a fork of the scanner.
19
+ *
20
+ * `demote` is applied in `audit()` rather than here, because severities are
21
+ * restamped from the rule registry after the checks run.
22
+ */
23
+ export type WriteOncePointerPolicy = 'report' | 'demote' | 'suppress';
24
+ /**
25
+ * X1: a foreign key with no index that can serve it.
26
+ *
27
+ * An index covers a FK when its *leading* columns are exactly the FK's
28
+ * columns (order among them is irrelevant for equality lookups) and it
29
+ * covers every row. Without one, `ON DELETE`/`ON UPDATE` checks on the
30
+ * referenced side and joins on the referencing side degrade to sequential
31
+ * scans.
32
+ *
33
+ * Partial and expression indexes do not count: the planner cannot rely on
34
+ * them for the referential-integrity lookup.
35
+ *
36
+ * `paths` supplies the access-path signals. They never remove a finding on
37
+ * their own — see {@link WriteOncePointerPolicy} and `classifyPaths`.
38
+ */
39
+ export declare function checkUnindexedForeignKeys(table: TableIndexSnapshot, paths?: Map<string, AccessPath>, onWriteOncePointer?: WriteOncePointerPolicy): Finding[];
40
+ /**
41
+ * X5: an index that another index already serves.
42
+ *
43
+ * Two shapes: an exact duplicate, or a non-unique index whose columns are a
44
+ * leading-column prefix of a wider index. Both cost write throughput and
45
+ * disk for nothing. Constraint-backed indexes (PK/UNIQUE/EXCLUDE) are never
46
+ * reported — dropping them would drop the constraint.
47
+ *
48
+ * An index attached to a parent's partitioned index is never reported either:
49
+ * it can only be dropped with the parent's index, and the parent carries that
50
+ * finding once instead of every partition repeating it. An index created on
51
+ * the partition alone is the partition's own, and is reported there.
52
+ */
53
+ export declare function checkRedundantIndexes(table: TableIndexSnapshot): Finding[];
54
+ /**
55
+ * X6: no primary key and no usable replica identity.
56
+ *
57
+ * Rows cannot be addressed individually: updates and deletes have no cheap
58
+ * lookup path, logical replication and CDC cannot identify old rows, and
59
+ * realtime/change-feed features break.
60
+ */
61
+ export declare function checkMissingPrimaryKey(table: TableIndexSnapshot): Finding | null;
62
+ /**
63
+ * X7: a search column with no index the search can use.
64
+ *
65
+ * A `tsvector` or `vector` column is exposed as a search field by
66
+ * `graphile-search` purely from its type — the schema promises a fast path.
67
+ * Without a GIN/GiST (full-text) or HNSW/IVFFlat (vector) index the promise
68
+ * is served by a sequential scan plus a per-row distance or match computation,
69
+ * which is the single worst plan the API can produce.
70
+ */
71
+ export declare function checkUnindexedSearchColumns(table: TableIndexSnapshot): Finding[];
72
+ /**
73
+ * X8: a sort-shaped column that leads no index.
74
+ *
75
+ * Every PostGraphile connection can be ordered by any column and paginated
76
+ * with a cursor over that order. Sorting on an unindexed column forces a full
77
+ * sort of the (RLS-filtered) result on every page, and keyset pagination
78
+ * degenerates into a scan-and-discard. Timestamp columns are singled out
79
+ * because they are what feeds are actually ordered by — a heuristic, hence
80
+ * `info` by default; turn it off with `perf.rules: { "X8": "off" }`.
81
+ */
82
+ export declare function checkUnindexedSortColumns(table: TableIndexSnapshot): Finding[];