hullproof 0.0.0-stage → 0.5.0

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 (363) hide show
  1. package/LICENSE +222 -0
  2. package/NOTICE +32 -0
  3. package/README.md +313 -2
  4. package/checks/db.firebase-rules-not-open.json +24 -0
  5. package/checks/db.rls-every-table.json +25 -0
  6. package/checks/log.no-credential-in-logs.json +30 -0
  7. package/checks/secrets.no-env-file-committed.json +31 -0
  8. package/checks/secrets.no-secret-in-public-env.json +37 -0
  9. package/checks/storage.firebase-rules-not-open.json +22 -0
  10. package/checks/supply.no-untrusted-event-in-run.json +23 -0
  11. package/checks/web.cors-no-reflected-origin.json +31 -0
  12. package/detectors/ai.json +25 -0
  13. package/detectors/auth.json +27 -0
  14. package/detectors/ci.json +19 -0
  15. package/detectors/containers.json +8 -0
  16. package/detectors/data_stores.json +38 -0
  17. package/detectors/frameworks.json +94 -0
  18. package/detectors/hosting.json +44 -0
  19. package/detectors/iac.json +43 -0
  20. package/detectors/languages.json +48 -0
  21. package/detectors/mobile.json +22 -0
  22. package/detectors/package_managers.json +39 -0
  23. package/detectors/payments.json +19 -0
  24. package/detectors/runtimes.json +17 -0
  25. package/dist/checks/code/admin-role.js +46 -0
  26. package/dist/checks/code/owner-filter.js +91 -0
  27. package/dist/checks/code/shared.js +84 -0
  28. package/dist/checks/code/webhook-verify.js +77 -0
  29. package/dist/checks/context.js +64 -0
  30. package/dist/checks/map/config-checks.js +74 -0
  31. package/dist/checks/map/entry-checks.js +97 -0
  32. package/dist/checks/map/shared.js +44 -0
  33. package/dist/checks/ops.js +307 -0
  34. package/dist/checks/pro-map.js +1 -0
  35. package/dist/checks/run.js +92 -0
  36. package/dist/checks/types.js +1 -0
  37. package/dist/ci/comment.js +44 -0
  38. package/dist/ci/sarif-check.js +157 -0
  39. package/dist/ci/sarif.js +61 -0
  40. package/dist/cli.js +358 -0
  41. package/dist/commands/ci.js +115 -0
  42. package/dist/commands/git.js +58 -0
  43. package/dist/commands/hooks.js +30 -0
  44. package/dist/commands/hunt-pro.js +1 -0
  45. package/dist/commands/hunt.js +86 -0
  46. package/dist/commands/map.js +33 -0
  47. package/dist/commands/mcp.js +17 -0
  48. package/dist/commands/pack.js +48 -0
  49. package/dist/commands/prove-pro.js +1 -0
  50. package/dist/commands/prove.js +173 -0
  51. package/dist/commands/report-gate.js +92 -0
  52. package/dist/commands/scan.js +158 -0
  53. package/dist/confirm/agreement.js +18 -0
  54. package/dist/confirm/chain.js +84 -0
  55. package/dist/confirm/confirm.js +112 -0
  56. package/dist/confirm/honesty.js +75 -0
  57. package/dist/confirm/lead.js +92 -0
  58. package/dist/confirm/merge.js +88 -0
  59. package/dist/confirm/query.js +145 -0
  60. package/dist/confirm/rule.js +86 -0
  61. package/dist/confirm/store.js +42 -0
  62. package/dist/confirm/summary.js +6 -0
  63. package/dist/confirm/synthesis.js +28 -0
  64. package/dist/discover/detectors.js +50 -0
  65. package/dist/discover/discover.js +270 -0
  66. package/dist/discover/extract.js +282 -0
  67. package/dist/discover/git.js +40 -0
  68. package/dist/discover/kit-files.js +47 -0
  69. package/dist/discover/walk.js +110 -0
  70. package/dist/env.js +57 -0
  71. package/dist/gate/gate.js +173 -0
  72. package/dist/hooks/install.js +153 -0
  73. package/dist/hooks/run.js +112 -0
  74. package/dist/map/analyze.js +201 -0
  75. package/dist/map/build.js +253 -0
  76. package/dist/map/candidates.js +49 -0
  77. package/dist/map/cblocks.js +77 -0
  78. package/dist/map/config.js +291 -0
  79. package/dist/map/dotnet.js +242 -0
  80. package/dist/map/entry.js +78 -0
  81. package/dist/map/expo.js +190 -0
  82. package/dist/map/go.js +292 -0
  83. package/dist/map/js-express.js +143 -0
  84. package/dist/map/js-misc.js +295 -0
  85. package/dist/map/js-next.js +292 -0
  86. package/dist/map/laravel.js +389 -0
  87. package/dist/map/lex-more.js +305 -0
  88. package/dist/map/lex.js +425 -0
  89. package/dist/map/light-index.js +191 -0
  90. package/dist/map/light-profiles.js +324 -0
  91. package/dist/map/light.js +574 -0
  92. package/dist/map/local-vocab.js +27 -0
  93. package/dist/map/mobile.js +326 -0
  94. package/dist/map/py-django.js +281 -0
  95. package/dist/map/py.js +391 -0
  96. package/dist/map/rails.js +391 -0
  97. package/dist/map/repo.js +178 -0
  98. package/dist/map/rust.js +312 -0
  99. package/dist/map/spring.js +207 -0
  100. package/dist/map/taint.js +96 -0
  101. package/dist/map/touches.js +355 -0
  102. package/dist/map/vocab.js +88 -0
  103. package/dist/mapping/controls.js +34 -0
  104. package/dist/mapping/dependency-policy.js +44 -0
  105. package/dist/mapping/map.js +44 -0
  106. package/dist/mapping/maps/gitleaks.json +12 -0
  107. package/dist/mapping/maps/osv-scanner.json +7 -0
  108. package/dist/mapping/maps/trivy.json +13 -0
  109. package/dist/mcp/confine.js +59 -0
  110. package/dist/mcp/protocol.js +140 -0
  111. package/dist/mcp/schema.js +43 -0
  112. package/dist/mcp/server.js +37 -0
  113. package/dist/mcp/tools.js +238 -0
  114. package/dist/packs/coverage.js +79 -0
  115. package/dist/packs/manifest.js +36 -0
  116. package/dist/packs/validate.js +271 -0
  117. package/dist/prove/container.js +276 -0
  118. package/dist/prove/exec-main.js +20 -0
  119. package/dist/prove/executor.js +149 -0
  120. package/dist/prove/local.js +131 -0
  121. package/dist/prove/probes.js +65 -0
  122. package/dist/prove/recipe.js +278 -0
  123. package/dist/prove/run.js +136 -0
  124. package/dist/prove/runner.js +1 -0
  125. package/dist/prove/safety.js +126 -0
  126. package/dist/prove/spec.js +357 -0
  127. package/dist/prove/store.js +65 -0
  128. package/dist/report/build.js +297 -0
  129. package/dist/report/entry-points.js +75 -0
  130. package/dist/report/hunt-section.js +71 -0
  131. package/dist/report/kit.js +41 -0
  132. package/dist/report/load.js +35 -0
  133. package/dist/report/messages.js +273 -0
  134. package/dist/report/prove-section.js +65 -0
  135. package/dist/report/report.js +156 -0
  136. package/dist/report/surface.js +21 -0
  137. package/dist/report/test-code.js +51 -0
  138. package/dist/report/tiers.js +44 -0
  139. package/dist/scanners/common.js +162 -0
  140. package/dist/scanners/gitleaks.js +90 -0
  141. package/dist/scanners/index.js +113 -0
  142. package/dist/scanners/osv-scanner.js +77 -0
  143. package/dist/scanners/run-targets.js +34 -0
  144. package/dist/scanners/secret-noise.js +89 -0
  145. package/dist/scanners/semgrep.js +142 -0
  146. package/dist/scanners/trivy.js +65 -0
  147. package/dist/scanners/types.js +1 -0
  148. package/dist/types/check.js +1 -0
  149. package/dist/types/codemap.js +1 -0
  150. package/dist/types/findings.js +5 -0
  151. package/dist/types/hunt.js +1 -0
  152. package/dist/types/index.js +1 -0
  153. package/dist/types/prove.js +1 -0
  154. package/dist/types/results.js +2 -0
  155. package/dist/types/scan.js +1 -0
  156. package/dist/types/stack.js +5 -0
  157. package/dist/util.js +71 -0
  158. package/dist/version.js +2 -0
  159. package/package.json +46 -4
  160. package/packs/android/fixture-result.json +43 -0
  161. package/packs/android/pack.json +56 -0
  162. package/packs/dotnet/fixture-result.json +62 -0
  163. package/packs/dotnet/pack.json +88 -0
  164. package/packs/expo/answer-key.json +51 -0
  165. package/packs/expo/fixture-result.json +62 -0
  166. package/packs/expo/pack.json +79 -0
  167. package/packs/firebase/answer-key.json +27 -0
  168. package/packs/firebase/fixture-result.json +43 -0
  169. package/packs/firebase/pack.json +58 -0
  170. package/packs/go/answer-key.json +101 -0
  171. package/packs/go/fixture-result.json +64 -0
  172. package/packs/go/pack.json +99 -0
  173. package/packs/ios/fixture-result.json +43 -0
  174. package/packs/ios/pack.json +55 -0
  175. package/packs/laravel/answer-key.json +84 -0
  176. package/packs/laravel/fixture-result.json +60 -0
  177. package/packs/laravel/pack.json +91 -0
  178. package/packs/node-ts/answer-key.json +220 -0
  179. package/packs/node-ts/fixture-result.json +114 -0
  180. package/packs/node-ts/pack.json +134 -0
  181. package/packs/python/answer-key.json +195 -0
  182. package/packs/python/fixture-result.json +119 -0
  183. package/packs/python/pack.json +121 -0
  184. package/packs/rails/answer-key.json +84 -0
  185. package/packs/rails/fixture-result.json +60 -0
  186. package/packs/rails/pack.json +78 -0
  187. package/packs/rust/fixture-result.json +43 -0
  188. package/packs/rust/pack.json +69 -0
  189. package/packs/spring/fixture-result.json +63 -0
  190. package/packs/spring/pack.json +103 -0
  191. package/packs/supabase/answer-key.json +52 -0
  192. package/packs/supabase/fixture-result.json +58 -0
  193. package/packs/supabase/pack.json +60 -0
  194. package/rules/gitleaks.toml +256 -0
  195. package/rules/semgrep/api-route-no-auth.yaml +31 -0
  196. package/rules/semgrep/cors-reflect-origin.yaml +48 -0
  197. package/rules/semgrep/eval-or-new-function.yaml +185 -0
  198. package/rules/semgrep/log-session-token.yaml +48 -0
  199. package/rules/semgrep/math-random-tokens.yaml +24 -0
  200. package/rules/semgrep/next-public-secret-name.yaml +32 -0
  201. package/rules/semgrep/postgrest-filter-injection.yaml +78 -0
  202. package/rules/semgrep/raw-sql-concat.yaml +42 -0
  203. package/rules/semgrep/read-by-id-no-owner.yaml +49 -0
  204. package/rules/semgrep/server-action-no-auth.yaml +26 -0
  205. package/rules/semgrep/sql-variants.yaml +99 -0
  206. package/rules/semgrep/ssrf-unguarded-url.yaml +137 -0
  207. package/rules/semgrep/subprocess-injection.yaml +163 -0
  208. package/rules/semgrep/taint-go.yaml +333 -0
  209. package/rules/semgrep/taint-java.yaml +317 -0
  210. package/rules/semgrep/taint-javascript.yaml +412 -0
  211. package/rules/semgrep/taint-php.yaml +361 -0
  212. package/rules/semgrep/taint-python.yaml +414 -0
  213. package/rules/semgrep/tool-endpoint-admin-client.yaml +86 -0
  214. package/rules/semgrep/user-metadata-authz.yaml +22 -0
  215. package/rules/semgrep/verify-result-unused.yaml +62 -0
  216. package/rules/semgrep/webhook-parse-before-verify.yaml +29 -0
  217. package/rules/test-artifact-ids.json +4 -0
  218. package/rules/tiers.json +427 -0
  219. package/schemas/check-results.schema.json +55 -0
  220. package/schemas/check.schema.json +41 -0
  221. package/schemas/codemap.schema.json +216 -0
  222. package/schemas/findings.schema.json +1048 -0
  223. package/schemas/hunt-output.schema.json +46 -0
  224. package/schemas/hunt-result.schema.json +58 -0
  225. package/schemas/results.schema.json +17 -0
  226. package/schemas/scan-hits.schema.json +32 -0
  227. package/schemas/scan-meta.schema.json +25 -0
  228. package/schemas/stack.schema.json +93 -0
  229. package/src/checks/README.md +32 -0
  230. package/src/checks/code/admin-role.ts +49 -0
  231. package/src/checks/code/owner-filter.ts +95 -0
  232. package/src/checks/code/shared.ts +66 -0
  233. package/src/checks/code/webhook-verify.ts +75 -0
  234. package/src/checks/context.ts +72 -0
  235. package/src/checks/map/config-checks.ts +79 -0
  236. package/src/checks/map/entry-checks.ts +116 -0
  237. package/src/checks/map/shared.ts +63 -0
  238. package/src/checks/ops.ts +280 -0
  239. package/src/checks/pro-map.ts +4 -0
  240. package/src/checks/run.ts +80 -0
  241. package/src/checks/types.ts +12 -0
  242. package/src/ci/comment.ts +52 -0
  243. package/src/ci/sarif-check.ts +100 -0
  244. package/src/ci/sarif.ts +72 -0
  245. package/src/cli.ts +273 -0
  246. package/src/commands/ci.ts +104 -0
  247. package/src/commands/git.ts +59 -0
  248. package/src/commands/hooks.ts +27 -0
  249. package/src/commands/hunt-pro.ts +4 -0
  250. package/src/commands/hunt.ts +68 -0
  251. package/src/commands/map.ts +27 -0
  252. package/src/commands/mcp.ts +18 -0
  253. package/src/commands/pack.ts +34 -0
  254. package/src/commands/prove-pro.ts +4 -0
  255. package/src/commands/prove.ts +126 -0
  256. package/src/commands/report-gate.ts +75 -0
  257. package/src/commands/scan.ts +156 -0
  258. package/src/confirm/agreement.ts +23 -0
  259. package/src/confirm/chain.ts +67 -0
  260. package/src/confirm/confirm.ts +107 -0
  261. package/src/confirm/honesty.ts +65 -0
  262. package/src/confirm/lead.ts +76 -0
  263. package/src/confirm/merge.ts +89 -0
  264. package/src/confirm/query.ts +126 -0
  265. package/src/confirm/rule.ts +76 -0
  266. package/src/confirm/store.ts +42 -0
  267. package/src/confirm/summary.ts +9 -0
  268. package/src/confirm/synthesis.ts +33 -0
  269. package/src/discover/detectors.ts +71 -0
  270. package/src/discover/discover.ts +253 -0
  271. package/src/discover/extract.ts +235 -0
  272. package/src/discover/git.ts +31 -0
  273. package/src/discover/kit-files.ts +37 -0
  274. package/src/discover/walk.ts +88 -0
  275. package/src/env.ts +51 -0
  276. package/src/gate/gate.ts +169 -0
  277. package/src/hooks/install.ts +142 -0
  278. package/src/hooks/run.ts +121 -0
  279. package/src/map/analyze.ts +219 -0
  280. package/src/map/build.ts +206 -0
  281. package/src/map/candidates.ts +47 -0
  282. package/src/map/cblocks.ts +55 -0
  283. package/src/map/config.ts +207 -0
  284. package/src/map/dotnet.ts +205 -0
  285. package/src/map/entry.ts +80 -0
  286. package/src/map/expo.ts +171 -0
  287. package/src/map/go.ts +271 -0
  288. package/src/map/js-express.ts +135 -0
  289. package/src/map/js-misc.ts +221 -0
  290. package/src/map/js-next.ts +282 -0
  291. package/src/map/laravel.ts +327 -0
  292. package/src/map/lex-more.ts +152 -0
  293. package/src/map/lex.ts +274 -0
  294. package/src/map/light-index.ts +134 -0
  295. package/src/map/light-profiles.ts +363 -0
  296. package/src/map/light.ts +484 -0
  297. package/src/map/local-vocab.ts +30 -0
  298. package/src/map/mobile.ts +263 -0
  299. package/src/map/py-django.ts +239 -0
  300. package/src/map/py.ts +329 -0
  301. package/src/map/rails.ts +291 -0
  302. package/src/map/repo.ts +150 -0
  303. package/src/map/rust.ts +256 -0
  304. package/src/map/spring.ts +185 -0
  305. package/src/map/taint.ts +86 -0
  306. package/src/map/touches.ts +296 -0
  307. package/src/map/vocab.ts +106 -0
  308. package/src/mapping/controls.ts +47 -0
  309. package/src/mapping/dependency-policy.ts +55 -0
  310. package/src/mapping/map.ts +59 -0
  311. package/src/mapping/maps/gitleaks.json +12 -0
  312. package/src/mapping/maps/osv-scanner.json +7 -0
  313. package/src/mapping/maps/trivy.json +13 -0
  314. package/src/mcp/confine.ts +44 -0
  315. package/src/mcp/protocol.ts +144 -0
  316. package/src/mcp/schema.ts +35 -0
  317. package/src/mcp/server.ts +32 -0
  318. package/src/mcp/tools.ts +235 -0
  319. package/src/packs/coverage.ts +98 -0
  320. package/src/packs/manifest.ts +97 -0
  321. package/src/packs/validate.ts +204 -0
  322. package/src/prove/container.ts +237 -0
  323. package/src/prove/exec-main.ts +16 -0
  324. package/src/prove/executor.ts +130 -0
  325. package/src/prove/local.ts +100 -0
  326. package/src/prove/probes.ts +68 -0
  327. package/src/prove/recipe.ts +215 -0
  328. package/src/prove/run.ts +124 -0
  329. package/src/prove/runner.ts +28 -0
  330. package/src/prove/safety.ts +98 -0
  331. package/src/prove/spec.ts +256 -0
  332. package/src/prove/store.ts +48 -0
  333. package/src/report/build.ts +304 -0
  334. package/src/report/entry-points.ts +78 -0
  335. package/src/report/hunt-section.ts +67 -0
  336. package/src/report/kit.ts +59 -0
  337. package/src/report/load.ts +49 -0
  338. package/src/report/messages.ts +270 -0
  339. package/src/report/prove-section.ts +61 -0
  340. package/src/report/report.ts +160 -0
  341. package/src/report/surface.ts +32 -0
  342. package/src/report/test-code.ts +56 -0
  343. package/src/report/tiers.ts +50 -0
  344. package/src/scanners/common.ts +149 -0
  345. package/src/scanners/gitleaks.ts +89 -0
  346. package/src/scanners/index.ts +103 -0
  347. package/src/scanners/osv-scanner.ts +90 -0
  348. package/src/scanners/run-targets.ts +35 -0
  349. package/src/scanners/secret-noise.ts +80 -0
  350. package/src/scanners/semgrep.ts +139 -0
  351. package/src/scanners/trivy.ts +66 -0
  352. package/src/scanners/types.ts +98 -0
  353. package/src/types/check.ts +52 -0
  354. package/src/types/codemap.ts +142 -0
  355. package/src/types/findings.ts +107 -0
  356. package/src/types/hunt.ts +97 -0
  357. package/src/types/index.ts +10 -0
  358. package/src/types/prove.ts +83 -0
  359. package/src/types/results.ts +15 -0
  360. package/src/types/scan.ts +63 -0
  361. package/src/types/stack.ts +61 -0
  362. package/src/util.ts +59 -0
  363. package/src/version.ts +2 -0
