@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
@@ -12,6 +12,76 @@ from dataclasses import dataclass
12
12
  from pathlib import Path
13
13
  from typing import Any, Dict, List, Optional, Tuple
14
14
 
15
+ JAVA_LEGACY_SHIM = "remediate/legacy_shims/java/OwnershipEnforcer.java"
16
+
17
+ _SPRING_BUILD_FILES = ("pom.xml", "build.gradle", "build.gradle.kts", "gradle.properties")
18
+ _SPRING_BOOT_VERSION_PATTERNS = (
19
+ re.compile(r"spring-boot-(?:starter-parent|dependencies)</artifactId>\s*<version>\s*(\d+)\."),
20
+ re.compile(r"<spring-boot\.version>\s*(\d+)\."),
21
+ re.compile(r"org\.springframework\.boot['\"]\)?\s+version\s+['\"](\d+)\."),
22
+ re.compile(r"org\.springframework\.boot:spring-boot[\w-]*:(\d+)\."),
23
+ re.compile(r"springBootVersion\s*=\s*['\"]?(\d+)\."),
24
+ )
25
+ _XML_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
26
+ _SPRING_CONTROLLER = re.compile(r"^\s*@(?:Rest)?Controller\b", re.MULTILINE)
27
+ _SPRING_MAPPING_LINE = re.compile(r"^\s*@(?:Get|Post|Put|Patch|Delete|Request)Mapping\b")
28
+ _JAVA_METHOD_LINE = re.compile(r"^\s*(?:public|protected)\b[^;=]*\(")
29
+ _JAVA_DECLARATION_LINE = re.compile(r"^\s*(?:public|protected|private|abstract|final|class)\b")
30
+ _JAVA_TYPE_NAME = re.compile(r"[A-Z][A-Za-z0-9_]*")
31
+ _JAVA_LITERALS_AND_COMMENTS = re.compile(
32
+ r""""(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])'|//[^\n]*|/\*.*?\*/""", re.DOTALL
33
+ )
34
+ _JAVA_PACKAGE_STATEMENT = re.compile(r"^[ \t]*package\s+[\w.]+\s*;[ \t]*(?:\r\n|\r|\n)", re.MULTILINE)
35
+
36
+
37
+ def _newline_of(content: str) -> str:
38
+ """Return the line ending a generated line must use to match the surrounding file."""
39
+ if "\r\n" in content:
40
+ return "\r\n"
41
+ return "\r" if "\r" in content else "\n"
42
+
43
+
44
+ def _declared_spring_boot_major(build_file: Path) -> Optional[int]:
45
+ """Return the Spring Boot major version a build file declares, if it declares one."""
46
+ try:
47
+ text = _XML_COMMENT.sub("", build_file.read_text(encoding="utf-8", errors="replace"))
48
+ except OSError:
49
+ return None
50
+ for pattern in _SPRING_BOOT_VERSION_PATTERNS:
51
+ match = pattern.search(text)
52
+ if match:
53
+ return int(match.group(1))
54
+ return None
55
+
56
+
57
+ def _spring_boot_major(source_file: Path) -> Optional[int]:
58
+ """Find the Spring Boot major version from the nearest build file that declares one."""
59
+ for directory in Path(source_file).resolve().parents:
60
+ for name in _SPRING_BUILD_FILES:
61
+ major = _declared_spring_boot_major(directory / name)
62
+ if major is not None:
63
+ return major
64
+ if (directory / ".git").exists():
65
+ break
66
+ return None
67
+
68
+
69
+ def _is_java_handler_line(lines: List[str], idx: int) -> bool:
70
+ """True when an annotation inserted above lines[idx] would land on a controller method."""
71
+ if not (_SPRING_MAPPING_LINE.match(lines[idx]) or _JAVA_METHOD_LINE.match(lines[idx])):
72
+ return False
73
+ declaration = next((line for line in lines[idx:] if _JAVA_DECLARATION_LINE.match(line)), "")
74
+ return "(" in declaration and not re.search(r"\bclass\b", declaration)
75
+
76
+
77
+ def _java_uses_type(content: str, type_name: str) -> bool:
78
+ """True when code outside strings and comments references type_name, other than declaring it."""
79
+ if not _JAVA_TYPE_NAME.fullmatch(type_name):
80
+ return False
81
+ code = _JAVA_LITERALS_AND_COMMENTS.sub(" ", content)
82
+ code = re.sub(rf"\b(?:class|interface|enum)\s+{type_name}\b", " ", code)
83
+ return re.search(rf"\b{type_name}\b", code) is not None
84
+
15
85
 
