@hoanghainh/komagu 2.4.0 → 2.6.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 (247) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE +202 -0
  3. package/README.md +1 -0
  4. package/assess/lsa/examples/eval-run-6/gptoss-a/ai-run-stats.json +87 -0
  5. package/assess/lsa/examples/eval-run-6/gptoss-a/auth-model.md +120 -0
  6. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/index.json +60 -0
  7. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/injection-001.json +383 -0
  8. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/injection-002.json +205 -0
  9. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/privilege-001.json +838 -0
  10. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/privilege-002.json +124 -0
  11. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-001.json +778 -0
  12. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-002.json +546 -0
  13. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-003.json +661 -0
  14. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-004.json +659 -0
  15. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-005.json +612 -0
  16. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-006.json +106 -0
  17. package/assess/lsa/examples/eval-run-6/gptoss-a/coverage.json +19 -0
  18. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-001.json +516 -0
  19. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-002.json +332 -0
  20. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-003.json +218 -0
  21. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-004.json +561 -0
  22. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-005.json +316 -0
  23. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-006.json +14 -0
  24. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/inj-injection-001.json +782 -0
  25. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/inj-injection-002.json +418 -0
  26. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/priv-privilege-001.json +627 -0
  27. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/priv-privilege-002.json +253 -0
  28. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-1.json +68 -0
  29. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-10.json +68 -0
  30. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-11.json +61 -0
  31. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-12.json +60 -0
  32. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-13.json +60 -0
  33. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-14.json +66 -0
  34. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-15.json +69 -0
  35. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-16.json +60 -0
  36. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-17.json +61 -0
  37. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-18.json +69 -0
  38. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-19.json +64 -0
  39. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-2.json +60 -0
  40. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-20.json +60 -0
  41. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-21.json +61 -0
  42. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-22.json +50 -0
  43. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-3.json +60 -0
  44. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-4.json +60 -0
  45. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-5.json +60 -0
  46. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-6.json +70 -0
  47. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-7.json +60 -0
  48. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-8.json +60 -0
  49. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-9.json +60 -0
  50. package/assess/lsa/examples/eval-run-6/gptoss-a/findings.json +4857 -0
  51. package/assess/lsa/examples/eval-run-6/gptoss-a/mass-assignment.candidates.json +232 -0
  52. package/assess/lsa/examples/eval-run-6/gptoss-a/report.ja.md +2112 -0
  53. package/assess/lsa/examples/eval-run-6/gptoss-a/report.md +2112 -0
  54. package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-001.json +425 -0
  55. package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-002.json +333 -0
  56. package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-003.json +400 -0
  57. package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-004.json +403 -0
  58. package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-005.json +398 -0
  59. package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-006.json +63 -0
  60. package/assess/lsa/examples/eval-run-6/gptoss-a/score.json +82 -0
  61. package/assess/lsa/examples/eval-run-6/gptoss-a/stack-profile.json +225 -0
  62. package/assess/lsa/examples/eval-run-6/gptoss-b/ai-run-stats.json +91 -0
  63. package/assess/lsa/examples/eval-run-6/gptoss-b/auth-model.md +120 -0
  64. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/index.json +60 -0
  65. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/injection-001.json +383 -0
  66. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/injection-002.json +205 -0
  67. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/privilege-001.json +838 -0
  68. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/privilege-002.json +124 -0
  69. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-001.json +778 -0
  70. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-002.json +546 -0
  71. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-003.json +661 -0
  72. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-004.json +659 -0
  73. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-005.json +612 -0
  74. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-006.json +106 -0
  75. package/assess/lsa/examples/eval-run-6/gptoss-b/coverage.json +21 -0
  76. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-001.json +593 -0
  77. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-002.json +367 -0
  78. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-003.json +207 -0
  79. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-004.json +477 -0
  80. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-005.json +341 -0
  81. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-006.json +14 -0
  82. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/inj-injection-001.json +821 -0
  83. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/inj-injection-002.json +410 -0
  84. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/priv-privilege-001.json +649 -0
  85. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/priv-privilege-002.json +222 -0
  86. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-1.json +60 -0
  87. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-10.json +67 -0
  88. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-11.json +68 -0
  89. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-12.json +60 -0
  90. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-13.json +60 -0
  91. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-14.json +64 -0
  92. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-15.json +70 -0
  93. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-16.json +60 -0
  94. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-17.json +61 -0
  95. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-18.json +60 -0
  96. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-19.json +65 -0
  97. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-2.json +60 -0
  98. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-20.json +60 -0
  99. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-21.json +62 -0
  100. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-22.json +50 -0
  101. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-3.json +60 -0
  102. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-4.json +60 -0
  103. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-5.json +60 -0
  104. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-6.json +60 -0
  105. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-7.json +64 -0
  106. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-8.json +60 -0
  107. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-9.json +60 -0
  108. package/assess/lsa/examples/eval-run-6/gptoss-b/findings.json +4966 -0
  109. package/assess/lsa/examples/eval-run-6/gptoss-b/mass-assignment.candidates.json +232 -0
  110. package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-001.json +425 -0
  111. package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-002.json +333 -0
  112. package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-003.json +400 -0
  113. package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-004.json +403 -0
  114. package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-005.json +398 -0
  115. package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-006.json +63 -0
  116. package/assess/lsa/examples/eval-run-6/gptoss-b/score.json +87 -0
  117. package/assess/lsa/examples/eval-run-6/gptoss-b/stack-profile.json +225 -0
  118. package/assess/lsa/examples/eval-run-6/ground-truth.json +851 -0
  119. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/ai-run-stats.json +96 -0
  120. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/auth-model.md +191 -0
  121. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/index.json +60 -0
  122. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/injection-001.json +383 -0
  123. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/injection-002.json +205 -0
  124. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/privilege-001.json +838 -0
  125. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/privilege-002.json +124 -0
  126. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-001.json +778 -0
  127. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-002.json +546 -0
  128. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-003.json +661 -0
  129. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-004.json +659 -0
  130. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-005.json +612 -0
  131. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-006.json +106 -0
  132. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/coverage.json +31 -0
  133. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-001.json +527 -0
  134. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-002.json +568 -0
  135. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-003.json +467 -0
  136. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-004.json +342 -0
  137. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-005.json +693 -0
  138. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-006.json +16 -0
  139. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/inj-injection-001.json +1123 -0
  140. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/inj-injection-002.json +391 -0
  141. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/priv-privilege-001.json +726 -0
  142. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/priv-privilege-002.json +306 -0
  143. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-1.json +137 -0
  144. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-10.json +106 -0
  145. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-11.json +81 -0
  146. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-12.json +135 -0
  147. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-13.json +135 -0
  148. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-14.json +135 -0
  149. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-15.json +110 -0
  150. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-16.json +116 -0
  151. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-17.json +141 -0
  152. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-18.json +135 -0
  153. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-19.json +140 -0
  154. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-2.json +135 -0
  155. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-20.json +82 -0
  156. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-21.json +74 -0
  157. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-22.json +99 -0
  158. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-3.json +140 -0
  159. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-4.json +145 -0
  160. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-5.json +135 -0
  161. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-6.json +135 -0
  162. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-7.json +87 -0
  163. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-8.json +125 -0
  164. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-9.json +135 -0
  165. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings.json +6989 -0
  166. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/mass-assignment.candidates.json +232 -0
  167. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/memory.log +36 -0
  168. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-001.json +425 -0
  169. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-002.json +333 -0
  170. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-003.json +400 -0
  171. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-004.json +403 -0
  172. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-005.json +398 -0
  173. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-006.json +63 -0
  174. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/score.json +106 -0
  175. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/stack-profile.json +225 -0
  176. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/auth-model.md +517 -0
  177. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-001.json +5 -0
  178. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-002.json +428 -0
  179. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-003.json +388 -0
  180. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-004.json +463 -0
  181. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-005.json +5 -0
  182. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-006.json +67 -0
  183. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/inj-injection-001.json +964 -0
  184. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings.partial.json +1872 -0
  185. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/memory.log +194 -0
  186. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/score.partial.json +108 -0
  187. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/stack-profile.json +225 -0
  188. package/assess/lsa/scripts/detect_stack.py +13 -5
  189. package/assess/lsa/scripts/find_injection.py +35 -2
  190. package/assess/lsa/scripts/java_routes.py +323 -0
  191. package/assess/lsa/scripts/laravel_routes.py +102 -0
  192. package/assess/lsa/scripts/lsa_common.py +72 -2
  193. package/assess/lsa/scripts/map_routes.py +40 -161
  194. package/assess/lsa/scripts/merge_findings.py +2 -0
  195. package/assess/lsa/tests/EVAL-RESULTS.md +126 -0
  196. package/assess/lsa/tests/test_eval_run_6.py +104 -0
  197. package/assess/lsa/tests/test_java_legacy_realistic.py +453 -0
  198. package/assess/lsa/tests/test_laravel_routes.py +95 -0
  199. package/contracts/schemas/persona-suite.schema.json +38 -2
  200. package/examples/member-portal/backend-dotnet/Dockerfile +2 -1
  201. package/examples/member-portal/backend-spring/Dockerfile +2 -0
  202. package/examples/member-portal/docker-compose.yml +1 -1
  203. package/examples/member-portal/frontend/Dockerfile +3 -2
  204. package/examples/member-portal/frontend/package-lock.json +24 -656
  205. package/examples/member-portal/frontend/package.json +1 -1
  206. package/operate/ai/client.py +22 -9
  207. package/operate/ai/runner.py +24 -4
  208. package/operate/cli/__init__.py +1 -1
  209. package/operate/cli/commands/fix.py +4 -0
  210. package/operate/cli/commands/leak_check.py +75 -0
  211. package/operate/cli/commands/protect.py +32 -3
  212. package/operate/cli/commands/scan.py +8 -1
  213. package/operate/cli/commands/test.py +131 -15
  214. package/operate/cli/main.py +5 -3
  215. package/operate/cross_user_tester/persona_matrix.py +118 -25
  216. package/operate/cross_user_tester/runner.py +190 -7
  217. package/operate/cross_user_tester/session_config.py +180 -0
  218. package/operate/cross_user_tester/session_http.py +288 -0
  219. package/operate/devsecops/leak_check.py +314 -0
  220. package/operate/remediator/runner.py +18 -1
  221. package/operate/remediator/strategies.py +2 -2
  222. package/operate/simulate/scenarios.py +2 -2
  223. package/operate/tests/mock_legacy_session_app.py +160 -0
  224. package/operate/tests/test_ai_reasoning_effort.py +65 -0
  225. package/operate/tests/test_ai_route_inventory.py +53 -0
  226. package/operate/tests/test_cross_user_session.py +565 -0
  227. package/operate/tests/test_docker_packaging.py +3 -3
  228. package/operate/tests/test_headless_ai.py +6 -0
  229. package/operate/tests/test_leak_check.py +250 -0
  230. package/operate/tests/test_legacy_session_edge.py +192 -0
  231. package/operate/tests/test_pilot_safeguards.py +245 -0
  232. package/operate/tests/test_remediator_java_legacy.py +330 -0
  233. package/package.json +2 -1
  234. package/remediate/authz/patcher.py +120 -11
  235. package/remediate/authz/tests/test_bola_patcher.py +4 -1
  236. package/remediate/edge/coraza/Caddyfile +1 -1
  237. package/remediate/edge/coraza/Dockerfile +9 -0
  238. package/remediate/edge/coraza/rules/coraza-crs-custom.conf +12 -0
  239. package/remediate/edge/coraza/virtual_patcher.py +24 -0
  240. package/remediate/edge/docker-compose.yml +7 -2
  241. package/remediate/edge/kong/kong-legacy-session.yaml +51 -0
  242. package/remediate/edge/mock-backend/Dockerfile +2 -0
  243. package/remediate/edge/tests/test_edge_security.py +21 -1
  244. package/remediate/resilience/terraform/backup_worm.tf +39 -1
  245. package/remediate/resilience/terraform/main.tf +15 -3
  246. package/remediate/resilience/terraform/variables.tf +11 -0
  247. package/remediate/resilience/tests/test_resilience.py +16 -0
