@safegres/test 0.0.2 → 1.30.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (483) hide show
  1. package/README.md +825 -412
  2. package/adapters.d.ts +8 -0
  3. package/adapters.js +18 -0
  4. package/ast/helpers.d.ts +16 -0
  5. package/ast/helpers.js +40 -0
  6. package/ast/parse.d.ts +31 -0
  7. package/ast/parse.js +87 -0
  8. package/ast/walk.d.ts +27 -0
  9. package/ast/walk.js +79 -0
  10. package/callgraph/baseline.d.ts +28 -0
  11. package/callgraph/baseline.js +55 -0
  12. package/callgraph/extract.d.ts +93 -0
  13. package/callgraph/extract.js +340 -0
  14. package/callgraph/graph.d.ts +82 -0
  15. package/callgraph/graph.js +276 -0
  16. package/checks/anti-patterns.d.ts +53 -0
  17. package/checks/anti-patterns.js +247 -0
  18. package/checks/column-grants.d.ts +20 -0
  19. package/checks/column-grants.js +74 -0
  20. package/checks/coverage.d.ts +43 -0
  21. package/checks/coverage.js +110 -0
  22. package/checks/definer-function.d.ts +91 -0
  23. package/checks/definer-function.js +458 -0
  24. package/checks/definer-view.d.ts +167 -0
  25. package/checks/definer-view.js +392 -0
  26. package/checks/indexes.d.ts +82 -0
  27. package/checks/indexes.js +263 -0
  28. package/checks/lattice.d.ts +179 -0
  29. package/checks/lattice.js +487 -0
  30. package/checks/object-acls.d.ts +32 -0
  31. package/checks/object-acls.js +129 -0
  32. package/checks/policy-index.d.ts +78 -0
  33. package/checks/policy-index.js +349 -0
  34. package/checks/revocable-grants.d.ts +140 -0
  35. package/checks/revocable-grants.js +377 -0
  36. package/checks/rls-flags.d.ts +25 -0
  37. package/checks/rls-flags.js +82 -0
  38. package/checks/role-reach.d.ts +329 -0
  39. package/checks/role-reach.js +277 -0
  40. package/checks/role-trust.d.ts +45 -0
  41. package/checks/role-trust.js +91 -0
  42. package/checks/set-role.d.ts +21 -0
  43. package/checks/set-role.js +66 -0
  44. package/checks/stats.d.ts +52 -0
  45. package/checks/stats.js +192 -0
  46. package/checks/view-exposure.d.ts +68 -0
  47. package/checks/view-exposure.js +236 -0
  48. package/checks/view-writes.d.ts +88 -0
  49. package/checks/view-writes.js +350 -0
  50. package/ci/baseline.d.ts +104 -0
  51. package/ci/baseline.js +111 -0
  52. package/ci/github.d.ts +47 -0
  53. package/ci/github.js +197 -0
  54. package/cli/audit.d.ts +3 -0
  55. package/cli/audit.js +560 -0
  56. package/cli/baseline.d.ts +3 -0
  57. package/cli/baseline.js +104 -0
  58. package/cli/commands.d.ts +2 -0
  59. package/cli/commands.js +60 -0
  60. package/cli/doctor.d.ts +3 -0
  61. package/cli/doctor.js +72 -0
  62. package/cli/eval.d.ts +3 -0
  63. package/cli/eval.js +153 -0
  64. package/cli/print-config.d.ts +3 -0
  65. package/cli/print-config.js +54 -0
  66. package/cli/shared.d.ts +43 -0
  67. package/cli/shared.js +153 -0
  68. package/cli.d.ts +2 -0
  69. package/cli.js +69 -0
  70. package/commands/audit.d.ts +65 -0
  71. package/commands/audit.js +779 -0
  72. package/commands/doctor.d.ts +19 -0
  73. package/commands/doctor.js +181 -0
  74. package/commands/eval.d.ts +67 -0
  75. package/commands/eval.js +93 -0
  76. package/config/fingerprint.d.ts +40 -0
  77. package/config/fingerprint.js +71 -0
  78. package/config/loader.d.ts +44 -0
  79. package/config/loader.js +210 -0
  80. package/config/presets.d.ts +95 -0
  81. package/config/presets.js +327 -0
  82. package/config/resolve.d.ts +47 -0
  83. package/config/resolve.js +192 -0
  84. package/config/schema.d.ts +54 -0
  85. package/config/schema.js +269 -0
  86. package/config/types.d.ts +562 -0
  87. package/config/validate.d.ts +14 -0
  88. package/config/validate.js +125 -0
  89. package/corpus/README.md +83 -0
  90. package/corpus/bootstrap.sql +14 -0
  91. package/corpus/cases/01-anon-write-grant/case.json +26 -0
  92. package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
  93. package/corpus/cases/02-grant-to-public/case.json +25 -0
  94. package/corpus/cases/02-grant-to-public/schema.sql +18 -0
  95. package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
  96. package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
  97. package/corpus/cases/04-permissive-write-policy/case.json +30 -0
  98. package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
  99. package/corpus/cases/05-public-read-policy/case.json +25 -0
  100. package/corpus/cases/05-public-read-policy/schema.sql +16 -0
  101. package/corpus/cases/06-update-without-with-check/case.json +25 -0
  102. package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
  103. package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
  104. package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
  105. package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
  106. package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
  107. package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
  108. package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
  109. package/corpus/cases/10-rls-not-forced/case.json +24 -0
  110. package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
  111. package/corpus/cases/11-session-user-in-policy/case.json +24 -0
  112. package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
  113. package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
  114. package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
  115. package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
  116. package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
  117. package/corpus/cases/14-dead-policy/case.json +24 -0
  118. package/corpus/cases/14-dead-policy/schema.sql +18 -0
  119. package/corpus/cases/15-unreachable-grant/case.json +24 -0
  120. package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
  121. package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
  122. package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
  123. package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
  124. package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
  125. package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
  126. package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
  127. package/corpus/cases/19-policy-column-cast/case.json +24 -0
  128. package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
  129. package/corpus/cases/20-stable-function-per-row/case.json +24 -0
  130. package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
  131. package/corpus/cases/21-table-without-primary-key/case.json +24 -0
  132. package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
  133. package/corpus/cases/22-redundant-index/case.json +24 -0
  134. package/corpus/cases/22-redundant-index/schema.sql +16 -0
  135. package/corpus/cases/23-tsvector-without-index/case.json +24 -0
  136. package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
  137. package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
  138. package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
  139. package/corpus/cases/25-definer-view-bypass/case.json +31 -0
  140. package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
  141. package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
  142. package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
  143. package/corpus/cases/27-definer-view-write/case.json +36 -0
  144. package/corpus/cases/27-definer-view-write/schema.sql +40 -0
  145. package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
  146. package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
  147. package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
  148. package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
  149. package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
  150. package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
  151. package/corpus/cases/31-matview-snapshot/case.json +32 -0
  152. package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
  153. package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
  154. package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
  155. package/corpus/cases/33-leaky-filter-view/case.json +36 -0
  156. package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
  157. package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
  158. package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
  159. package/corpus/cases/35-column-grant-projection/case.json +27 -0
  160. package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
  161. package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
  162. package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
  163. package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
  164. package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
  165. package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
  166. package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
  167. package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
  168. package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
  169. package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
  170. package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
  171. package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
  172. package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
  173. package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
  174. package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
  175. package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
  176. package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
  177. package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
  178. package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
  179. package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
  180. package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
  181. package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
  182. package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
  183. package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
  184. package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
  185. package/corpus/cases/41-unreadable-view-body/case.json +33 -0
  186. package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
  187. package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
  188. package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
  189. package/corpus/cases/42-search-column-indexed/case.json +21 -0
  190. package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
  191. package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
  192. package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
  193. package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
  194. package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
  195. package/corpus/cases/45-anon-foreign-table/case.json +27 -0
  196. package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
  197. package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
  198. package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
  199. package/corpus/cases/47-checked-writable-view/case.json +37 -0
  200. package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
  201. package/corpus/cases/48-definer-function-reach/case.json +38 -0
  202. package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
  203. package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
  204. package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
  205. package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
  206. package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
  207. package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
  208. package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
  209. package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
  210. package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
  211. package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
  212. package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
  213. package/corpus/index.d.ts +79 -0
  214. package/corpus/index.js +125 -0
  215. package/esm/adapters.d.ts +8 -0
  216. package/esm/adapters.js +6 -0
  217. package/esm/ast/helpers.d.ts +16 -0
  218. package/esm/ast/helpers.js +36 -0
  219. package/esm/ast/parse.d.ts +31 -0
  220. package/esm/ast/parse.js +82 -0
  221. package/esm/ast/walk.d.ts +27 -0
  222. package/esm/ast/walk.js +72 -0
  223. package/esm/callgraph/baseline.d.ts +28 -0
  224. package/esm/callgraph/baseline.js +48 -0
  225. package/esm/callgraph/extract.d.ts +93 -0
  226. package/esm/callgraph/extract.js +333 -0
  227. package/esm/callgraph/graph.d.ts +82 -0
  228. package/esm/callgraph/graph.js +273 -0
  229. package/esm/checks/anti-patterns.d.ts +53 -0
  230. package/esm/checks/anti-patterns.js +240 -0
  231. package/esm/checks/column-grants.d.ts +20 -0
  232. package/esm/checks/column-grants.js +71 -0
  233. package/esm/checks/coverage.d.ts +43 -0
  234. package/esm/checks/coverage.js +104 -0
  235. package/esm/checks/definer-function.d.ts +91 -0
  236. package/esm/checks/definer-function.js +452 -0
  237. package/esm/checks/definer-view.d.ts +167 -0
  238. package/esm/checks/definer-view.js +382 -0
  239. package/esm/checks/indexes.d.ts +82 -0
  240. package/esm/checks/indexes.js +256 -0
  241. package/esm/checks/lattice.d.ts +179 -0
  242. package/esm/checks/lattice.js +475 -0
  243. package/esm/checks/object-acls.d.ts +32 -0
  244. package/esm/checks/object-acls.js +125 -0
  245. package/esm/checks/policy-index.d.ts +78 -0
  246. package/esm/checks/policy-index.js +342 -0
  247. package/esm/checks/revocable-grants.d.ts +140 -0
  248. package/esm/checks/revocable-grants.js +373 -0
  249. package/esm/checks/rls-flags.d.ts +25 -0
  250. package/esm/checks/rls-flags.js +77 -0
  251. package/esm/checks/role-reach.d.ts +329 -0
  252. package/esm/checks/role-reach.js +270 -0
  253. package/esm/checks/role-trust.d.ts +45 -0
  254. package/esm/checks/role-trust.js +86 -0
  255. package/esm/checks/set-role.d.ts +21 -0
  256. package/esm/checks/set-role.js +63 -0
  257. package/esm/checks/stats.d.ts +52 -0
  258. package/esm/checks/stats.js +184 -0
  259. package/esm/checks/view-exposure.d.ts +68 -0
  260. package/esm/checks/view-exposure.js +231 -0
  261. package/esm/checks/view-writes.d.ts +88 -0
  262. package/esm/checks/view-writes.js +344 -0
  263. package/esm/ci/baseline.d.ts +104 -0
  264. package/esm/ci/baseline.js +107 -0
  265. package/esm/ci/github.d.ts +47 -0
  266. package/esm/ci/github.js +161 -0
  267. package/esm/cli/audit.d.ts +3 -0
  268. package/esm/cli/audit.js +525 -0
  269. package/esm/cli/baseline.d.ts +3 -0
  270. package/esm/cli/baseline.js +102 -0
  271. package/esm/cli/commands.d.ts +2 -0
  272. package/esm/cli/commands.js +53 -0
  273. package/esm/cli/doctor.d.ts +3 -0
  274. package/esm/cli/doctor.js +67 -0
  275. package/esm/cli/eval.d.ts +3 -0
  276. package/esm/cli/eval.js +115 -0
  277. package/esm/cli/print-config.d.ts +3 -0
  278. package/esm/cli/print-config.js +52 -0
  279. package/esm/cli/shared.d.ts +43 -0
  280. package/esm/cli/shared.js +112 -0
  281. package/esm/cli.d.ts +2 -0
  282. package/esm/cli.js +67 -0
  283. package/esm/commands/audit.d.ts +65 -0
  284. package/esm/commands/audit.js +776 -0
  285. package/esm/commands/doctor.d.ts +19 -0
  286. package/esm/commands/doctor.js +178 -0
  287. package/esm/commands/eval.d.ts +67 -0
  288. package/esm/commands/eval.js +90 -0
  289. package/esm/config/fingerprint.d.ts +40 -0
  290. package/esm/config/fingerprint.js +68 -0
  291. package/esm/config/loader.d.ts +44 -0
  292. package/esm/config/loader.js +172 -0
  293. package/esm/config/presets.d.ts +95 -0
  294. package/esm/config/presets.js +324 -0
  295. package/esm/config/resolve.d.ts +47 -0
  296. package/esm/config/resolve.js +180 -0
  297. package/esm/config/schema.d.ts +54 -0
  298. package/esm/config/schema.js +265 -0
  299. package/esm/config/types.d.ts +562 -0
  300. package/esm/config/validate.d.ts +14 -0
  301. package/esm/config/validate.js +122 -0
  302. package/esm/corpus/index.d.ts +79 -0
  303. package/esm/corpus/index.js +85 -0
  304. package/esm/exposure/adapters.d.ts +152 -0
  305. package/esm/exposure/adapters.js +463 -0
  306. package/esm/exposure/planes.d.ts +59 -0
  307. package/esm/exposure/planes.js +144 -0
  308. package/esm/exposure/reach.d.ts +78 -0
  309. package/esm/exposure/reach.js +105 -0
  310. package/esm/exposure/routines.d.ts +36 -0
  311. package/esm/exposure/routines.js +54 -0
  312. package/esm/index.d.ts +96 -0
  313. package/esm/index.js +57 -6
  314. package/esm/lint/index.d.ts +11 -0
  315. package/esm/lint/index.js +10 -0
  316. package/esm/perf/baseline.d.ts +40 -0
  317. package/esm/perf/baseline.js +83 -0
  318. package/esm/perf/explain.d.ts +46 -0
  319. package/esm/perf/explain.js +201 -0
  320. package/esm/pg/acl.d.ts +72 -0
  321. package/esm/pg/acl.js +186 -0
  322. package/esm/pg/behaviors.d.ts +114 -0
  323. package/esm/pg/behaviors.js +206 -0
  324. package/esm/pg/exposure.d.ts +74 -0
  325. package/esm/pg/exposure.js +198 -0
  326. package/esm/pg/functions.d.ts +52 -0
  327. package/esm/pg/functions.js +128 -0
  328. package/esm/pg/indexes.d.ts +192 -0
  329. package/esm/pg/indexes.js +395 -0
  330. package/esm/pg/introspect.d.ts +134 -0
  331. package/esm/pg/introspect.js +274 -0
  332. package/esm/pg/objects.d.ts +37 -0
  333. package/esm/pg/objects.js +108 -0
  334. package/esm/pg/paths.d.ts +114 -0
  335. package/esm/pg/paths.js +159 -0
  336. package/esm/pg/proc.d.ts +23 -0
  337. package/esm/pg/proc.js +55 -0
  338. package/esm/pg/reach-inputs.d.ts +47 -0
  339. package/esm/pg/reach-inputs.js +133 -0
  340. package/esm/pg/roles.d.ts +28 -0
  341. package/esm/pg/roles.js +50 -0
  342. package/esm/pg/stats.d.ts +65 -0
  343. package/esm/pg/stats.js +129 -0
  344. package/esm/pgpm-test.d.ts +22 -0
  345. package/esm/pgpm-test.js +39 -0
  346. package/esm/report/callgraph.d.ts +9 -0
  347. package/esm/report/callgraph.js +74 -0
  348. package/esm/report/compare.d.ts +143 -0
  349. package/esm/report/compare.js +195 -0
  350. package/esm/report/github.d.ts +74 -0
  351. package/esm/report/github.js +290 -0
  352. package/esm/report/json.d.ts +4 -0
  353. package/esm/report/json.js +3 -0
  354. package/esm/report/markdown.d.ts +25 -0
  355. package/esm/report/markdown.js +352 -0
  356. package/esm/report/pretty.d.ts +14 -0
  357. package/esm/report/pretty.js +318 -0
  358. package/esm/report/sarif.d.ts +40 -0
  359. package/esm/report/sarif.js +189 -0
  360. package/esm/report/view.d.ts +82 -0
  361. package/esm/report/view.js +126 -0
  362. package/esm/rules/registry.d.ts +66 -0
  363. package/esm/rules/registry.js +562 -0
  364. package/esm/score/score.d.ts +84 -0
  365. package/esm/score/score.js +233 -0
  366. package/esm/score/scorecards.d.ts +68 -0
  367. package/esm/score/scorecards.js +157 -0
  368. package/esm/types.d.ts +292 -0
  369. package/esm/types.js +26 -0
  370. package/esm/version.d.ts +8 -0
  371. package/esm/version.js +9 -0
  372. package/exposure/adapters.d.ts +152 -0
  373. package/exposure/adapters.js +468 -0
  374. package/exposure/planes.d.ts +59 -0
  375. package/exposure/planes.js +151 -0
  376. package/exposure/reach.d.ts +78 -0
  377. package/exposure/reach.js +109 -0
  378. package/exposure/routines.d.ts +36 -0
  379. package/exposure/routines.js +57 -0
  380. package/index.d.ts +96 -6
  381. package/index.js +229 -6
  382. package/lint/index.d.ts +11 -0
  383. package/lint/index.js +19 -0
  384. package/package.json +54 -48
  385. package/perf/baseline.d.ts +40 -0
  386. package/perf/baseline.js +92 -0
  387. package/perf/explain.d.ts +46 -0
  388. package/perf/explain.js +204 -0
  389. package/pg/acl.d.ts +72 -0
  390. package/pg/acl.js +191 -0
  391. package/pg/behaviors.d.ts +114 -0
  392. package/pg/behaviors.js +217 -0
  393. package/pg/exposure.d.ts +74 -0
  394. package/pg/exposure.js +205 -0
  395. package/pg/functions.d.ts +52 -0
  396. package/pg/functions.js +131 -0
  397. package/pg/indexes.d.ts +192 -0
  398. package/pg/indexes.js +400 -0
  399. package/pg/introspect.d.ts +134 -0
  400. package/pg/introspect.js +280 -0
  401. package/pg/objects.d.ts +37 -0
  402. package/pg/objects.js +111 -0
  403. package/pg/paths.d.ts +114 -0
  404. package/pg/paths.js +166 -0
  405. package/pg/proc.d.ts +23 -0
  406. package/pg/proc.js +58 -0
  407. package/pg/reach-inputs.d.ts +47 -0
  408. package/pg/reach-inputs.js +136 -0
  409. package/pg/roles.d.ts +28 -0
  410. package/pg/roles.js +54 -0
  411. package/pg/stats.d.ts +65 -0
  412. package/pg/stats.js +133 -0
  413. package/pgpm-test.d.ts +22 -0
  414. package/pgpm-test.js +42 -0
  415. package/report/callgraph.d.ts +9 -0
  416. package/report/callgraph.js +81 -0
  417. package/report/compare.d.ts +143 -0
  418. package/report/compare.js +205 -0
  419. package/report/github.d.ts +74 -0
  420. package/report/github.js +333 -0
  421. package/report/json.d.ts +4 -0
  422. package/report/json.js +6 -0
  423. package/report/markdown.d.ts +25 -0
  424. package/report/markdown.js +355 -0
  425. package/report/pretty.d.ts +14 -0
  426. package/report/pretty.js +324 -0
  427. package/report/sarif.d.ts +40 -0
  428. package/report/sarif.js +193 -0
  429. package/report/view.d.ts +82 -0
  430. package/report/view.js +132 -0
  431. package/rules/registry.d.ts +66 -0
  432. package/rules/registry.js +569 -0
  433. package/schema/safegres.schema.json +1119 -0
  434. package/score/score.d.ts +84 -0
  435. package/score/score.js +238 -0
  436. package/score/scorecards.d.ts +68 -0
  437. package/score/scorecards.js +162 -0
  438. package/types.d.ts +292 -0
  439. package/types.js +32 -0
  440. package/version.d.ts +8 -0
  441. package/version.js +12 -0
  442. package/LICENSE +0 -23
  443. package/admin.d.ts +0 -26
  444. package/admin.js +0 -182
  445. package/connect.d.ts +0 -19
  446. package/connect.js +0 -83
  447. package/dist/README.md +0 -557
  448. package/dist/package.json +0 -72
  449. package/esm/admin.js +0 -178
  450. package/esm/connect.js +0 -78
  451. package/esm/manager.js +0 -136
  452. package/esm/roles.js +0 -32
  453. package/esm/seed/adapters.js +0 -23
  454. package/esm/seed/csv.js +0 -44
  455. package/esm/seed/index.js +0 -16
  456. package/esm/seed/json.js +0 -18
  457. package/esm/seed/launchql.js +0 -19
  458. package/esm/seed/sqitch.js +0 -17
  459. package/esm/stream.js +0 -43
  460. package/esm/test-client.js +0 -150
  461. package/manager.d.ts +0 -25
  462. package/manager.js +0 -140
  463. package/roles.d.ts +0 -17
  464. package/roles.js +0 -38
  465. package/seed/adapters.d.ts +0 -4
  466. package/seed/adapters.js +0 -28
  467. package/seed/csv.d.ts +0 -9
  468. package/seed/csv.js +0 -49
  469. package/seed/index.d.ts +0 -16
  470. package/seed/index.js +0 -33
  471. package/seed/json.d.ts +0 -6
  472. package/seed/json.js +0 -21
  473. package/seed/launchql.d.ts +0 -2
  474. package/seed/launchql.js +0 -22
  475. package/seed/sqitch.d.ts +0 -2
  476. package/seed/sqitch.js +0 -20
  477. package/seed/types.d.ts +0 -13
  478. package/stream.d.ts +0 -2
  479. package/stream.js +0 -46
  480. package/test-client.d.ts +0 -49
  481. package/test-client.js +0 -154
  482. /package/{seed → config}/types.js +0 -0
  483. /package/esm/{seed → config}/types.js +0 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Proof by EXPLAIN (`--explain`).