16
86
  @dataclass
17
87
  class BOLARewriteResult:
@@ -84,43 +154,73 @@ class BOLAOwnershipPatcher:
84
154
 
85
155
  def ensure_imports(self, content: str, lang: str) -> Tuple[str, bool]:
86
156
  """Ensure necessary Komagu authorization import is present at the top of the file."""
157
+ newline = _newline_of(content)
87
158
  if lang in ("typescript", "javascript"):
88
159
  if "from '@hoanghainh/komagu'" in content or 'from "@hoanghainh/komagu"' in content:
89
160
  return content, False
90
- import_stmt = "import { RequireOwnership, OwnershipGuard, ResourceOperation } from '@hoanghainh/komagu';\n"
161
+ import_stmt = "import { RequireOwnership, OwnershipGuard, ResourceOperation } from '@hoanghainh/komagu';" + newline
91
162
  return import_stmt + content, True
92
163
 
93
164
  elif lang == "csharp":
94
165
  if "using Komagu.Security.Authorization;" in content:
95
166
  return content, False
96
- import_stmt = "using Komagu.Security.Authorization;\n"
167
+ import_stmt = "using Komagu.Security.Authorization;" + newline
97
168
  return import_stmt + content, True
98
169
 
99
170
  elif lang == "java":
100
- if "import io.komagu.security.authorization.RequireOwnership;" in content:
171
+ if "import io.komagu.security.spring.RequireOwnership;" in content:
101
172
  return content, False
102
- import_stmt = "import io.komagu.security.authorization.RequireOwnership;\nimport io.komagu.security.authorization.ResourceOperation;\n"
103
- if "package " in content:
104
- parts = content.split(";\n", 1)
105
- return parts[0] + ";\n\n" + import_stmt + parts[1], True
173
+ import_stmt = (
174
+ "import io.komagu.security.spring.RequireOwnership;" + newline
175
+ + "import io.komagu.security.authorization.ResourceOperation;" + newline
176
+ )
177
+ package = _JAVA_PACKAGE_STATEMENT.search(content)
178
+ if package:
179
+ return content[: package.end()] + newline + import_stmt + content[package.end():], True
106
180
  return import_stmt + content, True
107
181
 
108
182
  elif lang == "python":
109
183
  if "from remediate.authz.python.komagu_authz" in content:
110
184
  return content, False
111
- import_stmt = "from remediate.authz.python.komagu_authz import RequireOwnership, ResourceOperation\n"
185
+ import_stmt = "from remediate.authz.python.komagu_authz import RequireOwnership, ResourceOperation" + newline
112
186
  return import_stmt + content, True
113
187
 
114
188
  return content, False
115
189
 
190
+ def _java_manual_reason(
191
+ self, target_file: Path, content: str, lines: List[str], idx: int, resource: str
192
+ ) -> Optional[str]:
193
+ """Say why @RequireOwnership cannot be inserted here, or None when it can."""
194
+ if not _SPRING_CONTROLLER.search(content):
195
+ problem = "this file is not a Spring controller"
196
+ elif _spring_boot_major(target_file) != 3:
197
+ problem = "no Spring Boot 3 version was found in the build files"
198
+ elif not _is_java_handler_line(lines, idx):
199
+ problem = "the finding does not point at a controller method"
200
+ elif not _java_uses_type(content, resource):
201
+ problem = f"the entity class could not be confirmed from the route (inferred {resource!r})"
202
+ else:
203
+ return None
204
+ return (
205
+ f"@RequireOwnership needs a Spring Boot 3 controller method, but {problem}; requires manual "
206
+ f"remediation: call OwnershipEnforcer.requireOwnership(...) before the object is read or "
207
+ f"written (see {JAVA_LEGACY_SHIM})."
208
+ )
209
+
116
210
  def patch_file(
117
211
  self,
118
212
  target_file: Path,
119
213
  line_number: int,
120
214
  finding: Dict[str, Any],
215
+ content: Optional[str] = None,
121
216
  ) -> Tuple[str, BOLARewriteResult]:
122
- """Inject appropriate ownership guard above the method or route at line_number."""
123
- content = target_file.read_text(encoding="utf-8", errors="replace")
217
+ """Inject appropriate ownership guard above the method or route at line_number.
218
+
219
+ Pass the already-decoded `content` to keep the caller's encoding and pending edits.
220
+ Without it the file must be valid UTF-8: replacing bytes would corrupt the source.
221
+ """
222
+ if content is None:
223
+ content = Path(target_file).read_bytes().decode("utf-8")
124
224
  lines = content.splitlines(keepends=True)
125
225
  idx = line_number - 1
126
226
 
@@ -150,6 +250,7 @@ class BOLAOwnershipPatcher:
150
250
  explanation="Ownership guard already present at target line.",
151
251
  )
152
252
 
253
+ newline = _newline_of(content)
153
254
  guard_lines: List[str] = []
154
255
 
155
256
  if lang in ("typescript", "javascript"):
@@ -173,6 +274,14 @@ class BOLAOwnershipPatcher:
173
274
  guard_lines.append(f"{indent}[RequireOwnership(typeof({resource}), idRouteParameter: \"id\", operation: ResourceOperation.{cs_op})]\n")
174
275
 
175
276
  elif lang == "java":
277
+ manual_reason = self._java_manual_reason(target_file, content, lines, idx, resource)
278
+ if manual_reason:
279
+ return content, BOLARewriteResult(
280
+ original_line=target_line,
281
+ patched_line=target_line,
282
+ changed=False,
283
+ explanation=manual_reason,
284
+ )
176
285
  guard_lines.append(f"{indent}@RequireOwnership(entityClass = {resource}.class, operation = ResourceOperation.{operation})\n")
177
286
 
178
287
  elif lang == "python":
@@ -186,7 +295,7 @@ class BOLAOwnershipPatcher:
186
295
  explanation=f"Automated BOLA ownership guard not supported for language '{lang}'; requires manual remediation.",
187
296
  )
188
297
 
189
- guard_snippet = "".join(guard_lines)
298
+ guard_snippet = "".join(guard_lines).replace("\n", newline)
190
299
  lines.insert(idx, guard_snippet)
191
300
  patched_body = "".join(lines)
192
301
 
@@ -106,6 +106,9 @@ class TestBOLAOwnershipPatcher(unittest.TestCase):
106
106
 
107
107
  def test_patch_spring_boot_controller(self) -> None:
108
108
  """Spring Boot controller method must receive @RequireOwnership annotation and import."""
109
+ (self.test_dir / "build.gradle").write_text(
110
+ "plugins { id 'org.springframework.boot' version '3.2.1' }\n", encoding="utf-8"
111
+ )
109
112
  controller_file = self.test_dir / "OrderController.java"
