@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,280 @@
1
+ "use strict";
2
+ /**
3
+ * Single-query catalog introspection for the safegres audit.
4
+ *
5
+ * Pulls every relation + every policy + (parsed) role ACL per role in one shot,
6
+ * so Script A can process the whole database from one in-memory snapshot.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.POLICY_CMD_MAP = exports.ACL_PRIVILEGE_MAP = void 0;
10
+ exports.extensionFilter = extensionFilter;
11
+ exports.introspectTables = introspectTables;
12
+ exports.asExecutor = asExecutor;
13
+ /**
14
+ * Mapping of ACL single-letter codes (from `aclexplode`) to our privilege names.
15
+ * `r` = SELECT, `a` = INSERT, `w` = UPDATE, `d` = DELETE, etc.
16
+ */
17
+ exports.ACL_PRIVILEGE_MAP = {
18
+ r: 'SELECT',
19
+ a: 'INSERT',
20
+ w: 'UPDATE',
21
+ d: 'DELETE',
22
+ D: 'TRUNCATE',
23
+ x: 'REFERENCES',
24
+ t: 'TRIGGER'
25
+ };
26
+ exports.POLICY_CMD_MAP = {
27
+ r: 'SELECT',
28
+ a: 'INSERT',
29
+ w: 'UPDATE',
30
+ d: 'DELETE',
31
+ '*': 'ALL'
32
+ };
33
+ /**
34
+ * SQL predicate excluding extension relations, for a query where `c` is the
35
+ * `pg_class` row and `n` its namespace. `paramIndex` is the bind position
36
+ * holding {@link ExtensionScopeOptions.ignore}.
37
+ */
38
+ function extensionFilter(options, paramIndex) {
39
+ const skipOwned = options?.skipOwned ?? true;
40
+ const clauses = [];
41
+ if (skipOwned) {
42
+ // A partition carries no extension dependency of its own, so ownership
43
+ // has to be read off the partition tree it belongs to — at any depth.
44
+ clauses.push(`
45
+ AND NOT EXISTS (
46
+ SELECT 1 FROM pg_depend d
47
+ WHERE d.classid = 'pg_class'::regclass
48
+ AND d.refclassid = 'pg_extension'::regclass
49
+ AND d.deptype = 'e'
50
+ AND (
51
+ d.objid = c.oid
52
+ OR d.objid IN (SELECT relid FROM pg_partition_ancestors(c.oid))
53
+ )
54
+ )`);
55
+ }
56
+ clauses.push(`
57
+ AND NOT (n.oid = ANY (
58
+ SELECT e.extnamespace FROM pg_extension e WHERE e.extname = ANY($${paramIndex}::text[])
59
+ ))`);
60
+ return clauses.join('');
61
+ }
62
+ const DEFAULT_EXCLUDES = ['pg_catalog', 'information_schema', 'pg_toast'];
63
+ /**
64
+ * One-query snapshot of every table + its policies + its grants expanded per role.
65
+ *
66
+ * We deliberately avoid views that hide permissive vs restrictive distinctions
67
+ * (like `pg_policies`) — `pg_policy` gives us `polpermissive` directly.
68
+ */
69
+ async function introspectTables(exec, options = {}) {
70
+ const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
71
+ // PUBLIC (grantee oid 0) always survives the role filter: it is not a role
72
+ // that can be included or excluded — it is every role, including every one
73
+ // in the filter.
74
+ const roleFilter = options.roles && options.roles.length > 0
75
+ ? `AND (g.grantee_oid = 0 OR rol.rolname = ANY($3::text[]))`
76
+ : '';
77
+ const schemaFilter = options.schemas && options.schemas.length > 0
78
+ ? `AND n.nspname = ANY($1::text[])`
79
+ : `AND NOT (n.nspname = ANY($2::text[]))`;
80
+ const extFilter = extensionFilter(options.extensions, 4);
81
+ // We reference every param at least once (even if trivially) so Postgres
82
+ // can infer types for all of them. Unused parameters otherwise error with
83
+ // "could not determine data type of parameter $N".
84
+ const sql = `
85
+ WITH _params AS (
86
+ SELECT
87
+ $1::text[] AS include_schemas,
88
+ $2::text[] AS exclude_schemas,
89
+ $3::text[] AS role_filter,
90
+ $4::text[] AS ignore_extensions
91
+ ),
92
+ rels AS (
93
+ SELECT
94
+ n.nspname AS schema_name,
95
+ c.relname AS table_name,
96
+ c.oid AS oid,
97
+ c.relrowsecurity AS rls_enabled,
98
+ c.relforcerowsecurity AS rls_forced,
99
+ (c.relkind = 'p') AS is_partitioned,
100
+ pg_catalog.pg_get_userbyid(c.relowner) AS owner,
101
+ c.relacl AS relacl
102
+ FROM pg_class c
103
+ JOIN pg_namespace n ON n.oid = c.relnamespace
104
+ WHERE c.relkind IN ('r', 'p')
105
+ ${schemaFilter}${extFilter}
106
+ ),
107
+ grants_exploded AS (
108
+ SELECT
109
+ r.oid,
110
+ (aclexplode(r.relacl)).grantee AS grantee_oid,
111
+ (aclexplode(r.relacl)).privilege_type AS privilege_type,
112
+ (aclexplode(r.relacl)).is_grantable AS is_grantable
113
+ FROM rels r
114
+ WHERE r.relacl IS NOT NULL
115
+ ),
116
+ grants AS (
117
+ SELECT
118
+ g.oid,
119
+ CASE WHEN g.grantee_oid = 0 THEN 'PUBLIC' ELSE rol.rolname END AS grantee,
120
+ g.privilege_type,
121
+ g.is_grantable,
122
+ CASE
123
+ WHEN g.grantee_oid = 0 THEN false
124
+ ELSE COALESCE(rol.rolsuper OR rol.rolbypassrls, false)
125
+ END AS bypass_rls
126
+ FROM grants_exploded g
127
+ LEFT JOIN pg_roles rol ON rol.oid = g.grantee_oid
128
+ WHERE true
129
+ ${roleFilter}
130
+ ),
131
+ -- Column ACLs live on pg_attribute, not pg_class: a column grant is
132
+ -- invisible to relacl, and this is the only place it surfaces.
133
+ col_grants_exploded AS (
134
+ SELECT
135
+ r.oid,
136
+ a.attname AS column_name,
137
+ (aclexplode(a.attacl)).grantee AS grantee_oid,
138
+ (aclexplode(a.attacl)).privilege_type AS privilege_type,
139
+ (aclexplode(a.attacl)).is_grantable AS is_grantable
140
+ FROM rels r
141
+ JOIN pg_attribute a ON a.attrelid = r.oid
142
+ WHERE a.attacl IS NOT NULL
143
+ AND a.attnum > 0
144
+ AND NOT a.attisdropped
145
+ ),
146
+ col_grants AS (
147
+ SELECT
148
+ g.oid,
149
+ g.column_name,
150
+ CASE WHEN g.grantee_oid = 0 THEN 'PUBLIC' ELSE rol.rolname END AS grantee,
151
+ g.privilege_type,
152
+ g.is_grantable,
153
+ CASE
154
+ WHEN g.grantee_oid = 0 THEN false
155
+ ELSE COALESCE(rol.rolsuper OR rol.rolbypassrls, false)
156
+ END AS bypass_rls
157
+ FROM col_grants_exploded g
158
+ LEFT JOIN pg_roles rol ON rol.oid = g.grantee_oid
159
+ WHERE true
160
+ ${roleFilter}
161
+ ),
162
+ policies AS (
163
+ SELECT
164
+ p.polrelid AS oid,
165
+ p.polname AS name,
166
+ p.polcmd AS cmd,
167
+ p.polpermissive AS permissive,
168
+ CASE
169
+ WHEN p.polroles = ARRAY[0]::oid[] THEN ARRAY['PUBLIC']
170
+ ELSE COALESCE(
171
+ (SELECT array_agg(rolname) FROM pg_roles WHERE oid = ANY(p.polroles)),
172
+ ARRAY[]::text[]
173
+ )
174
+ END AS roles,
175
+ pg_get_expr(p.polqual, p.polrelid) AS using_expr,
176
+ pg_get_expr(p.polwithcheck, p.polrelid) AS with_check_expr
177
+ FROM pg_policy p
178
+ )
179
+ SELECT
180
+ r.schema_name,
181
+ r.table_name,
182
+ r.oid::int AS oid,
183
+ r.rls_enabled,
184
+ r.rls_forced,
185
+ r.is_partitioned,
186
+ r.owner,
187
+ COALESCE(
188
+ (SELECT jsonb_agg(jsonb_build_object(
189
+ 'role', g.grantee,
190
+ 'privilege', g.privilege_type,
191
+ 'grantable', g.is_grantable,
192
+ 'bypassRls', g.bypass_rls
193
+ )) FROM grants g WHERE g.oid = r.oid),
194
+ '[]'::jsonb
195
+ ) AS grants,
196
+ COALESCE(
197
+ (SELECT jsonb_agg(jsonb_build_object(
198
+ 'role', g.grantee,
199
+ 'column', g.column_name,
200
+ 'privilege', g.privilege_type,
201
+ 'grantable', g.is_grantable,
202
+ 'bypassRls', g.bypass_rls
203
+ ) ORDER BY g.grantee, g.column_name, g.privilege_type)
204
+ FROM col_grants g WHERE g.oid = r.oid),
205
+ '[]'::jsonb
206
+ ) AS column_grants,
207
+ COALESCE(
208
+ (SELECT jsonb_agg(jsonb_build_object(
209
+ 'name', p.name,
210
+ 'cmd', p.cmd::text,
211
+ 'permissive', p.permissive,
212
+ 'roles', to_jsonb(p.roles),
213
+ 'using', p.using_expr,
214
+ 'withCheck', p.with_check_expr
215
+ )) FROM policies p WHERE p.oid = r.oid),
216
+ '[]'::jsonb
217
+ ) AS policies
218
+ FROM rels r
219
+ ORDER BY r.schema_name, r.table_name
220
+ `;
221
+ const params = [
222
+ options.schemas ?? [],
223
+ excludes,
224
+ options.roles ?? [],
225
+ options.extensions?.ignore ?? []
226
+ ];
227
+ const { rows } = await exec.query(sql, params);
228
+ return rows.map((r) => ({
229
+ schema: r.schema_name,
230
+ name: r.table_name,
231
+ oid: r.oid,
232
+ rlsEnabled: r.rls_enabled,
233
+ rlsForced: r.rls_forced,
234
+ isPartitioned: r.is_partitioned,
235
+ owner: r.owner,
236
+ grants: r.grants.map((g) => ({
237
+ role: g.role,
238
+ privilege: normalizePrivilege(g.privilege),
239
+ grantable: g.grantable,
240
+ bypassRls: g.bypassRls === true
241
+ })),
242
+ columnGrants: r.column_grants.map((g) => ({
243
+ role: g.role,
244
+ column: g.column,
245
+ privilege: normalizePrivilege(g.privilege),
246
+ grantable: g.grantable,
247
+ bypassRls: g.bypassRls === true
248
+ })),
249
+ policies: r.policies.map((p) => ({
250
+ name: p.name,
251
+ cmd: exports.POLICY_CMD_MAP[p.cmd] ?? 'ALL',
252
+ permissive: p.permissive,
253
+ roles: p.roles,
254
+ using: p.using,
255
+ withCheck: p.withCheck
256
+ }))
257
+ }));
258
+ }
259
+ function normalizePrivilege(raw) {
260
+ const upper = raw.toUpperCase();
261
+ switch (upper) {
262
+ case 'SELECT':
263
+ case 'INSERT':
264
+ case 'UPDATE':
265
+ case 'DELETE':
266
+ case 'TRUNCATE':
267
+ case 'REFERENCES':
268
+ case 'TRIGGER':
269
+ return upper;
270
+ default:
271
+ // `r`, `a`, `w`, `d` — ACL single-letter form
272
+ return exports.ACL_PRIVILEGE_MAP[raw] ?? 'SELECT';
273
+ }
274
+ }
275
+ /**
276
+ * Convenience: accept any of `pg.Client` / `pg.Pool` / a custom executor.
277
+ */
278
+ function asExecutor(client) {
279
+ return client;
280
+ }
@@ -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[]>;
package/pg/objects.js ADDED
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ /**
3
+ * ACLs on the relations that are not tables.
4
+ *
5
+ * `introspectTables` reads `relkind IN ('r','p')` and `introspectViews` reads
6
+ * views and matviews, which leaves two kinds of relation carrying privileges
7
+ * that nothing in this package ever looked at:
8
+ *
9
+ * - **sequences** (`relkind = 'S'`), where `USAGE`/`UPDATE` let a role call
10
+ * `nextval`/`setval` and `SELECT` lets it read `last_value`; and
11
+ * - **foreign tables** (`relkind = 'f'`), which cannot carry RLS at all —
12
+ * `ALTER FOREIGN TABLE ... ENABLE ROW LEVEL SECURITY` is rejected by
13
+ * Postgres (verified on 18), so a grant on one is never row-filtered.
14
+ *
15
+ * Both are read here rather than folded into the table snapshot: neither has
16
+ * RLS state, policies or column ACLs, and giving them a `TableSnapshot` with
17
+ * those fields stubbed out would invite every table rule to grade something it
18
+ * does not understand.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.introspectObjectAcls = introspectObjectAcls;
22
+ const DEFAULT_EXCLUDES = ['pg_catalog', 'information_schema', 'pg_toast'];
23
+ async function introspectObjectAcls(exec, options = {}) {
24
+ const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
25
+ const schemaFilter = options.schemas && options.schemas.length > 0
26
+ ? 'AND n.nspname = ANY($1::text[])'
27
+ : 'AND NOT (n.nspname = ANY($2::text[]))';
28
+ const sql = `
29
+ WITH _params AS (
30
+ SELECT $1::text[] AS include_schemas, $2::text[] AS exclude_schemas
31
+ ),
32
+ rels AS (
33
+ SELECT
34
+ n.nspname AS schema_name,
35
+ c.relname AS object_name,
36
+ c.oid AS oid,
37
+ c.relkind AS relkind,
38
+ pg_catalog.pg_get_userbyid(c.relowner) AS owner,
39
+ c.relacl AS relacl
40
+ FROM pg_class c
41
+ JOIN pg_namespace n ON n.oid = c.relnamespace
42
+ WHERE c.relkind IN ('S', 'f')
43
+ ${schemaFilter}
44
+ ),
45
+ grants AS (
46
+ SELECT
47
+ r.oid,
48
+ CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE rol.rolname END AS grantee,
49
+ a.privilege_type,
50
+ a.is_grantable,
51
+ CASE
52
+ WHEN a.grantee = 0 THEN false
53
+ ELSE COALESCE(rol.rolsuper OR rol.rolbypassrls, false)
54
+ END AS bypass_rls
55
+ FROM rels r, aclexplode(r.relacl) a
56
+ LEFT JOIN pg_roles rol ON rol.oid = a.grantee
57
+ WHERE r.relacl IS NOT NULL
58
+ ),
59
+ -- A serial/identity column records an OWNED BY dependency on its
60
+ -- sequence. The role inserting into that table needs USAGE on it, so the
61
+ -- link is what separates a load-bearing grant from a gratuitous one.
62
+ owners AS (
63
+ SELECT
64
+ d.objid AS seq_oid,
65
+ n.nspname || '.' || c.relname || '.' || a.attname AS owned_by
66
+ FROM pg_depend d
67
+ JOIN pg_class c ON c.oid = d.refobjid
68
+ JOIN pg_namespace n ON n.oid = c.relnamespace
69
+ JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
70
+ WHERE d.classid = 'pg_class'::regclass
71
+ AND d.refclassid = 'pg_class'::regclass
72
+ AND d.deptype IN ('a', 'i')
73
+ AND d.objid IN (SELECT oid FROM rels WHERE relkind = 'S')
74
+ )
75
+ SELECT
76
+ r.schema_name,
77
+ r.object_name,
78
+ r.relkind,
79
+ r.owner,
80
+ o.owned_by,
81
+ srv.srvname AS server_name,
82
+ COALESCE(
83
+ (
84
+ SELECT jsonb_agg(jsonb_build_object(
85
+ 'role', g.grantee,
86
+ 'privilege', g.privilege_type,
87
+ 'grantable', g.is_grantable,
88
+ 'bypassRls', g.bypass_rls
89
+ ) ORDER BY g.grantee, g.privilege_type)
90
+ FROM grants g
91
+ WHERE g.oid = r.oid
92
+ ),
93
+ '[]'::jsonb
94
+ ) AS grants
95
+ FROM rels r
96
+ LEFT JOIN owners o ON o.seq_oid = r.oid
97
+ LEFT JOIN pg_foreign_table ft ON ft.ftrelid = r.oid
98
+ LEFT JOIN pg_foreign_server srv ON srv.oid = ft.ftserver
99
+ ORDER BY r.schema_name, r.object_name
100
+ `;
101
+ const { rows } = await exec.query(sql, [options.schemas ?? null, excludes]);
102
+ return rows.map((r) => ({
103
+ schema: r.schema_name,
104
+ name: r.object_name,
105
+ kind: r.relkind === 'S' ? 'sequence' : 'foreign table',
106
+ owner: r.owner,
107
+ grants: r.grants,
108
+ ...(r.server_name ? { server: r.server_name } : {}),
109
+ ...(r.owned_by ? { ownedBy: r.owned_by } : {})
110
+ }));
111
+ }
package/pg/paths.d.ts ADDED
@@ -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>;