@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
@@ -15,7 +15,7 @@
15
15
  "devDependencies": {
16
16
  "@types/react": "^18.3.5",
17
17
  "@types/react-dom": "^18.3.0",
18
- "@vitejs/plugin-react": "^4.3.1",
18
+ "@vitejs/plugin-react": "^6.3.0",
19
19
  "typescript": "^5.5.4",
20
20
  "vite": "^8.3.4"
21
21
  }
@@ -12,6 +12,9 @@ import urllib.error
12
12
  import urllib.request
13
13
  from typing import Any, Dict, List, Optional, Tuple
14
14
 
15
+ # OpenAI-compatible `reasoning_effort` values; Ollama's /v1 also accepts "none" to turn thinking off (#382)
16
+ REASONING_EFFORTS = ("none", "low", "medium", "high")
17
+
15
18
 
16
19
  class LLMClient:
17
20
  """Standard-library client for OpenAI-compatible /chat/completions endpoints."""
@@ -22,7 +25,10 @@ class LLMClient:
22
25
  model: str = "llama3.1",
23
26
  api_key: Optional[str] = None,
24
27
  timeout: int = 120,
28
+ reasoning_effort: Optional[str] = None,
25
29
  ) -> None:
30
+ if reasoning_effort is not None and reasoning_effort not in REASONING_EFFORTS:
31
+ raise ValueError(f"reasoning_effort must be one of {', '.join(REASONING_EFFORTS)}")
26
32
  raw_endpoint = endpoint.rstrip("/")
27
33
  if not raw_endpoint.endswith("/chat/completions"):
28
34
  self.endpoint = f"{raw_endpoint}/chat/completions"
@@ -31,6 +37,7 @@ class LLMClient:
31
37
  self.model = model
32
38
  self.api_key = api_key or "ollama"
33
39
  self.timeout = timeout
40
+ self.reasoning_effort = reasoning_effort
34
41
  # Running totals across chat() calls, read by the runner for ai-run-stats.json
35
42
  self.usage: Dict[str, float] = {"calls": 0, "prompt_tokens": 0, "completion_tokens": 0, "seconds": 0.0}
36
43
 
@@ -41,15 +48,7 @@ class LLMClient:
41
48
  max_tokens: Optional[int] = None,
42
49
  ) -> str:
43
50
  """Send a chat completion request and return the assistant response content."""
