@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
@@ -19,8 +19,12 @@ from pathlib import Path
19
19
  from typing import Dict, List, Optional, Tuple
20
20
 
21
21
  sys.path.insert(0, str(Path(__file__).resolve().parent))
22
- from lsa_common import (KIT_VERSION, compile_id_patterns, is_id_name, iter_source_files, # noqa: E402
23
- line_of, load_config, read_text, write_json)
22
+ from java_routes import (build_class_index, collect_spring_beans, entry_method_line, java_declared, # noqa: E402
23
+ struts2_action_extension,
24
+ struts_modules, xml_declared)
25
+ from lsa_common import (JAVA_WEB_ROOT, KIT_VERSION, call_args, compile_id_patterns, is_id_name, # noqa: E402
26
+ iter_source_files, line_of, load_config, read_text, split_top, write_json)
27
+ from laravel_routes import resolve_laravel_handlers, route_action # noqa: E402
24
28
  from nextjs_routes import middleware_note, nextjs_declared # noqa: E402
25
29
  from privilege import annotate_privilege # noqa: E402
26
30
 
@@ -250,8 +254,6 @@ def segments(text: str, decl_rx: re.Pattern) -> List[Tuple[re.Match, int, int]]:
250
254
  # Declared-route extractors
251
255
  # --------------------------------------------------------------------------------------
252
256
  PHP_METHOD = re.compile(r"^[ \t]*(?:(public|protected|private)\s+)?(?:static\s+)?function\s+&?(\w+)\s*\(", re.M)
253
- JAVA_METHOD_ANN = re.compile(r"@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|Path)\b\s*(\(([^)]*)\))?")
254
- JAVA_HTTP_ANN = re.compile(r"@(GET|POST|PUT|DELETE|PATCH)\b")
255
257
  CS_ACTION = re.compile(r"^[ \t]*public\s+(?!record\b|class\b)(?:virtual\s+|async\s+|override\s+|static\s+)*[A-Za-z0-9_<>., \[\]?]+\s+(\w+)\s*\(([^)]*)\)", re.M)
256
258
  VB_ACTION = re.compile(r"^[ \t]*Public\s+(?:Overridable\s+|Async\s+)*Function\s+(\w+)\s*\(([^)]*)\)\s+As\s+[\w.(<>) ]+", re.M | re.I)
