@safegres/test 0.0.1 → 1.30.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (483) hide show
  1. package/README.md +825 -412
  2. package/adapters.d.ts +8 -0
  3. package/adapters.js +18 -0
  4. package/ast/helpers.d.ts +16 -0
  5. package/ast/helpers.js +40 -0
  6. package/ast/parse.d.ts +31 -0
  7. package/ast/parse.js +87 -0
  8. package/ast/walk.d.ts +27 -0
  9. package/ast/walk.js +79 -0
  10. package/callgraph/baseline.d.ts +28 -0
  11. package/callgraph/baseline.js +55 -0
  12. package/callgraph/extract.d.ts +93 -0
  13. package/callgraph/extract.js +340 -0
  14. package/callgraph/graph.d.ts +82 -0
  15. package/callgraph/graph.js +276 -0
  16. package/checks/anti-patterns.d.ts +53 -0
  17. package/checks/anti-patterns.js +247 -0
  18. package/checks/column-grants.d.ts +20 -0
  19. package/checks/column-grants.js +74 -0
  20. package/checks/coverage.d.ts +43 -0
  21. package/checks/coverage.js +110 -0
  22. package/checks/definer-function.d.ts +91 -0
  23. package/checks/definer-function.js +458 -0
  24. package/checks/definer-view.d.ts +167 -0
  25. package/checks/definer-view.js +392 -0
  26. package/checks/indexes.d.ts +82 -0
  27. package/checks/indexes.js +263 -0
  28. package/checks/lattice.d.ts +179 -0
  29. package/checks/lattice.js +487 -0
  30. package/checks/object-acls.d.ts +32 -0
  31. package/checks/object-acls.js +129 -0
  32. package/checks/policy-index.d.ts +78 -0
  33. package/checks/policy-index.js +349 -0
  34. package/checks/revocable-grants.d.ts +140 -0
  35. package/checks/revocable-grants.js +377 -0
  36. package/checks/rls-flags.d.ts +25 -0
  37. package/checks/rls-flags.js +82 -0
  38. package/checks/role-reach.d.ts +329 -0
  39. package/checks/role-reach.js +277 -0
  40. package/checks/role-trust.d.ts +45 -0
  41. package/checks/role-trust.js +91 -0
  42. package/checks/set-role.d.ts +21 -0
  43. package/checks/set-role.js +66 -0
  44. package/checks/stats.d.ts +52 -0
  45. package/checks/stats.js +192 -0
  46. package/checks/view-exposure.d.ts +68 -0
  47. package/checks/view-exposure.js +236 -0
  48. package/checks/view-writes.d.ts +88 -0
  49. package/checks/view-writes.js +350 -0
  50. package/ci/baseline.d.ts +104 -0
  51. package/ci/baseline.js +111 -0
  52. package/ci/github.d.ts +47 -0
  53. package/ci/github.js +197 -0
  54. package/cli/audit.d.ts +3 -0
  55. package/cli/audit.js +560 -0
  56. package/cli/baseline.d.ts +3 -0
  57. package/cli/baseline.js +104 -0
  58. package/cli/commands.d.ts +2 -0
  59. package/cli/commands.js +60 -0
  60. package/cli/doctor.d.ts +3 -0
  61. package/cli/doctor.js +72 -0
  62. package/cli/eval.d.ts +3 -0
  63. package/cli/eval.js +153 -0
  64. package/cli/print-config.d.ts +3 -0
  65. package/cli/print-config.js +54 -0
  66. package/cli/shared.d.ts +43 -0
  67. package/cli/shared.js +153 -0
  68. package/cli.d.ts +2 -0
  69. package/cli.js +69 -0
  70. package/commands/audit.d.ts +65 -0
  71. package/commands/audit.js +779 -0
  72. package/commands/doctor.d.ts +19 -0
  73. package/commands/doctor.js +181 -0
  74. package/commands/eval.d.ts +67 -0
  75. package/commands/eval.js +93 -0
  76. package/config/fingerprint.d.ts +40 -0
  77. package/config/fingerprint.js +71 -0
  78. package/config/loader.d.ts +44 -0
  79. package/config/loader.js +210 -0
  80. package/config/presets.d.ts +95 -0
  81. package/config/presets.js +327 -0
  82. package/config/resolve.d.ts +47 -0
  83. package/config/resolve.js +192 -0
  84. package/config/schema.d.ts +54 -0
  85. package/config/schema.js +269 -0
  86. package/config/types.d.ts +562 -0
  87. package/config/validate.d.ts +14 -0
  88. package/config/validate.js +125 -0
  89. package/corpus/README.md +83 -0
  90. package/corpus/bootstrap.sql +14 -0
  91. package/corpus/cases/01-anon-write-grant/case.json +26 -0
  92. package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
  93. package/corpus/cases/02-grant-to-public/case.json +25 -0
  94. package/corpus/cases/02-grant-to-public/schema.sql +18 -0
  95. package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
  96. package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
  97. package/corpus/cases/04-permissive-write-policy/case.json +30 -0
  98. package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
  99. package/corpus/cases/05-public-read-policy/case.json +25 -0
  100. package/corpus/cases/05-public-read-policy/schema.sql +16 -0
  101. package/corpus/cases/06-update-without-with-check/case.json +25 -0
  102. package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
  103. package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
  104. package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
  105. package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
  106. package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
  107. package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
  108. package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
  109. package/corpus/cases/10-rls-not-forced/case.json +24 -0
  110. package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
  111. package/corpus/cases/11-session-user-in-policy/case.json +24 -0
  112. package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
  113. package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
  114. package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
  115. package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
  116. package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
  117. package/corpus/cases/14-dead-policy/case.json +24 -0
  118. package/corpus/cases/14-dead-policy/schema.sql +18 -0
  119. package/corpus/cases/15-unreachable-grant/case.json +24 -0
  120. package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
  121. package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
  122. package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
  123. package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
  124. package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
  125. package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
  126. package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
  127. package/corpus/cases/19-policy-column-cast/case.json +24 -0
  128. package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
  129. package/corpus/cases/20-stable-function-per-row/case.json +24 -0
  130. package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
  131. package/corpus/cases/21-table-without-primary-key/case.json +24 -0
  132. package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
  133. package/corpus/cases/22-redundant-index/case.json +24 -0
  134. package/corpus/cases/22-redundant-index/schema.sql +16 -0
  135. package/corpus/cases/23-tsvector-without-index/case.json +24 -0
  136. package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
  137. package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
  138. package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
  139. package/corpus/cases/25-definer-view-bypass/case.json +31 -0
  140. package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
  141. package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
  142. package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
  143. package/corpus/cases/27-definer-view-write/case.json +36 -0
  144. package/corpus/cases/27-definer-view-write/schema.sql +40 -0
  145. package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
  146. package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
  147. package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
  148. package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
  149. package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
  150. package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
  151. package/corpus/cases/31-matview-snapshot/case.json +32 -0
  152. package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
  153. package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
  154. package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
  155. package/corpus/cases/33-leaky-filter-view/case.json +36 -0
  156. package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
  157. package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
  158. package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
  159. package/corpus/cases/35-column-grant-projection/case.json +27 -0
  160. package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
  161. package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
  162. package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
  163. package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
  164. package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
  165. package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
  166. package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
  167. package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
  168. package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
  169. package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
  170. package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
  171. package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
  172. package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
  173. package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
  174. package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
  175. package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
  176. package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
  177. package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
  178. package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
  179. package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
  180. package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
  181. package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
  182. package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
  183. package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
  184. package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
  185. package/corpus/cases/41-unreadable-view-body/case.json +33 -0
  186. package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
  187. package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
  188. package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
  189. package/corpus/cases/42-search-column-indexed/case.json +21 -0
  190. package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
  191. package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
  192. package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
  193. package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
  194. package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
  195. package/corpus/cases/45-anon-foreign-table/case.json +27 -0
  196. package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
  197. package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
  198. package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
  199. package/corpus/cases/47-checked-writable-view/case.json +37 -0
  200. package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
  201. package/corpus/cases/48-definer-function-reach/case.json +38 -0
  202. package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
  203. package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
  204. package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
  205. package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
  206. package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
  207. package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
  208. package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
  209. package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
  210. package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
  211. package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
  212. package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
  213. package/corpus/index.d.ts +79 -0
  214. package/corpus/index.js +125 -0
  215. package/esm/adapters.d.ts +8 -0
  216. package/esm/adapters.js +6 -0
  217. package/esm/ast/helpers.d.ts +16 -0
  218. package/esm/ast/helpers.js +36 -0
  219. package/esm/ast/parse.d.ts +31 -0
  220. package/esm/ast/parse.js +82 -0
  221. package/esm/ast/walk.d.ts +27 -0
  222. package/esm/ast/walk.js +72 -0
  223. package/esm/callgraph/baseline.d.ts +28 -0
  224. package/esm/callgraph/baseline.js +48 -0
  225. package/esm/callgraph/extract.d.ts +93 -0
  226. package/esm/callgraph/extract.js +333 -0
  227. package/esm/callgraph/graph.d.ts +82 -0
  228. package/esm/callgraph/graph.js +273 -0
  229. package/esm/checks/anti-patterns.d.ts +53 -0
  230. package/esm/checks/anti-patterns.js +240 -0
  231. package/esm/checks/column-grants.d.ts +20 -0
  232. package/esm/checks/column-grants.js +71 -0
  233. package/esm/checks/coverage.d.ts +43 -0
  234. package/esm/checks/coverage.js +104 -0
  235. package/esm/checks/definer-function.d.ts +91 -0
  236. package/esm/checks/definer-function.js +452 -0
  237. package/esm/checks/definer-view.d.ts +167 -0
  238. package/esm/checks/definer-view.js +382 -0
  239. package/esm/checks/indexes.d.ts +82 -0
  240. package/esm/checks/indexes.js +256 -0
  241. package/esm/checks/lattice.d.ts +179 -0
  242. package/esm/checks/lattice.js +475 -0
  243. package/esm/checks/object-acls.d.ts +32 -0
  244. package/esm/checks/object-acls.js +125 -0
  245. package/esm/checks/policy-index.d.ts +78 -0
  246. package/esm/checks/policy-index.js +342 -0
  247. package/esm/checks/revocable-grants.d.ts +140 -0
  248. package/esm/checks/revocable-grants.js +373 -0
  249. package/esm/checks/rls-flags.d.ts +25 -0
  250. package/esm/checks/rls-flags.js +77 -0
  251. package/esm/checks/role-reach.d.ts +329 -0
  252. package/esm/checks/role-reach.js +270 -0
  253. package/esm/checks/role-trust.d.ts +45 -0
  254. package/esm/checks/role-trust.js +86 -0
  255. package/esm/checks/set-role.d.ts +21 -0
  256. package/esm/checks/set-role.js +63 -0
  257. package/esm/checks/stats.d.ts +52 -0
  258. package/esm/checks/stats.js +184 -0
  259. package/esm/checks/view-exposure.d.ts +68 -0
  260. package/esm/checks/view-exposure.js +231 -0
  261. package/esm/checks/view-writes.d.ts +88 -0
  262. package/esm/checks/view-writes.js +344 -0
  263. package/esm/ci/baseline.d.ts +104 -0
  264. package/esm/ci/baseline.js +107 -0
  265. package/esm/ci/github.d.ts +47 -0
  266. package/esm/ci/github.js +161 -0
  267. package/esm/cli/audit.d.ts +3 -0
  268. package/esm/cli/audit.js +525 -0
  269. package/esm/cli/baseline.d.ts +3 -0
  270. package/esm/cli/baseline.js +102 -0
  271. package/esm/cli/commands.d.ts +2 -0
  272. package/esm/cli/commands.js +53 -0
  273. package/esm/cli/doctor.d.ts +3 -0
  274. package/esm/cli/doctor.js +67 -0
  275. package/esm/cli/eval.d.ts +3 -0
  276. package/esm/cli/eval.js +115 -0
  277. package/esm/cli/print-config.d.ts +3 -0
  278. package/esm/cli/print-config.js +52 -0
  279. package/esm/cli/shared.d.ts +43 -0
  280. package/esm/cli/shared.js +112 -0
  281. package/esm/cli.d.ts +2 -0
  282. package/esm/cli.js +67 -0
  283. package/esm/commands/audit.d.ts +65 -0
  284. package/esm/commands/audit.js +776 -0
  285. package/esm/commands/doctor.d.ts +19 -0
  286. package/esm/commands/doctor.js +178 -0
  287. package/esm/commands/eval.d.ts +67 -0
  288. package/esm/commands/eval.js +90 -0
  289. package/esm/config/fingerprint.d.ts +40 -0
  290. package/esm/config/fingerprint.js +68 -0
  291. package/esm/config/loader.d.ts +44 -0
  292. package/esm/config/loader.js +172 -0
  293. package/esm/config/presets.d.ts +95 -0
  294. package/esm/config/presets.js +324 -0
  295. package/esm/config/resolve.d.ts +47 -0
  296. package/esm/config/resolve.js +180 -0
  297. package/esm/config/schema.d.ts +54 -0
  298. package/esm/config/schema.js +265 -0
  299. package/esm/config/types.d.ts +562 -0
  300. package/esm/config/validate.d.ts +14 -0
  301. package/esm/config/validate.js +122 -0
  302. package/esm/corpus/index.d.ts +79 -0
  303. package/esm/corpus/index.js +85 -0
  304. package/esm/exposure/adapters.d.ts +152 -0
  305. package/esm/exposure/adapters.js +463 -0
  306. package/esm/exposure/planes.d.ts +59 -0
  307. package/esm/exposure/planes.js +144 -0
  308. package/esm/exposure/reach.d.ts +78 -0
  309. package/esm/exposure/reach.js +105 -0
  310. package/esm/exposure/routines.d.ts +36 -0
  311. package/esm/exposure/routines.js +54 -0
  312. package/esm/index.d.ts +96 -0
  313. package/esm/index.js +57 -6
  314. package/esm/lint/index.d.ts +11 -0
  315. package/esm/lint/index.js +10 -0
  316. package/esm/perf/baseline.d.ts +40 -0
  317. package/esm/perf/baseline.js +83 -0
  318. package/esm/perf/explain.d.ts +46 -0
  319. package/esm/perf/explain.js +201 -0
  320. package/esm/pg/acl.d.ts +72 -0
  321. package/esm/pg/acl.js +186 -0
  322. package/esm/pg/behaviors.d.ts +114 -0
  323. package/esm/pg/behaviors.js +206 -0
  324. package/esm/pg/exposure.d.ts +74 -0
  325. package/esm/pg/exposure.js +198 -0
  326. package/esm/pg/functions.d.ts +52 -0
  327. package/esm/pg/functions.js +128 -0
  328. package/esm/pg/indexes.d.ts +192 -0
  329. package/esm/pg/indexes.js +395 -0
  330. package/esm/pg/introspect.d.ts +134 -0
  331. package/esm/pg/introspect.js +274 -0
  332. package/esm/pg/objects.d.ts +37 -0
  333. package/esm/pg/objects.js +108 -0
  334. package/esm/pg/paths.d.ts +114 -0
  335. package/esm/pg/paths.js +159 -0
  336. package/esm/pg/proc.d.ts +23 -0
  337. package/esm/pg/proc.js +55 -0
  338. package/esm/pg/reach-inputs.d.ts +47 -0
  339. package/esm/pg/reach-inputs.js +133 -0
  340. package/esm/pg/roles.d.ts +28 -0
  341. package/esm/pg/roles.js +50 -0
  342. package/esm/pg/stats.d.ts +65 -0
  343. package/esm/pg/stats.js +129 -0
  344. package/esm/pgpm-test.d.ts +22 -0
  345. package/esm/pgpm-test.js +39 -0
  346. package/esm/report/callgraph.d.ts +9 -0
  347. package/esm/report/callgraph.js +74 -0
  348. package/esm/report/compare.d.ts +143 -0
  349. package/esm/report/compare.js +195 -0
  350. package/esm/report/github.d.ts +74 -0
  351. package/esm/report/github.js +290 -0
  352. package/esm/report/json.d.ts +4 -0
  353. package/esm/report/json.js +3 -0
  354. package/esm/report/markdown.d.ts +25 -0
  355. package/esm/report/markdown.js +352 -0
  356. package/esm/report/pretty.d.ts +14 -0
  357. package/esm/report/pretty.js +318 -0
  358. package/esm/report/sarif.d.ts +40 -0
  359. package/esm/report/sarif.js +189 -0
  360. package/esm/report/view.d.ts +82 -0
  361. package/esm/report/view.js +126 -0
  362. package/esm/rules/registry.d.ts +66 -0
  363. package/esm/rules/registry.js +562 -0
  364. package/esm/score/score.d.ts +84 -0
  365. package/esm/score/score.js +233 -0
  366. package/esm/score/scorecards.d.ts +68 -0
  367. package/esm/score/scorecards.js +157 -0
  368. package/esm/types.d.ts +292 -0
  369. package/esm/types.js +26 -0
  370. package/esm/version.d.ts +8 -0
  371. package/esm/version.js +9 -0
  372. package/exposure/adapters.d.ts +152 -0
  373. package/exposure/adapters.js +468 -0
  374. package/exposure/planes.d.ts +59 -0
  375. package/exposure/planes.js +151 -0
  376. package/exposure/reach.d.ts +78 -0
  377. package/exposure/reach.js +109 -0
  378. package/exposure/routines.d.ts +36 -0
  379. package/exposure/routines.js +57 -0
  380. package/index.d.ts +96 -6
  381. package/index.js +229 -6
  382. package/lint/index.d.ts +11 -0
  383. package/lint/index.js +19 -0
  384. package/package.json +54 -48
  385. package/perf/baseline.d.ts +40 -0
  386. package/perf/baseline.js +92 -0
  387. package/perf/explain.d.ts +46 -0
  388. package/perf/explain.js +204 -0
  389. package/pg/acl.d.ts +72 -0
  390. package/pg/acl.js +191 -0
  391. package/pg/behaviors.d.ts +114 -0
  392. package/pg/behaviors.js +217 -0
  393. package/pg/exposure.d.ts +74 -0
  394. package/pg/exposure.js +205 -0
  395. package/pg/functions.d.ts +52 -0
  396. package/pg/functions.js +131 -0
  397. package/pg/indexes.d.ts +192 -0
  398. package/pg/indexes.js +400 -0
  399. package/pg/introspect.d.ts +134 -0
  400. package/pg/introspect.js +280 -0
  401. package/pg/objects.d.ts +37 -0
  402. package/pg/objects.js +111 -0
  403. package/pg/paths.d.ts +114 -0
  404. package/pg/paths.js +166 -0
  405. package/pg/proc.d.ts +23 -0
  406. package/pg/proc.js +58 -0
  407. package/pg/reach-inputs.d.ts +47 -0
  408. package/pg/reach-inputs.js +136 -0
  409. package/pg/roles.d.ts +28 -0
  410. package/pg/roles.js +54 -0
  411. package/pg/stats.d.ts +65 -0
  412. package/pg/stats.js +133 -0
  413. package/pgpm-test.d.ts +22 -0
  414. package/pgpm-test.js +42 -0
  415. package/report/callgraph.d.ts +9 -0
  416. package/report/callgraph.js +81 -0
  417. package/report/compare.d.ts +143 -0
  418. package/report/compare.js +205 -0
  419. package/report/github.d.ts +74 -0
  420. package/report/github.js +333 -0
  421. package/report/json.d.ts +4 -0
  422. package/report/json.js +6 -0
  423. package/report/markdown.d.ts +25 -0
  424. package/report/markdown.js +355 -0
  425. package/report/pretty.d.ts +14 -0
  426. package/report/pretty.js +324 -0
  427. package/report/sarif.d.ts +40 -0
  428. package/report/sarif.js +193 -0
  429. package/report/view.d.ts +82 -0
  430. package/report/view.js +132 -0
  431. package/rules/registry.d.ts +66 -0
  432. package/rules/registry.js +569 -0
  433. package/schema/safegres.schema.json +1119 -0
  434. package/score/score.d.ts +84 -0
  435. package/score/score.js +238 -0
  436. package/score/scorecards.d.ts +68 -0
  437. package/score/scorecards.js +162 -0
  438. package/types.d.ts +292 -0
  439. package/types.js +32 -0
  440. package/version.d.ts +8 -0
  441. package/version.js +12 -0
  442. package/LICENSE +0 -23
  443. package/admin.d.ts +0 -26
  444. package/admin.js +0 -182
  445. package/connect.d.ts +0 -19
  446. package/connect.js +0 -83
  447. package/dist/README.md +0 -557
  448. package/dist/package.json +0 -72
  449. package/esm/admin.js +0 -178
  450. package/esm/connect.js +0 -78
  451. package/esm/manager.js +0 -136
  452. package/esm/roles.js +0 -32
  453. package/esm/seed/adapters.js +0 -23
  454. package/esm/seed/csv.js +0 -44
  455. package/esm/seed/index.js +0 -16
  456. package/esm/seed/json.js +0 -18
  457. package/esm/seed/launchql.js +0 -19
  458. package/esm/seed/sqitch.js +0 -17
  459. package/esm/stream.js +0 -43
  460. package/esm/test-client.js +0 -150
  461. package/manager.d.ts +0 -25
  462. package/manager.js +0 -140
  463. package/roles.d.ts +0 -17
  464. package/roles.js +0 -38
  465. package/seed/adapters.d.ts +0 -4
  466. package/seed/adapters.js +0 -28
  467. package/seed/csv.d.ts +0 -9
  468. package/seed/csv.js +0 -49
  469. package/seed/index.d.ts +0 -16
  470. package/seed/index.js +0 -33
  471. package/seed/json.d.ts +0 -6
  472. package/seed/json.js +0 -21
  473. package/seed/launchql.d.ts +0 -2
  474. package/seed/launchql.js +0 -22
  475. package/seed/sqitch.d.ts +0 -2
  476. package/seed/sqitch.js +0 -20
  477. package/seed/types.d.ts +0 -13
  478. package/stream.d.ts +0 -2
  479. package/stream.js +0 -46
  480. package/test-client.d.ts +0 -49
  481. package/test-client.js +0 -154
  482. /package/{seed → config}/types.js +0 -0
  483. /package/esm/{seed → config}/types.js +0 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Single-query catalog introspection for the safegres audit.