110
113
  controller_file.write_text(
111
114
  "package com.example.demo;\n\n"
@@ -127,7 +130,7 @@ class TestBOLAOwnershipPatcher(unittest.TestCase):
127
130
 
128
131
  patched_content, res = self.patcher.patch_file(controller_file, 5, finding)
129
132
  self.assertTrue(res.changed)
130
- self.assertIn("import io.komagu.security.authorization.RequireOwnership;", patched_content)
133
+ self.assertIn("import io.komagu.security.spring.RequireOwnership;", patched_content)
131
134
  self.assertIn("@RequireOwnership(entityClass = Order.class, operation = ResourceOperation.READ)", patched_content)
132
135
 
133
136
  def test_patch_python_fastapi(self) -> None:
@@ -19,7 +19,7 @@
19
19
  `
20
20
  }
21
21
 
22
- reverse_proxy http://mock-backend:5000 {
22
+ reverse_proxy {$KOMAGU_UPSTREAM:http://mock-backend:5000} {
23
23
  header_up Host {upstream_hostport}
24
24
  header_up X-Real-IP {remote_host}
25
25
  }
@@ -7,3 +7,12 @@ RUN xcaddy build \
7
7
  FROM caddy:alpine
8
8
 
9
9
  COPY --from=builder /usr/bin/caddy /usr/bin/caddy
10
+
11
+ # Caddy stores runtime state under /data and /config; it listens on :8080, so no
12
+ # bind capability is needed
13
+ RUN addgroup -S caddy \
14
+ && adduser -S -G caddy caddy \
15
+ && mkdir -p /data/caddy /config/caddy \
16
+ && chown -R caddy:caddy /data /config
17
+
18
+ USER caddy
@@ -20,6 +20,18 @@ SecAction \
20
20
  t:none,\
21
21
  setvar:'tx.allowed_request_content_type=|application/x-www-form-urlencoded| |multipart/form-data| |text/xml| |application/xml| |application/soap+xml| |application/x-amf| |application/json| |application/cloudevents+json| |application/problem+json|'"
22
22
 
23
+ # Struts 1 token forms post a field named org.apache.struts.taglib.html.TOKEN, which CRS 944130 reads as a Java class.
24
+ # Only that field name is excluded, and the raw body of such a request; ARGS still carries every parsed field,
25
+ # so Java classes in any value or other field name still score.
26
+ SecRule ARGS_NAMES "@streq org.apache.struts.taglib.html.TOKEN" \
27
+ "id:200002,\
28
+ phase:2,\
29
+ pass,\
30
+ nolog,\
31
+ t:none,\
32
+ ctl:ruleRemoveTargetById=944130;ARGS_NAMES:org.apache.struts.taglib.html.TOKEN,\
33
+ ctl:ruleRemoveTargetById=944130;REQUEST_BODY"
34
+
23
35
  # Custom rule: Ensure SQL injection indicators in query string, URI, JSON body, or headers trigger HTTP 403
24
36
  # Tuned to prevent false positives on dashes, hash symbols, or benign words (e.g. ?q=a--b, ?name=C#)
25
37
  SecRule ARGS|REQUEST_URI|REQUEST_HEADERS "@rx (?i)(?:(?:\bunion\b[\s\S]*?\bselect\b)|(?:\b(?:or|and)\b\s+(?:(?:\d+\s*=\s*\d+)|(?:['\"][^'\"]+['\"]\s*=\s*['\"]?[^'\"\s]+)|true\b|false\b))|(?:;\s*(?:drop|alter|truncate|delete)\s+table\b)|(?:\-\-(?:[\s+\r\n\t]|$))|(?:\/\*[\s\S]*?\*\/)|(?:['\"`]\s*#|#\s+))" \
@@ -13,11 +13,15 @@ from pathlib import Path
13
13
  from typing import Any, Dict, List, Optional, Tuple, Union
14
14
 
15
15
 
16
+ SESSION_ROUTE_SUFFIXES = (".do", ".action", ".jsp", ".jspx")
17
+
18
+
16
19
  class VirtualPatcher:
17
20
  """Generates OWASP Coraza / ModSecurity SecRule virtual patches from LSA findings."""
18
21
 
19
22
  def __init__(self, start_rule_id: int = 1100001) -> None:
20
23
  self.current_rule_id = start_rule_id
24
+ self.skipped: List[str] = []
21
25
 
22
26
  def _sanitize_regex_path(self, path: str) -> str:
23
27
  """Convert a parameterized endpoint path like /api/orders/{id} into a regex."""