257
259
  WEBMETHOD = re.compile(r"(\[WebMethod[^\]]*\]|<WebMethod[^>]*>|\[OperationContract[^\]]*\]|<OperationContract[^>]*>)[\s\S]{0,300}?"
@@ -267,7 +269,7 @@ def php_declared(c: Collector, rel: str, text: str, stacks: set) -> bool:
267
269
  end = text.find(";", m.end())
268
270
  stmt = text[m.start(): end if end > 0 else m.end() + 200]
269
271
  c.add(stack="php-laravel", kind="declared_route", method=[verb.upper()], path="/" + path.lstrip("/"),
270
- handler={"file": rel, "line": line, "symbol": None}, id_params=c.path_params(path),
272
+ handler={"file": rel, "line": line, "symbol": route_action(stmt)}, id_params=c.path_params(path),
271
273
  guard_hints=c.guards(stmt, line) + c.guards(text[:m.start()][-3000:], 1)[:2],
272
274
  data_hints=[], notes="handler is in the controller named in this route")
273
275
  found = True
@@ -305,107 +307,6 @@ def php_declared(c: Collector, rel: str, text: str, stacks: set) -> bool:
305
307
  return found
306
308
 
307
309
 
308
- def java_declared(c: Collector, rel: str, text: str) -> bool:
309
- found = False
310
- cls_prefix = ""
311
- cm = re.search(r"@(RequestMapping|Path)\s*\(\s*(?:value\s*=\s*)?\"([^\"]*)\"[^)]*\)\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+)?(?:abstract\s+)?class", text)
312
- if cm:
313
- cls_prefix = cm.group(2)
314
- is_servlet = re.search(r"extends\s+HttpServlet", text)
315
- if is_servlet:
316
- ws = re.search(r"@WebServlet\s*\(\s*(?:urlPatterns\s*=\s*|value\s*=\s*)?\{?\s*\"([^\"]+)\"", text)
317
- for m in re.finditer(r"(?:public|protected)\s+void\s+(doGet|doPost|doPut|doDelete|service)\s*\(", text):
318
- start = m.start()
319
- nxt = re.search(r"\n\s*(?:public|protected|private)\s+[\w<>\[\]]+\s+\w+\s*\(", text[m.end():])
320
- body = text[start: m.end() + (nxt.start() if nxt else len(text))]
321
- line = line_of(text, start)
322
- verb = {"doGet": "GET", "doPost": "POST", "doPut": "PUT", "doDelete": "DELETE"}.get(m.group(1), "ANY")
323
- c.add(stack="java-servlet", kind="declared_route", method=[verb],
324
- path=ws.group(1) if ws else "<see web.xml servlet-mapping>",
325
- handler={"file": rel, "line": line, "symbol": m.group(1)},
326
- id_params=c.inputs("java", body, line), guard_hints=c.guards(body, line),
327
- data_hints=c.data_ops(body, line), notes="" if ws else "resolve path from web.xml")
328
- found = True
329
- return found
330
- anns = list(JAVA_METHOD_ANN.finditer(text))
331
- for m in anns:
332
- if cm and m.start() == cm.start():
333
- continue
334
- after = text[m.end(): m.end() + 600]
335
- # skip class-level annotation
336
- if re.match(r"\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+)?(?:abstract\s+)?class\b", after):
337
- continue
338
- sig = re.search(r"(?:(?:public|protected)\s+[\w<>\[\],.? ]+|fun|(?:override|suspend)\s+fun)\s+(\w+)\s*\(", after)
339
- if not sig:
340
- continue
341
- before = text[:m.start()]
342
- cls_m = list(re.finditer(r"@(?:RequestMapping|Path)\s*\(\s*(?:value\s*=\s*|path\s*=\s*)?\"([^\"]*)\"[^)]*\)[\s\S]{0,300}?(?:class\b)", before))
343
- method_cls_prefix = cls_m[-1].group(1) if cls_m else cls_prefix
344
- args = m.group(3) or ""
345
- pv = re.search(r"(?:value\s*=\s*|path\s*=\s*)?\{?\s*\"([^\"]*)\"", args)
346
- sub = pv.group(1) if pv else ""
347
- verbs = re.findall(r"RequestMethod\.(\w+)", args)
348
- ann = m.group(1)
349
- if ann.endswith("Mapping") and ann != "RequestMapping":
350
- verbs = [ann[:-7].upper()]
351
- if ann == "Path":
352
- hv = JAVA_HTTP_ANN.search(text[max(0, m.start() - 200): m.end() + 200])
353
- verbs = [hv.group(1)] if hv else ["ANY"]
354
- path = "/" + "/".join(p.strip("/") for p in (method_cls_prefix, sub) if p.strip("/"))
355
- nxt = re.search(r"\n\s*@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|Path)\b(?!Variable|Param)", text[m.end():])
356
- body = text[m.start(): m.end() + (nxt.start() if nxt else len(text))]
357
- line = line_of(text, m.start())
358
- c.add(stack="java-spring-mvc" if ann != "Path" else "java-jaxrs", kind="declared_route", method=verbs or ["ANY"],
359
- path=path, handler={"file": rel, "line": line, "symbol": sig.group(1)},
360
- id_params=c.path_params(path) + c.inputs("java", body, line),
361
- guard_hints=c.guards(text[:m.start()][-1500:], 1)[:2] + c.guards(body, line),
362
- data_hints=c.data_ops(body, line), notes="")
363
- found = True
364
- # Struts 1 / 2 Action classes without annotations are mapped from XML (see xml_declared)
365
- return found
366
-
367
-
368
- def xml_declared(c: Collector, rel: str, text: str, class_index: Dict[str, str]) -> bool:
369
- found = False
370
- if rel.endswith("web.xml"):
371
- servlets = dict(re.findall(r"<servlet>\s*<servlet-name>\s*([^<]+?)\s*</servlet-name>[\s\S]*?<servlet-class>\s*([^<]+?)\s*</servlet-class>", text))
372
- for m in re.finditer(r"<servlet-mapping>\s*<servlet-name>\s*([^<]+?)\s*</servlet-name>\s*<url-pattern>\s*([^<]+?)\s*</url-pattern>", text):
373
- name, pattern = m.group(1), m.group(2)
374
- cls = servlets.get(name, "")
375
- hfile = class_index.get(cls.split(".")[-1], rel)
376
- c.add(stack="java-servlet", kind="declared_route", method=["ANY"], path=pattern,
377
- handler={"file": hfile, "line": 1, "symbol": cls or name}, id_params=[], guard_hints=[],
378
- data_hints=[], notes=f"mapped in {rel}:{line_of(text, m.start())}")
379
- found = True
380
- if "struts-config" in rel:
381
- for m in re.finditer(r"<action\b([^>]*)>", text):
382
- attrs = dict(re.findall(r"(\w+)\s*=\s*\"([^\"]*)\"", m.group(1)))
383
- if "path" not in attrs:
384
- continue
385
- cls = attrs.get("type", "")
386
- hfile = class_index.get(cls.split(".")[-1], rel)
387
- note = f"declared in {rel}:{line_of(text, m.start())}"
388
- if attrs.get("parameter"):
389
- note += f"; DispatchAction parameter '{attrs['parameter']}' -> one endpoint per method"
390
- c.add(stack="java-struts1", kind="declared_route", method=["ANY"], path=attrs["path"] + ".do",
391
- handler={"file": hfile, "line": 1, "symbol": cls}, id_params=[], guard_hints=[], data_hints=[], notes=note)
392
- found = True
393
- if rel.endswith("struts.xml"):
394
- for pm in re.finditer(r"<package\b([^>]*)>([\s\S]*?)</package>", text):
395
- pattrs = dict(re.findall(r"(\w+)\s*=\s*\"([^\"]*)\"", pm.group(1)))
396
- ns = pattrs.get("namespace", "/").rstrip("/")
397
- for am in re.finditer(r"<action\b([^>]*)>", pm.group(2)):
398
- attrs = dict(re.findall(r"(\w+)\s*=\s*\"([^\"]*)\"", am.group(1)))
399
- cls = attrs.get("class", "")
400
- hfile = class_index.get(cls.split(".")[-1], rel)
401
- c.add(stack="java-struts2", kind="declared_route", method=["ANY"], path=f"{ns}/{attrs.get('name', '?')}.action",
402
- handler={"file": hfile, "line": 1, "symbol": f"{cls}#{attrs.get('method', 'execute')}"},
403
- id_params=[], guard_hints=[], data_hints=[],
404
- notes=f"declared in {rel}:{line_of(text, pm.start() + am.start())}; action setters are inputs")
405
- found = True
406
- return found
407
-
408
-
409
310
  def dotnet_declared(c: Collector, rel: str, text: str) -> bool:
410
311
  found = False
411
312
  cm = re.search(r"(?:\[ApiController\][\s\S]{0,100}?)?class\s+(\w+?)Controller\s*(?::\s*(\w+))?|Class\s+(\w+?)Controller[\s\S]{0,80}?Inherits\s+(\w+)", text, re.I)
@@ -560,48 +461,6 @@ GO_FRAMEWORKS = [("go-gin", "github.com/gin-gonic/gin"), ("go-echo", "github.com
560
461
  GO_PATTERN = re.compile(r"^(?:([A-Z]+)\s+)?(/\S*)$")
561
462
 
562
463
 
563
- def call_args(text: str, open_idx: int, limit: int = 4000) -> Tuple[str, int]:
564
- """Return (argument text, index after the closing paren) for the call whose '(' is at open_idx."""
565
- depth, i, end = 0, open_idx, min(len(text), open_idx + limit)
566
- while i < end:
567
- ch = text[i]
568
- if ch in "\"`":
569
- close = text.find(ch, i + 1)
570
- while ch == '"' and close != -1 and text[close - 1] == "\\":
571
- close = text.find(ch, close + 1)
572
- i = close if close != -1 else end
573
- elif ch in "([{":
574
- depth += 1
575
- elif ch in ")]}":
576
- depth -= 1
577
- if depth == 0:
578
- return text[open_idx + 1:i], i + 1
579
- i += 1
580
- return text[open_idx + 1:end], end
581
-
582
-
583
- def split_top(args: str) -> List[str]:
584
- """Split call arguments on top-level commas."""
585
- parts, depth, cur, quote = [], 0, [], None
586
- for ch in args:
587
- if quote:
588
- quote = None if ch == quote else quote
589
- elif ch in "\"`":
590
- quote = ch
591
- elif ch in "([{":
592
- depth += 1
593
- elif ch in ")]}":
594
- depth -= 1
595
- elif ch == "," and depth == 0:
596
- parts.append("".join(cur).strip())
597
- cur = []
598
- continue
599
- cur.append(ch)
600
- if "".join(cur).strip():
601
- parts.append("".join(cur).strip())
602
- return parts
603
-
604
-
605
464
  def guards_before(c: Collector, text: str, start: int, end: int, span: int = 500) -> List[Dict]:
606
465
  ws = max(0, start - span)
607
466
  return c.guards(text[ws:end], line_of(text, ws))
@@ -712,6 +571,9 @@ def file_endpoint(c: Collector, rel: str, text: str, lang: str, ext: str, target
712
571
  wr = wr.strip("/") + "/"
713
572
  if rel.startswith(wr):
714
573
  path = "/" + rel[len(wr):]
574
+ root = JAVA_WEB_ROOT.search(rel) if lang == "java" and path == "/" + rel else None
575
+ if root:
576
+ path = "/" + rel[root.end():]
715
577
  stack = {"php": "php", "java": "java-jsp", "dotnet": "dotnet-webforms" if ext in (".aspx",) else "dotnet-handlers",
716
578
  "asp": "classic-asp", "perl": "perl-cgi", "cfml": "coldfusion"}.get(lang, lang)
717
579
  if ext in (".asmx", ".svc"):
@@ -775,7 +637,11 @@ def _scan_route_file_batch(args: Tuple[List[Tuple[str, str]], Dict, set, str]) -
775
637
  declared = go_declared(c, rel, t)
776
638
  if declared or ext not in FILE_ENDPOINT_EXTS:
777
639
  continue
778
- if INCLUDE_DIR_RE.search(rel.rsplit("/", 1)[0] + "/" if "/" in rel else ""):
640
+ if ext == ".jsp":
641
+ # Tomcat serves any JSP outside WEB-INF, whatever its directory is called; those inside are only forwarded to
642
+ if "/WEB-INF/" in "/" + rel:
643
+ continue
644
+ elif INCLUDE_DIR_RE.search(rel.rsplit("/", 1)[0] + "/" if "/" in rel else ""):
779
645
  continue
780
646
  if framework_php and lang == "php" and re.search(r"(^|/)(application|system|app|vendor|routes|resources|storage|bootstrap)/", rel):
781
647
  continue
@@ -791,15 +657,22 @@ def map_routes(target: Path, config: Dict, stack_ids: Optional[set] = None, work
791
657
  files = list(iter_source_files(target, config))
792
658
  stack_ids = stack_ids or set()
793
659
  framework_php = bool(stack_ids & {"php-codeigniter", "php-cakephp", "php-laravel", "php-zend1"})
794
- class_index = {p.stem: rel for p, rel in files if p.suffix == ".java"}
660
+ class_index = build_class_index(rel for _, rel in files)
795
661
  texts: Dict[str, str] = {}
796
662
 
797
- # 1. XML pre-pass (web.xml / struts.xml declarative mappings)
798
- for p, rel in files:
799
- if p.suffix.lower() == ".xml":
800
- t = read_text(p, enc)
801
- if "<servlet" in t or "<action" in t:
802
- xml_declared(c, rel, t, class_index)
663
+ # 1. XML pre-pass (web.xml / Struts / Spring declarative mappings)
664
+ xml_texts = {rel: read_text(p, enc) for p, rel in files if p.suffix.lower() == ".xml"}
665
+ modules: Dict[str, str] = {}
666
+ beans: Dict[str, str] = {}
667
+ for rel, t in xml_texts.items():
668
+ if rel.endswith("web.xml"):
669
+ modules.update(struts_modules(rel, t))
670
+ if "<bean" in t:
671
+ beans.update(collect_spring_beans(t))
672
+ extension = struts2_action_extension(target, config, xml_texts.values(), enc)
673
+ for rel, t in xml_texts.items():
674
+ if "<servlet" in t or "<action" in t or "UrlHandlerMapping" in t:
675
+ xml_declared(c, rel, t, class_index, modules, beans, extension)
803
676
 
804
677
  xml_items = list(c.items)
805
678
 
@@ -865,20 +738,26 @@ def map_routes(target: Path, config: Dict, stack_ids: Optional[set] = None, work
865
738
  # post-pass 1: routes declared in XML -> enrich from the handler class file
866
739
  for it in c.items:
867
740
  hf = it["handler"]["file"]
741
+ if it["kind"] == "declared_route" and it["handler"]["line"] == 1 and hf in texts and hf.endswith(".java"):
742
+ it["handler"]["line"] = entry_method_line(texts[hf], it["handler"].get("symbol")) or 1
868
743
  if it["kind"] == "declared_route" and not it["id_params"] and hf in texts and hf.endswith(".java"):
869
744
  t = texts[hf]
870
745
  it["id_params"] = c.inputs("java", t)
871
746
  it["guard_hints"] = it["guard_hints"] or c.guards(t)
872
747
  it["data_hints"] = it["data_hints"] or c.data_ops(t)
748
+ # post-pass 1b: Laravel route declarations -> the controller method they name (#380)
749
+ c.items = resolve_laravel_handlers(c.items, target, enc, c, [rel for _, rel in files])
873
750
  # post-pass 2: servlet classes without @WebServlet -> take the path from web.xml
874
- xml_paths = {it["handler"]["file"]: it["path"] for it in c.items
875
- if it["stack"] == "java-servlet" and it["notes"].startswith("mapped in ")}
751
+ xml_paths: Dict[str, List[str]] = {}
752
+ for it in c.items:
753
+ if it["stack"] == "java-servlet" and it["notes"].startswith("mapped in "):
754
+ xml_paths.setdefault(it["handler"]["file"], []).append(it["path"])
876
755
  keep = []
877
756
  for it in c.items:
878
757
  if it["path"] == "<see web.xml servlet-mapping>" and it["handler"]["file"] in xml_paths:
879
- it["path"] = xml_paths[it["handler"]["file"]]
880
- it["notes"] = "path from web.xml"
881
- keep.append(it)
758
+ keep.extend(dict(it, path=mapped, notes="path from web.xml") for mapped in xml_paths[it["handler"]["file"]])
759
+ else:
760
+ keep.append(it)
882
761
  mapped_with_methods = {it["handler"]["file"] for it in keep if it["notes"] == "path from web.xml"}
883
762
  keep = [it for it in keep if not (it["notes"].startswith("mapped in ") and it["handler"]["file"] in mapped_with_methods)]
884
763
  for n, it in enumerate(keep, 1):
@@ -403,6 +403,8 @@ def main() -> int:
403
403
  "injection_candidates_without_verdict", "bfla_routes_without_verdict",
404
404
  "mass_candidates_without_verdict", "validation_errors",
405
405
  "new_issues_from_verifier"))
406
+ for error in cov.get("validation_errors") or []:
407
+ print(f"validation error (finding not merged): {error}", file=sys.stderr)
406
408
  print(json.dumps({"summary": summary, "coverage_gaps": gaps}, indent=2))
407
409
  return 1 if gaps else 0
408
410
 
@@ -333,3 +333,129 @@ and their tests still score against them.
333
333
  Run 5 rescored against the new ground truth (for comparison only; findings unchanged): BOLA 28 / 29
334
334
  (run A) and 16 / 29 (run B), precision 0.859 and 0.826, still no false positive on SAFE markers. Every
335
335
  new marker was already reported by both runs; Tier 1 lists all of them as BOLA route candidates.
336
+
337
+ ---
338
+
339
+ ## Run 6 — 2026-11-11, platform 2.5.0, kit 0.3.1 (#323 part 4: new harness, 121-item ground truth, redaction)
340
+
341
+ Issue #323. First run on the harness from #334 (no code fields in 4a / 4b templates, quote repair,
342
+ findings from flagged verdicts), the fixtures and ground truth from #336 (121 items: 29 BOLA, 34
343
+ SQL/NoSQL injection, 10 BFLA, 15 mass assignment, 33 SAFE), and secret redaction of code sent to the
344
+ model (v2.4.0). Outputs are archived in [`assess/lsa/examples/eval-run-6/`](../examples/eval-run-6/).
345
+ Runs 4–5 are not directly comparable; the Run 5 rows below are the Run 5 findings rescored against the
346
+ new ground truth.
347
+
348
+ | Item | Value |
349
+ |---|---|
350
+ | Pipeline | `komagu scan <repo> --no-cache --headless-ai http://localhost:11434/v1 --model <model> --ai-timeout 3600` from `main` at v2.5.0 |
351
+ | Hosting | Ollama 0.40.16 on the same Apple M4 Pro, 24 GB RAM, 100% GPU, local only; `gpt-oss:20b` on the llama.cpp runner with 32,768 context (as in Runs 4–5) |
352
+ | Scoring | `tests/score_findings.py --verdicts` (tolerance 3 lines) |
353
+ | Machine | No other heavy work while measuring; up to 5.7 GB swap in use during the `gpt-oss` runs |
354
+
355
+ ### Results after verification (`gpt-oss:20b`)
356
+
357
+ | Run | BOLA | Injection | BFLA marker | Mass marker | BFLA verdict | Mass verdict | FP on safe code | Precision | Rejected | Retries · repaired · parse fail | From verdicts | Min · calls · tokens |
358
+ |---|---|---|---|---|---|---|---|---|---|---|---|---|
359
+ | Run 5 A, rescored | 28/29 | 34/34 | 10/10 | 10/15 | 10/10 | 15/15 | 0 | 0.859 | 2 | 0 · — · 0 | — | 83 · 32 · 328K |
360
+ | Run 5 B, rescored | 16/29 | 32/34 | 9/10 | 14/15 | 10/10 | 15/15 | 0 | 0.826 | 3 | 1 · — · 1 | — | 86 · 31 · 369K |
361
+ | **Run 6 A** | **26/29** | **34/34** | **10/10** | **14/15** | **10/10** | **15/15** | **1** | **0.813** | 2 | 0 · 0 · 0 | 9 (5 BOLA, 4 mass) | 40 · 33 · 315K |
362
+ | **Run 6 B** | **26/29** | **34/34** | **10/10** | **14/15** | **10/10** | **14/15** | **1** | **0.787** | 1 | 1 · 0 · 0 | 14 (5 BOLA, 9 mass) | 56 · 34 · 373K |
363
+
364
+ - **The two runs now agree.** BOLA is 26 / 29 in both (Run 5: 28 and 16). No batch was lost: no parse
365
+ failure, no call error, no batch needed the quote repair, and the one retry (B, `injection-001`)
366
+ succeeded. The `routes-001` batch that failed in Run 4 B and Run 5 B now parses.
367
+ - **Mass assignment:** 14 / 15 as located findings in both runs (Run 5: 10 and 14). Findings written from
368
+ flagged verdicts closed the gap: the four candidates run A of Run 5 judged `vulnerable` without a
369
+ finding (`profile.php` ×2, `CustomerController.cs` ×2) are findings in both runs.
370
+ `php-plain/register.php:4` is still missed. In A the verifier rejected it because the INSERT "only"
371
+ writes `email`, `name` and `role`, missing that `extract($_POST)` can overwrite `$role` (the
372
+ fixture change in #336); in B the auditor did not flag it.
373
+ - **BFLA:** 10 / 10 in both runs, located and by verdict.
374
+ - **Misses:** `perl-cgi/cgi-bin/order.cgi:19` (both runs, missed since Run 4), `node-express/routes/orders.js:29`
375
+ (both), `java-legacy/.../OrderServlet.java:15` (A), `python-flask/app.py:12` (B).
376
+ - **False positives on SAFE markers: 1 per run** (Run 5: 0), both injection:
377
+ - A: `classic-asp/shop/member.asp:6`. The `UPDATE` starts on line 6, whose `name` value is escaped;
378
+ the concatenated `member_id` is on line 7, which is also reported. The finding is anchored one line
379
+ early rather than describing a new flaw, but it is scored as a false positive.
380
+ - B: `python-flask/app.py:12`. `order_id` is an `<int:...>` path parameter, so the `%` formatting
381
+ cannot inject. The model reported the route's missing ownership check as SQL injection, so the
382
+ BOLA marker on the same line was missed.
383
+ - **Precision 0.81 / 0.79.** Five of the unmatched findings per run come from verdicts: the BOLA
384
+ auditor flagged the Laravel routes `/register`, `/posts`, `/teams`, `/account/rename` and
385
+ `/account/profile`, and the generated findings point at their declarations in `routes/web.php`
386
+ (lines 6–10). The triage after Run 5 found these routes act on `$request->user()` or touch no
387
+ record, so they are likely false positives. Without them precision would be 0.85 / 0.83.
388
+ - **Duration:** 40 and 56 minutes (Run 5: 83 and 86). The model wrote fewer tokens (84K and 118K
389
+ completion tokens against about 109K and 111K); the Ollama version also changed (0.40.13 → 0.40.16).
390
+ - **`ai-raw/`** is written only for batches that needed a repair or failed to parse. None did, so neither
391
+ run has an `ai-raw/` folder.
392
+
393
+ ### Did redaction reduce recall?
394
+
395
+ No, and on this repo it cannot. Running the redaction used for model context
396
+ (`assess/lsa/scripts/redaction.py`, `redact_lines`) over every file of the eval repo changes **0 lines**:
397
+ the fixtures contain no secrets in the patterns it covers. The prompts sent in Run 6 are therefore the
398
+ same code as without redaction. The BOLA difference from Run 5 A (28 → 26) is run-to-run variance (the
399
+ same model put 16 in Run 5 B). This also means the eval repo does not test whether redaction costs
400
+ recall on code that does contain secrets; that is covered only by the unit tests
401
+ (`operate/tests/test_ai_redaction.py`, `test_no_planted_secret_reaches_the_model`).
402
+
403
+ ### `qwen3.5:9b` with a 16K context (not completed)
404
+
405
+ To fit the 24 GB machine, `qwen3.5:9b` was run with `num_ctx 16384` (a local tag built
406
+ `FROM qwen3.5:9b` with `PARAMETER num_ctx 16384`, MLX runner), same command and timeout as above. It did
407
+ not finish: the run was stopped by hand after 3 h 14 min, while the second injection batch was still
408
+ running and before the privilege (4c) and verifier (5) phases. The partial output is archived as
409
+ [`eval-run-6/qwen-16k-partial/`](../examples/eval-run-6/qwen-16k-partial/) (raw batch findings, the
410
+ per-minute memory log, and an indicative score of the unverified findings).
411
+
412
+ | Phase / batch | Time | Result |
413
+ |---|---|---|
414
+ | Stack detection | 2 min | done |
415
+ | BOLA `routes-001` (15 routes) | 60 min | **timed out**, empty |
416
+ | BOLA `routes-002` … `routes-004` | 5, 3 and 2 min | 8, 7 and 9 findings |
417
+ | BOLA `routes-005` | 60 min | **timed out**, empty |
418
+ | BOLA `routes-006` | 1 min | 1 finding |
419
+ | Injection `injection-001` | 11 min | 20 findings |
420
+ | Injection `injection-002` | 49 min+ | still running when stopped |
421
+
422
+ - **Memory:** the runner used 8.1 GB for the first hour, then grew to 13, 18 and finally 20 GB (from about
423
+ 10:55, after the first timeout). Swap rose from about 4 GB (other work on the machine) to 12.4 GB of
424
+ 13.3 GB; the run was stopped there to keep the machine usable. `gpt-oss:20b` stayed under 5.7 GB of
425
+ swap on the same machine.
426
+ - **Timeouts:** a batch that hits `--ai-timeout` is not retried (only empty or unparseable replies are),
427
+ so two of six BOLA batches were lost outright. The quick batches suggest the model can do the task;
428
+ the timed-out ones look like long reasoning that does not end within the context.
429
+ - **Indicative score** (findings before verification, from the batches that finished): BOLA 12/29,
430
+ injection 22/34, BFLA and mass assignment not reached, precision 0.73, no finding on a SAFE marker.
431
+ These numbers are not comparable with the `gpt-oss` rows: two BOLA batches, half the injection
432
+ candidates and the verifier are missing.
433
+
434
+ Thinking looks like the cause: on this 24 GB machine a 16K context with default reasoning is **not**
435
+ a working setting. The run below with reasoning off is.
436
+
437
+ ### `qwen3.5:9b` with a 16K context and reasoning off (#382)
438
+
439
+ Same tag, command and timeout, plus `--ai-reasoning-effort none` (#382), which sends
440
+ `"reasoning_effort": "none"` with every request; Ollama's `/v1` endpoint ignores `think: false` but
441
+ honours this field. One run, archived as [`eval-run-6/qwen-16k-nothink/`](../examples/eval-run-6/qwen-16k-nothink/)
442
+ (with `ai-raw/` for the two repaired verifier replies and the per-minute memory log).
443
+
444
+ | Run | BOLA (29) | Injection (34) | BFLA (10) | Mass (15) | BFLA verdict | Mass verdict | SAFE hit | Precision | Time |
445
+ |---|---|---|---|---|---|---|---|---|---|
446
+ | `gpt-oss:20b` A | 26 | 34 | 10 | 14 | 10 | 15 | 1 | 0.813 | 40 min |
447
+ | `gpt-oss:20b` B | 26 | 34 | 10 | 14 | 10 | 14 | 1 | 0.787 | 56 min |
448
+ | `qwen3.5:9b` 16K, reasoning off | 23 | 29 | 6 | 10 | 8 | 15 | 0 | 0.734 | 35 min |
449
+
450
+ - **Completed:** all phases, 33 calls, no timeout, no parse failure or call error, no retry, all findings
451
+ anchored. Two verifier replies were repaired (`verified-1`, `verified-9`); 11 findings were generated
452
+ from flagged verdicts (2 BOLA, 2 BFLA, 7 mass assignment).
453
+ - **Recall** is lower than `gpt-oss:20b` in every category: BFLA and mass assignment are mostly found as
454
+ verdicts (8/10, 15/15) but placed on the right line less often (6/10, 10/15).
455
+ - **Precision 0.73:** no finding on a SAFE marker, but 25 unmatched findings (23 BOLA, 2 BFLA), not yet triaged;
456
+ none is on `php-laravel/routes/web.php`.
457
+ - **Memory and time:** the runner stayed at 14–17 GB and swap peaked at 6.0 GB (about 3.7 GB before the
458
+ run, from other work), against 20 GB and 12.4 GB with reasoning on. 35 minutes in all, of which the
459
+ verifier took 17.
460
+ - **Minimum to run it:** a 24 GB machine is enough with `num_ctx 16384` and `--ai-reasoning-effort none`.
461
+ With reasoning on it needs more; on this evidence at least 32 GB RAM.
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ """Keeps the archived Evaluation Run 6 numbers (Issue #323) in step with the scorer and EVAL-RESULTS.md."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import unittest
12
+ from pathlib import Path
13
+
14
+ TESTS_DIR = Path(__file__).resolve().parent
15
+ LSA_ROOT = TESTS_DIR.parent
16
+ EVAL_RUN_6_DIR = LSA_ROOT / "examples" / "eval-run-6"
17
+ GROUND_TRUTH = EVAL_RUN_6_DIR / "ground-truth.json"
18
+ RUNS = ("gptoss-a", "gptoss-b", "qwen-16k-nothink")
19
+ QWEN_PARTIAL = EVAL_RUN_6_DIR / "qwen-16k-partial"
20
+ # (BOLA, injection, BFLA, mass assignment) found by location, then BFLA / mass found by auditor verdict
21
+ EXPECTED = {
22
+ "gptoss-a": {"marker": (26, 34, 10, 14), "verdict": (10, 15)},
23
+ "gptoss-b": {"marker": (26, 34, 10, 14), "verdict": (10, 14)},
24
+ "qwen-16k-nothink": {"marker": (23, 29, 6, 10), "verdict": (8, 15)},
25
+ }
26
+ # Findings that land on a SAFE marker, by category (see EVAL-RESULTS.md, Run 6)
27
+ SAFE_HITS = {
28
+ "gptoss-a": {"injection": ["classic-asp/shop/member.asp:6"]},
29
+ "gptoss-b": {"injection": ["python-flask/app.py:12"]},
30
+ "qwen-16k-nothink": {},
31
+ }
32
+
33
+
34
+ def score(run: str) -> dict:
35
+ proc = subprocess.run(
36
+ [sys.executable, str(TESTS_DIR / "score_findings.py"), "--verdicts", str(GROUND_TRUTH), str(EVAL_RUN_6_DIR / run)],
37
+ capture_output=True, text=True, check=True,
38
+ )
39
+ return json.loads(proc.stdout)
40
+
41
+
42
+ class TestEvaluationRun6(unittest.TestCase):
43
+ def test_ground_truth_is_the_121_item_set(self) -> None:
44
+ items = json.loads(GROUND_TRUTH.read_text(encoding="utf-8"))["items"]
45
+ self.assertEqual(len(items), 121)
46
+
47
+ def test_artifacts_exist(self) -> None:
48
+ for run in RUNS:
49
+ for name in ("findings.json", "coverage.json", "score.json", "ai-run-stats.json"):
50
+ self.assertTrue((EVAL_RUN_6_DIR / run / name).is_file(), f"{run}/{name}")
51
+
52
+ def test_archived_scores_match_the_scorer(self) -> None:
53
+ for run in RUNS:
54
+ with self.subTest(run=run):
55
+ archived = json.loads((EVAL_RUN_6_DIR / run / "score.json").read_text(encoding="utf-8"))
56
+ self.assertEqual(score(run), archived)
57
+
58
+ def test_numbers_match_eval_results(self) -> None:
59
+ for run, want in EXPECTED.items():
60
+ with self.subTest(run=run):
61
+ report = score(run)
62
+ pc, vl = report["per_category"], report["verdict_level"]
63
+ got = tuple(pc[c]["found"] for c in ("bola", "injection", "bfla", "mass_assignment"))
64
+ self.assertEqual(got, want["marker"])
65
+ self.assertEqual((vl["bfla"]["found"], vl["mass_assignment"]["found"]), want["verdict"])
66
+
67
+ def test_safe_marker_hits_are_the_documented_ones(self) -> None:
68
+ for run in RUNS:
69
+ with self.subTest(run=run):
70
+ report = score(run)
71
+ for cat, row in report["per_category"].items():
72
+ self.assertEqual(row["false_positive_on_safe_code"], SAFE_HITS[run].get(cat, []), cat)
73
+ for cat, row in report["verdict_level"].items():
74
+ self.assertEqual(row["flagged_safe"], [], cat)
75
+
76
+ def test_no_batch_lost_to_parse_or_call_errors(self) -> None:
77
+ for run in RUNS:
78
+ with self.subTest(run=run):
79
+ stats = json.loads((EVAL_RUN_6_DIR / run / "ai-run-stats.json").read_text(encoding="utf-8"))
80
+ self.assertEqual(stats["parse_failures"], {})
81
+ self.assertEqual(stats["call_errors"], {})
82
+ self.assertEqual(stats["anchoring"]["unanchored"], [])
83
+
84
+ def test_qwen_partial_score_matches_eval_results(self) -> None:
85
+ """The stopped qwen3.5:9b 16K run: indicative score of its unverified findings (EVAL-RESULTS, Run 6)."""
86
+ with tempfile.TemporaryDirectory() as tmp:
87
+ shutil.copy(QWEN_PARTIAL / "findings.partial.json", Path(tmp) / "findings.json")
88
+ proc = subprocess.run(
89
+ [sys.executable, str(TESTS_DIR / "score_findings.py"), str(GROUND_TRUTH), tmp],
90
+ capture_output=True, text=True, check=True,
91
+ )
92
+ report = json.loads(proc.stdout)
93
+ archived = json.loads((QWEN_PARTIAL / "score.partial.json").read_text(encoding="utf-8"))
94
+ self.assertEqual(report, archived)
95
+ pc = report["per_category"]
96
+ got = tuple(pc[c]["found"] for c in ("bola", "injection", "bfla", "mass_assignment"))
97
+ self.assertEqual(got, (12, 22, 0, 0))
98
+ self.assertEqual(report["precision"], 0.733)
99
+ for row in pc.values():
100
+ self.assertEqual(row["false_positive_on_safe_code"], [])
101
+
102
+
103
+ if __name__ == "__main__":
104
+ unittest.main()