@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,463 @@
1
+ /**
2
+ * Exposure adapters: how safegres learns what a stack exposes.
3
+ *
4
+ * An adapter is a value, not a module name. Built-ins are named exports of
5
+ * this module, a custom one is an object you construct in your own repo, and
6
+ * both are passed the same way — nothing is resolved by package name, and
7
+ * there is no plugin loader to reason about:
8
+ *
9
+ * ```ts
10
+ * import { constructiveAdapter, definePlanes } from 'safegres/adapters';
11
+ *
12
+ * export default defineConfig({
13
+ * exposure: { adapters: [constructiveAdapter, myAdapter], schemas: ['app_public'] }
14
+ * });
15
+ * ```
16
+ *
17
+ * `detect()` answers "is this stack present in this database?" and `resolve()`
18
+ * returns the planes it exposes — one per API where the stack has more than
19
+ * one, because "the admin API grades A and the public API grades C" is
20
+ * strictly more useful than their union.
21
+ */
22
+ import { introspectBehaviors, SYSTEM_SCHEMAS } from '../pg/behaviors';
23
+ import { computeApiReach } from './reach';
24
+ /**
25
+ * Constructive routing-plane introspection: an API exposes a set of schemas
26
+ * via `routing_public.apis` → `routing_public.api_schemas` →
27
+ * `metaschema_public.schema` (and the platform plane via `platform_apis` →
28
+ * `platform_api_schemas`). API-edge roles come from `apis.role_name` /
29
+ * `apis.anon_role`.
30
+ *
31
+ * Emits one `api` plane per API, plus a primary plane unioning them: the union
32
+ * is what the headline score has always been computed against, and the
33
+ * per-API planes are what let a reader see which API carries the debt.
34
+ */
35
+ export const constructiveAdapter = {
36
+ name: 'constructive',
37
+ async detect(exec) {
38
+ const { rows } = await exec.query(`SELECT
39
+ to_regclass('routing_public.apis') IS NOT NULL
40
+ AND to_regclass('routing_public.api_schemas') IS NOT NULL
41
+ AND to_regclass('metaschema_public.schema') IS NOT NULL AS ok`);
42
+ return rows[0]?.ok === true;
43
+ },
44
+ async resolve(exec) {
45
+ const { rows: hasPlatform } = await exec.query(`SELECT
46
+ to_regclass('routing_public.platform_apis') IS NOT NULL
47
+ AND to_regclass('routing_public.platform_api_schemas') IS NOT NULL AS ok`);
48
+ const sources = [
49
+ `SELECT a.name::text AS api, s.schema_name, a.role_name, a.anon_role
50
+ FROM routing_public.apis a
51
+ JOIN routing_public.api_schemas aps ON aps.api_id = a.id
52
+ JOIN metaschema_public.schema s ON s.id = aps.schema_id`
53
+ ];
54
+ if (hasPlatform[0]?.ok) {
55
+ sources.push(`SELECT a.name::text AS api, s.schema_name, a.role_name, a.anon_role
56
+ FROM routing_public.platform_apis a
57
+ JOIN routing_public.platform_api_schemas aps ON aps.api_id = a.id
58
+ JOIN metaschema_public.schema s ON s.id = aps.schema_id`);
59
+ }
60
+ const { rows } = await exec.query(sources.join(' UNION ALL '));
61
+ const entryOf = () => ({
62
+ schemas: new Set(),
63
+ roles: new Set(),
64
+ anonRoles: new Set()
65
+ });
66
+ const byApi = new Map();
67
+ const all = entryOf();
68
+ for (const r of rows) {
69
+ if (!r.schema_name)
70
+ continue;
71
+ const key = r.api ?? 'api';
72
+ const entry = byApi.get(key) ?? entryOf();
73
+ entry.schemas.add(r.schema_name);
74
+ all.schemas.add(r.schema_name);
75
+ for (const role of [r.role_name, r.anon_role]) {
76
+ if (role) {
77
+ entry.roles.add(role);
78
+ all.roles.add(role);
79
+ }
80
+ }
81
+ // `role_name` is the signed-in role (`authenticated`); only `anon_role`
82
+ // is reachable without credentials.
83
+ if (r.anon_role) {
84
+ entry.anonRoles.add(r.anon_role);
85
+ all.anonRoles.add(r.anon_role);
86
+ }
87
+ byApi.set(key, entry);
88
+ }
89
+ if (all.schemas.size === 0)
90
+ return [];
91
+ const planes = [
92
+ {
93
+ name: 'api',
94
+ kind: 'api',
95
+ primary: true,
96
+ schemas: sorted(all.schemas),
97
+ roles: sorted(all.roles),
98
+ anonRoles: sorted(all.anonRoles)
99
+ }
100
+ ];
101
+ // One plane per API, but only where there is more than one to tell apart:
102
+ // on a single-API database they would restate the primary plane.
103
+ if (byApi.size > 1) {
104
+ for (const [api, entry] of [...byApi.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
105
+ planes.push({
106
+ name: `api:${api}`,
107
+ kind: 'api',
108
+ schemas: sorted(entry.schemas),
109
+ roles: sorted(entry.roles),
110
+ anonRoles: sorted(entry.anonRoles)
111
+ });
112
+ }
113
+ }
114
+ return planes;
115
+ },
116
+ // A Constructive API *is* a PostGraphile API, so the reach question has the
117
+ // same answer. Delegated rather than duplicated, and defined below the
118
+ // delegate — see `postgraphileAdapter`.
119
+ reach: (exec, context) => postgraphileAdapter.reach(exec, context)
120
+ };
121
+ /**
122
+ * PostGraphile behaviors as relation-level reach.
123
+ *
124
+ * Deliberately *not* folded into {@link constructiveAdapter}, even though
125
+ * Constructive is a PostGraphile stack: behaviors are a Graphile convention
126
+ * that any Graphile database follows, and each adapter should say one true
127
+ * thing. This one contributes no planes — it has no idea which schemas an API
128
+ * serves — and only narrows the planes another adapter, or the config, already
129
+ * established.
130
+ */
131
+ export const postgraphileAdapter = {
132
+ name: 'postgraphile',
133
+ async detect(exec) {
134
+ const { rows } = await exec.query(`SELECT EXISTS (
135
+ SELECT 1 FROM pg_description
136
+ WHERE description ~ '(^|\n)@(behavior|forwardBehavior|backwardBehavior)\\s'
137
+ ) AS ok`);
138
+ return rows[0]?.ok === true;
139
+ },
140
+ async resolve() {
141
+ return [];
142
+ },
143
+ async reach(exec, context) {
144
+ const behaviors = await introspectBehaviors(exec, {
145
+ schemas: context.schemas,
146
+ excludeSchemas: context.excludeSchemas
147
+ });
148
+ // `schemas` empty means "the whole database", so the filter is built
149
+ // rather than passed as an always-bound parameter: an unreferenced
150
+ // placeholder has no inferable type and Postgres rejects the statement.
151
+ const params = [[...SYSTEM_SCHEMAS]];
152
+ const filters = [`n.nspname <> ALL ($1::text[])`];
153
+ if (context.schemas.length > 0) {
154
+ params.push(context.schemas);
155
+ filters.push(`n.nspname = ANY ($${params.length}::text[])`);
156
+ }
157
+ if (context.excludeSchemas && context.excludeSchemas.length > 0) {
158
+ params.push(context.excludeSchemas);
159
+ filters.push(`n.nspname <> ALL ($${params.length}::text[])`);
160
+ }
161
+ const { rows } = await exec.query(`SELECT n.nspname || '.' || c.relname AS relation,
162
+ co.conname AS constraint_name,
163
+ CASE WHEN co.oid IS NULL THEN NULL
164
+ ELSE fn.nspname || '.' || fc.relname END AS references
165
+ FROM pg_class c
166
+ JOIN pg_namespace n ON n.oid = c.relnamespace
167
+ LEFT JOIN pg_constraint co ON co.conrelid = c.oid AND co.contype = 'f'
168
+ LEFT JOIN pg_class fc ON fc.oid = co.confrelid
169
+ LEFT JOIN pg_namespace fn ON fn.oid = fc.relnamespace
170
+ WHERE c.relkind IN ('r', 'p')
171
+ AND ${filters.join('\n AND ')}`, params);
172
+ const relations = new Set();
173
+ const edges = [];
174
+ for (const row of rows) {
175
+ relations.add(row.relation);
176
+ if (row.constraint_name && row.references) {
177
+ edges.push({ from: row.relation, to: row.references, constraint: row.constraint_name });
178
+ }
179
+ }
180
+ return computeApiReach({ relations: [...relations].sort(), edges, behaviors });
181
+ }
182
+ };
183
+ /**
184
+ * PostgREST exposure, read from the catalog rather than inferred from a
185
+ * schema name: PostgREST serves exactly `pgrst.db_schemas`, which is set as a
186
+ * role or database GUC (`ALTER ROLE authenticator SET pgrst.db_schemas = …`)
187
+ * and therefore lands in `pg_db_role_setting`. `pgrst.db_anon_role` names the
188
+ * role an unauthenticated request runs as, and the connecting role
189
+ * (`authenticator` by convention) is the one that can `SET ROLE` to it.
190
+ *
191
+ * Supabase is PostgREST underneath, so this covers it too; the difference is
192
+ * the role vocabulary, which the `supabase` preset supplies.
193
+ */
194
+ export const postgrestAdapter = {
195
+ name: 'postgrest',
196
+ async detect(exec) {
197
+ return (await postgrestSettings(exec)).size > 0;
198
+ },
199
+ async resolve(exec) {
200
+ const settings = await postgrestSettings(exec);
201
+ const schemas = splitList(settings.get('pgrst.db_schemas'));
202
+ if (schemas.length === 0)
203
+ return [];
204
+ const anon = settings.get('pgrst.db_anon_role');
205
+ const { rows } = await exec.query(`SELECT r.rolname::text
206
+ FROM pg_db_role_setting s
207
+ JOIN pg_roles r ON r.oid = s.setrole
208
+ WHERE array_to_string(s.setconfig, ',') LIKE '%pgrst.%'`);
209
+ // The authenticator is the connecting role, not an API-edge role: it holds
210
+ // no data privileges of its own, so grading it would grade nothing.
211
+ const authenticators = rows.map((r) => r.rolname);
212
+ return [
213
+ {
214
+ name: 'api',
215
+ kind: 'api',
216
+ primary: true,
217
+ schemas,
218
+ roles: anon ? [anon] : [],
219
+ // db_anon_role is by definition the unauthenticated one.
220
+ anonRoles: anon ? [anon] : []
221
+ },
222
+ ...authenticators.map((role) => ({
223
+ name: `direct:${role}`,
224
+ kind: 'role',
225
+ roles: [role]
226
+ }))
227
+ ];
228
+ }
229
+ };
230
+ /**
231
+ * Supabase runs PostgREST, but configures it *outside* the database — the
232
+ * schema list is a platform setting, so `pgrst.db_schemas` is usually absent
233
+ * and `postgrestAdapter` correctly resolves nothing. Hence a separate
234
+ * adapter: it prefers the GUCs when a self-hosted stack sets them, and only
235
+ * falls back to the platform's fixed surface once it has *proved* it is
236
+ * looking at Supabase — `auth.users` plus the three roles every project has.
237
+ *
238
+ * The fallback is deliberately confined here. `postgrestAdapter` never
239
+ * guesses: an unconfigured PostgREST resolves no planes, which surfaces as
240
+ * unknown exposure (a capped score and a W1) rather than a wrong surface
241
+ * presented as fact.
242
+ */
243
+ export const supabaseAdapter = {
244
+ name: 'supabase',
245
+ async detect(exec) {
246
+ const { rows } = await exec.query(`SELECT to_regclass('auth.users') IS NOT NULL
247
+ AND (SELECT count(*) FROM pg_roles
248
+ WHERE rolname IN ('anon', 'authenticated', 'service_role')) = 3 AS ok`);
249
+ return rows[0]?.ok === true;
250
+ },
251
+ async resolve(exec) {
252
+ const configured = await postgrestAdapter.resolve(exec);
253
+ if (configured.length > 0)
254
+ return configured;
255
+ // Supabase's default exposure, and only reachable once detect() has
256
+ // confirmed this really is a Supabase database.
257
+ const { rows } = await exec.query(`SELECT nspname::text FROM pg_namespace
258
+ WHERE nspname IN ('public', 'graphql_public', 'storage')`);
259
+ const schemas = rows.map((r) => r.nspname).sort();
260
+ if (schemas.length === 0)
261
+ return [];
262
+ return [
263
+ {
264
+ name: 'api',
265
+ kind: 'api',
266
+ primary: true,
267
+ schemas,
268
+ roles: ['anon', 'authenticated'],
269
+ // `authenticated` still requires a sign-up; `anon` requires nothing.
270
+ anonRoles: ['anon']
271
+ },
272
+ // service_role bypasses RLS by design; planes.ts reports it as skipped
273
+ // rather than inventing an ordinary grade for it.
274
+ { name: 'direct:service_role', kind: 'role', roles: ['service_role'] }
275
+ ];
276
+ }
277
+ };
278
+ /**
279
+ * Hasura reads its exposed surface out of its own metadata: v2+ keeps one
280
+ * JSON document in `hdb_catalog.hdb_metadata`, older versions a row per
281
+ * tracked table in `hdb_catalog.hdb_table`. Only *tracked* tables are served,
282
+ * so the schemas that contain them are the surface.
283
+ */
284
+ export const hasuraAdapter = {
285
+ name: 'hasura',
286
+ async detect(exec) {
287
+ const { rows } = await exec.query(`SELECT to_regclass('hdb_catalog.hdb_metadata') IS NOT NULL
288
+ OR to_regclass('hdb_catalog.hdb_table') IS NOT NULL AS ok`);
289
+ return rows[0]?.ok === true;
290
+ },
291
+ async resolve(exec) {
292
+ const schemas = new Set();
293
+ const { rows: modern } = await exec.query(`SELECT to_regclass('hdb_catalog.hdb_metadata') IS NOT NULL AS ok`);
294
+ if (modern[0]?.ok) {
295
+ const { rows } = await exec.query(`SELECT DISTINCT t.value -> 'table' ->> 'schema' AS schema_name
296
+ FROM hdb_catalog.hdb_metadata m
297
+ CROSS JOIN LATERAL jsonb_array_elements(m.metadata -> 'sources') AS s(value)
298
+ CROSS JOIN LATERAL jsonb_array_elements(s.value -> 'tables') AS t(value)`);
299
+ for (const r of rows)
300
+ if (r.schema_name)
301
+ schemas.add(r.schema_name);
302
+ }
303
+ else {
304
+ const { rows } = await exec.query('SELECT DISTINCT table_schema::text AS schema_name FROM hdb_catalog.hdb_table');
305
+ for (const r of rows)
306
+ if (r.schema_name)
307
+ schemas.add(r.schema_name);
308
+ }
309
+ if (schemas.size === 0)
310
+ return [];
311
+ return [{ name: 'api', kind: 'api', primary: true, schemas: sorted(schemas) }];
312
+ }
313
+ };
314
+ /**
315
+ * PostGraphile publishes whatever schemas it was started with — a CLI/library
316
+ * argument, invisible from inside the database. So this adapter resolves the
317
+ * `graphile-starter` layout (`app_public` served, `app_hidden`/`app_private`
318
+ * not) and nothing else.
319
+ *
320
+ * That is naming-as-intent, which safegres otherwise refuses. The difference
321
+ * is consent: listing this adapter *is* the declaration that the convention
322
+ * holds here. When it doesn't, list `exposure.schemas` instead — explicit
323
+ * always wins.
324
+ *
325
+ * Reach is a separate question — *which schemas are served* versus *which
326
+ * relations the served schemas expose* — and is delegated to
327
+ * {@link postgraphileAdapter}, which answers it for any Graphile stack
328
+ * regardless of schema layout.
329
+ */
330
+ export const graphileAdapter = {
331
+ name: 'graphile',
332
+ async detect(exec) {
333
+ const { rows } = await exec.query("SELECT to_regnamespace('app_public') IS NOT NULL AS ok");
334
+ return rows[0]?.ok === true;
335
+ },
336
+ async resolve(exec) {
337
+ const { rows } = await exec.query(`SELECT nspname::text
338
+ FROM pg_namespace
339
+ WHERE nspname IN ('app_public', 'app_hidden', 'app_private')`);
340
+ const present = new Set(rows.map((r) => r.nspname));
341
+ if (!present.has('app_public'))
342
+ return [];
343
+ const visitors = await graphileVisitorRoles(exec);
344
+ const planes = [
345
+ {
346
+ name: 'api',
347
+ kind: 'api',
348
+ primary: true,
349
+ // app_hidden is reachable through app_public's views and functions —
350
+ // served indirectly, so it belongs to the surface.
351
+ schemas: ['app_public', ...(present.has('app_hidden') ? ['app_hidden'] : [])],
352
+ roles: visitors,
353
+ // PostGraphile runs *every* request as the visitor role, signed in or
354
+ // not — the caller is a JWT claim — so it is anonymous-reachable.
355
+ anonRoles: visitors
356
+ }
357
+ ];
358
+ if (present.has('app_private')) {
359
+ planes.push({ name: 'internal', kind: 'schema', schemas: ['app_private'] });
360
+ }
361
+ return planes;
362
+ },
363
+ reach: (exec, context) => postgraphileAdapter.reach(exec, context)
364
+ };
365
+ /**
366
+ * The role PostGraphile runs requests as. graphile-starter names it
367
+ * `<app>_visitor` — project-specific, so it cannot be hardcoded — and reaches
368
+ * it by `SET ROLE` from `<app>_authenticator`. Membership is the reliable
369
+ * signal: the authenticator is a LOGIN role whose only purpose is to become
370
+ * the visitor, so its grantees *are* the request roles. The `%_visitor`
371
+ * suffix is the fallback for a database whose authenticator is provisioned
372
+ * elsewhere (a managed connection pooler, a role created outside migrations).
373
+ *
374
+ * This matters because every unauthenticated request arrives as this role:
375
+ * logged-in or not, PostGraphile uses the same role and distinguishes callers
376
+ * by JWT claims, so it is by definition untrusted.
377
+ */
378
+ async function graphileVisitorRoles(exec) {
379
+ // Roles an `%_authenticator` may SET ROLE to — the request roles by
380
+ // construction, whatever they are named.
381
+ const { rows: granted } = await exec.query(`SELECT DISTINCT member.rolname::text AS rolname
382
+ FROM pg_auth_members m
383
+ JOIN pg_roles authenticator ON authenticator.oid = m.roleid
384
+ JOIN pg_roles member ON member.oid = m.member
385
+ WHERE authenticator.rolname LIKE '%\\_authenticator'`);
386
+ if (granted.length > 0)
387
+ return granted.map((r) => r.rolname).sort();
388
+ const { rows: bySuffix } = await exec.query(`SELECT rolname::text FROM pg_roles
389
+ WHERE rolname = 'visitor' OR rolname LIKE '%\\_visitor'`);
390
+ return bySuffix.map((r) => r.rolname).sort();
391
+ }
392
+ /** Every adapter a config may name with a string. */
393
+ export const BUILTIN_ADAPTERS = {
394
+ constructive: constructiveAdapter,
395
+ graphile: graphileAdapter,
396
+ hasura: hasuraAdapter,
397
+ postgraphile: postgraphileAdapter,
398
+ postgrest: postgrestAdapter,
399
+ supabase: supabaseAdapter
400
+ };
401
+ /**
402
+ * `pgrst.*` GUCs from `pg_db_role_setting`, whichever role or database they
403
+ * were set on. Reading the catalog rather than `current_setting()` is what
404
+ * makes this work from an audit connection that is not the API's.
405
+ */
406
+ async function postgrestSettings(exec) {
407
+ const { rows } = await exec.query(`SELECT unnest(setconfig) AS setting FROM pg_db_role_setting`);
408
+ const settings = new Map();
409
+ for (const { setting } of rows) {
410
+ const eq = setting.indexOf('=');
411
+ if (eq < 0)
412
+ continue;
413
+ const key = setting.slice(0, eq).trim().toLowerCase();
414
+ if (key.startsWith('pgrst.'))
415
+ settings.set(key, setting.slice(eq + 1).trim());
416
+ }
417
+ return settings;
418
+ }
419
+ function splitList(value) {
420
+ if (!value)
421
+ return [];
422
+ return [
423
+ ...new Set(value
424
+ .split(',')
425
+ .map((s) => s.trim().replace(/^"|"$/g, ''))
426
+ .filter(Boolean))
427
+ ].sort();
428
+ }
429
+ /**
430
+ * Wrap a fixed set of planes as an adapter — the escape hatch for a surface
431
+ * you know but cannot introspect (a schema list your deployment tooling
432
+ * knows, an API gateway's config read at build time).
433
+ */
434
+ export function definePlanes(name, planes) {
435
+ return {
436
+ name,
437
+ detect: async () => planes.length > 0,
438
+ resolve: async () => planes
439
+ };
440
+ }
441
+ /**
442
+ * Coerce whatever a config carried in `exposure.adapters` into adapters.
443
+ * Strings resolve against the built-in table only — an unknown name is an
444
+ * error rather than a silent no-op, because a typo'd adapter name would
445
+ * otherwise present as an unexposed database.
446
+ */
447
+ export function resolveAdapters(entries) {
448
+ if (!entries || entries.length === 0)
449
+ return [];
450
+ return entries.map((entry) => {
451
+ if (typeof entry !== 'string')
452
+ return entry;
453
+ const builtin = BUILTIN_ADAPTERS[entry];
454
+ if (!builtin) {
455
+ throw new Error(`unknown exposure adapter "${entry}" — built-ins: ${Object.keys(BUILTIN_ADAPTERS).join(', ')}`
456
+ + '; a custom adapter is passed as an object, not by name');
457
+ }
458
+ return builtin;
459
+ });
460
+ }
461
+ function sorted(values) {
462
+ return [...values].sort();
463
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Plane reach and per-plane scoring.
3
+ *
4
+ * A plane is an access path: a set of schemas (`api`, `schema`) or a set of
5
+ * roles (`role`). Reach is the set of relations a plane can touch — for a
6
+ * schema plane that is membership, for a role plane it is the effective-grant
7
+ * closure the lattice rules already compute (direct grants, grants TO PUBLIC,
8
+ * and role inheritance). Every plane is then scored by the *same* function on
9
+ * the *same* findings; only the finding set and the density denominator
10
+ * differ.
11
+ */
12
+ import type { RoleGraph } from '../checks/lattice';
13
+ import type { ScoringConfig } from '../config/types';
14
+ import type { ResolvedPlane } from '../pg/exposure';
15
+ import type { TableSnapshot } from '../pg/introspect';
16
+ import type { Finding, PlaneReport } from '../types';
17
+ import type { ApiReach, UnreachableRelation } from './reach';
18
+ /** A plane with its reach resolved against the catalog. */
19
+ export interface PlaneReach {
20
+ plane: ResolvedPlane;
21
+ /** `schema.table` keys the plane can touch. */
22
+ relations: Set<string>;
23
+ /** Schemas the plane touches (computed for role planes, declared otherwise). */
24
+ schemas: string[];
25
+ /** Most direct provenance across the plane's relations (role planes). */
26
+ reachedVia?: 'grant' | 'PUBLIC' | 'inheritance';
27
+ /** Why a declared plane was not graded. */
28
+ skipped?: string;
29
+ /**
30
+ * Relations in the plane's schemas that its API cannot address, subtracted
31
+ * from `relations`. Reported so the subtraction is auditable rather than
32
+ * invisible.
33
+ */
34
+ unaddressable?: UnreachableRelation[];
35
+ }
36
+ export declare function relationKey(schema: string, table: string): string;
37
+ /**
38
+ * Resolve what each plane can reach.
39
+ *
40
+ * A `role` plane for a superuser or `BYPASSRLS` role is refused rather than
41
+ * graded: RLS does not apply to it, so it reaches everything and grades 0 by
42
+ * construction — a number that teaches nothing and would read as a finding.
43
+ */
44
+ export declare function resolvePlaneReach(planes: ResolvedPlane[], tables: TableSnapshot[], graph: RoleGraph, apiReach?: ApiReach): PlaneReach[];
45
+ /**
46
+ * True when a finding is reachable on a plane. A finding with no relation
47
+ * (e.g. W1, an audit-level advisory) belongs to every plane; a schema-level
48
+ * finding (L4) belongs to the planes that touch the schema.
49
+ */
50
+ export declare function onPlane(finding: Finding, reach: PlaneReach): boolean;
51
+ /** Stamp `finding.planes` with every plane the finding is reachable on. */
52
+ export declare function stampPlanes(findings: Finding[], reaches: PlaneReach[]): void;
53
+ /**
54
+ * Score one secondary plane. Findings are re-stamped `exposed: true` because
55
+ * "exposed" is a statement about the *primary* surface: a relation that is
56
+ * internal to the API is not internal to a role holding a direct connection
57
+ * to it, which is the entire point of grading the plane separately.
58
+ */
59
+ export declare function scorePlane(reach: PlaneReach, securityFindings: Finding[], scoring: ScoringConfig | undefined, exposureKnown: boolean): PlaneReport;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Plane reach and per-plane scoring.
3
+ *
4
+ * A plane is an access path: a set of schemas (`api`, `schema`) or a set of
5
+ * roles (`role`). Reach is the set of relations a plane can touch — for a
6
+ * schema plane that is membership, for a role plane it is the effective-grant
7
+ * closure the lattice rules already compute (direct grants, grants TO PUBLIC,
8
+ * and role inheritance). Every plane is then scored by the *same* function on
9
+ * the *same* findings; only the finding set and the density denominator
10
+ * differ.
11
+ */
12
+ import { effectiveGrants } from '../checks/lattice';
13
+ import { computeScore } from '../score/score';
14
+ import { summarize } from '../types';
15
+ export function relationKey(schema, table) {
16
+ return `${schema}.${table}`;
17
+ }
18
+ /**
19
+ * Resolve what each plane can reach.
20
+ *
21
+ * A `role` plane for a superuser or `BYPASSRLS` role is refused rather than
22
+ * graded: RLS does not apply to it, so it reaches everything and grades 0 by
23
+ * construction — a number that teaches nothing and would read as a finding.
24
+ */
25
+ export function resolvePlaneReach(planes, tables, graph, apiReach) {
26
+ return planes.map((plane) => {
27
+ // A role plane is grant-truth and no API declaration narrows it: the
28
+ // GraphQL API not exposing a table says nothing about a role holding a
29
+ // direct connection to the database. This is the whole reason relation
30
+ // reach is safe to apply at all.
31
+ if (plane.kind === 'role')
32
+ return roleReach(plane, tables, graph);
33
+ const schemas = new Set(plane.schemas);
34
+ const relations = new Set(tables.filter((t) => schemas.has(t.schema)).map((t) => relationKey(t.schema, t.name)));
35
+ const unaddressable = (apiReach?.unreachable ?? []).filter((r) => relations.has(relationKey(r.schema, r.table)));
36
+ for (const r of unaddressable)
37
+ relations.delete(relationKey(r.schema, r.table));
38
+ return {
39
+ plane,
40
+ relations,
41
+ schemas: [...schemas].sort(),
42
+ ...(unaddressable.length > 0 ? { unaddressable } : {})
43
+ };
44
+ });
45
+ }
46
+ function roleReach(plane, tables, graph) {
47
+ const unsafe = plane.roles.filter((role) => {
48
+ const attrs = graph.get(role);
49
+ return attrs?.bypassRls === true || attrs?.isSuper === true;
50
+ });
51
+ if (unsafe.length > 0) {
52
+ return {
53
+ plane,
54
+ relations: new Set(),
55
+ schemas: [],
56
+ skipped: `${unsafe.join(', ')} bypasses row-level security — every relation is unmediated for it, `
57
+ + 'so a grade would say nothing about the schema'
58
+ };
59
+ }
60
+ const relations = new Set();
61
+ const schemas = new Set();
62
+ let via;
63
+ for (const table of tables) {
64
+ for (const role of plane.roles) {
65
+ const grants = effectiveGrants(table, role, graph);
66
+ if (grants.length === 0)
67
+ continue;
68
+ relations.add(relationKey(table.schema, table.name));
69
+ schemas.add(table.schema);
70
+ for (const grant of grants)
71
+ via = mostDirect(via, viaOf(grant.via));
72
+ break;
73
+ }
74
+ }
75
+ return { plane, relations, schemas: [...schemas].sort(), ...(via ? { reachedVia: via } : {}) };
76
+ }
77
+ function viaOf(via) {
78
+ if (via === 'direct')
79
+ return 'grant';
80
+ if (via === 'PUBLIC')
81
+ return 'PUBLIC';
82
+ return 'inheritance';
83
+ }
84
+ const VIA_RANK = {
85
+ grant: 0,
86
+ PUBLIC: 1,
87
+ inheritance: 2
88
+ };
89
+ function mostDirect(a, b) {
90
+ return a === undefined || VIA_RANK[b] < VIA_RANK[a] ? b : a;
91
+ }
92
+ /**
93
+ * True when a finding is reachable on a plane. A finding with no relation
94
+ * (e.g. W1, an audit-level advisory) belongs to every plane; a schema-level
95
+ * finding (L4) belongs to the planes that touch the schema.
96
+ */
97
+ export function onPlane(finding, reach) {
98
+ if (!finding.schema)
99
+ return true;
100
+ if (!finding.table)
101
+ return reach.schemas.includes(finding.schema);
102
+ return reach.relations.has(relationKey(finding.schema, finding.table));
103
+ }
104
+ /** Stamp `finding.planes` with every plane the finding is reachable on. */
105
+ export function stampPlanes(findings, reaches) {
106
+ const graded = reaches.filter((r) => !r.skipped);
107
+ if (graded.length === 0)
108
+ return;
109
+ for (const finding of findings) {
110
+ const names = graded.filter((r) => onPlane(finding, r)).map((r) => r.plane.name);
111
+ if (names.length > 0)
112
+ finding.planes = names;
113
+ }
114
+ }
115
+ /**
116
+ * Score one secondary plane. Findings are re-stamped `exposed: true` because
117
+ * "exposed" is a statement about the *primary* surface: a relation that is
118
+ * internal to the API is not internal to a role holding a direct connection
119
+ * to it, which is the entire point of grading the plane separately.
120
+ */
121
+ export function scorePlane(reach, securityFindings, scoring, exposureKnown) {
122
+ const findings = securityFindings
123
+ .filter((f) => onPlane(f, reach))
124
+ .map((f) => ({ ...f, exposed: true }));
125
+ const { plane } = reach;
126
+ return {
127
+ name: plane.name,
128
+ kind: plane.kind,
129
+ primary: plane.primary,
130
+ source: plane.source,
131
+ schemas: reach.schemas,
132
+ ...(plane.roles.length > 0 ? { roles: plane.roles } : {}),
133
+ exposedTables: reach.relations.size,
134
+ ...(reach.unaddressable && reach.unaddressable.length > 0
135
+ ? { unaddressableTables: reach.unaddressable.length }
136
+ : {}),
137
+ ...(reach.reachedVia ? { reachedVia: reach.reachedVia } : {}),
138
+ score: computeScore(findings, scoring, {
139
+ exposedTables: reach.relations.size,
140
+ exposureKnown
141
+ }),
142
+ summary: summarize(findings)
143
+ };
144
+ }