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