@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
@@ -0,0 +1,565 @@
1
+ #!/usr/bin/env python3
2
+ """komagu test on session-based Java legacy apps (#368)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import contextlib
7
+ import io
8
+ import json
9
+ import os
10
+ import sys
11
+ import tempfile
12
+ import unittest
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Tuple
15
+ from unittest import mock
16
+
17
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
18
+ if str(PROJECT_ROOT) not in sys.path:
19
+ sys.path.insert(0, str(PROJECT_ROOT))
20
+
21
+ from operate.cli.main import main as cli_main
22
+ from operate.cross_user_tester.mock_test_server import start_mock_server
23
+ from operate.cross_user_tester.runner import CrossUserTestRunner, TestCase, generate_test_cases_from_findings
24
+ from operate.cross_user_tester.session_config import SessionConfigError, read_cookie
25
+ from operate.cross_user_tester.session_http import (
26
+ ALLOWED,
27
+ DENIED,
28
+ INCONCLUSIVE,
29
+ DenialSignal,
30
+ classify,
31
+ decode_body,
32
+ encode_form,
33
+ extract_form_token,
34
+ safe_location,
35
+ )
36
+ from operate.tests.mock_legacy_session_app import (
37
+ ALICE_COOKIE,
38
+ BOB_COOKIE,
39
+ DENIED_TEXT,
40
+ EXPIRED_COOKIE,
41
+ TOKEN_FIELD,
42
+ start_legacy_app,
43
+ )
44
+
45
+ READ_FINDING: Dict[str, Any] = {
46
+ "finding_id": "BOLA-READ", "category": "bola", "title": "Order view", "status": "confirmed",
47
+ "endpoint": {"method": "GET", "path": "/order/view.do"},
48
+ "attack_vector": {"method": "GET", "path": "/order/view.do", "target_param": "id"},
49
+ }
50
+ WRITE_FINDING: Dict[str, Any] = {
51
+ "finding_id": "BOLA-WRITE", "category": "bola", "title": "Order update", "status": "confirmed",
52
+ "endpoint": {"method": "POST", "path": "/order/update.do"},
53
+ "attack_vector": {"method": "POST", "path": "/order/update.do", "target_param": "id",
54
+ "suggested_payload": "id={id}&status=tampered"},
55
+ }
56
+ SIGNAL = DenialSignal(deny_status=(404,), deny_redirects=("/login.do",), deny_markers=("denied",),
57
+ allow_redirects=("/list.do",), allow_markers=())
58
+
59
+
60
+ class TestClassify(unittest.TestCase):
61
+ def verdict(self, signal: DenialSignal, status: int, location: str = "", body: str = "") -> str:
62
+ return classify(signal, status, location, body)[0]
63
+
64
+ def test_declared_status_is_denied(self) -> None:
65
+ self.assertEqual(self.verdict(SIGNAL, 404), DENIED)
66
+
67
+ def test_undeclared_error_status_is_never_a_pass_or_breach(self) -> None:
68
+ for status in (0, 400, 403, 405, 500):
69
+ self.assertEqual(self.verdict(SIGNAL, status), INCONCLUSIVE, status)
70
+
71
+ def test_redirects(self) -> None:
72
+ self.assertEqual(self.verdict(SIGNAL, 302, "http://h/login.do;x=1"), DENIED)
73
+ self.assertEqual(self.verdict(SIGNAL, 302, "/order/list.do"), ALLOWED)
74
+ self.assertEqual(self.verdict(SIGNAL, 302, "/somewhere.do"), INCONCLUSIVE)
75
+
76
+ def test_error_page_with_200_is_denied(self) -> None:
77
+ self.assertEqual(self.verdict(SIGNAL, 200, body="access denied"), DENIED)
78
+
79
+ def test_200_without_marker_is_allowed(self) -> None:
80
+ self.assertEqual(self.verdict(SIGNAL, 200, body="order data"), ALLOWED)
81
+
82
+ def test_allow_marker_required_when_declared(self) -> None:
83
+ signal = DenialSignal(deny_markers=("denied",), allow_markers=("Order detail",))
84
+ self.assertEqual(self.verdict(signal, 200, body="Order detail"), ALLOWED)
85
+ self.assertEqual(self.verdict(signal, 200, body="validation failed"), INCONCLUSIVE)
86
+
87
+ def test_page_matching_both_signals_is_ambiguous(self) -> None:
88
+ signal = DenialSignal(deny_markers=("denied",), allow_markers=("Order detail",))
89
+ self.assertEqual(self.verdict(signal, 200, body="Order detail denied"), INCONCLUSIVE)
90
+
91
+ def test_reason_does_not_carry_session_id_from_url_rewriting(self) -> None:
92
+ _, reason = classify(SIGNAL, 302, "/x.do;jsessionid=SECRET123?token=SECRET456", "")
93
+ self.assertNotIn("SECRET", reason)
94
+
95
+
96
+ class TestHelpers(unittest.TestCase):
97
+ def test_safe_location_strips_session_and_query(self) -> None:
98
+ self.assertEqual(safe_location("http://h/a.do;jsessionid=abc?x=1"), "http://h/a.do")
99
+
100
+ def test_decode_windows_31j_header(self) -> None:
101
+ raw = DENIED_TEXT.encode("cp932")
102
+ self.assertIn(DENIED_TEXT, decode_body(raw, "text/html; charset=Windows-31J"))
103
+
104
+ def test_decode_meta_charset(self) -> None:
105
+ raw = f'<meta charset="Shift_JIS">{DENIED_TEXT}'.encode("cp932")
106
+ self.assertIn(DENIED_TEXT, decode_body(raw, "text/html"))
107
+
108
+ def test_decode_undeclared_shift_jis(self) -> None:
109
+ self.assertIn(DENIED_TEXT, decode_body(DENIED_TEXT.encode("cp932"), "text/html"))
110
+
111
+ def test_decode_unknown_charset_falls_back(self) -> None:
112
+ self.assertEqual(decode_body(b"abc", "text/html; charset=nope"), "abc")
113
+
114
+ def test_extract_token_any_attribute_order(self) -> None:
115
+ self.assertEqual(extract_form_token('<input value="T1" type="hidden" name="f">', "f"), {"f": "T1"})
116
+
117
+ def test_extract_token_from_meta(self) -> None:
118
+ self.assertEqual(extract_form_token('<meta name="_csrf" content="T2">', "_csrf"), {"_csrf": "T2"})
119
+
120
+ def test_extract_struts2_sends_companion_field(self) -> None:
121
+ html = '<input name="struts.token.name" value="token"><input name="token" value="T3">'
122
+ self.assertEqual(extract_form_token(html, "token"), {"struts.token.name": "token", "token": "T3"})
123
+
124
+ def test_extract_missing_token(self) -> None:
125
+ self.assertIsNone(extract_form_token("<form></form>", "f"))
126
+
127
+ def test_encode_form(self) -> None:
128
+ self.assertEqual(encode_form({"a": "1 2", "b": 3}), b"a=1+2&b=3")
129
+
130
+ def test_encode_form_rejects_nested_values(self) -> None:
131
+ with self.assertRaises(ValueError):
132
+ encode_form({"a": {"b": 1}})
133
+
134
+
135
+ class TestReadCookie(unittest.TestCase):
136
+ def test_env(self) -> None:
137
+ self.assertEqual(read_cookie("C", None, {"C": " JSESSIONID=a \n"}, "attacker"), "JSESSIONID=a")
138
+
139
+ def test_file(self) -> None:
140
+ with tempfile.TemporaryDirectory() as td:
141
+ path = Path(td) / "c.txt"
142
+ path.write_text("JSESSIONID=b\n", encoding="utf-8")
143
+ os.chmod(path, 0o600)
144
+ self.assertEqual(read_cookie(None, str(path), {}, "owner"), "JSESSIONID=b")
145
+
146
+ def test_both_sources_rejected(self) -> None:
147
+ with self.assertRaises(SessionConfigError):
148
+ read_cookie("C", "/f", {"C": "a=b"}, "attacker")
149
+
150
+ def test_unset_env_names_variable_only(self) -> None:
151
+ with self.assertRaises(SessionConfigError) as ctx:
152
+ read_cookie("NOPE", None, {}, "attacker")
153
+ self.assertIn("NOPE", str(ctx.exception))
154
+
155
+ def test_bad_values_are_rejected_without_echo(self) -> None:
156
+ for bad in ("JSESSIONID=SECRETVALUE\r\nX: y", "SECRETVALUE"):
157
+ with self.assertRaises(SessionConfigError) as ctx:
158
+ read_cookie("C", None, {"C": bad}, "attacker")
159
+ self.assertNotIn("SECRETVALUE", str(ctx.exception))
160
+
161
+
162
+ class TestFromScanRequests(unittest.TestCase):
163
+ def test_form_action_read_puts_id_in_query(self) -> None:
164
+ case = generate_test_cases_from_findings([READ_FINDING], default_resource_id="100")[0]
165
+ self.assertEqual(case.path, "/order/view.do?id=100")
166
+ self.assertEqual(case.body_format, "form")
167
+
168
+ def test_form_action_write_puts_id_in_body(self) -> None:
169
+ case = generate_test_cases_from_findings([WRITE_FINDING], default_resource_id="100")[0]
170
+ self.assertEqual(case.path, "/order/update.do")
171
+ self.assertEqual(case.body_format, "form")
172
+ self.assertEqual(case.payload, {"id": "100", "status": "tampered"})
173
+
174
+ def test_existing_query_is_extended(self) -> None:
175
+ finding = {**READ_FINDING, "attack_vector": {"path": "/order.do?method=view", "target_param": "orderId"}}
176
+ case = generate_test_cases_from_findings([finding], default_resource_id="7")[0]
177
+ self.assertEqual(case.path, "/order.do?method=view&orderId=7")
178
+
179
+ def test_placeholder_in_query_is_filled(self) -> None:
180
+ finding = {**READ_FINDING, "attack_vector": {"path": "/order/view.do?id={id}"}}
181
+ case = generate_test_cases_from_findings([finding], default_resource_id="7")[0]
182
+ self.assertEqual(case.path, "/order/view.do?id=7")
183
+
184
+ def test_rest_route_stays_json(self) -> None:
185
+ finding = {"finding_id": "BOLA-API", "category": "bola", "endpoint": {"method": "PUT", "path": "/api/orders"}}
186
+ case = generate_test_cases_from_findings([finding], default_resource_id="7")[0]
187
+ self.assertEqual(case.path, "/api/orders/7")
188
+ self.assertEqual(case.body_format, "json")
189
+
190
+ def test_forced_json_keeps_rest_shape_for_do_route(self) -> None:
191
+ case = generate_test_cases_from_findings([WRITE_FINDING], default_resource_id="7", body_format="json")[0]
192
+ self.assertEqual(case.body_format, "json")
193
+ self.assertEqual(case.path, "/order/update.do/7")
194
+
195
+ def test_sarif_form_action(self) -> None:
196
+ sarif = {"runs": [{"results": [{"ruleId": "bola.x", "message": {"text": "m"}, "properties": {
197
+ "finding_id": "S1", "endpoint": {"method": "GET", "path": "/order/view.do"},
198
+ "attack_vector": {"target_param": "id"}}}]}]}
199
+ case = generate_test_cases_from_findings(sarif, default_resource_id="9")[0]
200
+ self.assertEqual(case.path, "/order/view.do?id=9")
201
+
202
+
203
+ class SessionAppCase(unittest.TestCase):
204
+ def setUp(self) -> None:
205
+ self.server, self.base_url = start_legacy_app()
206
+ self.addCleanup(self.server.server_close)
207
+ self.addCleanup(self.server.shutdown)
208
+ tmp = tempfile.TemporaryDirectory()
209
+ self.addCleanup(tmp.cleanup)
210
+ self.dir = Path(tmp.name)
211
+ self.scan = self.dir / "findings.json"
212
+ self.scan.write_text(json.dumps({"findings": [READ_FINDING, WRITE_FINDING]}), encoding="utf-8")
213
+ self.out = self.dir / "out.json"
214
+ self.env = {"ATT_COOKIE": BOB_COOKIE, "OWN_COOKIE": ALICE_COOKIE}
215
+
216
+ def base_args(self) -> List[str]:
217
+ return [
218
+ "--no-banner", "test", "--url", self.base_url, "--from-scan", str(self.scan),
219
+ "--resource-id", "100", "--output", str(self.out),
220
+ "--attacker-cookie-env", "ATT_COOKIE", "--owner-cookie-env", "OWN_COOKIE",
221
+ ]
222
+
223
+ def strict_args(self) -> List[str]:
224
+ return self.base_args() + [
225
+ "--strict", "--session-check", "/home.do", "--session-marker", "Welcome",
226
+ "--deny-status", "404", "--deny-redirect", "/login.do", "--deny-marker", DENIED_TEXT,
227
+ "--allow-redirect", "/order/list.do",
228
+ "--csrf-page", "/order/edit.do?id=200", "--csrf-field", TOKEN_FIELD,
229
+ "--owner-control-writes",
230
+ ]
231
+
232
+ def run_cli(self, args: List[str]) -> Tuple[int, str]:
233
+ out, err = io.StringIO(), io.StringIO()
234
+ with mock.patch.dict(os.environ, self.env), contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
235
+ try:
236
+ code = cli_main(args)
237
+ except SystemExit as stop:
238
+ code = int(stop.code or 0)
239
+ return code, out.getvalue() + err.getvalue()
240
+
241
+ def report(self) -> Dict[str, Any]:
242
+ return json.loads(self.out.read_text(encoding="utf-8"))
243
+
244
+ def by_finding(self) -> Dict[str, Dict[str, Any]]:
245
+ return {r["finding_id"]: r for r in self.report()["results"]}
246
+
247
+
248
+ class TestRehearsal(SessionAppCase):
249
+ """A vulnerable route must fail before the fix and pass after it."""
250
+
251
+ def test_fails_before_fix(self) -> None:
252
+ code, _ = self.run_cli(self.strict_args())
253
+ self.assertEqual(code, 1)
254
+ results = self.by_finding()
255
+ self.assertTrue(results["BOLA-READ"]["is_breach"])
256
+ self.assertTrue(results["BOLA-WRITE"]["is_breach"])
257
+ self.assertIn(("bob", "100"), self.server.updates)
258
+
259
+ def assert_passes_after_fix(self, denial: str) -> None:
260
+ self.server.fixed = True
261
+ self.server.denial = denial
262
+ code, output = self.run_cli(self.strict_args())
263
+ self.assertEqual(code, 0, output)
264
+ results = self.by_finding()
265
+ self.assertTrue(results["BOLA-READ"]["is_secure"])
266
+ self.assertEqual(results["BOLA-READ"]["owner_control"], "passed")
267
+ self.assertTrue(results["BOLA-WRITE"]["is_secure"])
268
+ self.assertEqual(results["BOLA-WRITE"]["owner_control"], "passed")
269
+ self.assertEqual(self.server.updates, [("alice", "100")])
270
+
271
+ def test_passes_after_fix_with_200_error_page(self) -> None:
272
+ self.assert_passes_after_fix("error_page")
273
+
274
+ def test_passes_after_fix_with_login_redirect(self) -> None:
275
+ self.assert_passes_after_fix("login_redirect")
276
+
277
+ def test_passes_after_fix_with_404(self) -> None:
278
+ self.assert_passes_after_fix("not_found")
279
+
280
+ def test_trailing_slash_in_url(self) -> None:
281
+ args = self.strict_args()
282
+ args[args.index("--url") + 1] = self.base_url + "/"
283
+ self.server.fixed = True
284
+ code, output = self.run_cli(args)
285
+ self.assertEqual(code, 0, output)
286
+
287
+ def test_requests_are_session_and_form_shaped(self) -> None:
288
+ self.run_cli(self.strict_args())
289
+ post = next(s for s in self.server.seen if s.method == "POST")
290
+ self.assertEqual(post.cookie, BOB_COOKIE)
291
+ self.assertIsNone(post.authorization)
292
+ self.assertEqual(post.content_type, "application/x-www-form-urlencoded")
293
+ self.assertIn("id=100", post.body)
294
+ self.assertIn("status=tampered", post.body)
295
+
296
+ def test_login_redirect_is_not_followed(self) -> None:
297
+ self.server.fixed = True
298
+ self.server.denial = "login_redirect"
299
+ self.run_cli(self.strict_args())
300
+ self.assertFalse([s for s in self.server.seen if s.path.startswith("/login.do")])
301
+
302
+
303
+ class TestNoFalseVerdicts(SessionAppCase):
304
+ def test_expired_attacker_session_stops_the_run(self) -> None:
305
+ self.server.fixed = True
306
+ self.env["ATT_COOKIE"] = EXPIRED_COOKIE
307
+ code, output = self.run_cli(self.strict_args())
308
+ self.assertEqual(code, 2, output)
309
+ self.assertFalse([s for s in self.server.seen if s.path.startswith("/order/")])
310
+
311
+ def test_expired_owner_session_stops_the_run(self) -> None:
312
+ self.env["OWN_COOKIE"] = EXPIRED_COOKIE
313
+ code, _ = self.run_cli(self.strict_args())
314
+ self.assertEqual(code, 2)
315
+
316
+ def test_session_lost_during_run_invalidates_result(self) -> None:
317
+ self.server.fixed = True
318
+ original = self.server.orders
319
+
320
+ class DropsSessionAfterFirstUse(dict):
321
+ def __init__(inner, server: Any) -> None: # noqa: N805
322
+ super().__init__(original)
323
+ inner.server = server
324
+
325
+ def get(inner, key: str, default: Any = None) -> Any: # noqa: N805
326
+ inner.server.sessions.pop("sess-bob-0002", None)
327
+ return super().get(key, default)
328
+
329
+ self.server.orders = DropsSessionAfterFirstUse(self.server)
330
+ code, output = self.run_cli(self.strict_args())
331
+ self.assertEqual(code, 2, output)
332
+ self.assertFalse(self.report()["success"])
333
+
334
+ def test_missing_record_is_not_a_pass(self) -> None:
335
+ self.server.fixed = True
336
+ args = self.strict_args()
337
+ args[args.index("--resource-id") + 1] = "999"
338
+ code, _ = self.run_cli(args)
339
+ self.assertEqual(code, 1)
340
+ read = self.by_finding()["BOLA-READ"]
341
+ self.assertFalse(read["is_secure"])
342
+ self.assertFalse(read["is_breach"])
343
+ self.assertEqual(read["owner_control"], "failed")
344
+
345
+ def test_missing_csrf_token_is_not_a_pass(self) -> None:
346
+ self.server.fixed = True
347
+ self.server.hide_token = True
348
+ code, _ = self.run_cli(self.strict_args())
349
+ self.assertEqual(code, 1)
350
+ write = self.by_finding()["BOLA-WRITE"]
351
+ self.assertFalse(write["is_secure"])
352
+ self.assertFalse(write["is_breach"])
353
+ self.assertIn("token", write["error_message"])
354
+ self.assertFalse([s for s in self.server.seen if s.method == "POST"])
355
+
356
+ def test_undeclared_redirect_is_inconclusive(self) -> None:
357
+ args = [a for a in self.strict_args()]
358
+ i = args.index("--allow-redirect")
359
+ del args[i:i + 2]
360
+ code, _ = self.run_cli(args)
361
+ self.assertEqual(code, 1)
362
+ write = self.by_finding()["BOLA-WRITE"]
363
+ self.assertFalse(write["is_breach"])
364
+ self.assertFalse(write["is_secure"])
365
+
366
+ def test_strict_session_needs_signal_check_and_owner(self) -> None:
367
+ full = self.strict_args()
368
+
369
+ def without(flag: str) -> List[str]:
370
+ i = full.index(flag)
371
+ return full[:i] + full[i + 2:]
372
+
373
+ no_signal = full
374
+ for flag in ("--deny-status", "--deny-redirect", "--deny-marker"):
375
+ i = no_signal.index(flag)
376
+ no_signal = no_signal[:i] + no_signal[i + 2:]
377
+ for args in (no_signal, without("--session-check"), without("--owner-cookie-env")):
378
+ code, _ = self.run_cli(args)
379
+ self.assertEqual(code, 2, args)
380
+ self.assertEqual(self.server.seen, [])
381
+
382
+ def test_strict_refused_write_without_owner_control_is_not_a_pass(self) -> None:
383
+ self.server.fixed = True
384
+ args = [a for a in self.strict_args() if a != "--owner-control-writes"]
385
+ code, _ = self.run_cli(args)
386
+ self.assertEqual(code, 1)
387
+ write = self.by_finding()["BOLA-WRITE"]
388
+ self.assertFalse(write["is_secure"])
389
+ self.assertFalse(write["is_breach"])
390
+ self.assertEqual(write["owner_control"], "skipped_write")
391
+ self.assertIn("--owner-control-writes", write["error_message"])
392
+ self.assertEqual(self.server.updates, [])
393
+
394
+ def test_non_strict_refused_write_keeps_owner_data_untouched(self) -> None:
395
+ self.server.fixed = True
396
+ args = [a for a in self.strict_args() if a not in ("--owner-control-writes", "--strict")]
397
+ code, output = self.run_cli(args)
398
+ self.assertEqual(code, 0, output)
399
+ self.assertEqual(self.by_finding()["BOLA-WRITE"]["owner_control"], "skipped_write")
400
+ self.assertEqual(self.server.updates, [])
401
+
402
+ def test_owner_control_uses_owner_csrf_page(self) -> None:
403
+ self.server.fixed = True
404
+ code, output = self.run_cli(self.strict_args() + ["--owner-csrf-page", "/order/edit.do?id=100"])
405
+ self.assertEqual(code, 0, output)
406
+ pages = [s for s in self.server.seen if s.path == "/order/edit.do?id=100"]
407
+ self.assertEqual([s.cookie for s in pages], [ALICE_COOKIE])
408
+
409
+ def test_empty_signal_value_is_config_error(self) -> None:
410
+ for flag in ("--deny-marker", "--deny-redirect", "--allow-marker", "--allow-redirect"):
411
+ code, _ = self.run_cli(self.strict_args() + [flag, ""])
412
+ self.assertEqual(code, 2, flag)
413
+ self.assertEqual(self.server.seen, [])
414
+
415
+
416
+ class TestSecrets(SessionAppCase):
417
+ def test_cookie_cannot_be_given_as_argument(self) -> None:
418
+ for flag in ("--attacker-cookie", "--owner-cookie"):
419
+ code, _ = self.run_cli(["--no-banner", "test", "--url", self.base_url, flag, BOB_COOKIE])
420
+ self.assertEqual(code, 2)
421
+
422
+ def test_no_cookie_or_session_id_in_output_or_report(self) -> None:
423
+ for fixed, denial in ((False, "error_page"), (True, "login_redirect")):
424
+ self.server.fixed = fixed
425
+ self.server.denial = denial
426
+ args = self.strict_args()
427
+ i = args.index("--deny-redirect")
428
+ del args[i:i + 2] # the redirect is reported as undeclared, with its target
429
+ _, output = self.run_cli(args)
430
+ text = output + self.out.read_text(encoding="utf-8")
431
+ for secret in ("sess-bob", "sess-alice", "sess-rewritten", "jsessionid"):
432
+ self.assertNotIn(secret, text)
433
+
434
+ def test_cookie_file_source(self) -> None:
435
+ path = self.dir / "bob.cookie"
436
+ path.write_text(BOB_COOKIE + "\n", encoding="utf-8")
437
+ os.chmod(path, 0o600)
438
+ args = self.strict_args()
439
+ i = args.index("--attacker-cookie-env")
440
+ args[i:i + 2] = ["--attacker-cookie-file", str(path)]
441
+ code, _ = self.run_cli(args)
442
+ self.assertEqual(code, 1)
443
+ self.assertTrue(self.by_finding()["BOLA-READ"]["is_breach"])
444
+
445
+ def test_unset_cookie_variable_is_config_error(self) -> None:
446
+ del self.env["ATT_COOKIE"]
447
+ with mock.patch.dict(os.environ, {}, clear=False):
448
+ os.environ.pop("ATT_COOKIE", None)
449
+ code, _ = self.run_cli(self.strict_args())
450
+ self.assertEqual(code, 2)
451
+
452
+
453
+ class TestSuites(SessionAppCase):
454
+ def write_suite(self, suite: Any) -> str:
455
+ path = self.dir / "suite.json"
456
+ path.write_text(json.dumps(suite), encoding="utf-8")
457
+ return str(path)
458
+
459
+ def suite_args(self, path: str) -> List[str]:
460
+ return [
461
+ "--no-banner", "test", "--url", self.base_url, "--suite", path, "--output", str(self.out),
462
+ "--deny-marker", DENIED_TEXT, "--deny-redirect", "/login.do", "--allow-redirect", "/order/list.do",
463
+ ]
464
+
465
+ def test_v1_suite_form_and_csrf(self) -> None:
466
+ suite = {"tests": [{
467
+ "name": "update", "method": "POST", "path": "/order/update.do", "body_format": "form",
468
+ "payload": {"id": "100"}, "csrf": {"page": "/order/edit.do?id=200", "field": TOKEN_FIELD},
469
+ }]}
470
+ args = self.suite_args(self.write_suite(suite)) + ["--attacker-cookie-env", "ATT_COOKIE"]
471
+ code, _ = self.run_cli(args)
472
+ self.assertEqual(code, 1)
473
+ self.assertTrue(self.report()["results"][0]["is_breach"])
474
+ self.server.fixed = True
475
+ code, _ = self.run_cli(args)
476
+ self.assertEqual(code, 0)
477
+
478
+ def test_v1_suite_headers_are_sent(self) -> None:
479
+ suite = {"tests": [{"method": "GET", "path": "/order/view.do?id=100", "headers": {"X-Trace": "1", "Authorization": "Basic abc"}}]}
480
+ self.run_cli(self.suite_args(self.write_suite(suite)) + ["--attacker-cookie-env", "ATT_COOKIE"])
481
+ self.assertEqual(self.server.seen[-1].authorization, "Basic abc")
482
+
483
+ def test_v1_suite_bad_body_format_is_config_error(self) -> None:
484
+ suite = {"tests": [{"method": "POST", "path": "/x.do", "body_format": "xml"}]}
485
+ code, _ = self.run_cli(self.suite_args(self.write_suite(suite)))
486
+ self.assertEqual(code, 2)
487
+ self.assertEqual(self.server.seen, [])
488
+
489
+ def v2_suite(self) -> Dict[str, Any]:
490
+ return {
491
+ "version": 2,
492
+ "personas": {"owner": {"cookie_env": "OWN_COOKIE"}, "attacker": {"cookie_env": "ATT_COOKIE"}},
493
+ "routes": [
494
+ {"method": "GET", "path": "/order/view.do?id={id}", "params": {"id": "100"}, "allow": ["owner"]},
495
+ {"method": "POST", "path": "/order/update.do", "allow": ["owner"], "body_format": "form",
496
+ "payload": {"id": "100"}, "csrf": {"page": "/order/edit.do?id=200", "field": TOKEN_FIELD}},
497
+ ],
498
+ }
499
+
500
+ def test_v2_suite_cookie_personas(self) -> None:
501
+ args = self.suite_args(self.write_suite(self.v2_suite()))
502
+ code, _ = self.run_cli(args)
503
+ self.assertEqual(code, 1)
504
+ self.assertTrue(all(r["is_breach"] for r in self.report()["matrix_results"]))
505
+ self.server.fixed = True
506
+ code, _ = self.run_cli(args)
507
+ self.assertEqual(code, 0)
508
+
509
+ def test_v2_session_check_covers_cookie_personas(self) -> None:
510
+ self.env["ATT_COOKIE"] = EXPIRED_COOKIE
511
+ args = self.suite_args(self.write_suite(self.v2_suite())) + ["--session-check", "/home.do"]
512
+ code, _ = self.run_cli(args)
513
+ self.assertEqual(code, 2)
514
+
515
+ def test_v2_rejects_token_and_cookie_together(self) -> None:
516
+ suite = self.v2_suite()
517
+ suite["personas"]["attacker"]["token_env"] = "T"
518
+ code, _ = self.run_cli(self.suite_args(self.write_suite(suite)))
519
+ self.assertEqual(code, 2)
520
+
521
+ def test_v2_rejects_inline_cookie(self) -> None:
522
+ suite = self.v2_suite()
523
+ suite["personas"]["attacker"] = {"cookie": "JSESSIONID=x"}
524
+ code, _ = self.run_cli(self.suite_args(self.write_suite(suite)))
525
+ self.assertEqual(code, 2)
526
+
527
+
528
+ class TestFormWithoutSession(unittest.TestCase):
529
+ def test_nested_form_value_is_an_error_not_a_crash(self) -> None:
530
+ server, base_url = start_legacy_app()
531
+ self.addCleanup(server.server_close)
532
+ self.addCleanup(server.shutdown)
533
+ case = TestCase("t", "/order/update.do", "POST", payload={"a": {"b": 1}}, body_format="form")
534
+ res = CrossUserTestRunner(base_url, default_attacker_token="t").execute_test(case)
535
+ self.assertFalse(res.is_secure or res.is_breach)
536
+ self.assertEqual(server.seen, [])
537
+
538
+ def test_only_http_urls_are_sent(self) -> None:
539
+ case = TestCase("t", "/etc/passwd", "GET", body_format="form")
540
+ res = CrossUserTestRunner("file://").execute_test(case)
541
+ self.assertFalse(res.is_secure or res.is_breach)
542
+ self.assertIn("http", res.error_message)
543
+
544
+
545
+ class TestBearerUnchanged(unittest.TestCase):
546
+ @classmethod
547
+ def setUpClass(cls) -> None:
548
+ cls.server, _, cls.base_url = start_mock_server(port=0)
549
+
550
+ @classmethod
551
+ def tearDownClass(cls) -> None:
552
+ cls.server.server_close()
553
+
554
+ def test_json_body_and_404_invariant(self) -> None:
555
+ runner = CrossUserTestRunner(self.base_url, default_attacker_token="persona-b")
556
+ res = runner.execute_test(TestCase("t", "/api/orders/1", "PUT", payload={"a": 1}))
557
+ self.assertTrue(res.is_secure)
558
+ self.assertEqual(set(res.to_dict()), {
559
+ "name", "finding_id", "consensus_score", "method", "path", "status_code",
560
+ "is_breach", "is_secure", "duration_ms", "error_message",
561
+ })
562
+
563
+
564
+ if __name__ == "__main__":
565
+ unittest.main()
@@ -16,12 +16,12 @@ class TestDockerPackaging(unittest.TestCase):
16
16
  """Verify Docker container definition and CI/CD workflow."""
17
17
 
18
18
  def test_dockerfile_structure_and_security(self) -> None:
19
- """Dockerfile must exist, use python-slim, configure non-root user, and set entrypoint."""
19
+ """Dockerfile must exist, use a digest-pinned python-alpine base, configure non-root user, and set entrypoint."""
20
20
  self.assertTrue(DOCKERFILE.is_file(), f"Dockerfile not found at {DOCKERFILE}")
21
21
  content = DOCKERFILE.read_text(encoding="utf-8")
22
22
 
23
- self.assertRegex(content, r"(?m)^FROM python:3\.\d+-slim\b")
24
- self.assertIn("useradd", content)
23
+ self.assertRegex(content, r"(?m)^FROM python:3\.\d+-alpine@sha256:[0-9a-f]{64}\b")
24
+ self.assertIn("adduser -u 1000", content)
25
25
  self.assertIn("USER komagu", content)
26
26
  self.assertIn("WORKDIR /workspace", content)
27
27
  self.assertIn('ENTRYPOINT ["/app/bin/komagu"]', content)
@@ -98,6 +98,12 @@ class TestSanitizeFinding(unittest.TestCase):
98
98
  self.assertEqual(f["evidence"][0]["line"], 7)
99
99
  self.assertEqual(f["status"], "unverified")
100
100
 
101
+ def test_endpoint_method_list_becomes_string(self) -> None:
102
+ """XML-declared Struts routes reach the model as "method": ["ANY"], and models echo it (#374)."""
103
+ f = HeadlessAIRunner._sanitize_finding({"category": "bola", "subtype": "missing_ownership_check",
104
+ "endpoint": {"method": ["GET", "POST"], "path": "/a.do"}})
105
+ self.assertEqual(f["endpoint"]["method"], "GET|POST")
106
+
101
107
  def test_valid_subtype_is_kept(self) -> None:
102
108
  f = HeadlessAIRunner._sanitize_finding({"category": "bfla", "subtype": "role_from_request"})
103
109
  self.assertEqual(f["subtype"], "role_from_request")