@@ -37,6 +37,11 @@ STRONG_SQL = re.compile(
37
37
  r"(?i)(\bselect\b[\s\S]*?\bfrom\b|\bselect\s+[\w*]|\binsert\s+into\b|\bupdate\s+[\w\[\].`\"]+\s+set\b|\bupdate\s+[\w\[\].`\"]+\s*$"
38
38
  r"|\bdelete\s+from\b|\bwhere\b\s*[\w(`\[]|\border\s+by\b|\bgroup\s+by\b|\bvalues\s*\(|\bexec(ute)?\s+\w|\bunion\s+select\b"
39
39
  r"|\bfrom\s+[\w\[\].`\"]+\s+(where|order|group|inner|left|join)\b|\bhaving\b)")
40
+ # Java only: an HTML opening tag is markup, and a literal ending in WHERE is SQL/JPQL about to take a concatenated clause
41
+ HTML_OPEN_TAG = re.compile(r"</?[a-zA-Z][\w:-]*\b")
42
+ JAVA_TRAILING_WHERE = re.compile(r"(?i)\bwhere\s*\"$")
43
+ # JPQL positional parameter: the appended value is the numeric index of a bind mark, as in " = ?").append(size)
44
+ JAVA_POSITIONAL_APPEND = re.compile(r"\?\"\s*\)\s*\.\s*append\s*\(\s*(?:size|index|idx|i|n|pos|position|count|\w*[Ii]ndex)\s*\)\s*;?\s*$")
40
45
  WEAK_SQL = re.compile(r"(?i)\b(and|or|like|in|not|between|set|where|order\s+by|limit|offset|values|join|on)\b|=\s*['\"]?$|[(,=<>]\s*'?$")