44
- payload: Dict[str, Any] = {
45
- "model": self.model,
46
- "messages": messages,
47
- "temperature": temperature,
48
- }
49
- if max_tokens:
50
- payload["max_tokens"] = max_tokens
51
-
52
- data = json.dumps(payload).encode("utf-8")
51
+ data = json.dumps(self.build_payload(messages, temperature, max_tokens)).encode("utf-8")
53
52
  headers = {
54
53
  "Content-Type": "application/json",
55
54
  "Authorization": f"Bearer {self.api_key}",
@@ -74,6 +73,20 @@ class LLMClient:
74
73
  except urllib.error.URLError as e:
75
74
  raise RuntimeError(f"Failed to connect to LLM endpoint at {self.endpoint}: {e.reason}") from e
76
75
 
76
+ def build_payload(
77
+ self,
78
+ messages: List[Dict[str, str]],
79
+ temperature: float = 0.1,
80
+ max_tokens: Optional[int] = None,
81
+ ) -> Dict[str, Any]:
82
+ """Request body for /chat/completions; `reasoning_effort` is sent only when it was set."""
83
+ payload: Dict[str, Any] = {"model": self.model, "messages": messages, "temperature": temperature}
84
+ if max_tokens:
85
+ payload["max_tokens"] = max_tokens
86
+ if self.reasoning_effort:
87
+ payload["reasoning_effort"] = self.reasoning_effort
88
+ return payload
89
+
77
90
  def _record_usage(self, usage: Dict[str, Any], seconds: float) -> None:
78
91
  self.usage = {
79
92
  "calls": self.usage["calls"] + 1,
@@ -15,6 +15,7 @@ Standard-library only (Python 3.8+ compatible).
15
15
  from __future__ import annotations
16
16
 
17
17
  import json
18
+ import re
18
19
  from pathlib import Path
19
20
  import subprocess
20
21
  import sys
@@ -35,6 +36,7 @@ FINDING_STATUSES = {"unverified", "confirmed", "likely", "rejected", "needs_revi
35
36
 
36
37
 
37
38
  VERIFY_BATCH_SIZE = 5
39
+ ID_INPUT = re.compile(r"(?i)(?:^id$|id$|_no$|no$|key$|code$)")
38
40
  AUTH_MODEL_LIMIT = 6000
39
41
  RETRY_REMINDER = ("Your previous reply was empty or not valid JSON. Answer again with the JSON object only, "
40
42
  "exactly in the format requested above, with no other text.")
@@ -165,6 +167,7 @@ class HeadlessAIRunner:
165
167
  stats = {
166
168
  "model": getattr(self.client, "model", None),
167
169
  "endpoint": getattr(self.client, "endpoint", None),
170
+ "reasoning_effort": getattr(self.client, "reasoning_effort", None),
168
171
  "phases": self.phase_stats,
169
172
  "totals": self._usage(),
170
173
  "parse_failures": self.parse_failures,
@@ -298,8 +301,9 @@ Output MUST be Markdown format adhering to Komagu auth-model.md specification.
298
301
  {"name": p.get("name", "id"), "source": p.get("source", "path")}
299
302
  for p in c.get("id_params", [])
300
303
  ]
301
- if not id_inputs:
302
- id_inputs = [{"name": "id", "source": "path"}]
304
+ # No detected id input stays empty: inventing a path `id` made the BOLA auditor
305
+ # flag routes that take no object id (#380)
306
+ id_inputs = id_inputs or []
303
307
 
304
308
  mapped_routes.append({
305
309
  "route_id": route_id,
@@ -321,6 +325,16 @@ Output MUST be Markdown format adhering to Komagu auth-model.md specification.
321
325
 
322
326
  self._log("ok", f"Mapped {len(route_batches)} route inventories")
323
327
 
328
+ @staticmethod
329
+ def _with_route_target(finding: Dict[str, Any], route: Dict[str, Any]) -> Dict[str, Any]:
330
+ """Record the request parameter holding the object id, which `komagu test --from-scan` needs."""
331
+ attack_vector = finding.get("attack_vector") if isinstance(finding.get("attack_vector"), dict) else {}
332
+ if attack_vector.get("target_param"):
333
+ return finding
334
+ name = next((i.get("name") for i in route.get("id_inputs") or []
335
+ if isinstance(i, dict) and ID_INPUT.search(str(i.get("name") or ""))), None)
336
+ return {**finding, "attack_vector": {**attack_vector, "target_param": name}} if name else finding
337
+
324
338
  @staticmethod
325
339
  def _sanitize_finding(f: Dict[str, Any]) -> Dict[str, Any]:
326
340
  """Normalize finding fields to strictly comply with finding.schema.json."""
@@ -330,6 +344,11 @@ Output MUST be Markdown format adhering to Komagu auth-model.md specification.
330
344
  if isinstance(f.get("impact"), list):
331
345
  f["impact"] = "; ".join(str(i) for i in f["impact"])
332
346
 
347
+ # routes reach the model with a method list and models echo it; the schema wants one string joined with |
348
+ endpoint = f.get("endpoint")
349
+ if isinstance(endpoint, dict) and isinstance(endpoint.get("method"), list):
350
+ f["endpoint"] = {**endpoint, "method": "|".join(str(m) for m in endpoint["method"])}
351
+
333
352
  # a verdict written into subtype belongs in status; fall back to the category's first subtype
334
353
  allowed = CATEGORY_SUBTYPES.get(f.get("category"))
335
354
  if allowed and f.get("subtype") not in allowed:
@@ -461,9 +480,10 @@ Write one finding for every "vulnerable" verdict.
461
480
  raw_findings = parsed.get("findings", [])
462
481
  findings = self._anchor([self._sanitize_finding(f) for f in raw_findings])
463
482
  route_verdicts = parsed.get("route_verdicts", [])
483
+ by_route = {r.get("route_id"): r for r in routes_to_audit}
464
484
  findings = self._with_verdict_findings(
465
- "4a-bola-auditor", findings, route_verdicts, "route_id",
466
- {r.get("route_id"): r for r in routes_to_audit}, "bola")
485
+ "4a-bola-auditor", findings, route_verdicts, "route_id", by_route, "bola")
486
+ findings = [self._with_route_target(f, by_route.get(f.get("route_id")) or {}) for f in findings]
467
487
  except Exception:
468
488
  self._parse_failed("4a-bola-auditor", r_file.stem, reply)
469
489
  findings = []
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "2.4.0" # x-release-please-version
5
+ __version__ = "2.6.0" # x-release-please-version
@@ -130,6 +130,10 @@ def run_fix(args: argparse.Namespace) -> int:
130
130
  for plan in report.plans:
131
131
  if plan.diff:
132
132
  print(plan.diff)
133
+ else:
134
+ # stderr keeps stdout a valid patch; without this a run with only manual fixes prints nothing
135
+ print(f"[{plan.status.upper()}] {plan.finding_id} {plan.target_file}:{plan.start_line} - {plan.reason}",
136
+ file=sys.stderr)
133
137
  return 0
134
138
 
135
139
  step("[2/2] Remediation execution completed")
@@ -0,0 +1,75 @@
1
+ """Command: komagu leak-check - find secrets that reached pilot outputs (runbook §9)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import dataclasses
7
+ import json
8
+ import sys
9
+ from typing import Tuple
10
+
11
+ from operate.devsecops.leak_check import CanaryFileError, ScanResult, load_canaries, scan_paths
12
+
13
+
14
+ def register_subcommand(subparsers: argparse._SubParsersAction) -> None:
15
+ """Register the leak-check subcommand parser."""
16
+ parser = subparsers.add_parser(
17
+ "leak-check",
18
+ help="Scan ai-raw/, findings and reports for leaked secrets and client canary strings",
19
+ description=(
20
+ "Checks audit output for secrets the redaction step missed. Values are always masked in the report. "
21
+ "Exit codes: 0 clean, 1 suspected leak, 2 incomplete scan or bad input."
22
+ ),
23
+ formatter_class=argparse.RawDescriptionHelpFormatter,
24
+ epilog="""
25
+ Examples:
26
+ komagu leak-check ~/pilot/out --canary-file ~/pilot/canaries.txt
27
+ komagu leak-check ~/pilot/out/ai-raw ~/pilot/out/report.md --format json > ~/pilot/leak-check.json
28
+ """,
29
+ )
30
+ parser.add_argument("paths", nargs="+", metavar="PATH", help="Output directories or files to scan")
31
+ parser.add_argument(
32
+ "--canary-file",
33
+ help="UTF-8 file with one client-provided canary string per line; keep it outside the repository",
34
+ )
35
+ parser.add_argument("--format", choices=["text", "json"], default="text", help="Report format (default: text)")
36
+ parser.set_defaults(func=run)
37
+
38
+
39
+ def _print_text(result: ScanResult) -> None:
40
+ for hit in result.hits:
41
+ print(f"{hit.path}:{hit.line}: {hit.detector}: {hit.rule}: {hit.masked}")
42
+ for path, reason in result.unscanned:
43
+ print(f"NOT SCANNED {path}: {reason}")
44
+ print(
45
+ f"leak-check: {len(result.hits)} suspected leak(s), {result.files_scanned} file(s) scanned, "
46
+ f"{len(result.unscanned)} not scanned, {result.canaries_loaded} canary string(s)"
47
+ )
48
+
49
+
50
+ def _print_json(result: ScanResult) -> None:
51
+ report = {
52
+ "exitCode": result.exit_code,
53
+ "filesScanned": result.files_scanned,
54
+ "canariesLoaded": result.canaries_loaded,
55
+ "hits": [dataclasses.asdict(hit) for hit in result.hits],
56
+ "notScanned": [{"path": path, "reason": reason} for path, reason in result.unscanned],
57
+ }
58
+ print(json.dumps(report, indent=2, ensure_ascii=False))
59
+
60
+
61
+ def run(args: argparse.Namespace) -> int:
62
+ """Run the scan and return its exit code."""
63
+ canaries: Tuple[str, ...] = ()
64
+ if args.canary_file is not None:
65
+ try:
66
+ canaries = load_canaries(args.canary_file)
67
+ except CanaryFileError as exc:
68
+ print(f"leak-check: {exc}", file=sys.stderr)
69
+ return 2
70
+ result = scan_paths(args.paths, canaries)
71
+ if args.format == "json":
72
+ _print_json(result)
73
+ else:
74
+ _print_text(result)
75
+ return result.exit_code
@@ -9,6 +9,7 @@ import subprocess
9
9
  import sys
10
10
  from pathlib import Path
11
11
  from typing import Any, Dict
12
+ from urllib.parse import urlsplit
12
13
 
13
14
  from operate.cli.ui import (
14
15
  BOLD,
@@ -110,10 +111,31 @@ Documentation:
110
111
 
111
112
 
112
113
 
114
+ def _valid_target(url: str) -> bool:
115
+ parts = urlsplit(url)
116
+ return parts.scheme in ("http", "https") and bool(parts.hostname)
117
+
118
+
119
+ def _compose_env(args: argparse.Namespace, config_path: Path) -> Dict[str, str]:
120
+ """Settings the compose file reads; without them the stack would front its demo backend."""
121
+ env = dict(os.environ)
122
+ if args.target:
123
+ env["KOMAGU_UPSTREAM"] = args.target
124
+ if args.config:
125
+ env["KOMAGU_KONG_CONFIG"] = str(config_path)
126
+ if getattr(args, "patch_out", None):
127
+ env["KOMAGU_VIRTUAL_PATCH_FILE"] = str(Path(args.patch_out).resolve())
128
+ return env
129
+
130
+
113
131
  def execute_protect(args: argparse.Namespace) -> int:
114
132
  """Execute perimeter protection actions."""
115
133
  step("Validating Komagu Perimeter Protection assets")
116
134
 
135
+ if args.target and not _valid_target(args.target):
136
+ step_fail(f"--target must be an http(s) URL with a host: {args.target}")
137
+ return 2
138
+
117
139
  if not EDGE_DIR.is_dir():
118
140
  step_fail(f"Edge directory not found at {EDGE_DIR}")
119
141
  return 1
@@ -141,6 +163,8 @@ def execute_protect(args: argparse.Namespace) -> int:
141
163
  step_info(f"Synthesizing Coraza WAF virtual patches from: {scan_path.name}")
142
164
  out_file, manifest = patcher.patch_to_file(scan_path, target_conf)
143
165
  step_ok(f"Synthesized {len(manifest)} SecRules into {out_file}")
166
+ for note in patcher.skipped:
167
+ step_warn(note)
144
168
 
145
169
  if manifest:
146
170
  patch_rows = [(f"Rule {m['rule_id']} [{m['category'].upper()}]", f"{m['finding_id']} -> {m['endpoint']}") for m in manifest]
@@ -151,7 +175,7 @@ def execute_protect(args: argparse.Namespace) -> int:
151
175
  )
152
176
 
153
177
  # If --target is supplied, report upstream routing
154
- upstream_url = args.target or "http://coraza-waf:8080 (internal upstream)"
178
+ upstream_url = args.target or "http://mock-backend:5000 (demo backend)"
155
179
  if args.target:
156
180
  step_info(f"Configured upstream target: {args.target}")
157
181
 
@@ -160,11 +184,16 @@ def execute_protect(args: argparse.Namespace) -> int:
160
184
  compose_cmd = [docker_bin, "compose", "-f", str(EDGE_COMPOSE)] if docker_bin else None
161
185
 
162
186
  if args.up:
187
+ patch_out = getattr(args, "patch_out", None)
188
+ if patch_out and not Path(patch_out).is_file():
189
+ step_fail(f"Virtual patch file not found, Docker would mount a directory instead: {patch_out}")
190
+ return 2
163
191
  if args.dry_run or not docker_bin:
164
192
  step_info(f"[DRY-RUN] Would execute: docker compose -f {EDGE_COMPOSE} up -d")
165
193
  else:
166
194
  step("Starting perimeter containers (Kong Gateway + Coraza WAF + Mock Backend)")
167
- res = subprocess.run(compose_cmd + ["up", "-d"], capture_output=True, text=True)
195
+ res = subprocess.run(compose_cmd + ["up", "-d"], capture_output=True, text=True,
196
+ env=_compose_env(args, config_path))
168
197
  if res.returncode != 0:
169
198
  step_fail("Failed to start perimeter containers")
170
199
  print(res.stderr or res.stdout, file=sys.stderr)
@@ -196,7 +225,7 @@ def execute_protect(args: argparse.Namespace) -> int:
196
225
  ("Upstream Target", upstream_url),
197
226
  ("Public Proxy Port", "http://localhost:8000"),
198
227
  ("Kong Admin API", "http://localhost:8001"),
199
- ("Coraza WAF Port", "http://localhost:8080"),
228
+ ("Coraza WAF", "internal only, port 8080 is not published on the host"),
200
229
  ]
201
230
 
202
231
  print_summary_table(
@@ -161,6 +161,12 @@ Examples:
161
161
  default=120,
162
162
  help="Timeout in seconds per LLM request (default: 120)",
163
163
  )
164
+ parser.add_argument(
165
+ "--ai-reasoning-effort",
166
+ choices=("none", "low", "medium", "high"),
167
+ default=None,
168
+ help="Send reasoning_effort with each LLM request; 'none' turns thinking off on Ollama (default: not sent)",
169
+ )
164
170
  parser.set_defaults(func=execute_scan)
165
171
 
166
172
 
@@ -284,7 +290,8 @@ def execute_scan(args: argparse.Namespace) -> int:
284
290
  step_info(msg)
285
291
 
286
292
  try:
287
- client = LLMClient(endpoint=endpoint, model=model, api_key=api_key, timeout=timeout)
293
+ client = LLMClient(endpoint=endpoint, model=model, api_key=api_key, timeout=timeout,
294
+ reasoning_effort=getattr(args, "ai_reasoning_effort", None))
288
295
  runner = HeadlessAIRunner(target_path, out_dir, client, step_callback=_scan_callback)
289
296
  runner.run()
290
297
  except Exception as e:
@@ -7,7 +7,7 @@ import json
7
7
  import os
8
8
  import sys
9
9
  from pathlib import Path
10
- from typing import Any, Dict, List
10
+ from typing import Any, Dict, List, Mapping, Optional, Tuple
11
11
 
12
12
  from operate.cli.ui import (
13
13
  BOLD,
@@ -34,6 +34,17 @@ from operate.cross_user_tester.persona_matrix import (
34
34
  is_v2_suite,
35
35
  load_persona_suite,
36
36
  )
37
+ from operate.cross_user_tester.session_config import (
38
+ SessionOptions,
39
+ add_session_arguments,
40
+ build_session_options,
41
+ check_session,
42
+ cookie_file_is_open,
43
+ global_csrf,
44
+ read_cookie,
45
+ strict_gaps,
46
+ )
47
+ from operate.cross_user_tester.session_http import SessionConfigError
37
48
  from operate.cross_user_tester.runner import (
38
49
  CrossUserTestRunner,
39
50
  format_report,
@@ -46,6 +57,8 @@ REPO_ROOT = Path(__file__).resolve().parents[3]
46
57
  DEFAULT_SUITE = REPO_ROOT / "operate" / "tests" / "test_suite.json"
47
58
  # --token is passed to persona-matrix cases through this private env entry.
48
59
  CLI_TOKEN_ENV = "KOMAGU_CLI_TOKEN"
60
+ # The attacker's cookie reaches persona-matrix cases the same way, in a private copy of the environment.
61
+ CLI_COOKIE_ENV = "KOMAGU_CLI_COOKIE"
49
62
 
50
63
 
51
64
  def register_subcommand(subparsers: argparse._SubParsersAction) -> None:
@@ -75,6 +88,11 @@ Examples:
75
88
  # Persona matrix (suite "version": 2): BFLA and mass assignment checks
76
89
  KOMAGU_TOKEN_USER=... KOMAGU_TOKEN_ADMIN=... komagu test --url http://staging:8000 --suite ./personas.json
77
90
 
91
+ # Session-based Java app (JSP / Struts): cookies from the environment, declared denial signal
92
+ ATTACKER_COOKIE=... OWNER_COOKIE=... komagu test --url http://test:8080 --from-scan ./findings.json --strict \\
93
+ --attacker-cookie-env ATTACKER_COOKIE --owner-cookie-env OWNER_COOKIE \\
94
+ --session-check /home.do --deny-status 404 --deny-redirect /login.do
95
+
78
96
  Documentation:
79
97
  See docs/cli-guide.md#34-komagu-test--offensive-exploit-verification-bola-invariant--persona-matrix
80
98
  """,
@@ -126,7 +144,8 @@ Documentation:
126
144
  parser.add_argument(
127
145
  "--strict",
128
146
  action="store_true",
129
- help="Enforce strict 404 requirement (reject 401/403 which leak resource existence)",
147
+ help="Enforce strict 404 requirement (reject 401/403 which leak resource existence). "
148
+ "With session cookies, also requires a declared denial signal, --session-check and the owner's cookie",
130
149
  )
131
150
  parser.add_argument(
132
151
  "--token-env",
@@ -139,6 +158,7 @@ Documentation:
139
158
  dest="export_poc",
140
159
  help="Directory path to export standalone executable Python and cURL exploit PoC scripts",
141
160
  )
161
+ add_session_arguments(parser)
142
162
  parser.set_defaults(func=execute_test)
143
163
 
144
164
 
@@ -150,6 +170,19 @@ def execute_test(args: argparse.Namespace) -> int:
150
170
  tests: List[Any] = []
151
171
  matrix_cases: List[MatrixCase] = []
152
172
 
173
+ try:
174
+ session = build_session_options(args, os.environ)
175
+ csrf = global_csrf(args)
176
+ except SessionConfigError as e:
177
+ step_fail(str(e))
178
+ return 2
179
+ for cookie_file in (args.attacker_cookie_file, args.owner_cookie_file):
180
+ if cookie_file_is_open(cookie_file):
181
+ step_warn("A cookie file can be read by other users; restrict it with chmod 600")
182
+ # A session run sends the cookie only; the --token default must not go with it.
183
+ attacker_token = None if session and session.attacker_cookie else args.attacker_token
184
+ suite_body_format = "form" if args.body_format == "form" else "json"
185
+
153
186
  # 1. Ingest from scan output if provided
154
187
  if getattr(args, "from_scan", None):
155
188
  scan_file = Path(args.from_scan).resolve()
@@ -161,15 +194,19 @@ def execute_test(args: argparse.Namespace) -> int:
161
194
  scan_cases = generate_test_cases_from_findings(
162
195
  scan_data_or_path=scan_file,
163
196
  default_resource_id=args.resource_id,
164
- attacker_token=args.attacker_token,
197
+ attacker_token=attacker_token,
165
198
  min_consensus_score=min_c,
199
+ body_format=args.body_format,
200
+ csrf=csrf,
166
201
  )
167
202
  step_info(f"Synthesized {len(scan_cases)} active verification cases from scan findings")
168
203
  tests.extend(scan_cases)
169
- matrix_cases.extend(_matrix_cases_from_scan(scan_file, args, min_c))
204
+ matrix_cases.extend(_matrix_cases_from_scan(scan_file, args, min_c, session, csrf))
170
205
 
171
206
  # Export PoC exploits if requested
172
207
  if getattr(args, "export_poc", None):
208
+ if session and session.attacker_cookie:
209
+ step_warn("--export-poc writes Bearer scripts; they do not carry the session cookie")
173
210
  from operate.exploit.generator import ExploitGenerator
174
211
  gen = ExploitGenerator(
175
212
  base_url=args.base_url,
@@ -191,14 +228,20 @@ def execute_test(args: argparse.Namespace) -> int:
191
228
  return 2
192
229
  if is_v2_suite(suite_data):
193
230
  try:
194
- matrix_cases.extend(load_persona_suite(suite_data, default_id=args.resource_id))
195
- except SuiteError as e:
231
+ matrix_cases.extend(load_persona_suite(
232
+ suite_data, default_id=args.resource_id,
233
+ default_body_format=suite_body_format, default_csrf=csrf,
234
+ ))
235
+ except (SuiteError, SessionConfigError) as e:
196
236
  step_fail(f"Invalid persona suite {suite_path.name}: {e}")
197
237
  return 2
198
238
  step_info(f"Persona matrix: {len(matrix_cases)} BFLA / mass assignment cases")
199
239
  else:
200
240
  try:
201
- tests.extend(load_suite_from_file(str(suite_path)))
241
+ tests.extend(load_suite_from_file(str(suite_path), suite_body_format, csrf))
242
+ except SessionConfigError as e:
243
+ step_fail(f"Invalid test suite {suite_path.name}: {e}")
244
+ return 2
202
245
  except Exception as e:
203
246
  step_warn(f"Could not load test suite {suite_path}: {e}")
204
247
 
@@ -210,16 +253,30 @@ def execute_test(args: argparse.Namespace) -> int:
210
253
  generate_crud_test_cases(
211
254
  resource_path=crud_path,
212
255
  resource_id=args.resource_id,
213
- attacker_token=args.attacker_token,
256
+ attacker_token=attacker_token,
257
+ body_format=suite_body_format,
258
+ csrf=csrf,
214
259
  )
215
260
  )
216
261
 
217
262
 
218
263
  runner = CrossUserTestRunner(
219
264
  base_url=args.base_url,
220
- default_attacker_token=args.attacker_token,
265
+ default_attacker_token=attacker_token,
266
+ session=session,
221
267
  )
222
268
 
269
+ has_cookie_personas = any(c.persona.cookie_env or c.persona.cookie_file for c in matrix_cases)
270
+ if args.strict and has_cookie_personas:
271
+ gaps = strict_gaps(args, False, False)
272
+ if gaps:
273
+ step_fail("--strict on a session app needs " + "; ".join(gaps))
274
+ return 2
275
+ problem = _session_problem(args, session, matrix_cases, os.environ)
276
+ if problem:
277
+ step_fail(f"{problem}. No test was sent.")
278
+ return 2
279
+
223
280
  results = []
224
281
  success = True
225
282
  if tests:
@@ -230,7 +287,7 @@ def execute_test(args: argparse.Namespace) -> int:
230
287
  print(report_text)
231
288
 
232
289
  try:
233
- matrix_results = _run_matrix(matrix_cases, args)
290
+ matrix_results = _run_matrix(matrix_cases, args, session)
234
291
  except ValueError as e:
235
292
  step_fail(str(e))
236
293
  return 2
@@ -242,6 +299,12 @@ def execute_test(args: argparse.Namespace) -> int:
242
299
  print(matrix_text)
243
300
  success = success and matrix_ok
244
301
 
302
+ # A session lost during the run turns refusals into login redirects: the verdicts cannot be trusted.
303
+ lost_session = _session_problem(args, session, matrix_cases, os.environ)
304
+ if lost_session:
305
+ step_fail(f"{lost_session} after the run. The results below are not valid.")
306
+ success = False
307
+
245
308
  # Save output if requested
246
309
  if args.output:
247
310
  out_path = Path(args.output).resolve()
@@ -252,6 +315,8 @@ def execute_test(args: argparse.Namespace) -> int:
252
315
  "results": [r.to_dict() for r in results],
253
316
  "matrix_results": [r.to_dict() for r in matrix_results],
254
317
  }
318
+ if lost_session:
319
+ report_data["session_check"] = "failed_after_run"
255
320
  out_path.write_text(json.dumps(report_data, indent=2), encoding="utf-8")
256
321
  step_ok(f"Saved test report to {out_path}")
257
322
 
@@ -282,7 +347,7 @@ def execute_test(args: argparse.Namespace) -> int:
282
347
  if r.is_breach:
283
348
  verdict = color("CONFIRMED BREACH (EXPLOITABLE)", RED, BOLD)
284
349
  elif r.is_secure:
285
- verdict = color("PROTECTED (404 INVARIANT)", GREEN, BOLD)
350
+ verdict = color("PROTECTED (DENIAL SIGNAL)" if session else "PROTECTED (404 INVARIANT)", GREEN, BOLD)
286
351
  else:
287
352
  verdict = color(f"UNEXPECTED HTTP {r.status_code}", YELLOW)
288
353
  exploit_rows.append((f"{f_id} [{tc.method} {tc.path}]", f"Consensus: {c_score_str} | Verdict: {verdict}"))
@@ -293,29 +358,80 @@ def execute_test(args: argparse.Namespace) -> int:
293
358
  footer="Offensive Verification: Green = 404 Enforced, Red = Active BOLA Breach Confirmed.",
294
359
  )
295
360
 
361
+ if lost_session:
362
+ return 2
296
363
  return 0 if success else 1
297
364
 
298
365
 
299
- def _matrix_cases_from_scan(scan_file: Path, args: argparse.Namespace, min_consensus: float) -> List[MatrixCase]:
366
+ def _session_targets(
367
+ session: Optional[SessionOptions],
368
+ cases: List[MatrixCase],
369
+ env: Mapping[str, str],
370
+ ) -> List[Tuple[str, str]]:
371
+ """(label, cookie) for every session persona in this run."""
372
+ targets: List[Tuple[str, str]] = []
373
+ if session and session.attacker_cookie:
374
+ targets.append(("attacker", session.attacker_cookie))
375
+ if session and session.owner_cookie:
376
+ targets.append(("owner", session.owner_cookie))
377
+ for persona in {c.persona for c in cases if c.persona.cookie_env != CLI_COOKIE_ENV}:
378
+ cookie = read_cookie(persona.cookie_env, persona.cookie_file, env, persona.name)
379
+ if cookie:
380
+ targets.append((persona.name, cookie))
381
+ return targets
382
+
383
+
384
+ def _session_problem(
385
+ args: argparse.Namespace,
386
+ session: Optional[SessionOptions],
387
+ cases: List[MatrixCase],
388
+ env: Mapping[str, str],
389
+ ) -> Optional[str]:
390
+ """Why a session persona cannot be used, or None when all are alive."""
391
+ if session is None or not session.session_check:
392
+ return None
393
+ try:
394
+ targets = _session_targets(session, cases, env)
395
+ except SessionConfigError as err:
396
+ return str(err)
397
+ for label, cookie in targets:
398
+ problem = check_session(args.base_url, label, cookie, session.session_check, session.session_marker, session.denial)
399
+ if problem:
400
+ return problem
401
+ return None
402
+
403
+
404
+ def _matrix_cases_from_scan(
405
+ scan_file: Path,
406
+ args: argparse.Namespace,
407
+ min_consensus: float,
408
+ session: Optional[SessionOptions],
409
+ csrf: Optional[Any],
410
+ ) -> List[MatrixCase]:
300
411
  """BFLA and mass assignment findings from findings.json or SARIF."""
301
412
  try:
302
413
  data = json.loads(scan_file.read_text(encoding="utf-8"))
303
414
  except (OSError, ValueError):
304
415
  return []
305
416
  token_env = getattr(args, "token_env", None) or CLI_TOKEN_ENV
417
+ cookie_env = CLI_COOKIE_ENV if session and session.attacker_cookie else None
306
418
  cases = generate_matrix_cases_from_findings(
307
- data, token_env=token_env, default_id=args.resource_id, min_consensus=min_consensus
419
+ data, token_env=token_env, default_id=args.resource_id, min_consensus=min_consensus,
420
+ cookie_env=cookie_env, body_format=args.body_format, csrf=csrf,
308
421
  )
309
422
  if cases:
310
423
  step_info(f"Synthesized {len(cases)} BFLA / mass assignment cases from scan findings")
311
424
  return cases
312
425
 
313
426
 
314
- def _run_matrix(cases: List[MatrixCase], args: argparse.Namespace) -> List[Any]:
427
+ def _run_matrix(cases: List[MatrixCase], args: argparse.Namespace, session: Optional[SessionOptions]) -> List[Any]:
315
428
  if not cases:
316
429
  return []
317
430
  env: Dict[str, str] = dict(os.environ)
318
431
  if args.attacker_token:
319
432
  env.setdefault(CLI_TOKEN_ENV, args.attacker_token)
433
+ if session and session.attacker_cookie:
434
+ env[CLI_COOKIE_ENV] = session.attacker_cookie
320
435
  step(f"Executing {len(cases)} persona matrix cases")
321
- return PersonaMatrixRunner(args.base_url, env=env, strict=args.strict).run(cases)
436
+ denial = session.denial if session else None
437
+ return PersonaMatrixRunner(args.base_url, env=env, strict=args.strict, denial=denial).run(cases)