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