3
+ *
4
+ * Pulls every relation + every policy + (parsed) role ACL per role in one shot,
5
+ * so Script A can process the whole database from one in-memory snapshot.
6
+ */
7
+ import type { Client, Pool } from 'pg';
8
+ /** Minimal query interface — accepts pg.Client, pg.Pool, or any `{ query }`. */
9
+ export interface QueryExecutor {
10
+ query<T = unknown>(text: string, params?: unknown[]): Promise<{
11
+ rows: T[];
12
+ }>;
13
+ }
14
+ /**
15
+ * `USAGE` never appears on a table — it is the sequence privilege (`nextval`),
16
+ * and `EXECUTE` is the function one. Both are in this union because sequences
17
+ * and functions are graded through the same grant closure as everything else,
18
+ * and a reach path routinely starts on one and ends on a table.
19
+ */
20
+ export type PgPrivilege = 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'TRUNCATE' | 'REFERENCES' | 'TRIGGER' | 'USAGE' | 'EXECUTE';
21
+ /**
22
+ * Mapping of ACL single-letter codes (from `aclexplode`) to our privilege names.
23
+ * `r` = SELECT, `a` = INSERT, `w` = UPDATE, `d` = DELETE, etc.
24
+ */
25
+ export declare const ACL_PRIVILEGE_MAP: Record<string, PgPrivilege>;
26
+ export interface GrantInfo {
27
+ role: string;
28
+ privilege: PgPrivilege;
29
+ /** Was the grant delegated (i.e. `WITH GRANT OPTION`). */
30
+ grantable: boolean;
31
+ /**
32
+ * `true` if the grantee role is a superuser or has `BYPASSRLS`. RLS policies
33
+ * do not apply to this role, so coverage findings are suppressed for it.
34
+ * `false` for `PUBLIC` and all other grantees.
35
+ */
36
+ bypassRls: boolean;
37
+ }
38
+ /**
39
+ * A grant on one column (`pg_attribute.attacl`), which Postgres keeps entirely
40
+ * separate from the table's own ACL: `GRANT SELECT (secret) ON t TO anon`
41
+ * leaves `relacl` untouched, so any analysis that reads only `relacl`
42
+ * concludes the role reaches nothing.
43
+ *
44
+ * Only SELECT, INSERT, UPDATE and REFERENCES can be column-scoped.
45
+ */
46
+ export interface ColumnGrantInfo extends GrantInfo {
47
+ column: string;
48
+ }
49
+ /** `pg_policy.polcmd` uses: `r` SELECT, `a` INSERT, `w` UPDATE, `d` DELETE, `*` ALL. */
50
+ export type PolicyCmd = 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'ALL';
51
+ export declare const POLICY_CMD_MAP: Record<string, PolicyCmd>;
52
+ export interface PolicyInfo {
53
+ name: string;
54
+ cmd: PolicyCmd;
55
+ permissive: boolean;
56
+ /** Role names the policy applies to. `['PUBLIC']` if `polroles = {0}`. */
57
+ roles: string[];
58
+ using: string | null;
59
+ withCheck: string | null;
60
+ }
61
+ export interface TableSnapshot {
62
+ schema: string;
63
+ name: string;
64
+ oid: number;
65
+ rlsEnabled: boolean;
66
+ rlsForced: boolean;
67
+ /** `true` if the table is partitioned or is a view — used to skip irrelevant findings. */
68
+ isPartitioned: boolean;
69
+ owner: string;
70
+ grants: GrantInfo[];
71
+ /**
72
+ * Grants on individual columns. Disjoint from {@link TableSnapshot.grants}:
73
+ * a column grant never appears in `relacl`, and a table grant never appears
74
+ * in `attacl`, so a role can reach a relation through this list alone.
75
+ */
76
+ columnGrants: ColumnGrantInfo[];
77
+ policies: PolicyInfo[];
78
+ }
79
+ export interface IntrospectOptions {
80
+ /** Schemas to include. If omitted, all non-system schemas. */
81
+ schemas?: string[];
82
+ /**
83
+ * Schemas to exclude. Applied on top of `schemas`.
84
+ * Defaults to `['pg_catalog', 'information_schema', 'pg_toast']`.
85
+ */
86
+ excludeSchemas?: string[];
87
+ /**
88
+ * Roles to include when reporting grants. If omitted, all roles returned
89
+ * by {@link listAuditableRoles}.
90
+ */
91
+ roles?: string[];
92
+ /**
93
+ * How to treat relations that belong to an installed extension.
94
+ * Defaults to skipping them: they are the extension author's to secure and
95
+ * tune, not the application's, and they cannot be altered without breaking
96
+ * `pg_dump`/upgrade.
97
+ */
98
+ extensions?: ExtensionScopeOptions;
99
+ }
100
+ /**
101
+ * Extension relations are the `node_modules` of a database: physically present,
102
+ * scanned like anything else, and not yours to edit.
103
+ */
104
+ export interface ExtensionScopeOptions {
105
+ /**
106
+ * Skip relations an extension owns (`pg_depend.deptype = 'e'`) and their
107
+ * partitions. Default `true`.
108
+ */
109
+ skipOwned?: boolean;
110
+ /**
111
+ * Extension names whose *schemas* are skipped wholesale, for objects the
112
+ * extension creates at runtime and never registers as dependencies —
113
+ * `pg_partman`'s child partitions and templates being the motivating case.
114
+ * Unknown/uninstalled names are ignored.
115
+ */
116
+ ignore?: string[];
117
+ }
118
+ /**
119
+ * SQL predicate excluding extension relations, for a query where `c` is the
120
+ * `pg_class` row and `n` its namespace. `paramIndex` is the bind position
121
+ * holding {@link ExtensionScopeOptions.ignore}.
122
+ */
123
+ export declare function extensionFilter(options: ExtensionScopeOptions | undefined, paramIndex: number): string;
124
+ /**
125
+ * One-query snapshot of every table + its policies + its grants expanded per role.
126
+ *
127
+ * We deliberately avoid views that hide permissive vs restrictive distinctions
128
+ * (like `pg_policies`) — `pg_policy` gives us `polpermissive` directly.
129
+ */
130
+ export declare function introspectTables(exec: QueryExecutor, options?: IntrospectOptions): Promise<TableSnapshot[]>;
131
+ /**
132
+ * Convenience: accept any of `pg.Client` / `pg.Pool` / a custom executor.
133
+ */
134
+ export declare function asExecutor(client: Client | Pool | QueryExecutor): QueryExecutor;
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Single-query catalog introspection for the safegres audit.
3
+ *
4
+ * Pulls every relation + every policy + (parsed) role ACL per role in one shot,
5
+ * so Script A can process the whole database from one in-memory snapshot.
6
+ */
7
+ /**
8
+ * Mapping of ACL single-letter codes (from `aclexplode`) to our privilege names.
9
+ * `r` = SELECT, `a` = INSERT, `w` = UPDATE, `d` = DELETE, etc.
10
+ */
11
+ export const ACL_PRIVILEGE_MAP = {
12
+ r: 'SELECT',
13
+ a: 'INSERT',
14
+ w: 'UPDATE',
15
+ d: 'DELETE',
16
+ D: 'TRUNCATE',
17
+ x: 'REFERENCES',
18
+ t: 'TRIGGER'
19
+ };
20
+ export const POLICY_CMD_MAP = {
21
+ r: 'SELECT',
22
+ a: 'INSERT',
23
+ w: 'UPDATE',
24
+ d: 'DELETE',
25
+ '*': 'ALL'
26
+ };
27
+ /**
28
+ * SQL predicate excluding extension relations, for a query where `c` is the
29
+ * `pg_class` row and `n` its namespace. `paramIndex` is the bind position
30
+ * holding {@link ExtensionScopeOptions.ignore}.
31
+ */
32
+ export function extensionFilter(options, paramIndex) {
33
+ const skipOwned = options?.skipOwned ?? true;
34
+ const clauses = [];
35
+ if (skipOwned) {
36
+ // A partition carries no extension dependency of its own, so ownership
37
+ // has to be read off the partition tree it belongs to — at any depth.
38
+ clauses.push(`
39
+ AND NOT EXISTS (
40
+ SELECT 1 FROM pg_depend d
41
+ WHERE d.classid = 'pg_class'::regclass
42
+ AND d.refclassid = 'pg_extension'::regclass
43
+ AND d.deptype = 'e'
44
+ AND (
45
+ d.objid = c.oid
46
+ OR d.objid IN (SELECT relid FROM pg_partition_ancestors(c.oid))
47
+ )
48
+ )`);
49
+ }
50
+ clauses.push(`
51
+ AND NOT (n.oid = ANY (
52
+ SELECT e.extnamespace FROM pg_extension e WHERE e.extname = ANY($${paramIndex}::text[])
53
+ ))`);
54
+ return clauses.join('');
55
+ }
56
+ const DEFAULT_EXCLUDES = ['pg_catalog', 'information_schema', 'pg_toast'];
57
+ /**
58
+ * One-query snapshot of every table + its policies + its grants expanded per role.
59
+ *
60
+ * We deliberately avoid views that hide permissive vs restrictive distinctions
61
+ * (like `pg_policies`) — `pg_policy` gives us `polpermissive` directly.
62
+ */
63
+ export async function introspectTables(exec, options = {}) {
64
+ const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
65
+ // PUBLIC (grantee oid 0) always survives the role filter: it is not a role
66
+ // that can be included or excluded — it is every role, including every one
67
+ // in the filter.
68
+ const roleFilter = options.roles && options.roles.length > 0
69
+ ? `AND (g.grantee_oid = 0 OR rol.rolname = ANY($3::text[]))`
70
+ : '';
71
+ const schemaFilter = options.schemas && options.schemas.length > 0
72
+ ? `AND n.nspname = ANY($1::text[])`
73
+ : `AND NOT (n.nspname = ANY($2::text[]))`;
74
+ const extFilter = extensionFilter(options.extensions, 4);
75
+ // We reference every param at least once (even if trivially) so Postgres
76
+ // can infer types for all of them. Unused parameters otherwise error with
77
+ // "could not determine data type of parameter $N".
78
+ const sql = `
79
+ WITH _params AS (
80
+ SELECT
81
+ $1::text[] AS include_schemas,
82
+ $2::text[] AS exclude_schemas,
83
+ $3::text[] AS role_filter,
84
+ $4::text[] AS ignore_extensions
85
+ ),
86
+ rels AS (
87
+ SELECT
88
+ n.nspname AS schema_name,
89
+ c.relname AS table_name,
90
+ c.oid AS oid,
91
+ c.relrowsecurity AS rls_enabled,
92
+ c.relforcerowsecurity AS rls_forced,
93
+ (c.relkind = 'p') AS is_partitioned,
94
+ pg_catalog.pg_get_userbyid(c.relowner) AS owner,
95
+ c.relacl AS relacl
96
+ FROM pg_class c
97
+ JOIN pg_namespace n ON n.oid = c.relnamespace
98
+ WHERE c.relkind IN ('r', 'p')
99
+ ${schemaFilter}${extFilter}
100
+ ),
101
+ grants_exploded AS (
102
+ SELECT
103
+ r.oid,
104
+ (aclexplode(r.relacl)).grantee AS grantee_oid,
105
+ (aclexplode(r.relacl)).privilege_type AS privilege_type,
106
+ (aclexplode(r.relacl)).is_grantable AS is_grantable
107
+ FROM rels r
108
+ WHERE r.relacl IS NOT NULL
109
+ ),
110
+ grants AS (
111
+ SELECT
112
+ g.oid,
113
+ CASE WHEN g.grantee_oid = 0 THEN 'PUBLIC' ELSE rol.rolname END AS grantee,
114
+ g.privilege_type,
115
+ g.is_grantable,
116
+ CASE
117
+ WHEN g.grantee_oid = 0 THEN false
118
+ ELSE COALESCE(rol.rolsuper OR rol.rolbypassrls, false)
119
+ END AS bypass_rls
120
+ FROM grants_exploded g
121
+ LEFT JOIN pg_roles rol ON rol.oid = g.grantee_oid
122
+ WHERE true
123
+ ${roleFilter}
124
+ ),
125
+ -- Column ACLs live on pg_attribute, not pg_class: a column grant is
126
+ -- invisible to relacl, and this is the only place it surfaces.
127
+ col_grants_exploded AS (
128
+ SELECT
129
+ r.oid,
130
+ a.attname AS column_name,
131
+ (aclexplode(a.attacl)).grantee AS grantee_oid,
132
+ (aclexplode(a.attacl)).privilege_type AS privilege_type,
133
+ (aclexplode(a.attacl)).is_grantable AS is_grantable
134
+ FROM rels r
135
+ JOIN pg_attribute a ON a.attrelid = r.oid
136
+ WHERE a.attacl IS NOT NULL
137
+ AND a.attnum > 0
138
+ AND NOT a.attisdropped
139
+ ),
140
+ col_grants AS (
141
+ SELECT
142
+ g.oid,
143
+ g.column_name,
144
+ CASE WHEN g.grantee_oid = 0 THEN 'PUBLIC' ELSE rol.rolname END AS grantee,
145
+ g.privilege_type,
146
+ g.is_grantable,
147
+ CASE
148
+ WHEN g.grantee_oid = 0 THEN false
149
+ ELSE COALESCE(rol.rolsuper OR rol.rolbypassrls, false)
150
+ END AS bypass_rls
151
+ FROM col_grants_exploded g
152
+ LEFT JOIN pg_roles rol ON rol.oid = g.grantee_oid
153
+ WHERE true
154
+ ${roleFilter}
155
+ ),
156
+ policies AS (
157
+ SELECT
158
+ p.polrelid AS oid,
159
+ p.polname AS name,
160
+ p.polcmd AS cmd,
161
+ p.polpermissive AS permissive,
162
+ CASE
163
+ WHEN p.polroles = ARRAY[0]::oid[] THEN ARRAY['PUBLIC']
164
+ ELSE COALESCE(
165
+ (SELECT array_agg(rolname) FROM pg_roles WHERE oid = ANY(p.polroles)),
166
+ ARRAY[]::text[]
167
+ )
168
+ END AS roles,
169
+ pg_get_expr(p.polqual, p.polrelid) AS using_expr,
170
+ pg_get_expr(p.polwithcheck, p.polrelid) AS with_check_expr
171
+ FROM pg_policy p
172
+ )
173
+ SELECT
174
+ r.schema_name,
175
+ r.table_name,
176
+ r.oid::int AS oid,
177
+ r.rls_enabled,
178
+ r.rls_forced,
179
+ r.is_partitioned,
180
+ r.owner,
181
+ COALESCE(
182
+ (SELECT jsonb_agg(jsonb_build_object(
183
+ 'role', g.grantee,
184
+ 'privilege', g.privilege_type,
185
+ 'grantable', g.is_grantable,
186
+ 'bypassRls', g.bypass_rls
187
+ )) FROM grants g WHERE g.oid = r.oid),
188
+ '[]'::jsonb
189
+ ) AS grants,
190
+ COALESCE(
191
+ (SELECT jsonb_agg(jsonb_build_object(
192
+ 'role', g.grantee,
193
+ 'column', g.column_name,
194
+ 'privilege', g.privilege_type,
195
+ 'grantable', g.is_grantable,
196
+ 'bypassRls', g.bypass_rls
197
+ ) ORDER BY g.grantee, g.column_name, g.privilege_type)
198
+ FROM col_grants g WHERE g.oid = r.oid),
199
+ '[]'::jsonb
200
+ ) AS column_grants,
201
+ COALESCE(
202
+ (SELECT jsonb_agg(jsonb_build_object(
203
+ 'name', p.name,
204
+ 'cmd', p.cmd::text,
205
+ 'permissive', p.permissive,
206
+ 'roles', to_jsonb(p.roles),
207
+ 'using', p.using_expr,
208
+ 'withCheck', p.with_check_expr
209
+ )) FROM policies p WHERE p.oid = r.oid),
210
+ '[]'::jsonb
211
+ ) AS policies
212
+ FROM rels r
213
+ ORDER BY r.schema_name, r.table_name
214
+ `;
215
+ const params = [
216
+ options.schemas ?? [],
217
+ excludes,
218
+ options.roles ?? [],
219
+ options.extensions?.ignore ?? []
220
+ ];
221
+ const { rows } = await exec.query(sql, params);
222
+ return rows.map((r) => ({
223
+ schema: r.schema_name,
224
+ name: r.table_name,
225
+ oid: r.oid,
226
+ rlsEnabled: r.rls_enabled,
227
+ rlsForced: r.rls_forced,
228
+ isPartitioned: r.is_partitioned,
229
+ owner: r.owner,
230
+ grants: r.grants.map((g) => ({
231
+ role: g.role,
232
+ privilege: normalizePrivilege(g.privilege),
233
+ grantable: g.grantable,
234
+ bypassRls: g.bypassRls === true
235
+ })),
236
+ columnGrants: r.column_grants.map((g) => ({
237
+ role: g.role,
238
+ column: g.column,
239
+ privilege: normalizePrivilege(g.privilege),
240
+ grantable: g.grantable,
241
+ bypassRls: g.bypassRls === true
242
+ })),
243
+ policies: r.policies.map((p) => ({
244
+ name: p.name,
245
+ cmd: POLICY_CMD_MAP[p.cmd] ?? 'ALL',
246
+ permissive: p.permissive,
247
+ roles: p.roles,
248
+ using: p.using,
249
+ withCheck: p.withCheck
250
+ }))
251
+ }));
252
+ }
253
+ function normalizePrivilege(raw) {
254
+ const upper = raw.toUpperCase();
255
+ switch (upper) {
256
+ case 'SELECT':
257
+ case 'INSERT':
258
+ case 'UPDATE':
259
+ case 'DELETE':
260
+ case 'TRUNCATE':
261
+ case 'REFERENCES':
262
+ case 'TRIGGER':
263
+ return upper;
264
+ default:
265
+ // `r`, `a`, `w`, `d` — ACL single-letter form
266
+ return ACL_PRIVILEGE_MAP[raw] ?? 'SELECT';
267
+ }
268
+ }
269
+ /**
270
+ * Convenience: accept any of `pg.Client` / `pg.Pool` / a custom executor.
271
+ */
272
+ export function asExecutor(client) {
273
+ return client;
274
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * ACLs on the relations that are not tables.
3
+ *
4
+ * `introspectTables` reads `relkind IN ('r','p')` and `introspectViews` reads
5
+ * views and matviews, which leaves two kinds of relation carrying privileges
6
+ * that nothing in this package ever looked at:
7
+ *
8
+ * - **sequences** (`relkind = 'S'`), where `USAGE`/`UPDATE` let a role call
9
+ * `nextval`/`setval` and `SELECT` lets it read `last_value`; and
10
+ * - **foreign tables** (`relkind = 'f'`), which cannot carry RLS at all —
11
+ * `ALTER FOREIGN TABLE ... ENABLE ROW LEVEL SECURITY` is rejected by
12
+ * Postgres (verified on 18), so a grant on one is never row-filtered.
13
+ *
14
+ * Both are read here rather than folded into the table snapshot: neither has
15
+ * RLS state, policies or column ACLs, and giving them a `TableSnapshot` with
16
+ * those fields stubbed out would invite every table rule to grade something it
17
+ * does not understand.
18
+ */
19
+ import type { GrantInfo, IntrospectOptions, QueryExecutor } from './introspect';
20
+ export type PgObjectKind = 'sequence' | 'foreign table';
21
+ export interface ObjectAclSnapshot {
22
+ schema: string;
23
+ name: string;
24
+ kind: PgObjectKind;
25
+ owner: string;
26
+ grants: GrantInfo[];
27
+ /** The foreign server behind a foreign table; absent for a sequence. */
28
+ server?: string;
29
+ /**
30
+ * For a sequence, the `schema.table.column` it is attached to by an
31
+ * `OWNED BY` dependency (a `serial`/identity column records one). A role
32
+ * that inserts into that table legitimately needs `USAGE`, which is what
33
+ * keeps the sequence rule from recommending a revoke.
34
+ */
35
+ ownedBy?: string;
36
+ }
37
+ export declare function introspectObjectAcls(exec: QueryExecutor, options?: IntrospectOptions): Promise<ObjectAclSnapshot[]>;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * ACLs on the relations that are not tables.
3
+ *
4
+ * `introspectTables` reads `relkind IN ('r','p')` and `introspectViews` reads
5
+ * views and matviews, which leaves two kinds of relation carrying privileges
6
+ * that nothing in this package ever looked at:
7
+ *
8
+ * - **sequences** (`relkind = 'S'`), where `USAGE`/`UPDATE` let a role call
9
+ * `nextval`/`setval` and `SELECT` lets it read `last_value`; and
10
+ * - **foreign tables** (`relkind = 'f'`), which cannot carry RLS at all —
11
+ * `ALTER FOREIGN TABLE ... ENABLE ROW LEVEL SECURITY` is rejected by
12
+ * Postgres (verified on 18), so a grant on one is never row-filtered.
13
+ *
14
+ * Both are read here rather than folded into the table snapshot: neither has
15
+ * RLS state, policies or column ACLs, and giving them a `TableSnapshot` with
16
+ * those fields stubbed out would invite every table rule to grade something it
17
+ * does not understand.
18
+ */
19
+ const DEFAULT_EXCLUDES = ['pg_catalog', 'information_schema', 'pg_toast'];
20
+ export async function introspectObjectAcls(exec, options = {}) {
21
+ const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
22
+ const schemaFilter = options.schemas && options.schemas.length > 0
23
+ ? 'AND n.nspname = ANY($1::text[])'
24
+ : 'AND NOT (n.nspname = ANY($2::text[]))';
25
+ const sql = `
26
+ WITH _params AS (
27
+ SELECT $1::text[] AS include_schemas, $2::text[] AS exclude_schemas
28
+ ),
29
+ rels AS (
30
+ SELECT
31
+ n.nspname AS schema_name,
32
+ c.relname AS object_name,
33
+ c.oid AS oid,
34
+ c.relkind AS relkind,
35
+ pg_catalog.pg_get_userbyid(c.relowner) AS owner,
36
+ c.relacl AS relacl
37
+ FROM pg_class c
38
+ JOIN pg_namespace n ON n.oid = c.relnamespace
39
+ WHERE c.relkind IN ('S', 'f')
40
+ ${schemaFilter}
41
+ ),
42
+ grants AS (
43
+ SELECT
44
+ r.oid,
45
+ CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE rol.rolname END AS grantee,
46
+ a.privilege_type,
47
+ a.is_grantable,
48
+ CASE
49
+ WHEN a.grantee = 0 THEN false
50
+ ELSE COALESCE(rol.rolsuper OR rol.rolbypassrls, false)
51
+ END AS bypass_rls
52
+ FROM rels r, aclexplode(r.relacl) a
53
+ LEFT JOIN pg_roles rol ON rol.oid = a.grantee
54
+ WHERE r.relacl IS NOT NULL
55
+ ),
56
+ -- A serial/identity column records an OWNED BY dependency on its
57
+ -- sequence. The role inserting into that table needs USAGE on it, so the
58
+ -- link is what separates a load-bearing grant from a gratuitous one.
59
+ owners AS (
60
+ SELECT
61
+ d.objid AS seq_oid,
62
+ n.nspname || '.' || c.relname || '.' || a.attname AS owned_by
63
+ FROM pg_depend d
64
+ JOIN pg_class c ON c.oid = d.refobjid
65
+ JOIN pg_namespace n ON n.oid = c.relnamespace
66
+ JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
67
+ WHERE d.classid = 'pg_class'::regclass
68
+ AND d.refclassid = 'pg_class'::regclass
69
+ AND d.deptype IN ('a', 'i')
70
+ AND d.objid IN (SELECT oid FROM rels WHERE relkind = 'S')
71
+ )
72
+ SELECT
73
+ r.schema_name,
74
+ r.object_name,
75
+ r.relkind,
76
+ r.owner,
77
+ o.owned_by,
78
+ srv.srvname AS server_name,
79
+ COALESCE(
80
+ (
81
+ SELECT jsonb_agg(jsonb_build_object(
82
+ 'role', g.grantee,
83
+ 'privilege', g.privilege_type,
84
+ 'grantable', g.is_grantable,
85
+ 'bypassRls', g.bypass_rls
86
+ ) ORDER BY g.grantee, g.privilege_type)
87
+ FROM grants g
88
+ WHERE g.oid = r.oid
89
+ ),
90
+ '[]'::jsonb
91
+ ) AS grants
92
+ FROM rels r
93
+ LEFT JOIN owners o ON o.seq_oid = r.oid
94
+ LEFT JOIN pg_foreign_table ft ON ft.ftrelid = r.oid
95
+ LEFT JOIN pg_foreign_server srv ON srv.oid = ft.ftserver
96
+ ORDER BY r.schema_name, r.object_name
97
+ `;
98
+ const { rows } = await exec.query(sql, [options.schemas ?? null, excludes]);
99
+ return rows.map((r) => ({
100
+ schema: r.schema_name,
101
+ name: r.object_name,
102
+ kind: r.relkind === 'S' ? 'sequence' : 'foreign table',
103
+ owner: r.owner,
104
+ grants: r.grants,
105
+ ...(r.server_name ? { server: r.server_name } : {}),
106
+ ...(r.owned_by ? { ownedBy: r.owned_by } : {})
107
+ }));
108
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Access-path signals: independent, individually-auditable pieces of evidence
3
+ * about whether anything reads a foreign key.
4
+ *
5
+ * X1 ("foreign key with no covering index") is right about the mechanics — a
6
+ * `DELETE` on the parent really does scan the child — but it assumes the child
7
+ * is a relation somebody traverses. On a provisioning-config table that
8
+ * assumption is false, and acting on the finding buys a write penalty on every
9
+ * insert in exchange for speeding up a scan of one row.
10
+ *
11
+ * The tempting gate, `pg_class.reltuples`, is useless here: safegres grades an
12
+ * ephemeral CI database that has never held data, so every row estimate is 0
13
+ * at exactly the moment we grade. Worse, row count is the wrong question — a
14
+ * huge append-only log nobody joins on wants no FK index, and a tiny lookup
15
+ * table hammered by every request does. The property we need is *reachability*.
16
+ *
17
+ * Reachability is not one boolean, and this module deliberately does not
18
+ * compute one. It collects signals, each of which points in one direction and
19
+ * says why, and leaves the verdict to the caller:
20
+ *
21
+ * - `policy-read` and `view-read` are **reads**. They are decisive: the
22
+ * database itself traverses the column, so no amount of contrary evidence
23
+ * makes the path unreachable.
24
+ * - `write-once-pointer` and `config-record` are **shape**. They are not
25
+ * evidence of unreachability, only of a schema idiom — a `NOT NULL` key
26
+ * defaulting to the nil UUID is a slot a provisioner fills in, and a table
27
+ * with several of them looks like a config record rather than a relation.
28
+ *
29
+ * Shape alone must never suppress a finding. A generated API can expose a
30
+ * reverse relation over any foreign key regardless of how its default is
31
+ * written, and if it does, the path is reachable and the index is wanted.
32
+ *
33
+ * - `behavior-hidden` is the third kind, **declared**: the schema author has
34
+ * said the reverse relation is not in the generated API. It comes from the
35
+ * same behavior tags exposure reach reads, so the two axes agree about what
36
+ * the API contains rather than each guessing separately. It is reported and
37
+ * not acted on — a hidden relation is one missing path, not proof that
38
+ * nothing traverses the key, since the referential-integrity scan on a
39
+ * parent `DELETE` runs whatever the API exposes.
40
+ */
41
+ import type { TableIndexSnapshot } from './indexes';
42
+ import type { TableSnapshot } from './introspect';
43
+ /**
44
+ * Which way a signal points. `read` means something demonstrably traverses the
45
+ * key; `shape` means the schema resembles an idiom in which nothing does, which
46
+ * is a suspicion rather than a finding.
47
+ */
48
+ export type SignalDirection = 'read' | 'shape' | 'declared';
49
+ export type SignalName = 'policy-read' | 'view-read' | 'write-once-pointer' | 'config-record' | 'behavior-hidden';
50
+ export interface PathSignal {
51
+ name: SignalName;
52
+ direction: SignalDirection;
53
+ /** Why this signal fired, in one human-readable clause. */
54
+ detail: string;
55
+ }
56
+ /**
57
+ * The caller-facing summary of a path's signals. Note there is no `unreachable`
58
+ * member: nothing safegres can currently observe proves a path is unreachable,
59
+ * and inventing the state is how a scanner ends up recommending that you drop
60
+ * an index a live API is using.
61
+ */
62
+ export type PathAssessment =
63
+ /** At least one `read` signal. The key is traversed; X1 applies as written. */
64
+ 'read'
65
+ /** Only `shape` signals. Looks like a provisioning pointer; unproven. */
66
+ | 'write-once-shaped'
67
+ /** No signal fired either way. */
68
+ | 'unknown';
69
+ export interface AccessPath {
70
+ schema: string;
71
+ table: string;
72
+ /** Referencing column names, in constraint order. */
73
+ columns: string[];
74
+ /** The foreign-key constraint this path belongs to. */
75
+ constraint: string;
76
+ /** Every signal that fired, so a reviewer can audit the assessment. */
77
+ signals: PathSignal[];
78
+ assessment: PathAssessment;
79
+ }
80
+ export interface ClassifyOptions {
81
+ /**
82
+ * Write-once pointers a table needs before the `config-record` signal fires.
83
+ * Default 2. Lifting the test from the column to the table is what catches
84
+ * the nullable, undefaulted pointer sitting alongside the defaulted ones.
85
+ */
86
+ minPointers?: number;
87
+ /**
88
+ * `schema.table.constraint` keys whose reverse relation an API declares
89
+ * absent, as computed by exposure reach.
90
+ */
91
+ hiddenBackwardRelations?: Set<string>;
92
+ }
93
+ export declare const DEFAULT_MIN_POINTERS = 2;
94
+ export declare function isConstantDefault(defaultExpr: string | null): boolean;
95
+ /** Key for {@link AccessPath} lookups: the relation plus the constraint name. */
96
+ export declare function pathKey(schema: string, table: string, constraint: string): string;
97
+ /**
98
+ * One real reader ends the discussion, so a `read` signal always wins. Absent
99
+ * one, the shape has to hold at the *table* level before it means anything: a
100
+ * lone defaulted column is a habit, whereas a table built entirely out of them
101
+ * is an idiom. `write-once-pointer` on its own is therefore reported and not
102
+ * acted on.
103
+ */
104
+ export declare function assess(signals: PathSignal[]): PathAssessment;
105
+ /**
106
+ * Collect the signals for every foreign key in the snapshot.
107
+ *
108
+ * `tables` supplies the RLS policy predicates (as SQL text) and `viewBodies`
109
+ * the view definitions. Both are matched as whole-word tokens across the
110
+ * *whole* database rather than per relation — deliberately over-eager, because
111
+ * a spurious `read` signal costs one retained finding while a missing one
112
+ * could cost a dropped index.
113
+ */
114
+ export declare function classifyPaths(indexSnapshot: TableIndexSnapshot[], tables: TableSnapshot[], viewBodies: string[], options?: ClassifyOptions): Map<string, AccessPath>;