@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
@@ -23,10 +23,35 @@ import sys
23
23
  import re
24
24
  import time
25
25
  import urllib.error
26
+ import urllib.parse
26
27
  import urllib.request
28
+ from dataclasses import replace
27
29
  from pathlib import Path
28
30
  from typing import Any, Dict, List, Optional, Tuple, Union
29
31
 
32
+ if not __package__: # run directly as a script: the repository root is not on sys.path
33
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
34
+
35
+ from operate.cross_user_tester.session_config import SessionOptions # noqa: E402
36
+ from operate.cross_user_tester.session_http import ( # noqa: E402
37
+ ALLOWED,
38
+ DENIED,
39
+ INCONCLUSIVE,
40
+ WRITE_METHODS,
41
+ CsrfConfig,
42
+ DenialSignal,
43
+ HttpReply,
44
+ SessionRequest,
45
+ classify,
46
+ deliver,
47
+ is_form_action,
48
+ parse_body_format,
49
+ parse_csrf,
50
+ )
51
+
52
+ _ID_PLACEHOLDER = re.compile(r"\{[a-zA-Z0-9_-]*(?:id|ID)[a-zA-Z0-9_-]*\}")
53
+ _TAMPER_PAYLOAD = {"status": "tampered_by_attacker", "note": "Komagu BOLA exploit verification"}
54
+
30
55
 
31
56
  class TestCase:
32
57
  """Specification of a cross-user test case."""
@@ -44,6 +69,8 @@ class TestCase:
44
69
  insecure_status: Optional[List[int]] = None,
45
70
  finding_id: Optional[str] = None,
46
71
  consensus_score: Optional[float] = None,
72
+ body_format: str = "json",
73
+ csrf: Optional[CsrfConfig] = None,
47
74
  ) -> None:
48
75
  self.name = name
49
76
  self.finding_id = finding_id
@@ -63,6 +90,8 @@ class TestCase:
63
90
  self.headers = headers or {}
64
91
  self.expected_status = expected_status or [404]
65
92
  self.insecure_status = insecure_status or [200, 201, 202, 204]
93
+ self.body_format = body_format
94
+ self.csrf = csrf
66
95
 
67
96
 
68
97
  class TestResult:
@@ -77,6 +106,7 @@ class TestResult:
77
106
  is_secure: bool,
78
107
  duration_ms: float,
79
108
  error_message: Optional[str] = None,
109
+ owner_control: Optional[str] = None,
80
110
  ) -> None:
81
111
  self.test_case = test_case
82
112
  self.status_code = status_code
@@ -85,9 +115,10 @@ class TestResult:
85
115
  self.is_secure = is_secure
86
116
  self.duration_ms = duration_ms
87
117
  self.error_message = error_message
118
+ self.owner_control = owner_control
88
119
 
89
120
  def to_dict(self) -> Dict[str, Any]:
90
- return {
121
+ data: Dict[str, Any] = {
91
122
  "name": self.test_case.name,
92
123
  "finding_id": self.test_case.finding_id,
93
124
  "consensus_score": self.test_case.consensus_score,
@@ -99,6 +130,9 @@ class TestResult:
99
130
  "duration_ms": round(self.duration_ms, 2),
100
131
  "error_message": self.error_message,
101
132
  }
133
+ if self.owner_control is not None:
134
+ data["owner_control"] = self.owner_control
135
+ return data
102
136
 
103
137
 
104
138
 
@@ -112,15 +146,76 @@ class CrossUserTestRunner:
112
146
  auth_header: str = "Authorization",
113
147
  token_prefix: str = "Bearer ",
114
148
  timeout: float = 10.0,
149
+ session: Optional[SessionOptions] = None,
115
150
  ) -> None:
116
151
  self.base_url = base_url.rstrip("/")
117
152
  self.default_attacker_token = default_attacker_token
118
153
  self.auth_header = auth_header
119
154
  self.token_prefix = token_prefix
120
155
  self.timeout = timeout