41
46
  SQL_IDENT = re.compile(r"(?i)\b(order\s+by|group\s+by|asc|desc|limit)\b")
42
47
 
@@ -132,6 +137,20 @@ def raw_fragment_arg_is_constant(arg: str) -> bool:
132
137
  return False
133
138
 
134
139
 
140
+ def is_sql_literal(text: str, lang: str) -> bool:
141
+ if lang == "java":
142
+ return bool(STRONG_SQL.search(HTML_OPEN_TAG.sub("", text)) or JAVA_TRAILING_WHERE.search(text))
143
+ return bool(STRONG_SQL.search(text))
144
+
145
+
146
+ def java_continuation_is_dynamic(line: str, lit: re.Pattern) -> bool:
147
+ """True when a continuation line of a multi-line SQL statement joins a non-constant operand."""
148
+ if concat_kind(line, "java", lit) == "dynamic":
149
+ return True
150
+ leading = re.match(r"\s*\+\s*([A-Za-z_][\w.]*)", strip_literals(line, lit))
151
+ return bool(leading and not CONST_OPERAND.match(leading.group(1)))
152
+
153
+
135
154
  def strip_literals(line: str, lit: re.Pattern) -> str:
136
155
  return lit.sub('""', line)
137
156
 
@@ -249,7 +268,7 @@ class Finder:
249
268
  if com and com.search(line):
250
269
  continue
251
270
  lits = untagged(line, literals(line, lit), lang)
252
- if not any(STRONG_SQL.search(m.group(0)) for m in lits):
271
+ if not any(is_sql_literal(m.group(0), lang) for m in lits):
253
272
  continue
254
273
  if assign_rx:
255
274
  m = assign_rx.search(line)
@@ -259,6 +278,7 @@ class Finder:
259
278
  if am:
260
279
  sqlvars.setdefault(am.group(1), i)
261
280
  heredoc_end = None
281
+ open_sql = False
262
282
  # pass 2 — flag dynamic building
263
283
  for i, line in enumerate(lines, 1):
264
284
  if com and com.search(line):
@@ -283,7 +303,12 @@ class Finder:
283
303
  self.add(lang, rel, i, lines, "heredoc-interpolation", "string_interpolation", "sql", None, file_has_input)
284
304
  continue
285
305
  lits = untagged(line, literals(line, lit), lang)
286
- sql_lits = [m for m in lits if STRONG_SQL.search(m.group(0))]
306
+ sql_lits = [m for m in lits if is_sql_literal(m.group(0), lang)]
307
+ continuing = False
308
+ if lang == "java":
309
+ code = strip_literals(line, lit)
310
+ continuing = open_sql
311
+ open_sql = (bool(sql_lits) or open_sql) and not re.search(r"[;{}]", code)
287
312
  target_var = None
288
313
  if assign_rx:
289
314
  am = assign_rx.search(line)
@@ -294,6 +319,8 @@ class Finder:
294
319
  target_var = ap.group(1)
295
320
  is_sqlish = bool(sql_lits) or target_var is not None
296
321
  if not is_sqlish:
322
+ if continuing and java_continuation_is_dynamic(line, lit):
323
+ self.add(lang, rel, i, lines, "sql-concat", "string_concatenation", "sql", None, file_has_input)
297
324
  continue
298
325
  # interpolation inside a SQL literal (or SQL-variable literal)
299
326
  cand_lits = sql_lits or (lits if target_var else [])
@@ -317,6 +344,10 @@ class Finder:
317
344
  self.suppressed.append({"file": rel, "line": i, "reason": "concatenation with constant-looking operands only",
318
345
  "snippet": redact(snippet(lines, i))})
319
346
  continue
347
+ if lang == "java" and JAVA_POSITIONAL_APPEND.search(line):
348
+ self.suppressed.append({"file": rel, "line": i, "reason": "appended value is a positional bind index",
349
+ "snippet": redact(snippet(lines, i))})
350
+ continue
320
351
  if target_var and re.search(r"\.\s*(append|Append|AppendLine|concat)\s*\(\s*(?![\"'@\s])", line):
321
352
  self.add(lang, rel, i, lines, "sql-append", "string_concatenation", "sql", target_var, file_has_input)
322
353
  continue
@@ -350,6 +381,8 @@ class Finder:
350
381
  def scan_xml(self, rel: str, text: str) -> None:
351
382
  if not re.search(r"<mapper\b|<sqlMap\b|<!DOCTYPE\s+(mapper|sqlMap)", text):