@@ -0,0 +1,312 @@
1
+ import { empty, joinPath, makeEntry } from './entry.js';
2
+ import { closeOf, splitArgs } from './lex.js';
3
+ // ---- Cargo.toml ----------------------------------------------------------------------------------------------------------------------
4
+ export function readCargo(file, text, hasLock, binary) {
5
+ const lines = text.split('\n');
6
+ const m = { file, line: 1, name: null, edition: null, dependencies: [], release_debug: null, release_overflow_checks: null, unsafe_code_lint: null, has_lock: hasLock, binary };
7
+ let section = '';
8
+ let pending = null;
9
+ const addDep = (name, body, line) => {
10
+ const t = body.trim();
11
+ let source = 'registry';
12
+ let version = null;
13
+ if (t.startsWith('{')) {
14
+ if (/\bgit\s*=/.test(t))
15
+ source = 'git';
16
+ else if (/\bpath\s*=/.test(t))
17
+ source = 'path';
18
+ version = /\bversion\s*=\s*"([^"]*)"/.exec(t)?.[1] ?? null;
19
+ }
20
+ else
21
+ version = /^"([^"]*)"/.exec(t)?.[1] ?? null;
22
+ m.dependencies.push({ name, source, version, line });
23
+ };
24
+ for (let i = 0; i < lines.length; i++) {
25
+ const raw = lines[i].replace(/(^|\s)#.*$/, '').trimEnd();
26
+ const line = i + 1;
27
+ if (pending) {
28
+ pending.text += ' ' + raw.trim();
29
+ pending.depth += (raw.match(/\{/g) ?? []).length - (raw.match(/\}/g) ?? []).length;
30
+ if (pending.depth <= 0) {
31
+ addDep(pending.name, pending.text, pending.line);
32
+ pending = null;
33
+ }
34
+ continue;
35
+ }
36
+ const sec = /^\s*\[\[?([^\]]+)\]\]?\s*$/.exec(raw);
37
+ if (sec) {
38
+ section = sec[1].trim();
39
+ const named = /^(?:workspace\.|target\.[^.]+\.)?(?:dependencies|build-dependencies)\.(.+)$/.exec(section);
40
+ if (named)
41
+ m.dependencies.push({ name: named[1], source: 'registry', version: null, line });
42
+ continue;
43
+ }
44
+ const kv = /^\s*([A-Za-z0-9_.-]+)\s*=\s*(.*)$/.exec(raw);
45
+ if (!kv)
46
+ continue;
47
+ if (section === 'package') {
48
+ if (kv[1] === 'name')
49
+ m.name = /"([^"]*)"/.exec(kv[2])?.[1] ?? null;
50
+ if (kv[1] === 'edition')
51
+ m.edition = /"([^"]*)"/.exec(kv[2])?.[1] ?? null;
52
+ if (kv[1] === 'name')
53
+ m.line = line;
54
+ }
55
+ else if (/^(?:workspace\.|target\.[^.]+\.)?(?:dependencies|build-dependencies)$/.test(section)) {
56
+ const depth = (kv[2].match(/\{/g) ?? []).length - (kv[2].match(/\}/g) ?? []).length;
57
+ if (depth > 0)
58
+ pending = { name: kv[1], line, text: kv[2], depth };
59
+ else
60
+ addDep(kv[1], kv[2], line);
61
+ }
62
+ else if (/^dependencies\./.test(section) || /^(?:workspace\.|target\.[^.]+\.)?(?:dependencies|build-dependencies)\.[^.]+$/.test(section)) {
63
+ const dep = m.dependencies[m.dependencies.length - 1];
64
+ if (dep && kv[1] === 'git')
65
+ dep.source = 'git';
66
+ else if (dep && kv[1] === 'path' && dep.source === 'registry')
67
+ dep.source = 'path';
68
+ else if (dep && kv[1] === 'version')
69
+ dep.version = /"([^"]*)"/.exec(kv[2])?.[1] ?? null;
70
+ }
71
+ else if (section === 'profile.release') {
72
+ if (kv[1] === 'debug')
73
+ m.release_debug = /^(true|2|1|"full"|"limited")/.test(kv[2].trim()) ? true : /^(false|0)/.test(kv[2].trim()) ? false : null;
74
+ if (kv[1] === 'overflow-checks')
75
+ m.release_overflow_checks = /^true/.test(kv[2].trim()) ? true : /^false/.test(kv[2].trim()) ? false : null;
76
+ }
77
+ else if (section === 'lints.rust' || section === 'workspace.lints.rust') {
78
+ if (kv[1] === 'unsafe_code')
79
+ m.unsafe_code_lint = /"([^"]*)"/.exec(kv[2])?.[1] ?? /level\s*=\s*"([^"]*)"/.exec(kv[2])?.[1] ?? null;
80
+ }
81
+ }
82
+ m.dependencies.sort((a, b) => a.line - b.line || (a.name < b.name ? -1 : 1));
83
+ return m;
84
+ }
85
+ export function cargoConfig(repo) {
86
+ const manifests = repo.ctx.files.filter((f) => /(?:^|\/)Cargo\.toml$/.test(f) && !/(?:^|\/)(?:target|node_modules|vendor)\//.test(f));
87
+ if (!manifests.length)
88
+ return null;
89
+ const set = new Set(repo.ctx.files);
90
+ return {
91
+ manifests: manifests.map((f) => {
92
+ const dir = f.includes('/') ? f.slice(0, f.lastIndexOf('/')) : '';
93
+ const up = (name) => { for (let d = dir;; d = d.includes('/') ? d.slice(0, d.lastIndexOf('/')) : '') {
94
+ if (set.has(d ? `${d}/${name}` : name))
95
+ return true;
96
+ if (!d)
97
+ return false;
98
+ } };
99
+ const text = repo.ctx.text(f);
100
+ const binary = set.has(dir ? `${dir}/src/main.rs` : 'src/main.rs') || /^\s*\[\[bin\]\]/m.test(text);
101
+ return readCargo(f, text, up('Cargo.lock'), binary);
102
+ }),
103
+ };
104
+ }
105
+ const AUTH_TYPE = /\b(?:AuthUser|CurrentUser|AuthenticatedUser|Authenticated|SessionUser|LoggedIn|UserClaims|Claims|Identity|BearerAuth|AuthBearer|Jwt|JwtClaims|Principal)\b|TypedHeader\s*<\s*Authorization|\bBearerAuth\b|\bUser\s*\(/;
106
+ const ADMIN_TYPE = /\b(?:AdminUser|Admin|RequireAdmin|StaffUser|SuperUser|OwnerUser)\b/;
107
+ const AUTH_NAME = /auth|login|session|jwt|token|bearer|identity|credential/i;
108
+ const ROLE_NAME = /admin|role|permission|staff|owner|authorize|superuser/i;
109
+ const SIGNATURE = /\b(?:verify_signature|verify_webhook|verify_slice|constant_time_eq|Webhook::construct_event|construct_event|validate_signature|verify_hmac|hmac_sha256_verify)\b|\bHmac\s*::\s*<|\bMac\s*::\s*verify/;
110
+ const RATE = /\bGovernorLayer\b|\bRateLimitLayer\b|\btower_governor\b|\bRateLimiter\b/;
111
+ function fnDefs(repo) {
112
+ const out = new Map();
113
+ for (const f of repo.filesOf('rs')) {
114
+ const v = repo.view(f);
115
+ if (!v)
116
+ continue;
117
+ const m = v.masked;
118
+ for (const x of m.matchAll(/(?:^|\n)[ \t]*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+(\w+)\s*(?:<[^>\n]*>)?\s*\(/g)) {
119
+ const open = x.index + x[0].length - 1;
120
+ const pc = closeOf(m, open);
121
+ if (pc < 0)
122
+ continue;
123
+ let j = pc;
124
+ while (j < m.length && m[j] !== '{' && m[j] !== ';')
125
+ j++;
126
+ if (m[j] !== '{')
127
+ continue;
128
+ const e = closeOf(m, j);
129
+ if (e < 0)
130
+ continue;
131
+ const def = { view: v, name: x[1], start: x.index + x[0].indexOf('fn'), end: e, paramsText: v.text.slice(open + 1, pc - 1), bodyStart: j };
132
+ out.set(x[1], [...(out.get(x[1]) ?? []), def]);
133
+ }
134
+ }
135
+ return out;
136
+ }
137
+ // What a layer or middleware function says, from its name and, when the function is in the project, what its body answers.
138
+ function layerProtections(defs, view, at, expr) {
139
+ const out = [];
140
+ const names = [...expr.matchAll(/\b(?:from_fn(?:_with_state)?|map_request|from_extractor|from_extractor_with_state)\s*(?:::\s*<[^>]*>)?\s*\(\s*(?:[^,()]*,\s*)?([A-Za-z_][\w:]*)/g)].map((x) => x[1].split('::').pop());
141
+ const consts = [...expr.matchAll(/\b(\w*(?:Auth|Bearer|Jwt|Session)\w*Layer)\b/g)].map((x) => x[1]);
142
+ const line = view.lines.at(at);
143
+ const push = (kind, via, enforced, confidence) => { out.push({ kind, scope: 'middleware', file: view.file, line, via, enforced, confidence }); };
144
+ for (const n of [...names, ...consts]) {
145
+ const d = defs.get(n)?.[0];
146
+ const body = d ? d.view.text.slice(d.bodyStart, d.end) : '';
147
+ const rejects = /StatusCode::(?:UNAUTHORIZED|FORBIDDEN)|Err\s*\(/.test(body);
148
+ if (ROLE_NAME.test(n)) {
149
+ push('role_check', `layer ${n}`, d ? (rejects ? true : null) : null, d && rejects ? 'high' : 'medium');
150
+ if (AUTH_NAME.test(n))
151
+ push('session_read', `layer ${n}`, d ? (rejects ? true : null) : null, d && rejects ? 'high' : 'medium');
152
+ }
153
+ else if (AUTH_NAME.test(n))
154
+ push('session_read', `layer ${n}`, d ? (rejects ? true : null) : null, d && rejects ? 'high' : 'medium');
155
+ }
156
+ if (RATE.test(expr))
157
+ push('rate_limit', 'rate limit layer', null, 'medium');
158
+ return out;
159
+ }
160
+ const handlerProtections = (d, name) => {
161
+ if (!d)
162
+ return [];
163
+ const out = [];
164
+ const line = d.view.lines.at(d.start);
165
+ const p = d.paramsText;
166
+ if (ADMIN_TYPE.test(p)) {
167
+ out.push({ kind: 'role_check', scope: 'handler', file: d.view.file, line, via: `extractor ${ADMIN_TYPE.exec(p)[0]}`, enforced: true, confidence: 'medium' });
168
+ out.push({ kind: 'session_read', scope: 'handler', file: d.view.file, line, via: `extractor ${ADMIN_TYPE.exec(p)[0]}`, enforced: true, confidence: 'medium' });
169
+ }
170
+ else if (AUTH_TYPE.test(p))
171
+ out.push({ kind: 'session_read', scope: 'handler', file: d.view.file, line, via: `extractor ${AUTH_TYPE.exec(p)[0].trim()}`, enforced: true, confidence: 'medium' });
172
+ const body = d.view.text.slice(d.bodyStart, d.end);
173
+ if (SIGNATURE.test(body))
174
+ out.push({ kind: 'signature_verify', scope: 'handler', file: d.view.file, line: d.view.lines.at(d.bodyStart + (SIGNATURE.exec(body)?.index ?? 0)), via: SIGNATURE.exec(body)[0], enforced: /if\s+!|return\s+(?:Err|StatusCode::UNAUTHORIZED)|\?;/.test(body) ? true : null, confidence: 'high' });
175
+ void name;
176
+ return out;
177
+ };
178
+ function chainAt(v, from) {
179
+ const m = v.masked;
180
+ const calls = [];
181
+ let i = from + 'Router::new()'.length;
182
+ for (;;) {
183
+ const mm = /^\s*\.\s*(\w+)\s*(?:::\s*<[^>]*>)?\s*\(/.exec(m.slice(i, i + 400));
184
+ if (!mm)
185
+ break;
186
+ const open = i + mm[0].length - 1;
187
+ const close = closeOf(m, open);
188
+ if (close < 0)
189
+ break;
190
+ calls.push({ name: mm[1], open, close });
191
+ i = close;
192
+ }
193
+ return { view: v, calls };
194
+ }
195
+ export function rustFiles(repo) {
196
+ return repo.filesOf('rs').filter((f) => { const v = repo.view(f); return !!v && (/\bRouter\s*::\s*new\s*\(/.test(v.masked) || /#\s*\[\s*(?:get|post|put|delete|patch)\s*\(/.test(v.text)); });
197
+ }
198
+ export function extractRust(repo) {
199
+ const out = empty();
200
+ const defs = fnDefs(repo);
201
+ const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'any']);
202
+ const handlerOf = (expr) => {
203
+ // get(h).post(h2).layer(...) : the handler of each method, and the text of layers placed on the method router
204
+ const res = [];
205
+ let layers = '';
206
+ for (const x of expr.matchAll(/(?:^|\.|\b)(get|post|put|delete|patch|head|options|any|on|layer|route_layer)\s*\(((?:[^()]|\((?:[^()]|\([^()]*\))*\))*)\)/g)) {
207
+ if (x[1] === 'layer' || x[1] === 'route_layer') {
208
+ layers += ' ' + x[2];
209
+ continue;
210
+ }
211
+ if (!METHODS.has(x[1]))
212
+ continue;
213
+ const nm = /([A-Za-z_][\w:]*)\s*$/.exec(x[2].trim().replace(/\)\s*$/, ''))?.[1]?.split('::').pop();
214
+ if (nm)
215
+ res.push({ method: x[1].toUpperCase(), name: nm, layers: '' });
216
+ }
217
+ return res.map((r) => ({ ...r, layers }));
218
+ };
219
+ const build = (ch, vars, depth) => {
220
+ let routes = [];
221
+ const v = ch.view;
222
+ for (const c of ch.calls) {
223
+ const args = splitArgs(v.masked, c.open);
224
+ const txt = (k) => (args[k] ? v.text.slice(args[k].start, args[k].end) : '');
225
+ if (c.name === 'route' && args.length >= 2) {
226
+ const path = /"([^"]*)"/.exec(txt(0))?.[1];
227
+ if (path === undefined)
228
+ continue;
229
+ for (const h of handlerOf(txt(1)))
230
+ routes.push({ method: h.method, path, handler: h.name, view: v, at: c.open, prot: h.layers ? layerProtections(defs, v, c.open, h.layers) : [] });
231
+ }
232
+ else if ((c.name === 'nest' || c.name === 'merge') && depth < 6) {
233
+ const prefix = c.name === 'nest' ? /"([^"]*)"/.exec(txt(0))?.[1] ?? '' : '';
234
+ const target = c.name === 'nest' ? txt(1).trim() : txt(0).trim();
235
+ let sub = null;
236
+ const inl = /Router\s*::\s*new\s*\(\s*\)/.exec(target);
237
+ if (inl) {
238
+ const start = args[c.name === 'nest' ? 1 : 0].start + inl.index;
239
+ sub = chainAt(v, start);
240
+ }
241
+ else if (/^\w+$/.test(target))
242
+ sub = vars.get(target) ?? null;
243
+ if (!sub)
244
+ continue;
245
+ for (const r of build(sub, vars, depth + 1))
246
+ routes.push({ ...r, path: prefix ? joinPath(prefix, r.path) : r.path });
247
+ }
248
+ else if (c.name === 'layer' || c.name === 'route_layer') {
249
+ const ps = layerProtections(defs, v, c.open, txt(0));
250
+ if (ps.length)
251
+ routes = routes.map((r) => ({ ...r, prot: [...r.prot, ...ps] }));
252
+ }
253
+ }
254
+ return routes;
255
+ };
256
+ for (const file of rustFiles(repo)) {
257
+ const v = repo.view(file);
258
+ if (!v)
259
+ continue;
260
+ const m = v.masked;
261
+ // axum: chains, with `let name = Router::new()...;` variables resolved by name within the file
262
+ const vars = new Map();
263
+ const chains = [];
264
+ for (const x of m.matchAll(/\bRouter\s*::\s*new\s*\(\s*\)/g)) {
265
+ const before = m.slice(Math.max(0, x.index - 120), x.index);
266
+ const prevCall = /\.\s*(?:nest|merge)\s*\(\s*(?:"[^"]*"\s*,\s*)?$/.test(before);
267
+ if (prevCall)
268
+ continue;
269
+ const nm = /\blet\s+(?:mut\s+)?(\w+)(?:\s*:\s*[\w<>:, ]+)?\s*=\s*$/.exec(before)?.[1] ?? null;
270
+ const ch = chainAt(v, x.index);
271
+ if (nm)
272
+ vars.set(nm, ch);
273
+ chains.push({ name: nm, chain: ch });
274
+ }
275
+ // Roots: chains that no other chain nests or merges by name.
276
+ const used = new Set();
277
+ for (const { chain } of chains)
278
+ for (const c of chain.calls)
279
+ if (c.name === 'nest' || c.name === 'merge') {
280
+ const t = splitArgs(v.masked, c.open).map((a) => v.text.slice(a.start, a.end).trim());
281
+ for (const a of t)
282
+ if (/^\w+$/.test(a))
283
+ used.add(a);
284
+ }
285
+ const seen = new Set();
286
+ for (const { name, chain } of chains) {
287
+ if (name && used.has(name))
288
+ continue;
289
+ for (const r of build(chain, vars, 0)) {
290
+ const key = `${r.method} ${r.path} ${r.handler} ${r.at}`;
291
+ if (seen.has(key))
292
+ continue;
293
+ seen.add(key);
294
+ const d = (defs.get(r.handler) ?? []).find((x) => x.view.file === file) ?? defs.get(r.handler)?.[0];
295
+ const prot = [...r.prot, ...handlerProtections(d, r.handler)];
296
+ const analysis = { protections: prot, reads: [], data: [], outbound: [], taint: { names: new Set(), validated: new Set(), trusted: new Set() } };
297
+ const hv = d?.view ?? v;
298
+ out.entries.push(makeEntry(repo, {
299
+ kind: 'http_route', framework: 'axum', file, line: v.lines.at(r.at), method: r.method, path: r.path === '' ? '/' : joinPath('', r.path), name: r.handler,
300
+ confidence: d ? 'high' : 'medium', handlerView: hv, spec: { start: d?.start ?? r.at, end: d?.end ?? r.at, seeds: [] }, analysis,
301
+ }));
302
+ }
303
+ }
304
+ // actix web: #[get("/x")] above an async fn
305
+ for (const x of v.text.matchAll(/#\s*\[\s*(get|post|put|delete|patch|head)\s*\(\s*"([^"]*)"[^\]]*\]\s*(?:\n\s*#\s*\[[^\]]*\]\s*)*\n?\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/g)) {
306
+ const d = (defs.get(x[3]) ?? []).find((y) => y.view.file === file);
307
+ const analysis = { protections: handlerProtections(d, x[3]), reads: [], data: [], outbound: [], taint: { names: new Set(), validated: new Set(), trusted: new Set() } };
308
+ out.entries.push(makeEntry(repo, { kind: 'http_route', framework: 'actix-web', file, line: v.lines.at(x.index), method: x[1].toUpperCase(), path: joinPath('', x[2]), name: x[3], confidence: 'medium', handlerView: v, spec: { start: d?.start ?? x.index, end: d?.end ?? x.index, seeds: [] }, analysis }));
309
+ }
310
+ }
311
+ return out;
312
+ }
@@ -0,0 +1,207 @@
1
+ import { annotationBlocks, splitTop, strings } from './cblocks.js';
2
+ import { empty, joinPath, makeEntry, normPath } from './entry.js';
3
+ import { cMethod } from './light-index.js';
4
+ import { analyzeLight } from './light.js';
5
+ import { closeOf } from './lex.js';
6
+ const MAPPING_VERB = { GetMapping: 'GET', PostMapping: 'POST', PutMapping: 'PUT', DeleteMapping: 'DELETE', PatchMapping: 'PATCH' };
7
+ const REQUEST_SEEDS = /@(?:PathVariable|RequestParam|RequestBody|RequestHeader|CookieValue|ModelAttribute|MatrixVariable|RequestPart)\b/;
8
+ const SIMPLE_TYPES = /^(?:String|Long|Integer|int|long|UUID|Boolean|boolean|Double|double|LocalDate|BigDecimal)\b/;
9
+ const METHOD_SECURITY = /@(?:PreAuthorize|PostAuthorize|Secured|RolesAllowed)\b/;
10
+ export function springFiles(repo) {
11
+ return repo.filesOf('java').filter((f) => { const v = repo.view(f); return !!v && /@(?:Rest)?Controller\b/.test(v.masked); });
12
+ }
13
+ const EFFECT = [
14
+ [/^permitAll$|^anonymous$/, 'permit'], [/^authenticated$|^fullyAuthenticated$|^rememberMe$/, 'authenticated'],
15
+ [/^hasRole$|^hasAnyRole$|^hasAuthority$|^hasAnyAuthority$|^access$/, 'role'], [/^denyAll$/, 'deny'],
16
+ ];
17
+ function securityInfo(repo) {
18
+ const info = { rules: [], chains: 0, methodSecurity: false, starter: false, securityMatchers: false };
19
+ for (const f of ['pom.xml', 'build.gradle', 'build.gradle.kts'])
20
+ if (/spring-boot-starter-security|spring-security-web|spring-security-config/.test(repo.ctx.text(f)))
21
+ info.starter = true;
22
+ for (const file of repo.filesOf('java')) {
23
+ const v = repo.view(file);
24
+ if (!v)
25
+ continue;
26
+ const m = v.masked;
27
+ if (/@EnableMethodSecurity\b|@EnableGlobalMethodSecurity\b/.test(m))
28
+ info.methodSecurity = true;
29
+ if (!/\bSecurityFilterChain\b|\bWebSecurityConfigurerAdapter\b|\bauthorizeHttpRequests\b|\bauthorizeRequests\b/.test(m))
30
+ continue;
31
+ if (/\bsecurityMatchers?\s*\(/.test(m))
32
+ info.securityMatchers = true;
33
+ for (const start of [...m.matchAll(/\.\s*(?:authorizeHttpRequests|authorizeRequests)\s*\(/g)]) {
34
+ info.chains++;
35
+ const open = start.index + start[0].length - 1;
36
+ const close = closeOf(m, open);
37
+ let end = close > 0 ? close : open;
38
+ // authorizeRequests() with no argument is followed by a chain that runs to the end of the statement
39
+ if (close > 0 && close === open + 2) {
40
+ const semi = m.indexOf(';', close);
41
+ end = semi < 0 ? m.length : semi;
42
+ }
43
+ const region = m.slice(open, end);
44
+ const text = v.text.slice(open, end);
45
+ for (const tok of region.matchAll(/\.\s*(requestMatchers|antMatchers|mvcMatchers|regexMatchers|anyRequest)\s*\(/g)) {
46
+ const any = tok[1] === 'anyRequest';
47
+ let a = tok.index + tok[0].length - 1;
48
+ let argsText = '';
49
+ if (!any) {
50
+ const c = closeOf(region, a);
51
+ if (c < 0)
52
+ continue;
53
+ argsText = text.slice(a + 1, c - 1);
54
+ a = c;
55
+ }
56
+ else {
57
+ const c = closeOf(region, a);
58
+ a = c > 0 ? c : a + 1;
59
+ }
60
+ const eff = /^\s*\.\s*(permitAll|anonymous|authenticated|fullyAuthenticated|rememberMe|hasRole|hasAnyRole|hasAuthority|hasAnyAuthority|access|denyAll)\s*\(([^)]*)\)/.exec(region.slice(a));
61
+ if (!eff)
62
+ continue;
63
+ const effect = EFFECT.find(([re]) => re.test(eff[1]))[1];
64
+ const lits = strings(argsText);
65
+ const methods = [...argsText.matchAll(/\bHttpMethod\s*\.\s*([A-Z]+)/g)].map((x) => x[1]);
66
+ const roles = strings(text.slice(a, a + eff[0].length)).map((r) => r.replace(/^ROLE_/, ''));
67
+ const line = v.lines.at(open + tok.index);
68
+ info.rules.push({ methods, patterns: lits, any, effect, roles, file, line, readable: any || lits.length > 0 });
69
+ }
70
+ }
71
+ }
72
+ return info;
73
+ }
74
+ // An Ant style pattern from the filter chain against a path of the map (parameters written :name).
75
+ function antMatch(pattern, path) {
76
+ const re = pattern.replace(/[.+^$|\\()[\]]/g, '\\$&').replace(/\{[^/}]+\}/g, '[^/]+').replace(/\*\*/g, '\u0000').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]').replace(/\u0000/g, '.*');
77
+ const p = path.replace(/:\w+/g, 'x');
78
+ return new RegExp(`^${re}$`).test(p) || (pattern.endsWith('/**') && p === pattern.slice(0, -3));
79
+ }
80
+ function params(text) {
81
+ const seeds = [];
82
+ let principal = false;
83
+ for (const part of splitTop(text)) {
84
+ const name = /(\w+)\s*$/.exec(part.replace(/\s*=\s*[^,]*$/, ''))?.[1];
85
+ if (!name)
86
+ continue;
87
+ if (/@AuthenticationPrincipal\b|\b(?:Principal|Authentication)\s+\w+$/.test(part)) {
88
+ principal = true;
89
+ continue;
90
+ }
91
+ const bare = part.replace(/@\w+(?:\([^)]*\))?\s*/g, '').trim();
92
+ if (REQUEST_SEEDS.test(part) || (!/@/.test(part) && SIMPLE_TYPES.test(bare)))
93
+ seeds.push(name);
94
+ }
95
+ return { seeds, principal };
96
+ }
97
+ const pathsOf = (a, text) => {
98
+ if (!a || !a.args)
99
+ return [''];
100
+ const args = text.slice(a.args.start, a.args.end);
101
+ const named = /\b(?:value|path)\s*=\s*(\{[^}]*\}|"(?:[^"\\]|\\.)*")/.exec(args);
102
+ const lits = named ? strings(named[1]) : /^\s*(\{[^}]*\}|"(?:[^"\\]|\\.)*")/.exec(args) ? strings(/^\s*(\{[^}]*\}|"(?:[^"\\]|\\.)*")/.exec(args)[1]) : [];
103
+ return lits.length ? lits : [''];
104
+ };
105
+ export function extractSpring(repo, fn, local) {
106
+ const out = empty();
107
+ const sec = securityInfo(repo);
108
+ const gates = sec.rules.map((r, i) => ({
109
+ id: `spring:${r.file}:${r.line}:${i}`, kind: 'guard', framework: 'spring', file: r.file, line: r.line,
110
+ paths: r.any ? ['*'] : r.patterns.map((p) => p.replace(/\/?\*\*?.*$/, '') || '/'), methods: r.methods,
111
+ protections: ruleProtections(r), confidence: sec.securityMatchers ? 'medium' : 'high',
112
+ }));
113
+ out.gates.push(...gates);
114
+ const fileOf = (v) => {
115
+ const m = v.masked;
116
+ for (const cls of annotationBlocks(m, 0, m.length)) {
117
+ const names = cls.annos.map((a) => a.name);
118
+ if (!names.some((n) => n === 'RestController' || n === 'Controller'))
119
+ continue;
120
+ const decl = /^(?:(?:public|abstract|final|static)\s+)*class\s+(\w+)[^{]*\{/.exec(m.slice(cls.declStart, cls.declStart + 400));
121
+ if (!decl)
122
+ continue;
123
+ const className = decl[1];
124
+ const bodyOpen = cls.declStart + decl[0].length - 1;
125
+ const bodyClose = closeOf(m, bodyOpen);
126
+ if (bodyClose < 0)
127
+ continue;
128
+ const classMap = cls.annos.find((a) => a.name === 'RequestMapping');
129
+ const prefix = pathsOf(classMap, v.text)[0];
130
+ const classDeco = { view: v, start: cls.start, end: cls.end, scope: 'router' };
131
+ for (const blk of annotationBlocks(m, bodyOpen + 1, bodyClose - 1)) {
132
+ const mappings = blk.annos.filter((a) => a.name === 'RequestMapping' || MAPPING_VERB[a.name]);
133
+ if (!mappings.length)
134
+ continue;
135
+ const nameAt = /(\w+)\s*\(/.exec(m.slice(blk.declStart, blk.declStart + 300));
136
+ if (!nameAt)
137
+ continue;
138
+ const at = blk.declStart + nameAt.index;
139
+ const f = cMethod(m, at);
140
+ if (!f)
141
+ continue;
142
+ const pr = params(f.params);
143
+ const decos = [{ view: v, start: blk.start, end: blk.end, scope: 'wrapper' }];
144
+ if (cls.annos.some((a) => a !== classMap))
145
+ decos.push(classDeco);
146
+ const methodAt = blk.declStart;
147
+ for (const a of mappings) {
148
+ const verbs = MAPPING_VERB[a.name]
149
+ ? [MAPPING_VERB[a.name]]
150
+ : (() => { const mm = a.args ? /\bmethod\s*=\s*(\{[^}]*\}|[\w.]+)/.exec(v.text.slice(a.args.start, a.args.end)) : null; const vs = mm ? [...mm[1].matchAll(/\b([A-Z]{3,7})\b/g)].map((x) => x[1]).filter((x) => x !== 'RequestMethod') : []; return vs.length ? vs : ['ANY']; })();
151
+ for (const p of pathsOf(a, v.text)) {
152
+ const joined = joinPath(prefix, p) || '/';
153
+ const full = joined.startsWith('/') ? joined : `/${joined}`;
154
+ for (const verb of verbs) {
155
+ const extra = globalProtections(sec, verb, full);
156
+ const an = analyzeLight(v, { start: methodAt, end: f.end, seeds: pr.seeds, decorators: decos, extra, lookup: fn.find, local });
157
+ if (!sec.methodSecurity)
158
+ for (const q of an.protections)
159
+ if (METHOD_SECURITY.test(lineText(v, q.line))) {
160
+ q.enforced = false;
161
+ q.via += ' (method security is not enabled)';
162
+ }
163
+ out.entries.push(makeEntry(repo, {
164
+ kind: 'http_route', framework: 'spring', file: v.file, line: v.lines.at(mappings[0].start), method: verb, path: normPath(full), name: `${className}.${nameAt[1]}`,
165
+ confidence: 'high', handlerView: v, spec: { start: methodAt, end: f.end, seeds: pr.seeds }, analysis: an,
166
+ }));
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ };
173
+ for (const file of springFiles(repo)) {
174
+ const v = repo.view(file);
175
+ if (v)
176
+ fileOf(v);
177
+ }
178
+ return out;
179
+ }
180
+ const lineText = (v, line) => v.text.slice(v.lines.start(line), v.lines.start(line + 1));
181
+ function ruleProtections(r) {
182
+ const base = { scope: 'global', file: r.file, line: r.line, enforced: true, confidence: 'high' };
183
+ if (r.effect === 'authenticated')
184
+ return [{ ...base, kind: 'session_read', via: 'authorizeHttpRequests authenticated' }];
185
+ if (r.effect === 'role')
186
+ return [{ ...base, kind: 'session_read', via: 'authorizeHttpRequests role rule' }, { ...base, kind: 'role_check', via: `authorizeHttpRequests ${r.roles.length ? 'role ' + r.roles.join(', ') : 'role rule'}` }];
187
+ if (r.effect === 'deny')
188
+ return [{ ...base, kind: 'session_read', via: 'authorizeHttpRequests denyAll' }, { ...base, kind: 'role_check', via: 'authorizeHttpRequests denyAll' }];
189
+ return [];
190
+ }
191
+ // What the filter chain says about one route: the first rule that matches decides. Without a chain, the security starter's default needs a
192
+ // sign in for every request.
193
+ function globalProtections(sec, method, path) {
194
+ const conf = sec.securityMatchers ? 'medium' : 'high';
195
+ for (const r of sec.rules) {
196
+ if (!r.readable)
197
+ continue;
198
+ if (r.methods.length && method !== 'ANY' && !r.methods.includes(method))
199
+ continue;
200
+ if (!r.any && !r.patterns.some((p) => antMatch(p, path)))
201
+ continue;
202
+ return ruleProtections(r).map((q) => ({ ...q, confidence: conf }));
203
+ }
204
+ if (!sec.chains && sec.starter)
205
+ return [{ kind: 'session_read', scope: 'global', file: 'pom.xml', line: 1, via: 'Spring Security default: every request needs a sign in', enforced: true, confidence: 'medium' }];
206
+ return [];
207
+ }
@@ -0,0 +1,96 @@
1
+ import { statementEnd } from './lex.js';
2
+ import { SESSION_JS, SESSION_PY } from './vocab.js';
3
+ const KEYWORDS = new Set(['const', 'let', 'var', 'await', 'async', 'new', 'return', 'typeof', 'if', 'else', 'null', 'undefined', 'true', 'false', 'None', 'True', 'False', 'not', 'and', 'or', 'in', 'is', 'lambda', 'for', 'while', 'self', 'cls']);
4
+ // Identifiers in an expression that are values and not property names (not preceded by a dot).
5
+ export function identifiers(expr) {
6
+ const out = [];
7
+ for (const m of expr.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)/g))
8
+ if (!KEYWORDS.has(m[1]))
9
+ out.push(m[1]);
10
+ return out;
11
+ }
12
+ const SOURCE_JS = /\b(?:req|request|ctx|event)\s*[.\[]|\bsearchParams\b|\bformData\b|\bnextUrl\b|\bc\.req\b|\bawait\s+\w+\.(?:json|formData|text)\s*\(\)|\bURL\s*\(\s*(?:req|request)\.url/;
13
+ const SOURCE_PY = /\brequest\s*[.\[]|\brequest\b\.|\bflask\.request\b|\binput\s*\(|\bsys\.argv\b/;
14
+ // Values that come from the session and not from the request: req.user, current_user, session[...] and the like.
15
+ export const TRUSTED_CHAIN = /\b(?:req|request|ctx|event)\s*\.\s*(?:user|session|auth|locals\s*\.\s*user|state\s*\.\s*user)\b|\bcurrent_user\b|\bg\.user\b|\bsession\s*[\[.]|\bc\.get\(\s*['"]user/g;
16
+ // `seeds`: names that hold request data on entry. `validated` collects names assigned from a schema parse, which are
17
+ // still request data but are shaped by a schema (so passing one whole into a create is not a free for all).
18
+ export function taintOf(masked, original, seeds, lang, extraSession = []) {
19
+ const names = new Set(seeds);
20
+ const validated = new Set();
21
+ const trusted = new Set();
22
+ const isSource = lang === 'js' ? SOURCE_JS : SOURCE_PY;
23
+ const sessionPats = [...(lang === 'js' ? SESSION_JS : SESSION_PY), ...extraSession].filter((p) => p.confidence !== 'low');
24
+ const fromSession = (expr) => sessionPats.some((p) => { p.re.lastIndex = 0; return p.re.test(expr); });
25
+ const tainted = (expr) => { const e = expr.replace(TRUSTED_CHAIN, ' '); return isSource.test(e) || identifiers(e).some((i) => names.has(i) && !trusted.has(i)); };
26
+ const parsed = (expr) => /\.(?:safeParse|parse|parseAsync|safeParseAsync|model_validate|parse_obj|load|validate)\s*\(/.test(expr) || /\bvalidated|\.data\b/.test(expr) && identifiers(expr).some((i) => validated.has(i));
27
+ const assignments = [];
28
+ if (lang === 'js') {
29
+ for (const m of masked.matchAll(/\b(?:const|let|var)\s+(?:(\w+)|\{([^}]*(?:\{[^}]*\}[^}]*)*)\}|\[([^\]]*)\])\s*(?::[^=\n]+)?=(?!=)/g)) {
30
+ const rhsStart = m.index + m[0].length;
31
+ const rhsEnd = statementEnd(masked, rhsStart);
32
+ const lhs = m[1] ? [m[1]] : identifiers((m[2] ?? m[3] ?? '').replace(/(\w+)\s*:/g, ' ')).concat(identifiers((m[2] ?? '')));
33
+ assignments.push({ lhs, rhs: original.slice(rhsStart, rhsEnd) });
34
+ }
35
+ for (const m of masked.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)\s*=(?![=>])/g)) {
36
+ const rhsStart = m.index + m[0].length;
37
+ assignments.push({ lhs: [m[1]], rhs: original.slice(rhsStart, statementEnd(masked, rhsStart)) });
38
+ }
39
+ }
40
+ else {
41
+ for (const m of masked.matchAll(/^[ \t]*([A-Za-z_][\w]*(?:\s*,\s*[A-Za-z_]\w*)*)\s*(?::[^=\n]+)?=(?!=)/gm)) {
42
+ const rhsStart = m.index + m[0].length;
43
+ assignments.push({ lhs: m[1].split(',').map((s) => s.trim()), rhs: original.slice(rhsStart, statementEnd(masked, rhsStart)) });
44
+ }
45
+ for (const m of masked.matchAll(/\bfor\s+([A-Za-z_]\w*)\s+in\s+([^\n:]+):/g))
46
+ assignments.push({ lhs: [m[1]], rhs: m[2] });
47
+ }
48
+ for (let pass = 0; pass < 4; pass++) {
49
+ let changed = false;
50
+ for (const a of assignments) {
51
+ if (fromSession(a.rhs)) {
52
+ for (const l of a.lhs)
53
+ if (!trusted.has(l)) {
54
+ trusted.add(l);
55
+ names.delete(l);
56
+ changed = true;
57
+ }
58
+ continue;
59
+ }
60
+ if (!tainted(a.rhs))
61
+ continue;
62
+ const fromSchema = parsed(a.rhs);
63
+ for (const l of a.lhs) {
64
+ if (!names.has(l)) {
65
+ names.add(l);
66
+ changed = true;
67
+ }
68
+ if (fromSchema && !validated.has(l)) {
69
+ validated.add(l);
70
+ changed = true;
71
+ }
72
+ }
73
+ }
74
+ if (!changed)
75
+ break;
76
+ }
77
+ return { names, validated, trusted };
78
+ }
79
+ export const isTainted = (t, expr, lang) => {
80
+ const e = expr.replace(TRUSTED_CHAIN, ' ');
81
+ return (lang === 'js' ? SOURCE_JS : SOURCE_PY).test(e) || identifiers(e).some((i) => t.names.has(i) && !t.trusted.has(i));
82
+ };
83
+ // Whether a session derived value reaches this text: a trusted name, a chain such as req.user or current_user, or a field of
84
+ // a context object that the framework hands in (ctx.userId, auth.uid, session.user.id).
85
+ export const hasSessionValue = (t, expr) => {
86
+ TRUSTED_CHAIN.lastIndex = 0;
87
+ if (TRUSTED_CHAIN.test(expr) || identifiers(expr).some((i) => t.trusted.has(i)))
88
+ return true;
89
+ for (const m of expr.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)((?:\s*\??\.\s*[\w$]+)+)/g)) {
90
+ if (t.names.has(m[1]) && !t.trusted.has(m[1]))
91
+ continue;
92
+ if (/^(?:ctx|context|auth|session|principal|identity|viewer|currentUser|me|actor|caller|user|usr|account|current)$/i.test(m[1]) && /user|id|uid|sub|account|tenant|org|owner/i.test(m[2]))
93
+ return true;
94
+ }
95
+ return false;
96
+ };