156
+ self.session = session
157
+
158
+ def _auth_header(self, test: TestCase) -> Optional[Tuple[str, str]]:
159
+ token = test.attacker_token or self.default_attacker_token
160
+ return (self.auth_header, f"{self.token_prefix}{token}".strip()) if token else None
161
+
162
+ def _deliver(
163
+ self,
164
+ test: TestCase,
165
+ cookie: Optional[str],
166
+ auth: Optional[Tuple[str, str]],
167
+ csrf_page: Optional[str] = None,
168
+ ) -> HttpReply:
169
+ csrf = replace(test.csrf, page=csrf_page) if test.csrf and csrf_page else test.csrf
170
+ request = SessionRequest(
171
+ method=test.method, path=test.path, cookie=cookie, auth_header=auth,
172
+ payload=test.payload, body_format=test.body_format, csrf=csrf, headers=test.headers,
173
+ )
174
+ return deliver(self.base_url, request, self.timeout)
175
+
176
+ def _owner_control(self, test: TestCase, options: SessionOptions, signal: DenialSignal) -> Tuple[str, Optional[str]]:
177
+ """Replay a refused request as the owner: it must work, or the refusal proves nothing."""
178
+ if not options.owner_cookie:
179
+ return "not_run", None
180
+ if test.method in WRITE_METHODS and not options.owner_control_writes:
181
+ if options.strict:
182
+ return "skipped_write", "--strict cannot pass a refused write without --owner-control-writes"
183
+ return "skipped_write", None
184
+ reply = self._deliver(test, options.owner_cookie, None, options.owner_csrf_page)
185
+ verdict, reason = classify(signal, reply.status, reply.location, reply.body) if not reply.error else (INCONCLUSIVE, reply.error)
186
+ if verdict == ALLOWED:
187
+ return "passed", None
188
+ return "failed", f"owner control failed ({reason}): the route or resource ID cannot prove a refusal"
189
+
190
+ def _execute_session_test(self, test: TestCase) -> TestResult:
191
+ """Legacy path: no redirects followed, verdict from the denial signal."""
192
+ options = self.session or SessionOptions()
193
+ signal = options.denial or DenialSignal(deny_status=tuple(test.expected_status))
194
+ auth = None if options.attacker_cookie else self._auth_header(test)
195
+ start_time = time.time()
196
+ reply = self._deliver(test, options.attacker_cookie, auth)
197
+ verdict, reason = classify(signal, reply.status, reply.location, reply.body) if not reply.error else (INCONCLUSIVE, reply.error)
198
+ control: Optional[str] = None
199
+ if verdict == DENIED:
200
+ control, problem = self._owner_control(test, options, signal)
201
+ if problem:
202
+ verdict, reason = INCONCLUSIVE, problem
203
+ return TestResult(
204
+ test_case=test,
205
+ status_code=reply.status,
206
+ response_body=reply.body[:1000],
207
+ is_breach=verdict == ALLOWED,
208
+ is_secure=verdict == DENIED,
209
+ duration_ms=(time.time() - start_time) * 1000.0,
210
+ error_message=reason if verdict == INCONCLUSIVE else None,
211
+ owner_control=control,
212
+ )
121
213
 
122
214
  def execute_test(self, test: TestCase) -> TestResult:
123
215
  """Execute a single test case against the target API."""
216
+ if self.session is not None or test.csrf is not None or test.body_format == "form":
217
+ return self._execute_session_test(test)
218
+
124
219
  url = f"{self.base_url}{test.path}"
125
220
  headers = dict(test.headers)
126
221
 
@@ -183,6 +278,8 @@ def generate_crud_test_cases(
183
278
  name_prefix: str = "Resource",
184
279
  attacker_token: Optional[str] = None,
185
280
  sample_payload: Optional[Dict[str, Any]] = None,
281
+ body_format: str = "json",
282
+ csrf: Optional[CsrfConfig] = None,
186
283
  ) -> List[TestCase]:
187
284
  """Generate the standard 3-operation (GET, PUT, DELETE) cross-user test cases."""