352
383
  return
384
+ if re.search(r"<#(?:if|list|assign|macro|include)\b", text):
385
+ return # FreeMarker code-generator template: ${...} is the template language, not MyBatis
353
386
  lines = text.splitlines()
354
387
  in_stmt = False
355
388
  for i, line in enumerate(lines, 1):
@@ -0,0 +1,323 @@
1
+ """Declared-route extractors for Java web stacks (web.xml, Struts 1/2, Spring XML, annotations).
2
+
3
+ Used by map_routes.py. Python 3.8+, standard library only.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ from pathlib import Path
9
+ from typing import Dict, Iterable, List, Optional, Tuple
10
+
11
+ from lsa_common import blank_java_comments, call_args, iter_source_files, line_of, read_text, split_top
12
+
13
+ JAVA_METHOD_ANN = re.compile(r"@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|Path)\b")
14
+ JAVA_HTTP_ANN = re.compile(r"@(GET|POST|PUT|DELETE|PATCH)\b")
15
+ JAVA_CLASS_AFTER = re.compile(r"\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:(?:public|protected|private|abstract|final|static)\s+)*"
16
+ r"(?:class|interface)\b")
17
+ # The return type may follow annotations, as in: public @ResponseBody String show(
18
+ JAVA_HANDLER_SIG = re.compile(r"(?:(?:public|protected)\s+(?:@\w+(?:\([^)]*\))?\s+|(?:final|static|synchronized)\s+)*"
19
+ r"[\w<>\[\],.?\s]+?|fun|(?:override|suspend)\s+fun)\s+(\w+)\s*\(")
20
+ JAVA_NEXT_ANN = re.compile(r"\n\s*@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|Path)\b(?!Variable|Param)")
21
+ STRING_LITERAL = re.compile(r"\"((?:[^\"\\]|\\.)*)\"")
22
+ JAVA_STRING_CONSTANT = re.compile(r"\bfinal\s+String\s+(\w+)\s*=\s*\"([^\"]*)\"\s*;")
23
+ SPRING_PLACEHOLDER = re.compile(r"\$\{[^}]*\}")
24
+ XML_COMMENT = re.compile(r"<!--[\s\S]*?-->")
25
+ STRUTS2_MARKER = re.compile(r"<!DOCTYPE\s+struts\b(?!-)|<struts[\s>]")
26
+ STRUTS_EXT_CONSTANT = re.compile(r"<constant\s+name=\"struts\.action\.extension\"\s+value=\"([^\"]*)\"")
27
+ STRUTS_EXT_PROPERTY = re.compile(r"^\s*struts\.action\.extension\s*[=:]\s*(\S*)", re.M)
28
+ STRUTS_MODULE_PARAM = re.compile(r"<param-name>\s*config/([^<\s]+)\s*</param-name>\s*<param-value>\s*([^<]+?)\s*</param-value>")
29
+ SPRING_BEAN = re.compile(r"<bean\b([^>]*)>")
30
+ SPRING_URL_MAPPER = re.compile(r"<bean\b[^>]*class=\"[^\"]*UrlHandlerMapping\"[^>]*>")
31
+ SPRING_URL_PROPS = re.compile(r"<props>([\s\S]*?)</props>")
32
+ SPRING_URL_PROP = re.compile(r"<prop\s+key=\"([^\"]+)\"\s*>\s*([^<]+?)\s*</prop>")
33
+ XML_ATTR = re.compile(r"(\w+)\s*=\s*\"([^\"]*)\"")
34
+
35
+ DEFAULT_ACTION_EXTENSION = "action"
36
+
37
+
38
+ def strip_xml_comments(text: str) -> str:
39
+ """Blank XML comments out, keeping every other character at its original offset."""
40
+ return XML_COMMENT.sub(lambda m: re.sub(r"[^\n]", " ", m.group(0)), text)
41
+
42
+
43
+ def build_class_index(rels: Iterable[str]) -> Dict[str, List[str]]:
44
+ """Map a simple class name to every Java source file with that name."""
45
+ index: Dict[str, List[str]] = {}
46
+ for rel in rels:
47
+ if rel.endswith(".java"):
48
+ index.setdefault(rel.rsplit("/", 1)[-1][:-5], []).append(rel)
49
+ return index
50
+
51
+
52
+ def resolve_class(cls: str, index: Dict[str, List[str]], default: str) -> str:
53
+ """Find the source file of a class name, matching on the package path when one is given."""
54
+ name = cls.split("$", 1)[0]
55
+ candidates = index.get(name.split(".")[-1], [])
56
+ if "." not in name:
57
+ return candidates[0] if candidates else default
58
+ suffix = name.replace(".", "/") + ".java"
59
+ return next((rel for rel in candidates if rel == suffix or rel.endswith("/" + suffix)), default)
60
+
61
+
62
+ def _annotation_args(text: str, m: re.Match) -> Tuple[str, int]:
63
+ """Return (argument text, offset after the annotation)."""
64
+ paren = re.match(r"\s*\(", text[m.end():])
65
+ if not paren:
66
+ return "", m.end()
67
+ return call_args(text, m.end() + paren.end() - 1)
68
+
69
+
70
+ def _annotation_attrs(args: str) -> Dict[str, str]:
71
+ attrs: Dict[str, str] = {}
72
+ for part in split_top(args):
73
+ named = re.match(r"\s*(\w+)\s*=", part)
74
+ if named:
75
+ attrs[named.group(1)] = part[named.end():]
76
+ else:
77
+ attrs["value"] = part
78
+ return attrs
79
+
80
+
81
+ def _literals(expr: str) -> List[str]:
82
+ return STRING_LITERAL.findall(expr or "")
83
+
84
+
85
+ def _string_constants(text: str) -> Dict[str, str]:
86
+ return dict(JAVA_STRING_CONSTANT.findall(text))
87
+
88
+
89
+ def _resolve_term(term: str, constants: Dict[str, str]) -> str:
90
+ term = term.strip()
91
+ literal = STRING_LITERAL.fullmatch(term)
92
+ if literal:
93
+ return literal.group(1)
94
+ name = term.rsplit(".", 1)[-1]
95
+ return constants.get(name, "{" + name + "}" if name else "")
96
+
97
+
98
+ def _path_values(expr: str, constants: Dict[str, str]) -> List[str]:
99
+ """Evaluate a mapping attribute: a literal, a constant, a concatenation of both, or an array of those."""
100
+ inner = expr.strip()
101
+ if inner.startswith("{") and inner.endswith("}"):
102
+ inner = inner[1:-1]
103
+ return ["".join(_resolve_term(t, constants) for t in re.split(r"\+(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", element))
104
+ for element in split_top(inner)]
105
+
106
+
107
+ def _mapped_paths(attrs: Dict[str, str], constants: Optional[Dict[str, str]] = None) -> List[str]:
108
+ """Every mapped path with its required request parameters as a query string."""
109
+ paths = _path_values(attrs.get("value", "") or attrs.get("path", ""), constants or {}) or [""]
110
+ required = [p for p in _literals(attrs.get("params", "")) if not p.startswith("!")]
111
+ query = "?" + "&".join(required) if required else ""
112
+ return [p + query for p in paths]
113
+
114
+
115
+ def _join_path(prefix: str, sub: str) -> str:
116
+ sub_path, sep, query = sub.partition("?")
117
+ return "/" + "/".join(p.strip("/") for p in (prefix, sub_path) if p.strip("/")) + sep + query
118
+
119
+
120
+ def _class_mappings(text: str, constants: Dict[str, str]) -> List[Tuple[int, List[str]]]:
121
+ """(offset, mapped paths) of every class-level @RequestMapping / @Path, in source order."""
122
+ out: List[Tuple[int, List[str]]] = []
123
+ for m in JAVA_METHOD_ANN.finditer(text):
124
+ args, end = _annotation_args(text, m)
125
+ if JAVA_CLASS_AFTER.match(text[end:end + 600]):
126
+ out.append((m.start(), [p.partition("?")[0] for p in _mapped_paths(_annotation_attrs(args), constants)]))
127
+ return out
128
+
129
+
130
+ def _servlet_declared(c, rel: str, text: str) -> bool:
131
+ found = False
132
+ ws = re.search(r"@WebServlet\s*\(\s*(?:urlPatterns\s*=\s*|value\s*=\s*)?\{?\s*\"([^\"]+)\"", text)
133
+ for m in re.finditer(r"(?:public|protected)\s+void\s+(doGet|doPost|doPut|doDelete|service)\s*\(", text):
134
+ start = m.start()
135
+ nxt = re.search(r"\n\s*(?:public|protected|private)\s+[\w<>\[\]]+\s+\w+\s*\(", text[m.end():])
136
+ body = text[start: m.end() + (nxt.start() if nxt else len(text))]
137
+ line = line_of(text, start)
138
+ verb = {"doGet": "GET", "doPost": "POST", "doPut": "PUT", "doDelete": "DELETE"}.get(m.group(1), "ANY")
139
+ c.add(stack="java-servlet", kind="declared_route", method=[verb],
140
+ path=ws.group(1) if ws else "<see web.xml servlet-mapping>",
141
+ handler={"file": rel, "line": line, "symbol": m.group(1)},
142
+ id_params=c.inputs("java", body, line), guard_hints=c.guards(body, line),
143
+ data_hints=c.data_ops(body, line), notes="" if ws else "resolve path from web.xml")
144
+ found = True
145
+ return found
146
+
147
+
148
+ def java_declared(c, rel: str, raw: str) -> bool:
149
+ text = blank_java_comments(raw)
150
+ if re.search(r"extends\s+HttpServlet", text):
151
+ return _servlet_declared(c, rel, text)
152
+ found = False
153
+ constants = _string_constants(text)
154
+ classes = _class_mappings(text, constants)
155
+ for m in JAVA_METHOD_ANN.finditer(text):
156
+ args, ann_end = _annotation_args(text, m)
157
+ after = text[ann_end: ann_end + 600]
158
+ if JAVA_CLASS_AFTER.match(after):
159
+ continue
160
+ sig = JAVA_HANDLER_SIG.search(after)
161
+ if not sig:
162
+ continue
163
+ attrs = _annotation_attrs(args)
164
+ ann = m.group(1)
165
+ verbs = re.findall(r"RequestMethod\.(\w+)", args)
166
+ if ann.endswith("Mapping") and ann != "RequestMapping":
167
+ verbs = [ann[:-7].upper()]
168
+ if ann == "Path":
169
+ hv = JAVA_HTTP_ANN.search(text[max(0, m.start() - 200): ann_end + 200])
170
+ verbs = [hv.group(1)] if hv else ["ANY"]
171
+ nxt = JAVA_NEXT_ANN.search(text, ann_end)
172
+ body = text[m.start(): nxt.start() if nxt else len(text)]
173
+ line = line_of(text, m.start())
174
+ enclosing = [paths for offset, paths in classes if offset < m.start()]
175
+ for prefix in (enclosing[-1] if enclosing else [""]):
176
+ for sub in _mapped_paths(attrs, constants):
177
+ path = _join_path(prefix, sub)
178
+ c.add(stack="java-spring-mvc" if ann != "Path" else "java-jaxrs", kind="declared_route",
179
+ method=verbs or ["ANY"], path=path,
180
+ handler={"file": rel, "line": line, "symbol": sig.group(1)},
181
+ id_params=c.path_params(SPRING_PLACEHOLDER.sub("", path)) + c.inputs("java", body, line),
182
+ guard_hints=c.guards(text[:m.start()][-1500:], 1)[:2] + c.guards(body, line),
183
+ data_hints=c.data_ops(body, line), notes="")
184
+ found = True
185
+ # Struts 1 / 2 Action classes without annotations are mapped from XML (see xml_declared)
186
+ return found
187
+
188
+
189
+ def struts_modules(rel: str, text: str) -> Dict[str, str]:
190
+ """Struts 1 module prefix for every config file a web.xml assigns to a module."""
191
+ webapp = rel.rsplit("WEB-INF/", 1)[0]
192
+ modules: Dict[str, str] = {}
193
+ for m in STRUTS_MODULE_PARAM.finditer(strip_xml_comments(text)):
194
+ for value in m.group(2).split(","):
195
+ modules[webapp + value.strip().lstrip("/")] = "/" + m.group(1)
196
+ return modules
197
+
198
+
199
+ def struts2_action_extension(target: Path, config: Dict, xml_texts: Iterable[str], encodings: List[str]) -> str:
200
+ """The URL extension of Struts 2 actions: struts.properties overrides constants in struts.xml."""
201
+ value: Optional[str] = None
202
+ for text in xml_texts:
203
+ m = STRUTS_EXT_CONSTANT.search(strip_xml_comments(text))
204
+ value = m.group(1) if m else value
205
+ for path, rel in iter_source_files(target, config, exts={".properties"}):
206
+ if rel.rsplit("/", 1)[-1] == "struts.properties":
207
+ m = STRUTS_EXT_PROPERTY.search(read_text(path, encodings))
208
+ value = m.group(1) if m else value
209
+ if value is None:
210
+ return DEFAULT_ACTION_EXTENSION
211
+ return next((e for e in value.split(",") if e), "")
212
+
213
+
214
+ def collect_spring_beans(text: str) -> Dict[str, str]:
215
+ """Map bean id / name to class for one Spring XML file."""
216
+ beans: Dict[str, str] = {}
217
+ for m in SPRING_BEAN.finditer(strip_xml_comments(text)):
218
+ attrs = dict(XML_ATTR.findall(m.group(1)))
219
+ if attrs.get("class"):
220
+ for key in filter(None, (attrs.get("id"), attrs.get("name"))):
221
+ beans[key] = attrs["class"]
222
+ return beans
223
+
224
+
225
+ def entry_method_line(text: str, symbol: Optional[str]) -> Optional[int]:
226
+ """Line of the method a route declared in XML dispatches to, or None when the class has no known entry method."""
227
+ named = symbol.split("#", 1)[1] if symbol and "#" in symbol else None
228
+ names = [re.escape(named)] if named else ["execute", "doPost", "doGet", "service", "handleRequest"]
229
+ for name in names:
230
+ m = re.search(rf"\b(?:public|protected)\b[^;{{}}(]*\b{name}\s*\(", text)
231
+ if m:
232
+ return text.count("\n", 0, m.end()) + 1
233
+ return None
234
+
235
+
236
+ def _spring_url_mappings(c, rel: str, text: str, class_index: Dict[str, List[str]], beans: Dict[str, str]) -> bool:
237
+ found = False
238
+ mappers = list(SPRING_URL_MAPPER.finditer(text))
239
+ for i, mapper in enumerate(mappers):
240
+ # nested <bean> elements (interceptors) make </bean> useless as the end of the mapper definition
241
+ end = mappers[i + 1].start() if i + 1 < len(mappers) else len(text)
242
+ props = SPRING_URL_PROPS.search(text, mapper.end(), end)
243
+ if not props:
244
+ continue
245
+ for m in SPRING_URL_PROP.finditer(props.group(1)):
246
+ bean = m.group(2)
247
+ cls = beans.get(bean, "")
248
+ c.add(stack="java-spring-mvc", kind="declared_route", method=["ANY"], path=m.group(1),
249
+ handler={"file": resolve_class(cls, class_index, rel) if cls else rel, "line": 1, "symbol": cls or bean},
250
+ id_params=[], guard_hints=[], data_hints=[],
251
+ notes=f"declared in {rel}:{line_of(text, props.start(1) + m.start())}; UrlHandlerMapping bean '{bean}'")
252
+ found = True
253
+ return found
254
+
255
+
256
+ def _web_xml_routes(c, rel: str, text: str, class_index: Dict[str, List[str]]) -> bool:
257
+ found = False
258
+ servlets: Dict[str, str] = {}
259
+ for sm in re.finditer(r"<servlet>([\s\S]*?)</servlet>", text):
260
+ name = re.search(r"<servlet-name>\s*([^<]+?)\s*</servlet-name>", sm.group(1))
261
+ cls = re.search(r"<servlet-class>\s*([^<]+?)\s*</servlet-class>", sm.group(1))
262
+ if name and cls:
263
+ servlets[name.group(1)] = cls.group(1)
264
+ for m in re.finditer(r"<servlet-mapping>([\s\S]*?)</servlet-mapping>", text):
265
+ name = re.search(r"<servlet-name>\s*([^<]+?)\s*</servlet-name>", m.group(1))
266
+ if not name:
267
+ continue
268
+ cls = servlets.get(name.group(1), "")
269
+ for pattern in re.findall(r"<url-pattern>\s*([^<]+?)\s*</url-pattern>", m.group(1)):
270
+ c.add(stack="java-servlet", kind="declared_route", method=["ANY"], path=pattern,
271
+ handler={"file": resolve_class(cls, class_index, rel), "line": 1, "symbol": cls or name.group(1)},
272
+ id_params=[], guard_hints=[], data_hints=[], notes=f"mapped in {rel}:{line_of(text, m.start())}")
273
+ found = True
274
+ return found
275
+
276
+
277
+ def _struts1_routes(c, rel: str, text: str, class_index: Dict[str, List[str]], module: str) -> bool:
278
+ found = False
279
+ for m in re.finditer(r"<action\b([^>]*)>", text):
280
+ attrs = dict(XML_ATTR.findall(m.group(1)))
281
+ if "path" not in attrs:
282
+ continue
283
+ cls = attrs.get("type", "")
284
+ note = f"declared in {rel}:{line_of(text, m.start())}"
285
+ if attrs.get("parameter"):
286
+ note += f"; DispatchAction parameter '{attrs['parameter']}' -> one endpoint per method"
287
+ c.add(stack="java-struts1", kind="declared_route", method=["ANY"], path=module + attrs["path"] + ".do",
288
+ handler={"file": resolve_class(cls, class_index, rel), "line": 1, "symbol": cls}, id_params=[],
289
+ guard_hints=[], data_hints=[], notes=note)
290
+ found = True
291
+ return found
292
+
293
+
294
+ def _struts2_routes(c, rel: str, text: str, class_index: Dict[str, List[str]], extension: str) -> bool:
295
+ found = False
296
+ suffix = "." + extension if extension else ""
297
+ for pm in re.finditer(r"<package\b([^>]*)>([\s\S]*?)</package>", text):
298
+ ns = dict(XML_ATTR.findall(pm.group(1))).get("namespace", "/").rstrip("/")
299
+ for am in re.finditer(r"<action\b([^>]*)>", pm.group(2)):
300
+ attrs = dict(XML_ATTR.findall(am.group(1)))
301
+ cls = attrs.get("class", "")
302
+ c.add(stack="java-struts2", kind="declared_route", method=["ANY"], path=f"{ns}/{attrs.get('name', '?')}{suffix}",
303
+ handler={"file": resolve_class(cls, class_index, rel), "line": 1, "symbol": f"{cls}#{attrs.get('method', 'execute')}"},
304
+ id_params=[], guard_hints=[], data_hints=[],
305
+ notes=f"declared in {rel}:{line_of(text, pm.start(2) + am.start())}; action setters are inputs")
306
+ found = True
307
+ return found
308
+
309
+
310
+ def xml_declared(c, rel: str, raw: str, class_index: Dict[str, List[str]], modules: Optional[Dict[str, str]] = None,
311
+ beans: Optional[Dict[str, str]] = None, action_extension: str = DEFAULT_ACTION_EXTENSION) -> bool:
312
+ """Extract every route a deployment or framework XML file declares, ignoring commented-out ones."""
313
+ text = strip_xml_comments(raw)
314
+ found = False
315
+ if rel.endswith("web.xml"):
316
+ found = _web_xml_routes(c, rel, text, class_index) or found
317
+ if "struts-config" in rel:
318
+ found = _struts1_routes(c, rel, text, class_index, (modules or {}).get(rel, "")) or found
319
+ if rel.endswith("struts.xml") or STRUTS2_MARKER.search(text):
320
+ found = _struts2_routes(c, rel, text, class_index, action_extension) or found
321
+ if "UrlHandlerMapping" in text:
322
+ found = _spring_url_mappings(c, rel, text, class_index, beans or {}) or found
323
+ return found
@@ -0,0 +1,102 @@
1
+ """Resolve Laravel route declarations to the controller method that handles them (#380).
2
+
3
+ `Route::post('/x', [AccountController::class, 'register'])` is found in `routes/web.php`, but the
4
+ code an auditor needs is `AccountController::register`. Without this step every Laravel route
5
+ pointed at its declaration line, so the auditors read the route file and never the controller.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from pathlib import Path
11
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
12
+
13
+ from lsa_common import line_of, read_text
14
+
15
+ # [XController::class, 'method'] or 'XController@method' / 'App\Http\Controllers\XController@method'
16
+ ROUTE_ACTION = re.compile(
17
+ r"\[\s*\\?([\w\\]+)::class\s*,\s*['\"](\w+)['\"]\s*\]"
18
+ r"|['\"]\\?([\w\\]+)@(\w+)['\"]"
19
+ )
20
+ PHP_FUNCTION = re.compile(
21
+ r"^[ \t]*(?:(?:public|protected|private|static|final|abstract)\s+)*function\s+&?(\w+)\s*\(", re.M)
22
+
23
+
24
+ def route_action(stmt: str) -> Optional[str]:
25
+ """`Class@method` named by a Laravel route statement (class without namespace), or None."""
26
+ m = ROUTE_ACTION.search(stmt)
27
+ if not m:
28
+ return None
29
+ cls, method = (m.group(1), m.group(2)) if m.group(1) else (m.group(3), m.group(4))
30
+ return f"{cls.rsplit(chr(92), 1)[-1]}@{method}"
31
+
32
+
33
+ def php_class_index(rels: Iterable[str]) -> Dict[str, List[str]]:
34
+ """Map a PHP file's base name (the class name by PSR-4 convention) to every file with that name."""
35
+ index: Dict[str, List[str]] = {}
36
+ for rel in rels:
37
+ if rel.endswith(".php"):
38
+ index.setdefault(rel.rsplit("/", 1)[-1][:-4], []).append(rel)
39
+ return index
40
+
41
+
42
+ def find_method(text: str, cls: str, method: str) -> Optional[Tuple[int, int]]:
43
+ """(start, end) offsets of `method` in class `cls`, or None when the class or method is absent."""
44
+ if not re.search(rf"\bclass\s+{re.escape(cls)}\b", text):
45
+ return None
46
+ starts = list(PHP_FUNCTION.finditer(text))
47
+ for i, m in enumerate(starts):
48
+ if m.group(1) == method:
49
+ end = starts[i + 1].start() if i + 1 < len(starts) else len(text)
50
+ return m.start(), end
51
+ return None
52
+
53
+
54
+ def _merge(existing: List[Dict[str, Any]], extra: List[Dict[str, Any]],
55
+ key: Callable[[Dict[str, Any]], Any]) -> List[Dict[str, Any]]:
56
+ seen = {key(d) for d in existing}
57
+ return existing + [d for d in extra if key(d) not in seen]
58
+
59
+
60
+ def _locate(index: Dict[str, List[str]], texts: Dict[str, str], target: Path, enc: Any,
61
+ cls: str, method: str) -> Optional[Tuple[str, int, int]]:
62
+ for rel in index.get(cls, []):
63
+ if rel not in texts:
64
+ texts[rel] = read_text(target / rel, enc)
65
+ span = find_method(texts[rel], cls, method)
66
+ if span:
67
+ return rel, span[0], span[1]
68
+ return None
69
+
70
+
71
+ def resolve_laravel_handlers(items: List[Dict[str, Any]], target: Path, enc: Any, collector: Any,
72
+ rels: Iterable[str]) -> List[Dict[str, Any]]:
73
+ """Return items with Laravel routes pointed at their controller method, when it can be found.
74
+
75
+ Inputs, guard hints and data hints from the method body are added to those taken from the route
76
+ statement. A route whose controller or method is not in the repo keeps its declaration.
77
+ """
78
+ index = php_class_index(rels)
79
+ texts: Dict[str, str] = {}
80
+ out: List[Dict[str, Any]] = []
81
+ for it in items:
82
+ symbol = (it.get("handler") or {}).get("symbol") or ""
83
+ found = (_locate(index, texts, target, enc, *symbol.split("@", 1))
84
+ if it.get("stack") == "php-laravel" and "@" in symbol else None)
85
+ if not found:
86
+ out.append(it)
87
+ continue
88
+ rel, start, end = found
89
+ line = line_of(texts[rel], start)
90
+ body = texts[rel][start:end]
91
+ out.append({
92
+ **it,
93
+ "handler": {"file": rel, "line": line, "symbol": symbol},
94
+ "id_params": _merge(list(it.get("id_params") or []), collector.inputs("php", body, line),
95
+ lambda d: (d.get("name"), d.get("source"))),
96
+ "guard_hints": _merge(list(it.get("guard_hints") or []), collector.guards(body, line),
97
+ lambda d: d.get("hint")),
98
+ "data_hints": _merge(list(it.get("data_hints") or []), collector.data_ops(body, line),
99
+ lambda d: (d.get("op"), d.get("line"))),
100
+ "notes": f"declared in {it['handler']['file']}:{it['handler']['line']}",
101
+ })
102
+ return out
@@ -82,12 +82,32 @@ def read_text(path: Path, encodings: List[str]) -> str:
82
82
  return data.decode("latin-1", errors="replace")
83
83
 
84
84
 
85
+ # Default exclude names that are also ordinary Java package and web directory names
86
+ AMBIGUOUS_DIR_EXCLUDES = {"vendor/", "packages/", "bin/", "obj/", "target/", "build/", "dist/",
87
+ "test/", "tests/", "spec/", "docs/", "out/"}
88
+ JAVA_PACKAGE_ROOT = re.compile(r"(?:^|/)src/main/java/|(?:^|/)(?=(?:com|org|net|jp|vn|cn|kr|de|fr|uk|io|edu|gov|biz|info)/[\w-]+/)")
89
+ JAVA_WEB_ROOT = re.compile(r"(?:^|/)(?:src/main/webapp|WebContent|WebRoot|webapp)/")
90
+ JAVA_TREE_EXTS = (".java", ".kt", ".groovy", ".jsp", ".xml")
91
+
92
+
93
+ def _inside_java_tree(rel_slash: str, index: int, is_dir: bool) -> bool:
94
+ """True when the directory at `index` is a package inside a Java source tree or a directory under a web root."""
95
+ name = rel_slash.rstrip("/")
96
+ package_root = JAVA_PACKAGE_ROOT.search(rel_slash)
97
+ if package_root and package_root.start() <= index and (is_dir or name.endswith(JAVA_TREE_EXTS)):
98
+ return True
99
+ web_root = JAVA_WEB_ROOT.search(rel_slash)
100
+ return bool(web_root and web_root.start() <= index and (is_dir or name.endswith(".jsp")))
101
+
102
+
85
103
  def _excluded(rel: str, excludes: List[str]) -> bool:
86
- rel_slash = rel.replace(os.sep, "/") + ("/" if not rel.endswith("/") else "")
104
+ is_dir = rel.endswith("/")
105
+ rel_slash = rel.replace(os.sep, "/") + ("" if is_dir else "/")
87
106
  for ex in excludes:
88
107
  ex = ex.replace("\\", "/")
89
108
  if ex.endswith("/"):
90
- if rel_slash.startswith(ex) or ("/" + ex) in ("/" + rel_slash):
109
+ index = ("/" + rel_slash).find("/" + ex)
110
+ if index != -1 and not (ex in AMBIGUOUS_DIR_EXCLUDES and _inside_java_tree(rel_slash, index, is_dir)):
91
111
  return True
92
112
  elif re.fullmatch(ex.replace(".", r"\.").replace("*", ".*"), rel.replace(os.sep, "/")):
93
113
  return True
@@ -137,6 +157,56 @@ def iter_source_files(root: Path, config: Dict, exts: Optional[set] = None) -> I
137
157
  yield p, rel
138
158
 
139
159
 
160
+ JAVA_COMMENT = re.compile(r"(?m:^[ \t]*)(?:/\*[\s\S]*?\*/|//[^\n]*)")
161
+
162
+
163
+ def blank_java_comments(text: str) -> str:
164
+ """Blank whole-line Java comments, keeping every other character at its original offset."""
165
+ return JAVA_COMMENT.sub(lambda m: re.sub(r"[^\n]", " ", m.group(0)), text)
166
+
167
+
168
+ def call_args(text: str, open_idx: int, limit: int = 4000) -> Tuple[str, int]:
169
+ """Return (argument text, index after the closing paren) for the call whose '(' is at open_idx."""
170
+ depth, i, end = 0, open_idx, min(len(text), open_idx + limit)
171
+ while i < end:
172
+ ch = text[i]
173
+ if ch in "\"`":
174
+ close = text.find(ch, i + 1)
175
+ while ch == '"' and close != -1 and text[close - 1] == "\\":
176
+ close = text.find(ch, close + 1)
177
+ i = close if close != -1 else end
178
+ elif ch in "([{":
179
+ depth += 1
180
+ elif ch in ")]}":
181
+ depth -= 1
182
+ if depth == 0:
183
+ return text[open_idx + 1:i], i + 1
184
+ i += 1
185
+ return text[open_idx + 1:end], end
186
+
187
+
188
+ def split_top(args: str) -> List[str]:
189
+ """Split call arguments on top-level commas."""
190
+ parts, depth, cur, quote = [], 0, [], None
191
+ for ch in args:
192
+ if quote:
193
+ quote = None if ch == quote else quote
194
+ elif ch in "\"`":
195
+ quote = ch
196
+ elif ch in "([{":
197
+ depth += 1
198
+ elif ch in ")]}":
199
+ depth -= 1
200
+ elif ch == "," and depth == 0:
201
+ parts.append("".join(cur).strip())
202
+ cur = []
203
+ continue
204
+ cur.append(ch)
205
+ if "".join(cur).strip():
206
+ parts.append("".join(cur).strip())
207
+ return parts
208
+
209
+
140
210
  def line_of(text: str, index: int) -> int:
141
211
  return text.count("\n", 0, index) + 1
142
212