@hoanghainh/komagu 2.5.0 → 2.6.1

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 (225) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/assess/lsa/examples/eval-run-6/gptoss-a/ai-run-stats.json +87 -0
  3. package/assess/lsa/examples/eval-run-6/gptoss-a/auth-model.md +120 -0
  4. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/index.json +60 -0
  5. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/injection-001.json +383 -0
  6. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/injection-002.json +205 -0
  7. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/privilege-001.json +838 -0
  8. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/privilege-002.json +124 -0
  9. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-001.json +778 -0
  10. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-002.json +546 -0
  11. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-003.json +661 -0
  12. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-004.json +659 -0
  13. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-005.json +612 -0
  14. package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-006.json +106 -0
  15. package/assess/lsa/examples/eval-run-6/gptoss-a/coverage.json +19 -0
  16. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-001.json +516 -0
  17. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-002.json +332 -0
  18. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-003.json +218 -0
  19. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-004.json +561 -0
  20. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-005.json +316 -0
  21. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-006.json +14 -0
  22. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/inj-injection-001.json +782 -0
  23. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/inj-injection-002.json +418 -0
  24. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/priv-privilege-001.json +627 -0
  25. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/priv-privilege-002.json +253 -0
  26. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-1.json +68 -0
  27. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-10.json +68 -0
  28. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-11.json +61 -0
  29. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-12.json +60 -0
  30. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-13.json +60 -0
  31. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-14.json +66 -0
  32. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-15.json +69 -0
  33. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-16.json +60 -0
  34. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-17.json +61 -0
  35. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-18.json +69 -0
  36. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-19.json +64 -0
  37. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-2.json +60 -0
  38. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-20.json +60 -0
  39. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-21.json +61 -0
  40. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-22.json +50 -0
  41. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-3.json +60 -0
  42. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-4.json +60 -0
  43. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-5.json +60 -0
  44. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-6.json +70 -0
  45. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-7.json +60 -0
  46. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-8.json +60 -0
  47. package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-9.json +60 -0
  48. package/assess/lsa/examples/eval-run-6/gptoss-a/findings.json +4857 -0
  49. package/assess/lsa/examples/eval-run-6/gptoss-a/mass-assignment.candidates.json +232 -0
  50. package/assess/lsa/examples/eval-run-6/gptoss-a/report.ja.md +2112 -0
  51. package/assess/lsa/examples/eval-run-6/gptoss-a/report.md +2112 -0
  52. package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-001.json +425 -0
  53. package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-002.json +333 -0
  54. package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-003.json +400 -0
  55. package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-004.json +403 -0
  56. package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-005.json +398 -0
  57. package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-006.json +63 -0
  58. package/assess/lsa/examples/eval-run-6/gptoss-a/score.json +82 -0
  59. package/assess/lsa/examples/eval-run-6/gptoss-a/stack-profile.json +225 -0
  60. package/assess/lsa/examples/eval-run-6/gptoss-b/ai-run-stats.json +91 -0
  61. package/assess/lsa/examples/eval-run-6/gptoss-b/auth-model.md +120 -0
  62. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/index.json +60 -0
  63. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/injection-001.json +383 -0
  64. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/injection-002.json +205 -0
  65. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/privilege-001.json +838 -0
  66. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/privilege-002.json +124 -0
  67. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-001.json +778 -0
  68. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-002.json +546 -0
  69. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-003.json +661 -0
  70. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-004.json +659 -0
  71. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-005.json +612 -0
  72. package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-006.json +106 -0
  73. package/assess/lsa/examples/eval-run-6/gptoss-b/coverage.json +21 -0
  74. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-001.json +593 -0
  75. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-002.json +367 -0
  76. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-003.json +207 -0
  77. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-004.json +477 -0
  78. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-005.json +341 -0
  79. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-006.json +14 -0
  80. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/inj-injection-001.json +821 -0
  81. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/inj-injection-002.json +410 -0
  82. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/priv-privilege-001.json +649 -0
  83. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/priv-privilege-002.json +222 -0
  84. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-1.json +60 -0
  85. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-10.json +67 -0
  86. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-11.json +68 -0
  87. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-12.json +60 -0
  88. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-13.json +60 -0
  89. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-14.json +64 -0
  90. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-15.json +70 -0
  91. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-16.json +60 -0
  92. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-17.json +61 -0
  93. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-18.json +60 -0
  94. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-19.json +65 -0
  95. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-2.json +60 -0
  96. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-20.json +60 -0
  97. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-21.json +62 -0
  98. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-22.json +50 -0
  99. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-3.json +60 -0
  100. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-4.json +60 -0
  101. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-5.json +60 -0
  102. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-6.json +60 -0
  103. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-7.json +64 -0
  104. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-8.json +60 -0
  105. package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-9.json +60 -0
  106. package/assess/lsa/examples/eval-run-6/gptoss-b/findings.json +4966 -0
  107. package/assess/lsa/examples/eval-run-6/gptoss-b/mass-assignment.candidates.json +232 -0
  108. package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-001.json +425 -0
  109. package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-002.json +333 -0
  110. package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-003.json +400 -0
  111. package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-004.json +403 -0
  112. package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-005.json +398 -0
  113. package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-006.json +63 -0
  114. package/assess/lsa/examples/eval-run-6/gptoss-b/score.json +87 -0
  115. package/assess/lsa/examples/eval-run-6/gptoss-b/stack-profile.json +225 -0
  116. package/assess/lsa/examples/eval-run-6/ground-truth.json +851 -0
  117. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/ai-run-stats.json +96 -0
  118. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/auth-model.md +191 -0
  119. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/index.json +60 -0
  120. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/injection-001.json +383 -0
  121. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/injection-002.json +205 -0
  122. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/privilege-001.json +838 -0
  123. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/privilege-002.json +124 -0
  124. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-001.json +778 -0
  125. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-002.json +546 -0
  126. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-003.json +661 -0
  127. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-004.json +659 -0
  128. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-005.json +612 -0
  129. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-006.json +106 -0
  130. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/coverage.json +31 -0
  131. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-001.json +527 -0
  132. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-002.json +568 -0
  133. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-003.json +467 -0
  134. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-004.json +342 -0
  135. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-005.json +693 -0
  136. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-006.json +16 -0
  137. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/inj-injection-001.json +1123 -0
  138. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/inj-injection-002.json +391 -0
  139. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/priv-privilege-001.json +726 -0
  140. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/priv-privilege-002.json +306 -0
  141. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-1.json +137 -0
  142. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-10.json +106 -0
  143. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-11.json +81 -0
  144. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-12.json +135 -0
  145. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-13.json +135 -0
  146. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-14.json +135 -0
  147. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-15.json +110 -0
  148. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-16.json +116 -0
  149. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-17.json +141 -0
  150. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-18.json +135 -0
  151. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-19.json +140 -0
  152. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-2.json +135 -0
  153. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-20.json +82 -0
  154. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-21.json +74 -0
  155. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-22.json +99 -0
  156. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-3.json +140 -0
  157. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-4.json +145 -0
  158. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-5.json +135 -0
  159. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-6.json +135 -0
  160. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-7.json +87 -0
  161. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-8.json +125 -0
  162. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-9.json +135 -0
  163. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings.json +6989 -0
  164. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/mass-assignment.candidates.json +232 -0
  165. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/memory.log +36 -0
  166. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-001.json +425 -0
  167. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-002.json +333 -0
  168. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-003.json +400 -0
  169. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-004.json +403 -0
  170. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-005.json +398 -0
  171. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-006.json +63 -0
  172. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/score.json +106 -0
  173. package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/stack-profile.json +225 -0
  174. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/auth-model.md +517 -0
  175. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-001.json +5 -0
  176. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-002.json +428 -0
  177. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-003.json +388 -0
  178. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-004.json +463 -0
  179. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-005.json +5 -0
  180. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-006.json +67 -0
  181. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/inj-injection-001.json +964 -0
  182. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings.partial.json +1872 -0
  183. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/memory.log +194 -0
  184. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/score.partial.json +108 -0
  185. package/assess/lsa/examples/eval-run-6/qwen-16k-partial/stack-profile.json +225 -0
  186. package/assess/lsa/scripts/laravel_routes.py +102 -0
  187. package/assess/lsa/scripts/map_routes.py +4 -1
  188. package/assess/lsa/tests/EVAL-RESULTS.md +126 -0
  189. package/assess/lsa/tests/test_eval_run_6.py +104 -0
  190. package/assess/lsa/tests/test_laravel_routes.py +95 -0
  191. package/examples/member-portal/backend-dotnet/Auth/DemoAuthenticationHandler.cs +75 -0
  192. package/examples/member-portal/backend-dotnet/Program.cs +1 -72
  193. package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/Application.kt +4 -0
  194. package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/controller/Controllers.kt +3 -0
  195. package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/model/Entities.kt +4 -0
  196. package/examples/member-portal/tests/test_d6_demo_app.py +4 -4
  197. package/examples/member-portal/tests/test_d6_remediation_verification.py +6 -6
  198. package/operate/ai/client.py +27 -10
  199. package/operate/ai/runner.py +4 -2
  200. package/operate/cli/__init__.py +1 -1
  201. package/operate/cli/commands/scan.py +8 -1
  202. package/operate/cross_user_tester/runner.py +4 -1
  203. package/operate/devsecops/sbom.py +12 -4
  204. package/operate/simulate/scenarios.py +11 -2
  205. package/operate/tests/test_ai_reasoning_effort.py +65 -0
  206. package/operate/tests/test_ai_route_inventory.py +53 -0
  207. package/operate/tests/test_audit_minimum_versions.py +89 -0
  208. package/operate/tests/test_cli.py +1 -1
  209. package/operate/tests/test_scheme_and_xml_guards.py +67 -0
  210. package/package.json +1 -1
  211. package/remediate/authz/dotnet/src/Komagu.Security.Authorization/LoggingSecurityAuditSink.cs +19 -3
  212. package/remediate/authz/golang/authz_test.go +5 -2
  213. package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/DefaultKomaguSecurityManager.kt +74 -78
  214. package/remediate/authz/spring/src/main/kotlin/io/komagu/security/spring/OwnershipAspect.kt +129 -158
  215. package/remediate/deps/patcher.py +2 -1
  216. package/remediate/edge/mock-backend/app.py +2 -1
  217. package/remediate/edge/tests/test_edge_security.py +5 -5
  218. package/remediate/ekyc/requirements.txt +1 -1
  219. package/remediate/identity/dotnet/src/Komagu.Security.Identity/OidcServiceCollectionExtensions.cs +19 -6
  220. package/remediate/identity/python/requirements.txt +2 -2
  221. package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/KomaguIdentityAutoConfiguration.kt +31 -38
  222. package/remediate/legacy_shims/tests/test_legacy_shims.py +8 -8
  223. package/remediate/query/dotnet/src/Komagu.Security.Query/SafeSortAllowList.cs +5 -2
  224. package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/SafeSortAllowList.kt +10 -11
  225. package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/detekt/StringBuiltSqlQueryRule.kt +19 -24