188
285
  return [
@@ -200,6 +297,8 @@ def generate_crud_test_cases(
200
297
  resource_id=resource_id,
201
298
  attacker_token=attacker_token,
202
299
  payload=sample_payload or {"status": "tampered_by_attacker"},
300
+ body_format=body_format,
301
+ csrf=csrf,
203
302
  ),
204
303
  TestCase(
205
304
  name=f"{name_prefix} - Cross-User Destruction (DELETE)",
@@ -207,11 +306,16 @@ def generate_crud_test_cases(
207
306
  method="DELETE",
208
307
  resource_id=resource_id,
209
308
  attacker_token=attacker_token,
309
+ csrf=csrf,
210
310
  ),
211
311
  ]
212
312
 
213
313
 
214
- def load_suite_from_file(file_path: str) -> List[TestCase]:
314
+ def load_suite_from_file(
315
+ file_path: str,
316
+ default_body_format: str = "json",
317
+ default_csrf: Optional[CsrfConfig] = None,
318
+ ) -> List[TestCase]:
215
319
  """Load test cases from a JSON configuration file."""
216
320
  with open(file_path, "r", encoding="utf-8") as f:
217
321
  data = json.load(f)
@@ -220,6 +324,9 @@ def load_suite_from_file(file_path: str) -> List[TestCase]:
220
324
  items = data if isinstance(data, list) else data.get("tests", [])
221
325
 
222
326
  for item in items:
327
+ where = f"test '{item.get('name', item.get('path'))}'"
328
+ body_format = parse_body_format(item.get("body_format"), where, default_body_format)
329
+ csrf = parse_csrf(item.get("csrf"), where, default_csrf)
223
330
  # If item specifies auto_crud, generate 3 CRUD operations
224
331
  if item.get("auto_crud"):
225
332
  cases = generate_crud_test_cases(
@@ -228,6 +335,8 @@ def load_suite_from_file(file_path: str) -> List[TestCase]:
228
335
  name_prefix=item.get("name", item["path"]),
229
336
  attacker_token=item.get("attacker_token"),
230
337
  sample_payload=item.get("payload"),
338
+ body_format=body_format,
339
+ csrf=csrf,
231
340
  )
232
341
  test_cases.extend(cases)
233
342
  else:
@@ -242,17 +351,70 @@ def load_suite_from_file(file_path: str) -> List[TestCase]:
242
351
  headers=item.get("headers"),
243
352
  expected_status=item.get("expected_status"),
244
353
  insecure_status=item.get("insecure_status"),
354
+ body_format=body_format,
355
+ csrf=csrf,
245
356
  )
246
357
  )
247
358
  return test_cases
248
359
 
249
360
 
361
+ def _has_id_placeholder(path: str) -> bool:
362
+ return "{id}" in path or ":id" in path or bool(_ID_PLACEHOLDER.search(path))
363
+
364
+
365
+ _WRITE_WORDS = {"update", "save", "delete", "remove", "create", "add", "insert", "submit", "regist", "register",
366
+ "cancel", "approve", "modify", "change"}
367
+
368
+
369
+ def _concrete_method(method: str, path: str) -> str:
370
+ """Routes declared in XML accept any verb; a write action needs POST to do anything."""
371
+ if method.strip().upper() != "ANY":
372
+ return method
373
+ action = path.split("?", 1)[0].rsplit("/", 1)[-1].rsplit(".", 1)[0]
374
+ words = {word.lower() for word in re.findall(r"[A-Z]?[a-z]+", action)}
375
+ return "POST" if words & _WRITE_WORDS else "GET"
376
+
377
+
378
+ def _uses_form(path: str, body_format: str) -> bool:
379
+ return body_format == "form" or (body_format == "auto" and is_form_action(path))
380
+
381
+
382
+ def _suggested_form(attack_vector: Dict[str, Any], resource_id: str) -> Dict[str, Any]:
383
+ """Form fields from attack_vector.suggested_payload ('id={id}&status=x'), if it has that shape."""
384
+ raw = attack_vector.get("suggested_payload")
385
+ if not isinstance(raw, str) or "=" not in raw:
386
+ return {}
387
+ return dict(urllib.parse.parse_qsl(raw.replace("{id}", resource_id), keep_blank_values=True))
388
+
389
+
390
+ def _form_action_request(
391
+ path: str,
392
+ method: str,
393
+ attack_vector: Dict[str, Any],
394
+ resource_id: str,
395
+ ) -> Tuple[str, Optional[Dict[str, Any]]]:
396
+ """A legacy action takes the id as a parameter, not as a path segment."""
397
+ param = str(attack_vector.get("target_param") or "id")
398
+ in_path = _has_id_placeholder(path)
399
+ if method in ("POST", "PUT", "PATCH"):
400
+ payload: Dict[str, Any] = _suggested_form(attack_vector, resource_id) or dict(_TAMPER_PAYLOAD)
401
+ if not in_path:
402
+ payload.setdefault(param, resource_id)
403
+ return path, payload
404
+ if in_path:
405
+ return path, None
406
+ separator = "&" if "?" in path else "?"
407
+ return f"{path}{separator}{param}={urllib.parse.quote(resource_id, safe='')}", None
408
+
409
+
250
410
  def generate_test_cases_from_findings(
251
411
  scan_data_or_path: Union[str, Path, Dict[str, Any], List[Dict[str, Any]]],
252
412
  default_resource_id: str = "12345",
253
413
  attacker_token: Optional[str] = None,
254
414
  min_consensus_score: float = 0.0,
255
415
  include_likely: bool = True,
416
+ body_format: str = "auto",
417
+ csrf: Optional[CsrfConfig] = None,
256
418
  ) -> List[TestCase]:
257
419
  """Synthesize active BOLA verification test cases from LSA findings.json or SARIF data.
258
420
 
@@ -298,6 +460,7 @@ def generate_test_cases_from_findings(
298
460
  ep = item.get("endpoint") or {}
299
461
  path = av.get("path") or ep.get("path")
300
462
  method = av.get("method") or ep.get("method") or "GET"
463
+ method = _concrete_method(method, av.get("path") or ep.get("path") or "")
301
464
 
302
465
  # Fallback: attempt to find path in evidence or title
303
466
  if not path:
@@ -311,7 +474,7 @@ def generate_test_cases_from_findings(
311
474
 
312
475
  # Determine resource_id to inject
313
476
  target_id = default_resource_id
314
- if "{id}" not in path and ":id" not in path and not re.search(r"\{[a-zA-Z0-9_-]*(?:id|ID)[a-zA-Z0-9_-]*\}", path):
477
+ if not _has_id_placeholder(path):
315
478
  if not path.endswith("/"):
316
479
  target_path = f"{path}/{target_id}"
317
480
  else:
@@ -324,19 +487,25 @@ def generate_test_cases_from_findings(
324
487
  for m in methods:
325
488
  payload = None
326
489
  if m in ("POST", "PUT", "PATCH"):
327
- payload = {"status": "tampered_by_attacker", "note": "Komagu BOLA exploit verification"}
490
+ payload = dict(_TAMPER_PAYLOAD)
491
+ is_form = _uses_form(path, body_format)
492
+ case_path = target_path
493
+ if is_form:
494
+ case_path, payload = _form_action_request(path, m, av, target_id)
328
495
 
329
496
  tc_name = f"[{fid}] {item.get('title', target_path)} ({m})"
330
497
  test_cases.append(
331
498
  TestCase(
332
499
  name=tc_name,
333
- path=target_path,
500
+ path=case_path,
334
501
  method=m,
335
502
  resource_id=target_id,
336
503
  attacker_token=attacker_token,
337
504
  payload=payload,
338
505
  finding_id=fid,
339
506
  consensus_score=c_score,
507
+ body_format="form" if is_form else "json",
508
+ csrf=csrf,
340
509
  )
341
510
  )
342
511
 
@@ -360,7 +529,7 @@ def generate_test_cases_from_findings(
360
529
  # Try to extract path and method from SARIF properties or message
361
530
  ep = props.get("endpoint") or {}
362
531
  path = ep.get("path")
363
- method = ep.get("method") or "GET"
532
+ method = _concrete_method(ep.get("method") or "GET", path or "")
364
533
 
365
534
  if not path:
366
535
  m = re.search(r"(?:GET|POST|PUT|DELETE|PATCH)?\s*(/[a-zA-Z0-9_\-\./\{\}:]+)", msg)
@@ -371,11 +540,18 @@ def generate_test_cases_from_findings(
371
540
  continue
372
541
 
373
542
  target_id = default_resource_id
374
- if "{id}" not in path and ":id" not in path and not re.search(r"\{[a-zA-Z0-9_-]*(?:id|ID)[a-zA-Z0-9_-]*\}", path):
543
+ if not _has_id_placeholder(path):
375
544
  target_path = f"{path}/{target_id}" if not path.endswith("/") else f"{path}{target_id}"
376
545
  else:
377
546
  target_path = path
378
547
 
548
+ is_form = _uses_form(path, body_format)
549
+ sarif_payload: Optional[Dict[str, Any]] = None
550
+ if is_form:
551
+ target_path, sarif_payload = _form_action_request(
552
+ path, str(method).upper(), props.get("attack_vector") or {}, target_id
553
+ )
554
+
379
555
  fid = props.get("finding_id") or result.get("ruleId", "BOLA")
380
556
  tc_name = f"[{fid}] SARIF {result.get('ruleId', 'BOLA')} ({method})"
381
557
  test_cases.append(
@@ -385,8 +561,11 @@ def generate_test_cases_from_findings(
385
561
  method=method,
386
562
  resource_id=target_id,
387
563
  attacker_token=attacker_token,
564
+ payload=sarif_payload,
388
565
  finding_id=fid,
389
566
  consensus_score=float(props.get("consensus_score", 1.0)) if "consensus_score" in props else None,
567
+ body_format="form" if is_form else "json",
568
+ csrf=csrf,
390
569
  )
391
570
  )
392
571
 
@@ -426,6 +605,10 @@ def format_report(results: List[TestResult], base_url: str) -> Tuple[str, bool]:
426
605
  f"[PASS - SECURE] {t.method} {t.path} -> "
427
606
  f"HTTP {res.status_code} ({res.duration_ms:.1f}ms)"
428
607
  )
608
+ if res.owner_control == "skipped_write":
609
+ lines.append(" Note: owner control skipped for this write; the route was not proven to work.")
610
+ elif res.owner_control == "not_run":
611
+ lines.append(" Note: no owner session; the route and resource ID were not proven to work.")
429
612
  else:
430
613
  error_count += 1
431
614
  lines.append(
@@ -0,0 +1,180 @@
1
+ """Operator configuration for testing session-based legacy apps.
2
+
3
+ Cookie values are read from environment variables or files only. There is
4
+ deliberately no flag that takes a cookie value, and no message repeats one.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import os
11
+ import stat
12
+ from dataclasses import dataclass, field
13
+ from typing import List, Mapping, Optional
14
+
15
+ from operate.cross_user_tester.session_http import (
16
+ DEFAULT_DENY_STATUS,
17
+ STRICT_DENY_STATUS,
18
+ CsrfConfig,
19
+ DenialSignal,
20
+ SessionConfigError,
21
+ SessionRequest,
22
+ fetch_page,
23
+ )
24
+
25
+ __all__ = ["SessionConfigError", "SessionOptions", "read_cookie"]
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class SessionOptions:
30
+ attacker_cookie: Optional[str] = field(default=None, repr=False)
31
+ owner_cookie: Optional[str] = field(default=None, repr=False)
32
+ denial: Optional[DenialSignal] = None
33
+ owner_control_writes: bool = False
34
+ owner_csrf_page: Optional[str] = None
35
+ strict: bool = False
36
+ session_check: Optional[str] = None
37
+ session_marker: Optional[str] = None
38
+
39
+
40
+ def _validated(value: str, label: str) -> str:
41
+ cookie = value.strip()
42
+ if not cookie:
43
+ raise SessionConfigError(f"{label}: the session cookie is empty")
44
+ if "=" not in cookie or not cookie.isascii() or any(ord(c) < 32 or ord(c) == 127 for c in cookie):
45
+ raise SessionConfigError(f"{label}: the session cookie must be one line such as NAME=VALUE; NAME2=VALUE2")
46
+ return cookie
47
+
48
+
49
+ def read_cookie(env_name: Optional[str], file_path: Optional[str], env: Mapping[str, str], label: str) -> Optional[str]:
50
+ """Read a persona's Cookie header value. None when no source is configured."""
51
+ if env_name and file_path:
52
+ raise SessionConfigError(f"{label}: use either a cookie environment variable or a cookie file, not both")
53
+ if env_name:
54
+ if not env.get(env_name):
55
+ raise SessionConfigError(f"{label}: environment variable {env_name} is not set")
56
+ return _validated(env[env_name], label)
57
+ if file_path:
58
+ try:
59
+ with open(file_path, "r", encoding="utf-8") as handle:
60
+ return _validated(handle.read(), label)
61
+ except (OSError, UnicodeDecodeError) as err:
62
+ raise SessionConfigError(f"{label}: cannot read the cookie file ({type(err).__name__})") from err
63
+ return None
64
+
65
+
66
+ def cookie_file_is_open(file_path: Optional[str]) -> bool:
67
+ """True when group or others can read a cookie file."""
68
+ if not file_path or os.name != "posix":
69
+ return False
70
+ try:
71
+ return bool(stat.S_IMODE(os.stat(file_path).st_mode) & 0o077)
72
+ except OSError:
73
+ return False
74
+
75
+
76
+ def add_session_arguments(parser: argparse.ArgumentParser) -> None:
77
+ """Flags for session-based apps. None of them takes a secret value."""
78
+ group = parser.add_argument_group("session-based apps (JSP / Struts / old Spring MVC)")
79
+ group.add_argument("--attacker-cookie-env", metavar="VAR", help="Environment variable holding the attacker's Cookie value, e.g. JSESSIONID=...")
80
+ group.add_argument("--attacker-cookie-file", metavar="PATH", help="File holding the attacker's Cookie value")
81
+ group.add_argument("--owner-cookie-env", metavar="VAR", help="Environment variable holding the resource owner's Cookie value")
82
+ group.add_argument("--owner-cookie-file", metavar="PATH", help="File holding the resource owner's Cookie value")
83
+ group.add_argument("--body-format", choices=["auto", "json", "form"], default="auto",
84
+ help="Request body encoding. auto: form for --from-scan routes ending in .do, .action, .jsp or .jspx; otherwise JSON")
85
+ group.add_argument("--csrf-page", metavar="PATH", help="Page the attacker can open that contains a CSRF or Struts token")
86
+ group.add_argument("--csrf-field", metavar="NAME", help="Token field name, e.g. org.apache.struts.taglib.html.TOKEN")
87
+ group.add_argument("--deny-status", type=int, action="append", metavar="CODE", help="Status that means refused (repeatable)")
88
+ group.add_argument("--deny-redirect", action="append", metavar="TEXT", help="Redirect target containing TEXT means refused (repeatable)")
89
+ group.add_argument("--deny-marker", action="append", metavar="TEXT", help="2xx body containing TEXT means refused (repeatable)")
90
+ group.add_argument("--allow-redirect", action="append", metavar="TEXT", help="Redirect target containing TEXT means the request succeeded (repeatable)")
91
+ group.add_argument("--allow-marker", action="append", metavar="TEXT", help="If set, a 2xx body must contain TEXT to count as served data (repeatable)")
92
+ group.add_argument("--session-check", metavar="PATH", help="Page every session persona can open; checked before and after the run")
93
+ group.add_argument("--session-marker", metavar="TEXT", help="Text that --session-check page must contain")
94
+ group.add_argument("--owner-control-writes", action="store_true",
95
+ help="Also replay writes as the owner after a denial. This changes the owner's data. Required for --strict to pass a refused write")
96
+ group.add_argument("--owner-csrf-page", metavar="PATH", help="Token page for the owner's replayed writes (default: --csrf-page)")
97
+
98
+
99
+ def _signal(args: argparse.Namespace) -> DenialSignal:
100
+ fallback = STRICT_DENY_STATUS if args.strict else DEFAULT_DENY_STATUS
101
+ texts = (args.deny_redirect or []) + (args.deny_marker or []) + (args.allow_redirect or []) + (args.allow_marker or [])
102
+ if not all(texts):
103
+ raise SessionConfigError("--deny-redirect, --deny-marker, --allow-redirect and --allow-marker cannot be empty")
104
+ return DenialSignal(
105
+ deny_status=tuple(args.deny_status or fallback),
106
+ deny_redirects=tuple(args.deny_redirect or ()),
107
+ deny_markers=tuple(args.deny_marker or ()),
108
+ allow_redirects=tuple(args.allow_redirect or ()),
109
+ allow_markers=tuple(args.allow_marker or ()),
110
+ )
111
+
112
+
113
+ def declared_denial(args: argparse.Namespace) -> bool:
114
+ return bool(args.deny_status or args.deny_redirect or args.deny_marker)
115
+
116
+
117
+ def strict_gaps(args: argparse.Namespace, cookie_attacker: bool, cookie_owner: bool) -> List[str]:
118
+ """What --strict still needs to give a trustworthy verdict on a session app."""
119
+ gaps: List[str] = []
120
+ if not declared_denial(args):
121
+ gaps.append("a denial signal (--deny-status, --deny-redirect or --deny-marker)")
122
+ if not args.session_check:
123
+ gaps.append("--session-check, so an expired session is not read as a refusal")
124
+ if cookie_attacker and not cookie_owner:
125
+ gaps.append("the owner's cookie, so a missing record is not read as a refusal")
126
+ return gaps
127
+
128
+
129
+ def build_session_options(args: argparse.Namespace, env: Mapping[str, str]) -> Optional[SessionOptions]:
130
+ """Options for this run, or None when it is an ordinary Bearer run."""
131
+ attacker = read_cookie(args.attacker_cookie_env, args.attacker_cookie_file, env, "attacker")
132
+ owner = read_cookie(args.owner_cookie_env, args.owner_cookie_file, env, "owner")
133
+ if not (attacker or owner or declared_denial(args) or args.allow_redirect or args.allow_marker or args.session_check):
134
+ return None
135
+ if args.strict and attacker:
136
+ gaps = strict_gaps(args, True, bool(owner))
137
+ if gaps:
138
+ raise SessionConfigError("--strict on a session app needs " + "; ".join(gaps))
139
+ return SessionOptions(
140
+ attacker_cookie=attacker,
141
+ owner_cookie=owner,
142
+ denial=_signal(args),
143
+ owner_control_writes=args.owner_control_writes,
144
+ owner_csrf_page=args.owner_csrf_page,
145
+ strict=args.strict,
146
+ session_check=args.session_check,
147
+ session_marker=args.session_marker,
148
+ )
149
+
150
+
151
+ def global_csrf(args: argparse.Namespace) -> Optional[CsrfConfig]:
152
+ if bool(args.csrf_page) != bool(args.csrf_field):
153
+ raise SessionConfigError("--csrf-page and --csrf-field must be given together")
154
+ if not args.csrf_page:
155
+ return None
156
+ if not args.csrf_page.startswith("/"):
157
+ raise SessionConfigError("--csrf-page must start with '/'")
158
+ return CsrfConfig(page=args.csrf_page, field=args.csrf_field)
159
+
160
+
161
+ def check_session(
162
+ base_url: str,
163
+ label: str,
164
+ cookie: str,
165
+ path: str,
166
+ marker: Optional[str],
167
+ denial: Optional[DenialSignal],
168
+ timeout: float = 10.0,
169
+ ) -> Optional[str]:
170
+ """Return why this session is unusable, or None when it is alive."""
171
+ reply = fetch_page(base_url.rstrip("/"), path, SessionRequest("GET", path, cookie=cookie), timeout)
172
+ if reply.error:
173
+ return f"{label}: session check failed ({reply.error})"
174
+ if not 200 <= reply.status < 300:
175
+ return f"{label}: session check {path} returned HTTP {reply.status}; the session is expired or invalid"
176
+ if denial and any(m in reply.body for m in denial.deny_markers):
177
+ return f"{label}: session check {path} shows a denial page"
178
+ if marker and marker not in reply.body:
179
+ return f"{label}: session check {path} does not contain the expected text"
180
+ return None