3
+ *
4
+ * The `X*` rules infer from the catalog that a query *shape* has no index to
5
+ * serve it. This turns the inference into evidence: for each finding that has
6
+ * a probe shape, plan the query the finding is about and read the plan back.
7
+ *
8
+ * The planner is the authority, so the interesting outcome is disagreement —
9
+ * a finding whose probe plans as an index scan is **refuted** (some index the
10
+ * catalog rules didn't credit does serve it) and stops counting against the
11
+ * score. The reverse is not symmetrical: an empty or unanalyzed table always
12
+ * seq-scans, so a seq scan only *confirms* a finding above the row floor;
13
+ * below it the probe is inconclusive and the finding is left exactly as it
14
+ * was.
15
+ *
16
+ * Probes use `EXPLAIN (GENERIC_PLAN)` — parameters stay parameters, nothing
17
+ * is executed, and no value has to be invented for a column whose domain we
18
+ * know nothing about. That requires PostgreSQL 16+.
19
+ */
20
+ /** The minimum server version supporting `EXPLAIN (GENERIC_PLAN)`. */
21
+ const MIN_SERVER_VERSION = 160000;
22
+ /**
23
+ * Plan a probe for every probeable perf finding and annotate it in place.
24
+ * Findings the planner refutes are acknowledged (reported, unscored).
25
+ */
26
+ export async function proveFindings(exec, findings, tables, options = {}) {
27
+ const minRows = options.minRows ?? 1000;
28
+ const report = { probed: 0, confirmed: 0, refuted: 0, inconclusive: 0 };
29
+ const { rows: versionRows } = await exec.query(`SELECT setting FROM pg_settings WHERE name = 'server_version_num'`);
30
+ const version = versionRows[0];
31
+ if (Number(version?.setting ?? 0) < MIN_SERVER_VERSION) {
32
+ report.unavailable =
33
+ `EXPLAIN (GENERIC_PLAN) requires PostgreSQL 16 or later (server is ${version?.setting ?? 'unknown'}) — no findings were probed`;
34
+ return report;
35
+ }
36
+ for (const finding of findings) {
37
+ if (!finding.schema || !finding.table)
38
+ continue;
39
+ const table = tables.get(`${finding.schema}.${finding.table}`);
40
+ if (!table)
41
+ continue;
42
+ const probe = buildProbe(finding, table);
43
+ if (!probe)
44
+ continue;
45
+ const plan = await planProbe(exec, probe.sql);
46
+ if (!plan)
47
+ continue;
48
+ report.probed += 1;
49
+ const nodes = collectNodeTypes(plan);
50
+ const servedByIndex = nodes.some((n) => n.includes('Index'));
51
+ const predicted = probe.expect === 'sort'
52
+ ? nodes.includes('Sort') || nodes.includes('Incremental Sort')
53
+ : nodes.includes('Seq Scan');
54
+ const evidence = {
55
+ source: 'explain',
56
+ probe: probe.sql,
57
+ plan: nodes.join(' → '),
58
+ status: 'inconclusive'
59
+ };
60
+ if (servedByIndex && !predicted) {
61
+ evidence.status = 'refuted';
62
+ report.refuted += 1;
63
+ finding.acknowledged = true;
64
+ finding.severity = 'info';
65
+ finding.message += ' — refuted by EXPLAIN (the planner serves this shape with an index)';
66
+ finding.hint =
67
+ 'The planner chose an index for the probe query, so this finding does not describe a real plan. Reported for review, excluded from the perf score.';
68
+ }
69
+ else if (predicted && estimatedRows(table) >= minRows) {
70
+ evidence.status = 'confirmed';
71
+ report.confirmed += 1;
72
+ }
73
+ else {
74
+ report.inconclusive += 1;
75
+ evidence.note = estimatedRows(table) < minRows
76
+ ? `table has ~${Math.max(estimatedRows(table), 0)} estimated rows (< ${minRows}); a sequential scan is the correct plan at this size, so the probe proves nothing`
77
+ : 'the probe plan matched neither an index scan nor the predicted scan';
78
+ }
79
+ finding.evidence = evidence;
80
+ }
81
+ return report;
82
+ }
83
+ /**
84
+ * The query a finding is a claim about. `null` for findings with no probeable
85
+ * shape (X5 redundant indexes, X6 missing PK, S*, P*) — nothing is planned
86
+ * speculatively, so `--explain` never invents work.
87
+ */
88
+ function buildProbe(finding, table) {
89
+ const relation = `${quote(table.schema)}.${quote(table.name)}`;
90
+ const context = finding.context ?? {};
91
+ switch (finding.code) {
92
+ case 'X1': {
93
+ const columns = asStringArray(context.columns);
94
+ if (columns.length === 0)
95
+ return null;
96
+ const predicates = columns
97
+ .map((name, i) => equality(table, name, i + 1))
98
+ .filter((p) => p !== null);
99
+ if (predicates.length !== columns.length)
100
+ return null;
101
+ return { sql: `SELECT 1 FROM ${relation} WHERE ${predicates.join(' AND ')}`, expect: 'seq-scan' };
102
+ }
103
+ case 'X2': {
104
+ const predicate = typeof context.column === 'string'
105
+ ? equality(table, context.column, 1)
106
+ : null;
107
+ if (!predicate)
108
+ return null;
109
+ return { sql: `SELECT 1 FROM ${relation} WHERE ${predicate}`, expect: 'seq-scan' };
110
+ }
111
+ case 'X7': {
112
+ const column = typeof context.column === 'string' ? context.column : null;
113
+ if (!column)
114
+ return null;
115
+ const type = columnType(table, column);
116
+ if (!type)
117
+ return null;
118
+ if (type === 'tsvector') {
119
+ return {
120
+ sql: `SELECT 1 FROM ${relation} WHERE ${quote(column)} @@ $1::tsquery`,
121
+ expect: 'seq-scan'
122
+ };
123
+ }
124
+ // Vector search is an ordering, not a filter: the index either serves
125
+ // the distance order or the plan sorts every row.
126
+ return {
127
+ sql: `SELECT 1 FROM ${relation} ORDER BY ${quote(column)} <-> $1::${type} LIMIT 10`,
128
+ expect: 'sort'
129
+ };
130
+ }
131
+ case 'X8': {
132
+ const column = typeof context.column === 'string' ? context.column : null;
133
+ if (!column)
134
+ return null;
135
+ return {
136
+ sql: `SELECT 1 FROM ${relation} ORDER BY ${quote(column)} DESC LIMIT 100`,
137
+ expect: 'sort'
138
+ };
139
+ }
140
+ default:
141
+ return null;
142
+ }
143
+ }
144
+ async function planProbe(exec, sql) {
145
+ try {
146
+ const { rows } = await exec.query(`EXPLAIN (GENERIC_PLAN, FORMAT JSON) ${sql}`);
147
+ const first = rows[0];
148
+ if (!first)
149
+ return null;
150
+ // node-postgres returns the single JSON column under 'QUERY PLAN'.
151
+ const value = first['QUERY PLAN'] ?? Object.values(first)[0];
152
+ return typeof value === 'string' ? JSON.parse(value) : value;
153
+ }
154
+ catch {
155
+ // A probe that cannot be planned (missing operator class, permissions,
156
+ // an exotic type) is simply not evidence.
157
+ return null;
158
+ }
159
+ }
160
+ /** Plan node types, outermost first. */
161
+ function collectNodeTypes(plan) {
162
+ const out = [];
163
+ const visit = (node) => {
164
+ if (Array.isArray(node)) {
165
+ for (const item of node)
166
+ visit(item);
167
+ return;
168
+ }
169
+ if (!node || typeof node !== 'object')
170
+ return;
171
+ const record = node;
172
+ if (typeof record['Node Type'] === 'string')
173
+ out.push(record['Node Type']);
174
+ for (const key of ['Plan', 'Plans']) {
175
+ if (key in record)
176
+ visit(record[key]);
177
+ }
178
+ };
179
+ visit(plan);
180
+ return out;
181
+ }
182
+ function equality(table, column, param) {
183
+ const type = columnType(table, column);
184
+ if (!type)
185
+ return null;
186
+ return `${quote(column)} = $${param}::${type}`;
187
+ }
188
+ function columnType(table, column) {
189
+ return table.columns.find((c) => c.name === column)?.type ?? null;
190
+ }
191
+ function estimatedRows(table) {
192
+ return table.estimatedRows;
193
+ }
194
+ function asStringArray(value) {
195
+ if (!Array.isArray(value))
196
+ return [];
197
+ return value.filter((v) => typeof v === 'string');
198
+ }
199
+ function quote(identifier) {
200
+ return `"${identifier.replace(/"/g, '""')}"`;
201
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Role-graph and schema-ACL introspection for the grant/policy lattice.
3
+ *
4
+ * The table snapshot reports grants exactly as the catalog stores them —
5
+ * `GRANT TO PUBLIC` is one row, membership in a granted role is zero rows.
6
+ * The lattice rules reason about *effective* access, which needs:
7
+ *
8
+ * - the role inheritance closure (`pg_auth_members`, INHERIT-respecting);
9
+ * - which roles bypass RLS entirely (superuser / BYPASSRLS);
10
+ * - schema USAGE ACLs (an object grant without USAGE on its schema is
11
+ * unreachable);
12
+ * - whether a schema holds any function the role can EXECUTE (so "dead
13
+ * schema USAGE" doesn't false-positive on function-only schemas).
14
+ */
15
+ import type { QueryExecutor } from './introspect';
16
+ export interface RoleAttributes {
17
+ name: string;
18
+ /** Superuser or BYPASSRLS — policies never apply. */
19
+ bypassRls: boolean;
20
+ /** Superuser — ACL checks never apply either. */
21
+ isSuper: boolean;
22
+ /**
23
+ * Roles this role inherits privileges from, transitively. Only memberships
24
+ * that actually confer inheritance (INHERIT) are followed.
25
+ */
26
+ inheritsFrom: string[];
27
+ /**
28
+ * Roles this role can assume with `SET ROLE`, transitively. Distinct from
29
+ * `inheritsFrom`: on PG16+ a membership can confer the ability to `SET ROLE`
30
+ * (`pg_auth_members.set_option`) *without* passive inheritance, so a role
31
+ * that reaches nothing by inheritance can still execute with a target
32
+ * role's full privileges by assuming it. Excludes the role itself.
33
+ */
34
+ canSetRole: string[];
35
+ }
36
+ export interface SchemaAclGrant {
37
+ role: string;
38
+ privilege: 'USAGE' | 'CREATE';
39
+ }
40
+ export interface SchemaAclInfo {
41
+ schema: string;
42
+ owner: string;
43
+ grants: SchemaAclGrant[];
44
+ /** Roles (incl. 'PUBLIC') holding EXECUTE on at least one function in the schema. */
45
+ executeRoles: string[];
46
+ }
47
+ /**
48
+ * Every non-system role with its RLS-bypass flags, its transitive
49
+ * INHERIT-following membership closure, and its transitive `SET ROLE`
50
+ * (`set_option`) closure.
51
+ */
52
+ export declare function introspectRoleGraph(exec: QueryExecutor): Promise<Map<string, RoleAttributes>>;
53
+ export interface SchemaAclOptions {
54
+ schemas?: string[];
55
+ excludeSchemas?: string[];
56
+ }
57
+ /**
58
+ * Schema ACLs plus, per schema, the roles holding EXECUTE on at least one of
59
+ * its functions. A NULL `nspacl` means the default ACL (owner-only + nothing
60
+ * for PUBLIC on non-`public` schemas); a NULL `proacl` means EXECUTE for
61
+ * PUBLIC, which is why the EXECUTE aggregation has to account for it.
62
+ */
63
+ export declare function introspectSchemaAcls(exec: QueryExecutor, options?: SchemaAclOptions): Promise<SchemaAclInfo[]>;
64
+ /**
65
+ * The role can enter the schema, so a grant on something inside it is reach.
66
+ *
67
+ * The same gate L3 applies to a table grant, applied to a schema's contents:
68
+ * without `USAGE` the object cannot be named, and Postgres's default
69
+ * EXECUTE-to-PUBLIC would otherwise make every function in an internal schema
70
+ * look reachable by every role in the database.
71
+ */
72
+ export declare function canEnterSchema(schema: string, role: string, graph: Map<string, RoleAttributes>, schemaAcls: Map<string, SchemaAclInfo>): boolean;
package/esm/pg/acl.js ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Role-graph and schema-ACL introspection for the grant/policy lattice.
3
+ *
4
+ * The table snapshot reports grants exactly as the catalog stores them —
5
+ * `GRANT TO PUBLIC` is one row, membership in a granted role is zero rows.
6
+ * The lattice rules reason about *effective* access, which needs:
7
+ *
8
+ * - the role inheritance closure (`pg_auth_members`, INHERIT-respecting);
9
+ * - which roles bypass RLS entirely (superuser / BYPASSRLS);
10
+ * - schema USAGE ACLs (an object grant without USAGE on its schema is
11
+ * unreachable);
12
+ * - whether a schema holds any function the role can EXECUTE (so "dead
13
+ * schema USAGE" doesn't false-positive on function-only schemas).
14
+ */
15
+ /**
16
+ * Every non-system role with its RLS-bypass flags, its transitive
17
+ * INHERIT-following membership closure, and its transitive `SET ROLE`
18
+ * (`set_option`) closure.
19
+ */
20
+ export async function introspectRoleGraph(exec) {
21
+ const { rows: roleRows } = await exec.query(`
22
+ SELECT rolname, rolsuper, rolbypassrls, rolinherit
23
+ FROM pg_roles
24
+ WHERE rolname NOT LIKE 'pg\\_%'
25
+ ORDER BY rolname
26
+ `);
27
+ let memberRows;
28
+ try {
29
+ const { rows } = await exec.query(`
30
+ SELECT m.rolname AS member, p.rolname AS parent,
31
+ am.inherit_option AS inherits, am.set_option AS "canSet"
32
+ FROM pg_auth_members am
33
+ JOIN pg_roles m ON m.oid = am.member
34
+ JOIN pg_roles p ON p.oid = am.roleid
35
+ WHERE m.rolname NOT LIKE 'pg\\_%' AND p.rolname NOT LIKE 'pg\\_%'
36
+ `);
37
+ memberRows = rows;
38
+ }
39
+ catch (e) {
40
+ if (!isUndefinedColumn(e))
41
+ throw e;
42
+ const { rows } = await exec.query(`
43
+ SELECT m.rolname AS member, p.rolname AS parent,
44
+ m.rolinherit AS inherits, true AS "canSet"
45
+ FROM pg_auth_members am
46
+ JOIN pg_roles m ON m.oid = am.member
47
+ JOIN pg_roles p ON p.oid = am.roleid
48
+ WHERE m.rolname NOT LIKE 'pg\\_%' AND p.rolname NOT LIKE 'pg\\_%'
49
+ `);
50
+ memberRows = rows;
51
+ }
52
+ const parents = new Map();
53
+ const setTargets = new Map();
54
+ for (const m of memberRows) {
55
+ if (m.inherits) {
56
+ const list = parents.get(m.member) ?? [];
57
+ list.push(m.parent);
58
+ parents.set(m.member, list);
59
+ }
60
+ if (m.canSet) {
61
+ const list = setTargets.get(m.member) ?? [];
62
+ list.push(m.parent);
63
+ setTargets.set(m.member, list);
64
+ }
65
+ }
66
+ const graph = new Map();
67
+ for (const r of roleRows) {
68
+ graph.set(r.rolname, {
69
+ name: r.rolname,
70
+ bypassRls: r.rolsuper || r.rolbypassrls,
71
+ isSuper: r.rolsuper,
72
+ inheritsFrom: closure(r.rolname, parents),
73
+ canSetRole: closure(r.rolname, setTargets)
74
+ });
75
+ }
76
+ return graph;
77
+ }
78
+ /** Transitive parents of `role`, excluding itself, cycle-safe. */
79
+ function closure(role, parents) {
80
+ const seen = new Set();
81
+ const stack = [...(parents.get(role) ?? [])];
82
+ while (stack.length > 0) {
83
+ const next = stack.pop();
84
+ if (next === role || seen.has(next))
85
+ continue;
86
+ seen.add(next);
87
+ stack.push(...(parents.get(next) ?? []));
88
+ }
89
+ return [...seen].sort();
90
+ }
91
+ function isUndefinedColumn(e) {
92
+ return e.code === '42703';
93
+ }
94
+ const DEFAULT_EXCLUDES = ['pg_catalog', 'information_schema', 'pg_toast'];
95
+ /**
96
+ * Schema ACLs plus, per schema, the roles holding EXECUTE on at least one of
97
+ * its functions. A NULL `nspacl` means the default ACL (owner-only + nothing
98
+ * for PUBLIC on non-`public` schemas); a NULL `proacl` means EXECUTE for
99
+ * PUBLIC, which is why the EXECUTE aggregation has to account for it.
100
+ */
101
+ export async function introspectSchemaAcls(exec, options = {}) {
102
+ const excludes = [...DEFAULT_EXCLUDES, ...(options.excludeSchemas ?? [])];
103
+ const schemaFilter = options.schemas && options.schemas.length > 0
104
+ ? `AND n.nspname = ANY($1::text[])`
105
+ : `AND NOT (n.nspname = ANY($2::text[]))`;
106
+ const sql = `
107
+ WITH _params AS (
108
+ SELECT $1::text[] AS include_schemas, $2::text[] AS exclude_schemas
109
+ ),
110
+ ns AS (
111
+ SELECT n.oid, n.nspname, pg_get_userbyid(n.nspowner) AS owner, n.nspacl
112
+ FROM pg_namespace n
113
+ WHERE n.nspname NOT LIKE 'pg\\_%'
114
+ ${schemaFilter}
115
+ ),
116
+ schema_grants AS (
117
+ SELECT
118
+ ns.oid,
119
+ CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE rol.rolname END AS grantee,
120
+ a.privilege_type
121
+ FROM ns, aclexplode(ns.nspacl) a
122
+ LEFT JOIN pg_roles rol ON rol.oid = a.grantee
123
+ WHERE ns.nspacl IS NOT NULL
124
+ ),
125
+ fn_execute AS (
126
+ SELECT
127
+ p.pronamespace AS oid,
128
+ CASE
129
+ WHEN p.proacl IS NULL THEN 'PUBLIC'
130
+ WHEN a.grantee = 0 THEN 'PUBLIC'
131
+ ELSE rol.rolname
132
+ END AS grantee
133
+ FROM pg_proc p
134
+ LEFT JOIN LATERAL aclexplode(p.proacl) a
135
+ ON p.proacl IS NOT NULL AND a.privilege_type = 'EXECUTE'
136
+ LEFT JOIN pg_roles rol ON rol.oid = a.grantee
137
+ WHERE p.pronamespace IN (SELECT oid FROM ns)
138
+ )
139
+ SELECT
140
+ ns.nspname AS schema_name,
141
+ ns.owner,
142
+ COALESCE(
143
+ (SELECT jsonb_agg(DISTINCT jsonb_build_object('role', g.grantee, 'privilege', g.privilege_type))
144
+ FROM schema_grants g WHERE g.oid = ns.oid),
145
+ '[]'::jsonb
146
+ ) AS grants,
147
+ COALESCE(
148
+ (SELECT array_agg(DISTINCT f.grantee) FROM fn_execute f
149
+ WHERE f.oid = ns.oid AND f.grantee IS NOT NULL),
150
+ ARRAY[]::text[]
151
+ ) AS execute_roles
152
+ FROM ns
153
+ ORDER BY ns.nspname
154
+ `;
155
+ const { rows } = await exec.query(sql, [options.schemas ?? [], excludes]);
156
+ return rows.map((r) => ({
157
+ schema: r.schema_name,
158
+ owner: r.owner,
159
+ grants: r.grants
160
+ .filter((g) => g.privilege === 'USAGE' || g.privilege === 'CREATE')
161
+ .map((g) => ({ role: g.role, privilege: g.privilege })),
162
+ executeRoles: r.execute_roles
163
+ }));
164
+ }
165
+ /**
166
+ * The role can enter the schema, so a grant on something inside it is reach.
167
+ *
168
+ * The same gate L3 applies to a table grant, applied to a schema's contents:
169
+ * without `USAGE` the object cannot be named, and Postgres's default
170
+ * EXECUTE-to-PUBLIC would otherwise make every function in an internal schema
171
+ * look reachable by every role in the database.
172
+ */
173
+ export function canEnterSchema(schema, role, graph, schemaAcls) {
174
+ const acl = schemaAcls.get(schema);
175
+ if (!acl)
176
+ return true; // not introspected: assume reachable rather than silently drop
177
+ const attrs = graph.get(role);
178
+ if (attrs?.isSuper)
179
+ return true;
180
+ if (role === acl.owner)
181
+ return true;
182
+ const usage = new Set(acl.grants.filter((g) => g.privilege === 'USAGE').map((g) => g.role));
183
+ if (usage.has('PUBLIC') || usage.has(role))
184
+ return true;
185
+ return (attrs?.inheritsFrom ?? []).some((a) => usage.has(a) || a === acl.owner);
186
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * PostGraphile *behaviors*, read from the object comments they are declared in.
3
+ *
4
+ * A behavior string is the schema author stating which parts of the generated
5
+ * API an object participates in — `@behavior -list -connection` on a foreign
6
+ * key constraint says the reverse relation is not exposed. That is a different
7
+ * kind of evidence from anything else safegres collects: `pg_class.reltuples`
8
+ * and `pg_stat_user_indexes.idx_scan` are *measurements*, and safegres grades
9
+ * an ephemeral CI database that has never held data, so both read zero at
10
+ * exactly the moment they would have to mean something. A declaration reads the
11
+ * same in CI as in production.
12
+ *
13
+ * Read from the comment rather than from a running Graphile instance, or from
14
+ * whatever tables the author generated the comment out of:
15
+ *
16
+ * - `@behavior` in a comment is the PostGraphile v5 convention, so this works
17
+ * on any Graphile database rather than one project's metadata schema.
18
+ * - The comment is the composed value — one tag per object — so it is already
19
+ * the author's resolved intent.
20
+ * - A live instance would additionally resolve preset defaults, but requires an
21
+ * API to be running. safegres grades a database.
22
+ *
23
+ * The consequence of that last point is the rule this module exists to enforce:
24
+ * **only an explicit negative fragment is evidence.** Presets grant most
25
+ * behaviors by default, so the *absence* of `+list` says nothing whatsoever. A
26
+ * scanner that reads silence as denial is a scanner that tells you to drop an
27
+ * index a live API is using.
28
+ */
29
+ import type { IntrospectOptions, QueryExecutor } from './introspect';
30
+ /** One `[+|-]scope` term of a behavior string. */
31
+ export interface BehaviorFragment {
32
+ modifier: '+' | '-';
33
+ /**
34
+ * The scope as written, e.g. `list`, `resource:connection`, `*`. Scope paths
35
+ * are `:`-separated and increasingly specific left to right.
36
+ */
37
+ scope: string;
38
+ }
39
+ /**
40
+ * The three behavior tags a relation can carry.
41
+ *
42
+ * `@behavior` applies to *both* directions of a foreign key. Graphile also
43
+ * accepts `@forwardBehavior` and `@backwardBehavior`, which apply to only one
44
+ * — and the distinction is not cosmetic: denying `list` with a plain
45
+ * `@behavior` removes the forward `post.author` field as well as the reverse
46
+ * `author.posts` list, which is almost never what the author meant.
47
+ */
48
+ export interface ConstraintBehaviors {
49
+ /** `@behavior` — applies to both directions. */
50
+ both?: string;
51
+ /** `@forwardBehavior` — the referencing table's field pointing at the parent. */
52
+ forward?: string;
53
+ /** `@backwardBehavior` — the referenced table's field listing the children. */
54
+ backward?: string;
55
+ }
56
+ /** Behaviors found on each kind of object, keyed as described on each map. */
57
+ export interface BehaviorSnapshot {
58
+ /** `schema.table` → behavior string. */
59
+ tables: Map<string, string>;
60
+ /** `schema.table.column` → behavior string. */
61
+ columns: Map<string, string>;
62
+ /** `schema.table.constraint` → behavior string. */
63
+ constraints: Map<string, string>;
64
+ /** `schema.table.constraint` → the directional tags on that constraint. */
65
+ constraintDirections: Map<string, ConstraintBehaviors>;
66
+ }
67
+ export declare function emptyBehaviorSnapshot(): BehaviorSnapshot;
68
+ /**
69
+ * The behavior governing one direction of a relation: the directional tag when
70
+ * present, else the undirected one. They do not merge — Graphile resolves the
71
+ * specific tag against the preset, and treating `@behavior -list` as an extra
72
+ * fragment on top of `@backwardBehavior +list` would invert the author's
73
+ * override.
74
+ */
75
+ export declare function directionalBehavior(behaviors: ConstraintBehaviors | undefined, direction: 'forward' | 'backward'): string | undefined;
76
+ /** Catalog schemas no API surface ever contains. */
77
+ export declare const SYSTEM_SCHEMAS: string[];
78
+ /**
79
+ * Extract the `@behavior` smart tag from an object comment.
80
+ *
81
+ * Smart tags occupy the leading lines of the comment, `@tag [value]`, and stop
82
+ * at the first line that is not one; everything after is the description. Only
83
+ * `@behavior` is returned — `@omit` is deliberately not translated here, since
84
+ * its v4 semantics ("remove from the schema") are not the same question as a
85
+ * behavior fragment and conflating them would put a guess in the evidence.
86
+ */
87
+ export declare function parseBehaviorTag(comment: string | null | undefined): string | null;
88
+ /** Every behavior tag on a comment, by direction. */
89
+ export declare function parseBehaviorTags(comment: string | null | undefined): ConstraintBehaviors;
90
+ /** Split a behavior string into fragments. A bare scope is a grant, as in PostGraphile. */
91
+ export declare function parseFragments(behavior: string | null | undefined): BehaviorFragment[];
92
+ /**
93
+ * Resolve one ability against a behavior string.
94
+ *
95
+ * `undefined` means *undeclared*, and is the answer that matters: it is not
96
+ * `false`. Later fragments win, matching PostGraphile — `-* +list` grants
97
+ * `list` and denies the rest.
98
+ */
99
+ export declare function resolveAbility(behavior: string | null | undefined, ability: string): boolean | undefined;
100
+ /**
101
+ * True when *every* one of `abilities` is explicitly denied.
102
+ *
103
+ * Every, not some: a relation reachable as a single record is still reachable,
104
+ * and one undeclared ability is enough to leave the question open.
105
+ */
106
+ export declare function deniesAll(behavior: string | null | undefined, abilities: string[]): boolean;
107
+ /**
108
+ * Behavior tags on every table, column and constraint in scope.
109
+ *
110
+ * Extension-owned objects are not filtered here: the maps are only ever
111
+ * consulted for relations that survived the scan's own filtering, so an extra
112
+ * entry costs a map slot and changes nothing.
113
+ */
114
+ export declare function introspectBehaviors(exec: QueryExecutor, options?: Pick<IntrospectOptions, 'schemas' | 'excludeSchemas'>): Promise<BehaviorSnapshot>;