@@ -9,9 +9,13 @@ import json
9
9
  import re
10
10
  import time
11
11
  import urllib.error
12
+ import urllib.parse
12
13
  import urllib.request
13
14
  from typing import Any, Dict, List, Optional, Tuple
14
15
 
16
+ # OpenAI-compatible `reasoning_effort` values; Ollama's /v1 also accepts "none" to turn thinking off (#382)
17
+ REASONING_EFFORTS = ("none", "low", "medium", "high")
18
+
15
19
 
16
20
  class LLMClient:
17
21
  """Standard-library client for OpenAI-compatible /chat/completions endpoints."""
@@ -22,7 +26,12 @@ class LLMClient:
22
26
  model: str = "llama3.1",
23
27
  api_key: Optional[str] = None,
24
28
  timeout: int = 120,
29
+ reasoning_effort: Optional[str] = None,
25
30
  ) -> None:
31
+ if reasoning_effort is not None and reasoning_effort not in REASONING_EFFORTS:
32
+ raise ValueError(f"reasoning_effort must be one of {', '.join(REASONING_EFFORTS)}")
33
+ if urllib.parse.urlparse(endpoint).scheme.lower() not in ("http", "https"):
34
+ raise ValueError("LLM endpoint must be an http:// or https:// URL")
26
35
  raw_endpoint = endpoint.rstrip("/")