@@ -121,6 +125,20 @@ SecRule REQUEST_URI "@rx {path_regex}" \\
121
125
  """
122
126
  return rule
123
127
 
128
+ @staticmethod
129
+ def _finding_path(finding: Dict[str, Any]) -> Optional[str]:
130
+ return (finding.get("attack_vector") or {}).get("path") or (finding.get("endpoint") or {}).get("path")
131
+
132
+ @staticmethod
133
+ def _is_session_route(path: Optional[str]) -> bool:
134
+ """Legacy form actions sign users in with a session cookie, so no Authorization header is expected."""
135
+ return bool(path) and path.split("?", 1)[0].lower().endswith(SESSION_ROUTE_SUFFIXES)
136
+
137
+ def _skip(self, rules: List[str], fid: str, reason: str) -> None:
138
+ note = f"{fid}: no virtual patch, {reason}"
139
+ self.skipped.append(note)
140
+ rules.append(f"# {note}\n")
141
+
124
142
  def generate_patches_from_findings(
125
143
  self,
126
144
  findings_data: Union[Dict[str, Any], List[Dict[str, Any]]],
@@ -146,6 +164,9 @@ SecRule REQUEST_URI "@rx {path_regex}" \\
146
164
  continue
147
165
 
148
166
  if cat == "bola" or fid.startswith("BOLA"):
167
+ if self._is_session_route(self._finding_path(item)):
168
+ self._skip(rules, fid, "a WAF cannot check object ownership on a session-cookie route; fix it in code")
169
+ continue
149
170
  rule_text = self.generate_bola_rule(item)
150
171
  rules.append(rule_text)
151
172
  manifest.append({
@@ -155,6 +176,9 @@ SecRule REQUEST_URI "@rx {path_regex}" \\
155
176
  "endpoint": item.get("endpoint", {}).get("path", ""),
156
177
  })
157
178
  elif cat in ("sql_injection", "nosql_injection") or fid.startswith("INJ"):
179
+ if not self._finding_path(item):
180
+ self._skip(rules, fid, "the finding names no route; the general CRS and Komagu SQLi rules still apply")
181
+ continue
158
182
  rule_text = self.generate_sqli_rule(item)
159
183
  rules.append(rule_text)
160
184
  manifest.append({
@@ -13,9 +13,9 @@ services:
13
13
  KONG_ADMIN_LISTEN: 0.0.0.0:8001
14
14
  KOMAGU_JWT_SECRET: ${KOMAGU_JWT_SECRET:-komagu-default-test-jwt-secret-key-32chars}
15
15
  volumes:
16
- - ./kong/kong.yaml:/kong/declarative/kong.yaml:ro
16
+ - ${KOMAGU_KONG_CONFIG:-./kong/kong.yaml}:/kong/declarative/kong.yaml:ro
17
17
  ports:
18
- - "8000:8000" # Public API Gateway proxy port
18
+ - "${KOMAGU_EDGE_BIND:-0.0.0.0}:8000:8000" # Public API Gateway proxy port
19
19
  - "127.0.0.1:8001:8001" # Kong Admin API bound strictly to loopback interface on host
20
20
  depends_on:
21
21
  - coraza-waf
@@ -34,10 +34,15 @@ services:
34
34
  context: ./coraza
35
35
  image: komagu-coraza-caddy:local
36
36
  container_name: komagu-coraza-waf
37
+ environment:
38
+ KOMAGU_UPSTREAM: ${KOMAGU_UPSTREAM:-http://mock-backend:5000}
39
+ extra_hosts:
40
+ - "host.docker.internal:host-gateway"
37
41
  volumes:
38
42
  - ./coraza/Caddyfile:/etc/caddy/Caddyfile:ro
39
43
  - ./coraza/coraza.conf:/etc/coraza/coraza.conf:ro
40
44
  - ./coraza/rules:/etc/coraza/rules:ro
45
+ - ${KOMAGU_VIRTUAL_PATCH_FILE:-./coraza/rules/virtual-patches.conf}:/etc/coraza/rules/virtual-patches.conf:ro
41
46
  expose:
42
47
  - "8080"
43
48
  depends_on:
@@ -0,0 +1,51 @@
1
+ # Kong profile for a legacy application that signs users in with a session cookie (JSESSIONID) and form actions.
2
+ # Every path goes to Coraza, and rate limiting is per client IP because these requests carry no JWT.
3
+ # Use it with: komagu protect --target <url> --config remediate/edge/kong/kong-legacy-session.yaml --up
4
+ _format_version: "3.0"
5
+
6
+ services:
7
+ - name: komagu-legacy-app
8
+ url: http://coraza-waf:8080
9
+ connect_timeout: 5000
10
+ read_timeout: 30000
11
+ write_timeout: 30000
12
+ routes:
13
+ - name: legacy-all-paths
14
+ paths:
15
+ - /
16
+ strip_path: false
17
+ preserve_host: false
18
+ protocols:
19
+ - http
20
+ - https
21
+ plugins:
22
+ - name: rate-limiting
23
+ config:
24
+ minute: 120
25
+ second: 10
26
+ limit_by: ip
27
+ policy: redis
28
+ fault_tolerant: false
29
+ redis:
30
+ host: redis
31
+ port: 6379
32
+ timeout: 2000
33
+ database: 0
34
+ hide_client_headers: false
35
+ error_code: 429
36
+ error_message: "Rate limit exceeded. Please back off and retry."
37
+
38
+ plugins:
39
+ - name: correlation-id
40
+ config:
41
+ header_name: X-Correlation-ID
42
+ generator: uuid#counter
43
+ echo_downstream: true
44
+
45
+ - name: response-transformer
46
+ config:
47
+ remove:
48
+ headers:
49
+ - X-Kong-Upstream-Latency
50
+ - X-Kong-Proxy-Latency
51
+ - Server
@@ -5,4 +5,6 @@ COPY app.py .
5
5
 
6
6
  EXPOSE 5000
7
7
 
8
+ USER nobody
9
+
8
10
  CMD ["python", "-u", "app.py"]
@@ -380,9 +380,12 @@ class TestCorazaWafContainer(unittest.TestCase):
380
380
  _docker("rm", "-f", cls.container)
381
381
 
382
382
  @classmethod
383
- def _status(cls, path, body=None):
383
+ def _status(cls, path, body=None, form=None):
384
384
  headers = {"Content-Type": "application/json"} if body is not None else {}
385
385
  data = json.dumps(body).encode("utf-8") if body is not None else None
386
+ if form is not None:
387
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
388
+ data = form.encode("ascii")
386
389
  req = urllib.request.Request(f"http://127.0.0.1:{cls.port}{path}", data=data, headers=headers)
387
390
  try:
388
391
  with urllib.request.urlopen(req, timeout=5) as resp:
@@ -423,6 +426,23 @@ class TestCorazaWafContainer(unittest.TestCase):
423
426
  self.assertNotEqual(self._status("/api/v1/orders?q=salt+and+pepper=yes"), 403)
424
427
  self.assertNotEqual(self._status("/api/v1/orders?name=C%23"), 403)
425
428
 
429
+ def test_struts_token_field_name_not_blocked(self):
430
+ """CRS 944130 reads the Struts 1 token field name as a Java class, which blocked every token form (#374)."""
431
+ self._require_running()
432
+ self.assertNotEqual(self._status("/order/update.do?org.apache.struts.taglib.html.TOKEN=abc123"), 403)
433
+
434
+ def _post_form(self, body):
435
+ return self._status("/order/update.do", form=body)
436
+
437
+ def test_struts_token_form_post_not_blocked(self):
438
+ self._require_running()
439
+ self.assertNotEqual(self._post_form("org.apache.struts.taglib.html.TOKEN=abc123&id=1&note=hello"), 403)
440
+
441
+ def test_java_class_in_a_value_still_blocked(self):
442
+ self._require_running()
443
+ self.assertEqual(self._status("/order/update.do?note=java.lang.Runtime"), 403)
444
+ self.assertEqual(self._post_form("org.apache.struts.taglib.html.TOKEN=abc123&note=java.lang.Runtime"), 403)
445
+
426
446
 
427
447
  class TestCorazaWafSkipLogic(unittest.TestCase):
428
448
  """
@@ -148,6 +148,8 @@ resource "aws_s3_bucket_versioning" "logs_versioning" {
148
148
  }
149
149
  }
150
150
 
151
+ # S3 server access logging requires SSE-S3 on the destination bucket: AWS does
152
+ # not support SSE-KMS here, so a customer managed key cannot be used.
151
153
  resource "aws_s3_bucket_server_side_encryption_configuration" "logs_encryption" {
152
154
  provider = aws.secondary
153
155
  bucket = aws_s3_bucket.worm_access_logs.id
@@ -344,6 +346,19 @@ resource "aws_s3_bucket_policy" "deny_destructive_actions" {
344
346
  }
345
347
 
346
348
  # 10. Cross-Region Replication Infrastructure (Primary Tokyo -> Secondary Osaka)
349
+ resource "aws_kms_key" "primary_state_key" {
350
+ count = var.enable_cross_region_replication ? 1 : 0
351
+ provider = aws.primary
352
+ description = "KMS Key for Komagu primary state backups in Tokyo"
353
+ deletion_window_in_days = 30
354
+ enable_key_rotation = true
355
+
356
+ tags = {
357
+ Name = "${var.project_name}-primary-state-key"
358
+ Environment = var.environment
359
+ }
360
+ }
361
+
347
362
  resource "aws_s3_bucket" "primary_backup" {
348
363
  count = var.enable_cross_region_replication ? 1 : 0
349
364
  provider = aws.primary
@@ -382,8 +397,11 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "primary_encryptio
382
397
  bucket = aws_s3_bucket.primary_backup[0].id
383
398
 
384
399
  rule {
400
+ bucket_key_enabled = true
401
+
385
402
  apply_server_side_encryption_by_default {
386
- sse_algorithm = "AES256"
403
+ kms_master_key_id = aws_kms_key.primary_state_key[0].arn
404
+ sse_algorithm = "aws:kms"
387
405
  }
388
406
  }
389
407
  }
@@ -465,6 +483,16 @@ resource "aws_iam_policy" "replication_policy" {
465
483
  ]
466
484
  Effect = "Allow"
467
485
  Resource = "${aws_s3_bucket.immutable_worm_backup.arn}/*"
486
+ },
487
+ {
488
+ Action = ["kms:Decrypt"]
489
+ Effect = "Allow"
490
+ Resource = aws_kms_key.primary_state_key[0].arn
491
+ },
492
+ {
493
+ Action = ["kms:Encrypt", "kms:GenerateDataKey*"]
494
+ Effect = "Allow"
495
+ Resource = aws_kms_key.worm_backup_key.arn
468
496
  }
469
497
  ]
470
498
  })
@@ -486,9 +514,19 @@ resource "aws_s3_bucket_replication_configuration" "crr" {
486
514
  id = "ReplicateToOsakaWORM"
487
515
  status = "Enabled"
488
516
 
517
+ source_selection_criteria {
518
+ sse_kms_encrypted_objects {
519
+ status = "Enabled"
520
+ }
521
+ }
522
+
489
523
  destination {
490
524
  bucket = aws_s3_bucket.immutable_worm_backup.arn
491
525
  storage_class = "STANDARD"
526
+
527
+ encryption_configuration {
528
+ replica_kms_key_id = aws_kms_key.worm_backup_key.arn
529
+ }
492
530
  }
493
531
  }
494
532
 
@@ -64,7 +64,7 @@ resource "aws_subnet" "primary_public_1a" {
64
64
  vpc_id = aws_vpc.primary.id
65
65
  cidr_block = "10.100.1.0/24"
66
66
  availability_zone = "${var.primary_region}a"
67
- map_public_ip_on_launch = true
67
+ map_public_ip_on_launch = false # Internet-facing resources must request a public IP explicitly
68
68
 
69
69
  tags = {
70
70
  Name = "${var.project_name}-primary-public-1a"
@@ -76,7 +76,7 @@ resource "aws_subnet" "primary_public_1c" {
76
76
  vpc_id = aws_vpc.primary.id
77
77
  cidr_block = "10.100.2.0/24"
78
78
  availability_zone = "${var.primary_region}c"
79
- map_public_ip_on_launch = true
79
+ map_public_ip_on_launch = false # Internet-facing resources must request a public IP explicitly
80
80
 
81
81
  tags = {
82
82
  Name = "${var.project_name}-primary-public-1c"
@@ -102,10 +102,22 @@ resource "aws_security_group" "primary_default_sg" {
102
102
  description = "Default egress-only security group for primary workloads"
103
103
 
104
104
  egress {
105
+ description = "Workload traffic inside the primary VPC"
105
106
  from_port = 0
106
107
  to_port = 0
107
108
  protocol = "-1"
108
- cidr_blocks = ["0.0.0.0/0"]
109
+ cidr_blocks = [aws_vpc.primary.cidr_block]
110
+ }
111
+
112
+ dynamic "egress" {
113
+ for_each = length(var.workload_egress_cidrs) > 0 ? [1] : []
114
+ content {
115
+ description = "HTTPS to operator-approved external destinations"
116
+ from_port = 443
117
+ to_port = 443
118
+ protocol = "tcp"
119
+ cidr_blocks = var.workload_egress_cidrs
120
+ }
109
121
  }
110
122
 
111
123
  tags = {
@@ -35,3 +35,14 @@ variable "enable_cross_region_replication" {
35
35
  type = bool
36
36
  default = true
37
37
  }
38
+
39
+ variable "workload_egress_cidrs" {
40
+ description = "External CIDR ranges primary workloads may reach over HTTPS. Empty keeps egress inside the VPC."
41
+ type = list(string)
42
+ default = []
43
+
44
+ validation {
45
+ condition = !contains(var.workload_egress_cidrs, "0.0.0.0/0") && !contains(var.workload_egress_cidrs, "::/0")
46
+ error_message = "workload_egress_cidrs must list specific ranges, not 0.0.0.0/0 or ::/0."
47
+ }
48
+ }
@@ -66,6 +66,22 @@ class TestMultiRegionResilience(unittest.TestCase):
66
66
  self.assertIn("aws_route_table_association", content)
67
67
  self.assertIn("aws_security_group", content)
68
68
 
69
+ def test_03b_terraform_network_and_encryption_hardening(self) -> None:
70
+ """Verifies no automatic public IPs, no open egress and CMK-encrypted primary backups."""
71
+ tf_dir = os.path.join(MODULE_ROOT, "terraform")
72
+ with open(os.path.join(tf_dir, "main.tf"), "r", encoding="utf-8") as f:
73
+ main_tf = f.read()
74
+ with open(os.path.join(tf_dir, "backup_worm.tf"), "r", encoding="utf-8") as f:
75
+ worm_tf = f.read()
76
+
77
+ self.assertNotIn("map_public_ip_on_launch = true", main_tf)
78
+ sg_start = main_tf.index('resource "aws_security_group" "primary_default_sg"')
79
+ sg_block = main_tf[sg_start:main_tf.index("# 2. Secondary Standby DR VPC")]
80
+ self.assertNotIn("0.0.0.0/0", sg_block)
81
+
82
+ self.assertIn("aws_kms_key.primary_state_key[0].arn", worm_tf)
83
+ self.assertIn("replica_kms_key_id = aws_kms_key.worm_backup_key.arn", worm_tf)
84
+
69
85
  def test_04_dr_snapshot_creation_and_integrity(self) -> None:
70
86
  """Verifies immutable snapshot creation and cryptographic integrity check."""
71
87
  mgr = DRManager(secondary_region="ap-northeast-3")