27
36
  if not raw_endpoint.endswith("/chat/completions"):
28
37
  self.endpoint = f"{raw_endpoint}/chat/completions"
@@ -31,6 +40,7 @@ class LLMClient:
31
40
  self.model = model
32
41
  self.api_key = api_key or "ollama"
33
42
  self.timeout = timeout
43
+ self.reasoning_effort = reasoning_effort
34
44
  # Running totals across chat() calls, read by the runner for ai-run-stats.json
35
45
  self.usage: Dict[str, float] = {"calls": 0, "prompt_tokens": 0, "completion_tokens": 0, "seconds": 0.0}
36
46
 
@@ -41,15 +51,7 @@ class LLMClient:
41
51
  max_tokens: Optional[int] = None,
42
52
  ) -> str:
43
53
  """Send a chat completion request and return the assistant response content."""
44
- payload: Dict[str, Any] = {
45
- "model": self.model,
46
- "messages": messages,
47
- "temperature": temperature,
48
- }
49
- if max_tokens:
50
- payload["max_tokens"] = max_tokens
51
-
52
- data = json.dumps(payload).encode("utf-8")
54
+ data = json.dumps(self.build_payload(messages, temperature, max_tokens)).encode("utf-8")
53
55
  headers = {
54
56
  "Content-Type": "application/json",
55
57
  "Authorization": f"Bearer {self.api_key}",
@@ -59,7 +61,8 @@ class LLMClient:
59
61
  req = urllib.request.Request(self.endpoint, data=data, headers=headers, method="POST")
60
62
  started = time.monotonic()
61
63
  try:
62
- with urllib.request.urlopen(req, timeout=self.timeout) as resp:
64
+ # The endpoint scheme is checked to be http(s) in __init__.
65
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp: # nosec B310
63
66
  body = resp.read().decode("utf-8")
64
67
  res_json = json.loads(body)
65
68
  self._record_usage(res_json.get("usage") or {}, time.monotonic() - started)
@@ -74,6 +77,20 @@ class LLMClient:
74
77
  except urllib.error.URLError as e:
75
78
  raise RuntimeError(f"Failed to connect to LLM endpoint at {self.endpoint}: {e.reason}") from e
76
79
 
80
+ def build_payload(
81
+ self,
82
+ messages: List[Dict[str, str]],
83
+ temperature: float = 0.1,
84
+ max_tokens: Optional[int] = None,
85
+ ) -> Dict[str, Any]:
86
+ """Request body for /chat/completions; `reasoning_effort` is sent only when it was set."""
87
+ payload: Dict[str, Any] = {"model": self.model, "messages": messages, "temperature": temperature}
88
+ if max_tokens:
89
+ payload["max_tokens"] = max_tokens
90
+ if self.reasoning_effort:
91
+ payload["reasoning_effort"] = self.reasoning_effort
92
+ return payload
93
+
77
94
  def _record_usage(self, usage: Dict[str, Any], seconds: float) -> None:
78
95
  self.usage = {
79
96
  "calls": self.usage["calls"] + 1,
@@ -167,6 +167,7 @@ class HeadlessAIRunner:
167
167
  stats = {
168
168
  "model": getattr(self.client, "model", None),
169
169
  "endpoint": getattr(self.client, "endpoint", None),
170
+ "reasoning_effort": getattr(self.client, "reasoning_effort", None),
170
171
  "phases": self.phase_stats,
171
172
  "totals": self._usage(),
172
173
  "parse_failures": self.parse_failures,
@@ -300,8 +301,9 @@ Output MUST be Markdown format adhering to Komagu auth-model.md specification.
300
301
  {"name": p.get("name", "id"), "source": p.get("source", "path")}
301
302
  for p in c.get("id_params", [])
302
303
  ]
303
- if not id_inputs:
304
- id_inputs = [{"name": "id", "source": "path"}]
304
+ # No detected id input stays empty: inventing a path `id` made the BOLA auditor
305
+ # flag routes that take no object id (#380)
306
+ id_inputs = id_inputs or []
305
307
 
306
308
  mapped_routes.append({
307
309
  "route_id": route_id,
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "2.5.0" # x-release-please-version
5
+ __version__ = "2.6.1" # x-release-please-version
@@ -161,6 +161,12 @@ Examples:
161
161
  default=120,
162
162
  help="Timeout in seconds per LLM request (default: 120)",
163
163
  )
164
+ parser.add_argument(
165
+ "--ai-reasoning-effort",
166
+ choices=("none", "low", "medium", "high"),
167
+ default=None,
168
+ help="Send reasoning_effort with each LLM request; 'none' turns thinking off on Ollama (default: not sent)",
169
+ )
164
170
  parser.set_defaults(func=execute_scan)
165
171
 
166
172
 
@@ -284,7 +290,8 @@ def execute_scan(args: argparse.Namespace) -> int:
284
290
  step_info(msg)
285
291
 
286
292
  try:
287
- client = LLMClient(endpoint=endpoint, model=model, api_key=api_key, timeout=timeout)
293
+ client = LLMClient(endpoint=endpoint, model=model, api_key=api_key, timeout=timeout,
294
+ reasoning_effort=getattr(args, "ai_reasoning_effort", None))
288
295
  runner = HeadlessAIRunner(target_path, out_dir, client, step_callback=_scan_callback)
289
296
  runner.run()
290
297
  except Exception as e:
@@ -242,7 +242,10 @@ class CrossUserTestRunner:
242
242
  error_message = None
243
243
 
244
244
  try:
245
- with urllib.request.urlopen(req, timeout=self.timeout) as resp:
245
+ if urllib.parse.urlparse(url).scheme.lower() not in ("http", "https"):
246
+ raise ValueError("base URL must be an http:// or https:// URL")
247
+ # The scheme is checked just above.
248
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp: # nosec B310
246
249
  status_code = resp.status
247
250
  raw = resp.read()
248
251
  response_body = raw.decode("utf-8", errors="replace")[:1000]
@@ -24,6 +24,16 @@ import xml.etree.ElementTree as ET
24
24
  from typing import Any, Dict, List, Optional
25
25
 
26
26
 
27
+ def _parse_xml_without_dtd(file_path: str) -> ET.Element:
28
+ """Parse a manifest from the scanned project, refusing DTDs (entity expansion)."""
29
+ with open(file_path, "r", encoding="utf-8", errors="replace") as handle:
30
+ text = handle.read()
31
+ if "<!DOCTYPE" in text.upper() or "<!ENTITY" in text.upper():
32
+ raise ValueError(f"{file_path}: XML with a DTD is not parsed")
33
+ # DTDs are refused above, so no entity can be declared or expanded.
34
+ return ET.fromstring(text) # nosec B314
35
+
36
+
27
37
  class Component:
28
38
  """Represents a software package or component in an SBOM."""
29
39
 
@@ -195,8 +205,7 @@ class SBOMGenerator:
195
205
  def _parse_csproj(self, file_path: str) -> List[Component]:
196
206
  components = []
197
207
  try:
198
- tree = ET.parse(file_path)
199
- root = tree.getroot()
208
+ root = _parse_xml_without_dtd(file_path)
200
209
  for pkg in root.findall(".//PackageReference"):
201
210
  name = pkg.get("Include") or pkg.get("Update")
202
211
  version = pkg.get("Version")
@@ -213,8 +222,7 @@ class SBOMGenerator:
213
222
  def _parse_pom_xml(self, file_path: str) -> List[Component]:
214
223
  components = []
215
224
  try:
216
- tree = ET.parse(file_path)
217
- root = tree.getroot()
225
+ root = _parse_xml_without_dtd(file_path)
218
226
  ns = {"m": "http://maven.apache.org/POM/4.0.0"}
219
227
  # Search both with namespace and without
220
228
  deps = root.findall(".//m:dependency", ns) or root.findall(".//dependency")
@@ -66,6 +66,11 @@ class ScenarioResult:
66
66
  }
67
67
 
68
68
 
69
+ def _require_http(target: str) -> None:
70
+ if urllib.parse.urlparse(target).scheme.lower() not in ("http", "https"):
71
+ raise ValueError("target must be an http:// or https:// URL")
72
+
73
+
69
74
  class BaseScenario:
70
75
  """Base class for BAS attack scenarios."""
71
76
 
@@ -101,6 +106,7 @@ class TimesCarBreachScenario(BaseScenario):
101
106
  sample_ids: Optional[List[str]] = None,
102
107
  timeout: float = 10.0,
103
108
  ) -> None:
109
+ _require_http(target)
104
110
  super().__init__(target, timeout)
105
111
  self.endpoint_pattern = endpoint_pattern
106
112
  self.attacker_token = attacker_token
@@ -145,7 +151,8 @@ class TimesCarBreachScenario(BaseScenario):
145
151
  response_sample = ""
146
152
 
147
153
  try:
148
- with urllib.request.urlopen(req, timeout=self.timeout) as resp:
154
+ # self.target is checked to be http(s) in __init__.
155
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp: # nosec B310
149
156
  status_code = resp.status
150
157
  body = resp.read().decode("utf-8", errors="replace")[:200]
151
158
  response_sample = body
@@ -218,6 +225,7 @@ class MetabaseZeroDayScenario(BaseScenario):
218
225
  target_param: str = "q",
219
226
  timeout: float = 10.0,
220
227
  ) -> None:
228
+ _require_http(target)
221
229
  super().__init__(target, timeout)
222
230
  self.query_path = query_path
223
231
  self.target_param = target_param
@@ -269,7 +277,8 @@ class MetabaseZeroDayScenario(BaseScenario):
269
277
  response_sample = ""
270
278
 
271
279
  try:
272
- with urllib.request.urlopen(req, timeout=self.timeout) as resp:
280
+ # self.target is checked to be http(s) in __init__.
281
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp: # nosec B310
273
282
  status_code = resp.status
274
283
  body = resp.read().decode("utf-8", errors="replace")[:300]
275
284
  response_sample = body
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python3
2
+ """`--ai-reasoning-effort` reaches each /chat/completions request and ai-run-stats.json (#382)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import shutil
9
+ import tempfile
10
+ import unittest
11
+ from pathlib import Path
12
+
13
+ from operate.ai.client import LLMClient
14
+ from operate.ai.runner import HeadlessAIRunner
15
+ from operate.cli.commands.scan import register_subcommand
16
+
17
+ MESSAGES = [{"role": "user", "content": "audit"}]
18
+
19
+
20
+ class TestReasoningEffortPayload(unittest.TestCase):
21
+ def test_not_sent_by_default(self) -> None:
22
+ payload = LLMClient(model="m").build_payload(MESSAGES)
23
+ self.assertEqual(payload, {"model": "m", "messages": MESSAGES, "temperature": 0.1})
24
+
25
+ def test_sent_when_set(self) -> None:
26
+ payload = LLMClient(model="m", reasoning_effort="none").build_payload(MESSAGES, max_tokens=50)
27
+ self.assertEqual(payload["reasoning_effort"], "none")
28
+ self.assertEqual(payload["max_tokens"], 50)
29
+
30
+ def test_unknown_value_is_rejected(self) -> None:
31
+ with self.assertRaises(ValueError):
32
+ LLMClient(reasoning_effort="off")
33
+
34
+
35
+ class TestReasoningEffortCli(unittest.TestCase):
36
+ def parse(self, *argv: str) -> argparse.Namespace:
37
+ parser = argparse.ArgumentParser()
38
+ register_subcommand(parser.add_subparsers(dest="command"))
39
+ return parser.parse_args(["scan", ".", *argv])
40
+
41
+ def test_flag_is_optional(self) -> None:
42
+ self.assertIsNone(self.parse().ai_reasoning_effort)
43
+ self.assertEqual(self.parse("--ai-reasoning-effort", "none").ai_reasoning_effort, "none")
44
+
45
+ def test_flag_rejects_unknown_values(self) -> None:
46
+ with self.assertRaises(SystemExit):
47
+ self.parse("--ai-reasoning-effort", "off")
48
+
49
+
50
+ class TestReasoningEffortStats(unittest.TestCase):
51
+ def setUp(self) -> None:
52
+ self.tmp = Path(tempfile.mkdtemp(prefix="komagu_effort_"))
53
+
54
+ def tearDown(self) -> None:
55
+ shutil.rmtree(self.tmp, ignore_errors=True)
56
+
57
+ def test_value_is_recorded(self) -> None:
58
+ runner = HeadlessAIRunner(self.tmp, self.tmp / "out", LLMClient(model="m", reasoning_effort="low"))
59
+ (self.tmp / "out").mkdir(parents=True, exist_ok=True)
60
+ stats = json.loads(runner.write_run_stats().read_text(encoding="utf-8"))
61
+ self.assertEqual(stats["reasoning_effort"], "low")
62
+
63
+
64
+ if __name__ == "__main__":
65
+ unittest.main()
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env python3
2
+ """Route inventory built by the headless runner from Tier 1 route batches (#380)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import shutil
8
+ import tempfile
9
+ import unittest
10
+ from pathlib import Path
11
+
12
+ from operate.ai.runner import HeadlessAIRunner
13
+
14
+
15
+ class _NoCallClient:
16
+ model = "none"
17
+ endpoint = "test://"
18
+
19
+ def chat(self, messages, temperature=0.1): # noqa: ANN001
20
+ raise AssertionError("route inventory must not call the model")
21
+
22
+
23
+ class TestRouteInventory(unittest.TestCase):
24
+ def setUp(self) -> None:
25
+ self.tmp = Path(tempfile.mkdtemp(prefix="komagu_inventory_"))
26
+ self.out = self.tmp / "out"
27
+ (self.out / "batches").mkdir(parents=True)
28
+
29
+ def tearDown(self) -> None:
30
+ shutil.rmtree(self.tmp, ignore_errors=True)
31
+
32
+ def inventory(self, candidates: list) -> list:
33
+ (self.out / "batches" / "routes-001.json").write_text(json.dumps({"candidates": candidates}), encoding="utf-8")
34
+ HeadlessAIRunner(self.tmp, self.out, _NoCallClient()).map_routes_inventory()
35
+ return json.loads((self.out / "routes" / "routes-001.json").read_text(encoding="utf-8"))["routes"]
36
+
37
+ def test_route_without_id_params_gets_no_invented_id(self) -> None:
38
+ routes = self.inventory([{"route_id": "R-0001", "method": ["POST"], "path": "/register",
39
+ "handler": {"file": "app/Http/Controllers/AccountController.php", "line": 11},
40
+ "id_params": []}])
41
+ self.assertEqual(routes[0]["id_inputs"], [])
42
+
43
+ def test_detected_id_params_are_kept(self) -> None:
44
+ routes = self.inventory([{"route_id": "R-0001", "method": ["GET"], "path": "/orders/{id}",
45
+ "handler": {"file": "routes/web.php", "line": 3},
46
+ "id_params": [{"name": "id", "source": "path", "is_id_like": True},
47
+ {"name": "orderId", "source": "query"}]}])
48
+ self.assertEqual(routes[0]["id_inputs"], [{"name": "id", "source": "path"},
49
+ {"name": "orderId", "source": "query"}])
50
+
51
+
52
+ if __name__ == "__main__":
53
+ unittest.main()
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python3
2
+ """`.github/scripts/audit_minimum_versions.py`: audits the lowest version each requirement allows (#390)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import importlib.util
7
+ import os
8
+ import tempfile
9
+ import unittest
10
+ from pathlib import Path
11
+ from unittest import mock
12
+
13
+ SCRIPT = Path(__file__).resolve().parents[2] / ".github" / "scripts" / "audit_minimum_versions.py"
14
+ _spec = importlib.util.spec_from_file_location("audit_minimum_versions", SCRIPT)
15
+ amv = importlib.util.module_from_spec(_spec)
16
+ _spec.loader.exec_module(amv)
17
+
18
+
19
+ def _report(*vulns: tuple) -> dict:
20
+ deps = {}
21
+ for name, version, vuln_id, fixes in vulns:
22
+ deps.setdefault((name, version), []).append({"id": vuln_id, "fix_versions": fixes})
23
+ return {"dependencies": [{"name": n, "version": v, "vulns": vs} for (n, v), vs in deps.items()]}
24
+
25
+
26
+ class TestMinimumPins(unittest.TestCase):
27
+ def test_lower_bounds_and_exact_pins_become_pins(self) -> None:
28
+ text = "# comment\npyjwt>=2.15.0\ncryptography[ssh] >= 50.0.0 # note\nrequests==2.32.3\n"
29
+ self.assertEqual(amv.minimum_pins(text), ["pyjwt==2.15.0", "cryptography==50.0.0", "requests==2.32.3"])
30
+
31
+ def test_unversioned_and_upper_bounds_are_ignored(self) -> None:
32
+ self.assertEqual(amv.minimum_pins("flask\nurllib3<3\n-r other.txt\n"), [])
33
+
34
+
35
+ class TestClassify(unittest.TestCase):
36
+ def test_fixable_and_unfixable_are_split_and_deduplicated(self) -> None:
37
+ report = _report(
38
+ ("pyjwt", "2.8.0", "PYSEC-1", ["2.12.0"]),
39
+ ("pyjwt", "2.8.0", "PYSEC-1", ["2.12.0"]),
40
+ ("pyjwt", "2.8.0", "PYSEC-2", []),
41
+ )
42
+ fixable, unfixable = amv.classify(report)
43
+ self.assertEqual(fixable, ["pyjwt 2.8.0: PYSEC-1 (fixed in 2.12.0)"])
44
+ self.assertEqual(unfixable, ["pyjwt 2.8.0: PYSEC-2 (no fixed version)"])
45
+
46
+
47
+ class TestMain(unittest.TestCase):
48
+ def setUp(self) -> None:
49
+ self.tmp = tempfile.mkdtemp(prefix="komagu_amv_")
50
+ self.req = os.path.join(self.tmp, "requirements.txt")
51
+ self.summary = os.path.join(self.tmp, "summary.md")
52
+ with open(self.req, "w", encoding="utf-8") as handle:
53
+ handle.write("pyjwt>=2.8.0\n")
54
+
55
+ def _run(self, report: dict) -> int:
56
+ with mock.patch.object(amv, "audit", return_value=report) as audit, \
57
+ mock.patch("builtins.print"):
58
+ code = amv.main(["--summary", self.summary, self.req])
59
+ audit.assert_called_once_with(["pyjwt==2.8.0"])
60
+ return code
61
+
62
+ def test_fixable_vulnerability_at_the_minimum_fails(self) -> None:
63
+ self.assertEqual(self._run(_report(("pyjwt", "2.8.0", "PYSEC-1", ["2.12.0"]))), 1)
64
+ with open(self.summary, encoding="utf-8") as handle:
65
+ self.assertIn("FAIL pyjwt 2.8.0: PYSEC-1", handle.read())
66
+
67
+ def test_vulnerability_without_a_fix_is_only_reported(self) -> None:
68
+ self.assertEqual(self._run(_report(("pyjwt", "2.8.0", "PYSEC-2", []))), 0)
69
+ with open(self.summary, encoding="utf-8") as handle:
70
+ self.assertIn("note pyjwt 2.8.0: PYSEC-2", handle.read())
71
+
72
+ def test_clean_minimum_passes(self) -> None:
73
+ self.assertEqual(self._run({"dependencies": [{"name": "pyjwt", "version": "2.8.0", "vulns": []}]}), 0)
74
+
75
+
76
+ class TestRepositoryMinimums(unittest.TestCase):
77
+ """Minimums raised in #390: these versions had fixable advisories."""
78
+
79
+ def test_raised_minimums(self) -> None:
80
+ root = SCRIPT.parents[2]
81
+ ekyc = amv.minimum_pins((root / "remediate/ekyc/requirements.txt").read_text(encoding="utf-8"))
82
+ identity = amv.minimum_pins((root / "remediate/identity/python/requirements.txt").read_text(encoding="utf-8"))
83
+ self.assertIn("cryptography==50.0.0", ekyc)
84
+ self.assertIn("cryptography==50.0.0", identity)
85
+ self.assertIn("pyjwt==2.15.0", identity)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ unittest.main()
@@ -61,7 +61,7 @@ class TestKomaguCLI(unittest.TestCase):
61
61
  def test_subcommand_parser_builds_cleanly(self) -> None:
62
62
  """In-process parser must parse subcommands without throwing."""
63
63
  parser = build_parser()
64
- args_scan = parser.parse_args(["scan", "/tmp", "--format", "html"])
64
+ args_scan = parser.parse_args(["scan", "./src", "--format", "html"])
65
65
  self.assertEqual(args_scan.subcommand, "scan")
66
66
  self.assertEqual(args_scan.format, "html")
67
67
 
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ """Guards behind the Bandit B310 / B314 suppressions (#385).
3
+
4
+ Each `# nosec` added for #385 relies on a check: outbound requests only use
5
+ http(s) URLs, and SBOM manifests with a DTD are not parsed.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import shutil
12
+ import tempfile
13
+ import unittest
14
+
15
+ from operate.ai.client import LLMClient
16
+ from operate.cross_user_tester.runner import CrossUserTestRunner, TestCase
17
+ from operate.devsecops.sbom import SBOMGenerator
18
+ from operate.simulate.scenarios import MetabaseZeroDayScenario, TimesCarBreachScenario
19
+
20
+ CSPROJ = '<Project Sdk="Microsoft.NET.Sdk"><ItemGroup><PackageReference Include="Dapper" Version="2.1.28" /></ItemGroup></Project>'
21
+ CSPROJ_WITH_DTD = (
22
+ '<?xml version="1.0"?>\n<!DOCTYPE Project [<!ENTITY a "aaaaaaaaaa">]>\n'
23
+ '<Project Sdk="Microsoft.NET.Sdk"><ItemGroup><PackageReference Include="&a;" Version="1.0.0" /></ItemGroup></Project>'
24
+ )
25
+
26
+
27
+ class TestHttpOnlyUrls(unittest.TestCase):
28
+ def test_llm_client_accepts_http_and_https(self) -> None:
29
+ self.assertEqual(LLMClient("http://127.0.0.1:11434/v1").endpoint, "http://127.0.0.1:11434/v1/chat/completions")
30
+ self.assertEqual(LLMClient("https://llm.example/v1/chat/completions").endpoint, "https://llm.example/v1/chat/completions")
31
+
32
+ def test_llm_client_rejects_other_schemes(self) -> None:
33
+ for endpoint in ("file:///etc/passwd", "ftp://host/v1", "127.0.0.1:11434/v1"):
34
+ with self.subTest(endpoint=endpoint), self.assertRaises(ValueError):
35
+ LLMClient(endpoint)
36
+
37
+ def test_scenarios_reject_other_schemes(self) -> None:
38
+ for scenario in (TimesCarBreachScenario, MetabaseZeroDayScenario):
39
+ with self.subTest(scenario=scenario.__name__), self.assertRaises(ValueError):
40
+ scenario("file:///etc")
41
+
42
+ def test_bearer_runner_does_not_open_other_schemes(self) -> None:
43
+ res = CrossUserTestRunner("file://").execute_test(TestCase("t", "/etc/passwd", "GET"))
44
+ self.assertFalse(res.is_secure or res.is_breach)
45
+ self.assertEqual(res.status_code, 0)
46
+ self.assertIn("http", res.error_message or "")
47
+
48
+
49
+ class TestSbomRefusesDtd(unittest.TestCase):
50
+ def setUp(self) -> None:
51
+ self.root = tempfile.mkdtemp(prefix="komagu_sbom_dtd_")
52
+ self.addCleanup(shutil.rmtree, self.root, True)
53
+
54
+ def _scan(self, content: str) -> list:
55
+ with open(os.path.join(self.root, "Service.csproj"), "w", encoding="utf-8") as handle:
56
+ handle.write(content)
57
+ return [c.name for c in SBOMGenerator(root_dir=self.root).scan()]
58
+
59
+ def test_plain_manifest_is_parsed(self) -> None:
60
+ self.assertIn("Dapper", self._scan(CSPROJ))
61
+
62
+ def test_manifest_with_dtd_is_skipped(self) -> None:
63
+ self.assertEqual(self._scan(CSPROJ_WITH_DTD), [])
64
+
65
+
66
+ if __name__ == "__main__":
67
+ unittest.main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoanghainh/komagu",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "Security-First Legacy Modernization Platform CLI (LSA Kit, IAM Perimeter, Cross-User Testing)",
5
5
  "main": "remediate/authz/nodejs/src/index.js",
6
6
  "types": "remediate/authz/nodejs/src/index.d.ts",
@@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging;
5
5
  /// <summary>
6
6
  /// Default security audit sink implementation logging structured events via ILogger.
7
7
  /// </summary>
8
- public sealed class LoggingSecurityAuditSink : ISecurityAuditSink
8
+ public sealed partial class LoggingSecurityAuditSink : ISecurityAuditSink
9
9
  {
10
10
  private readonly ILogger<LoggingSecurityAuditSink> _logger;
11
11
 
@@ -18,8 +18,8 @@ public sealed class LoggingSecurityAuditSink : ISecurityAuditSink
18
18
  {
19
19
  ArgumentNullException.ThrowIfNull(auditEvent);
20
20
 
21
- _logger.LogWarning(
22
- "[SECURITY AUDIT] {EventName} | TraceId: {TraceId} | Actor: {ActorUserId} | Tenant: {ActorTenantId} | Target: {ResourceType}/{ResourceId} | Op: {Operation} | IP: {ClientIp} | Reason: {Reason}",
21
+ LogAuditEvent(
22
+ _logger,
23
23
  auditEvent.EventName,
24
24
  auditEvent.TraceId ?? "none",
25
25
  auditEvent.ActorUserId ?? "anonymous",
@@ -32,4 +32,20 @@ public sealed class LoggingSecurityAuditSink : ISecurityAuditSink
32
32
 
33
33
  return ValueTask.CompletedTask;
34
34
  }
35
+
36
+ [LoggerMessage(
37
+ EventId = 1,
38
+ Level = LogLevel.Warning,
39
+ Message = "[SECURITY AUDIT] {EventName} | TraceId: {TraceId} | Actor: {ActorUserId} | Tenant: {ActorTenantId} | Target: {ResourceType}/{ResourceId} | Op: {Operation} | IP: {ClientIp} | Reason: {Reason}")]
40
+ private static partial void LogAuditEvent(
41
+ ILogger logger,
42
+ string eventName,
43
+ string traceId,
44
+ string actorUserId,
45
+ string actorTenantId,
46
+ string resourceType,
47
+ string resourceId,
48
+ ResourceOperation operation,
49
+ string clientIp,
50
+ string reason);
35
51
  }
@@ -1,6 +1,7 @@
1
1
  package golang
2
2
 
3
3
  import (
4
+ "errors"
4
5
  "net/http"
5
6
  "net/http/httptest"
6
7
  "testing"
@@ -67,7 +68,7 @@ func TestAuthorizer_AttackerDeniedWith404AndAudit(t *testing.T) {
67
68
  // Verify RequireOwnership returns ErrResourceNotFound
68
69
  sink.Events = nil // Reset sink
69
70
  err := auth.RequireOwnership(attackerClaims, order, OpRead)
70
- if err != ErrResourceNotFound {
71
+ if !errors.Is(err, ErrResourceNotFound) {
71
72
  t.Fatalf("expected ErrResourceNotFound, got: %v", err)
72
73
  }
73
74
 
@@ -129,7 +130,9 @@ func TestAuthorizer_HTTPMiddleware(t *testing.T) {
129
130
 
130
131
  handler := auth.Middleware(claimsExtractor, loader, OpRead)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
131
132
  w.WriteHeader(http.StatusOK)
132
- w.Write([]byte(`{"status":"ok"}`))
133
+ if _, err := w.Write([]byte(`{"status":"ok"}`)); err != nil {
134
+ t.Errorf("write response: %v", err)
135
+ }
133
136
  }))
134
137
 
135
138
  // 1. Owner request -> 200 OK