@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
@@ -24,6 +24,7 @@ from java_routes import (build_class_index, collect_spring_beans, entry_method_l
24
24
  struts_modules, xml_declared)
25
25
  from lsa_common import (JAVA_WEB_ROOT, KIT_VERSION, call_args, compile_id_patterns, is_id_name, # noqa: E402
26
26
  iter_source_files, line_of, load_config, read_text, split_top, write_json)
27
+ from laravel_routes import resolve_laravel_handlers, route_action # noqa: E402
27
28
  from nextjs_routes import middleware_note, nextjs_declared # noqa: E402
28
29
  from privilege import annotate_privilege # noqa: E402
29
30
 
@@ -268,7 +269,7 @@ def php_declared(c: Collector, rel: str, text: str, stacks: set) -> bool:
268
269
  end = text.find(";", m.end())
269
270
  stmt = text[m.start(): end if end > 0 else m.end() + 200]
270
271
  c.add(stack="php-laravel", kind="declared_route", method=[verb.upper()], path="/" + path.lstrip("/"),
271
- handler={"file": rel, "line": line, "symbol": None}, id_params=c.path_params(path),
272
+ handler={"file": rel, "line": line, "symbol": route_action(stmt)}, id_params=c.path_params(path),
272
273
  guard_hints=c.guards(stmt, line) + c.guards(text[:m.start()][-3000:], 1)[:2],
273
274
  data_hints=[], notes="handler is in the controller named in this route")
274
275
  found = True
@@ -744,6 +745,8 @@ def map_routes(target: Path, config: Dict, stack_ids: Optional[set] = None, work
744
745
  it["id_params"] = c.inputs("java", t)
745
746
  it["guard_hints"] = it["guard_hints"] or c.guards(t)
746
747
  it["data_hints"] = it["data_hints"] or c.data_ops(t)
748
+ # post-pass 1b: Laravel route declarations -> the controller method they name (#380)
749
+ c.items = resolve_laravel_handlers(c.items, target, enc, c, [rel for _, rel in files])
747
750
  # post-pass 2: servlet classes without @WebServlet -> take the path from web.xml
748
751
  xml_paths: Dict[str, List[str]] = {}
749
752
  for it in c.items:
@@ -333,3 +333,129 @@ and their tests still score against them.
333
333
  Run 5 rescored against the new ground truth (for comparison only; findings unchanged): BOLA 28 / 29
334
334
  (run A) and 16 / 29 (run B), precision 0.859 and 0.826, still no false positive on SAFE markers. Every
335
335
  new marker was already reported by both runs; Tier 1 lists all of them as BOLA route candidates.
336
+
337
+ ---
338
+
339
+ ## Run 6 — 2026-11-11, platform 2.5.0, kit 0.3.1 (#323 part 4: new harness, 121-item ground truth, redaction)
340
+
341
+ Issue #323. First run on the harness from #334 (no code fields in 4a / 4b templates, quote repair,
342
+ findings from flagged verdicts), the fixtures and ground truth from #336 (121 items: 29 BOLA, 34
343
+ SQL/NoSQL injection, 10 BFLA, 15 mass assignment, 33 SAFE), and secret redaction of code sent to the
344
+ model (v2.4.0). Outputs are archived in [`assess/lsa/examples/eval-run-6/`](../examples/eval-run-6/).
345
+ Runs 4–5 are not directly comparable; the Run 5 rows below are the Run 5 findings rescored against the
346
+ new ground truth.
347
+
348
+ | Item | Value |
349
+ |---|---|
350
+ | Pipeline | `komagu scan <repo> --no-cache --headless-ai http://localhost:11434/v1 --model <model> --ai-timeout 3600` from `main` at v2.5.0 |
351
+ | Hosting | Ollama 0.40.16 on the same Apple M4 Pro, 24 GB RAM, 100% GPU, local only; `gpt-oss:20b` on the llama.cpp runner with 32,768 context (as in Runs 4–5) |
352
+ | Scoring | `tests/score_findings.py --verdicts` (tolerance 3 lines) |
353
+ | Machine | No other heavy work while measuring; up to 5.7 GB swap in use during the `gpt-oss` runs |
354
+
355
+ ### Results after verification (`gpt-oss:20b`)
356
+
357
+ | Run | BOLA | Injection | BFLA marker | Mass marker | BFLA verdict | Mass verdict | FP on safe code | Precision | Rejected | Retries · repaired · parse fail | From verdicts | Min · calls · tokens |
358
+ |---|---|---|---|---|---|---|---|---|---|---|---|---|
359
+ | Run 5 A, rescored | 28/29 | 34/34 | 10/10 | 10/15 | 10/10 | 15/15 | 0 | 0.859 | 2 | 0 · — · 0 | — | 83 · 32 · 328K |
360
+ | Run 5 B, rescored | 16/29 | 32/34 | 9/10 | 14/15 | 10/10 | 15/15 | 0 | 0.826 | 3 | 1 · — · 1 | — | 86 · 31 · 369K |
361
+ | **Run 6 A** | **26/29** | **34/34** | **10/10** | **14/15** | **10/10** | **15/15** | **1** | **0.813** | 2 | 0 · 0 · 0 | 9 (5 BOLA, 4 mass) | 40 · 33 · 315K |
362
+ | **Run 6 B** | **26/29** | **34/34** | **10/10** | **14/15** | **10/10** | **14/15** | **1** | **0.787** | 1 | 1 · 0 · 0 | 14 (5 BOLA, 9 mass) | 56 · 34 · 373K |
363
+
364
+ - **The two runs now agree.** BOLA is 26 / 29 in both (Run 5: 28 and 16). No batch was lost: no parse
365
+ failure, no call error, no batch needed the quote repair, and the one retry (B, `injection-001`)
366
+ succeeded. The `routes-001` batch that failed in Run 4 B and Run 5 B now parses.
367
+ - **Mass assignment:** 14 / 15 as located findings in both runs (Run 5: 10 and 14). Findings written from
368
+ flagged verdicts closed the gap: the four candidates run A of Run 5 judged `vulnerable` without a
369
+ finding (`profile.php` ×2, `CustomerController.cs` ×2) are findings in both runs.
370
+ `php-plain/register.php:4` is still missed. In A the verifier rejected it because the INSERT "only"
371
+ writes `email`, `name` and `role`, missing that `extract($_POST)` can overwrite `$role` (the
372
+ fixture change in #336); in B the auditor did not flag it.
373
+ - **BFLA:** 10 / 10 in both runs, located and by verdict.
374
+ - **Misses:** `perl-cgi/cgi-bin/order.cgi:19` (both runs, missed since Run 4), `node-express/routes/orders.js:29`
375
+ (both), `java-legacy/.../OrderServlet.java:15` (A), `python-flask/app.py:12` (B).
376
+ - **False positives on SAFE markers: 1 per run** (Run 5: 0), both injection:
377
+ - A: `classic-asp/shop/member.asp:6`. The `UPDATE` starts on line 6, whose `name` value is escaped;
378
+ the concatenated `member_id` is on line 7, which is also reported. The finding is anchored one line
379
+ early rather than describing a new flaw, but it is scored as a false positive.
380
+ - B: `python-flask/app.py:12`. `order_id` is an `<int:...>` path parameter, so the `%` formatting
381
+ cannot inject. The model reported the route's missing ownership check as SQL injection, so the
382
+ BOLA marker on the same line was missed.
383
+ - **Precision 0.81 / 0.79.** Five of the unmatched findings per run come from verdicts: the BOLA
384
+ auditor flagged the Laravel routes `/register`, `/posts`, `/teams`, `/account/rename` and
385
+ `/account/profile`, and the generated findings point at their declarations in `routes/web.php`
386
+ (lines 6–10). The triage after Run 5 found these routes act on `$request->user()` or touch no
387
+ record, so they are likely false positives. Without them precision would be 0.85 / 0.83.
388
+ - **Duration:** 40 and 56 minutes (Run 5: 83 and 86). The model wrote fewer tokens (84K and 118K
389
+ completion tokens against about 109K and 111K); the Ollama version also changed (0.40.13 → 0.40.16).
390
+ - **`ai-raw/`** is written only for batches that needed a repair or failed to parse. None did, so neither
391
+ run has an `ai-raw/` folder.
392
+
393
+ ### Did redaction reduce recall?
394
+
395
+ No, and on this repo it cannot. Running the redaction used for model context
396
+ (`assess/lsa/scripts/redaction.py`, `redact_lines`) over every file of the eval repo changes **0 lines**:
397
+ the fixtures contain no secrets in the patterns it covers. The prompts sent in Run 6 are therefore the
398
+ same code as without redaction. The BOLA difference from Run 5 A (28 → 26) is run-to-run variance (the
399
+ same model put 16 in Run 5 B). This also means the eval repo does not test whether redaction costs
400
+ recall on code that does contain secrets; that is covered only by the unit tests
401
+ (`operate/tests/test_ai_redaction.py`, `test_no_planted_secret_reaches_the_model`).
402
+
403
+ ### `qwen3.5:9b` with a 16K context (not completed)
404
+
405
+ To fit the 24 GB machine, `qwen3.5:9b` was run with `num_ctx 16384` (a local tag built
406
+ `FROM qwen3.5:9b` with `PARAMETER num_ctx 16384`, MLX runner), same command and timeout as above. It did
407
+ not finish: the run was stopped by hand after 3 h 14 min, while the second injection batch was still
408
+ running and before the privilege (4c) and verifier (5) phases. The partial output is archived as
409
+ [`eval-run-6/qwen-16k-partial/`](../examples/eval-run-6/qwen-16k-partial/) (raw batch findings, the
410
+ per-minute memory log, and an indicative score of the unverified findings).
411
+
412
+ | Phase / batch | Time | Result |
413
+ |---|---|---|
414
+ | Stack detection | 2 min | done |
415
+ | BOLA `routes-001` (15 routes) | 60 min | **timed out**, empty |
416
+ | BOLA `routes-002` … `routes-004` | 5, 3 and 2 min | 8, 7 and 9 findings |
417
+ | BOLA `routes-005` | 60 min | **timed out**, empty |
418
+ | BOLA `routes-006` | 1 min | 1 finding |
419
+ | Injection `injection-001` | 11 min | 20 findings |
420
+ | Injection `injection-002` | 49 min+ | still running when stopped |
421
+
422
+ - **Memory:** the runner used 8.1 GB for the first hour, then grew to 13, 18 and finally 20 GB (from about
423
+ 10:55, after the first timeout). Swap rose from about 4 GB (other work on the machine) to 12.4 GB of
424
+ 13.3 GB; the run was stopped there to keep the machine usable. `gpt-oss:20b` stayed under 5.7 GB of
425
+ swap on the same machine.
426
+ - **Timeouts:** a batch that hits `--ai-timeout` is not retried (only empty or unparseable replies are),
427
+ so two of six BOLA batches were lost outright. The quick batches suggest the model can do the task;
428
+ the timed-out ones look like long reasoning that does not end within the context.
429
+ - **Indicative score** (findings before verification, from the batches that finished): BOLA 12/29,
430
+ injection 22/34, BFLA and mass assignment not reached, precision 0.73, no finding on a SAFE marker.
431
+ These numbers are not comparable with the `gpt-oss` rows: two BOLA batches, half the injection
432
+ candidates and the verifier are missing.
433
+
434
+ Thinking looks like the cause: on this 24 GB machine a 16K context with default reasoning is **not**
435
+ a working setting. The run below with reasoning off is.
436
+
437
+ ### `qwen3.5:9b` with a 16K context and reasoning off (#382)
438
+
439
+ Same tag, command and timeout, plus `--ai-reasoning-effort none` (#382), which sends
440
+ `"reasoning_effort": "none"` with every request; Ollama's `/v1` endpoint ignores `think: false` but
441
+ honours this field. One run, archived as [`eval-run-6/qwen-16k-nothink/`](../examples/eval-run-6/qwen-16k-nothink/)
442
+ (with `ai-raw/` for the two repaired verifier replies and the per-minute memory log).
443
+
444
+ | Run | BOLA (29) | Injection (34) | BFLA (10) | Mass (15) | BFLA verdict | Mass verdict | SAFE hit | Precision | Time |
445
+ |---|---|---|---|---|---|---|---|---|---|
446
+ | `gpt-oss:20b` A | 26 | 34 | 10 | 14 | 10 | 15 | 1 | 0.813 | 40 min |
447
+ | `gpt-oss:20b` B | 26 | 34 | 10 | 14 | 10 | 14 | 1 | 0.787 | 56 min |
448
+ | `qwen3.5:9b` 16K, reasoning off | 23 | 29 | 6 | 10 | 8 | 15 | 0 | 0.734 | 35 min |
449
+
450
+ - **Completed:** all phases, 33 calls, no timeout, no parse failure or call error, no retry, all findings
451
+ anchored. Two verifier replies were repaired (`verified-1`, `verified-9`); 11 findings were generated
452
+ from flagged verdicts (2 BOLA, 2 BFLA, 7 mass assignment).
453
+ - **Recall** is lower than `gpt-oss:20b` in every category: BFLA and mass assignment are mostly found as
454
+ verdicts (8/10, 15/15) but placed on the right line less often (6/10, 10/15).
455
+ - **Precision 0.73:** no finding on a SAFE marker, but 25 unmatched findings (23 BOLA, 2 BFLA), not yet triaged;
456
+ none is on `php-laravel/routes/web.php`.
457
+ - **Memory and time:** the runner stayed at 14–17 GB and swap peaked at 6.0 GB (about 3.7 GB before the
458
+ run, from other work), against 20 GB and 12.4 GB with reasoning on. 35 minutes in all, of which the
459
+ verifier took 17.
460
+ - **Minimum to run it:** a 24 GB machine is enough with `num_ctx 16384` and `--ai-reasoning-effort none`.
461
+ With reasoning on it needs more; on this evidence at least 32 GB RAM.
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ """Keeps the archived Evaluation Run 6 numbers (Issue #323) in step with the scorer and EVAL-RESULTS.md."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import unittest
12
+ from pathlib import Path
13
+
14
+ TESTS_DIR = Path(__file__).resolve().parent
15
+ LSA_ROOT = TESTS_DIR.parent
16
+ EVAL_RUN_6_DIR = LSA_ROOT / "examples" / "eval-run-6"
17
+ GROUND_TRUTH = EVAL_RUN_6_DIR / "ground-truth.json"
18
+ RUNS = ("gptoss-a", "gptoss-b", "qwen-16k-nothink")
19
+ QWEN_PARTIAL = EVAL_RUN_6_DIR / "qwen-16k-partial"
20
+ # (BOLA, injection, BFLA, mass assignment) found by location, then BFLA / mass found by auditor verdict
21
+ EXPECTED = {
22
+ "gptoss-a": {"marker": (26, 34, 10, 14), "verdict": (10, 15)},
23
+ "gptoss-b": {"marker": (26, 34, 10, 14), "verdict": (10, 14)},
24
+ "qwen-16k-nothink": {"marker": (23, 29, 6, 10), "verdict": (8, 15)},
25
+ }
26
+ # Findings that land on a SAFE marker, by category (see EVAL-RESULTS.md, Run 6)
27
+ SAFE_HITS = {
28
+ "gptoss-a": {"injection": ["classic-asp/shop/member.asp:6"]},
29
+ "gptoss-b": {"injection": ["python-flask/app.py:12"]},
30
+ "qwen-16k-nothink": {},
31
+ }
32
+
33
+
34
+ def score(run: str) -> dict:
35
+ proc = subprocess.run(
36
+ [sys.executable, str(TESTS_DIR / "score_findings.py"), "--verdicts", str(GROUND_TRUTH), str(EVAL_RUN_6_DIR / run)],
37
+ capture_output=True, text=True, check=True,
38
+ )
39
+ return json.loads(proc.stdout)
40
+
41
+
42
+ class TestEvaluationRun6(unittest.TestCase):
43
+ def test_ground_truth_is_the_121_item_set(self) -> None:
44
+ items = json.loads(GROUND_TRUTH.read_text(encoding="utf-8"))["items"]
45
+ self.assertEqual(len(items), 121)
46
+
47
+ def test_artifacts_exist(self) -> None:
48
+ for run in RUNS:
49
+ for name in ("findings.json", "coverage.json", "score.json", "ai-run-stats.json"):
50
+ self.assertTrue((EVAL_RUN_6_DIR / run / name).is_file(), f"{run}/{name}")
51
+
52
+ def test_archived_scores_match_the_scorer(self) -> None:
53
+ for run in RUNS:
54
+ with self.subTest(run=run):
55
+ archived = json.loads((EVAL_RUN_6_DIR / run / "score.json").read_text(encoding="utf-8"))
56
+ self.assertEqual(score(run), archived)
57
+
58
+ def test_numbers_match_eval_results(self) -> None:
59
+ for run, want in EXPECTED.items():
60
+ with self.subTest(run=run):
61
+ report = score(run)
62
+ pc, vl = report["per_category"], report["verdict_level"]
63
+ got = tuple(pc[c]["found"] for c in ("bola", "injection", "bfla", "mass_assignment"))
64
+ self.assertEqual(got, want["marker"])
65
+ self.assertEqual((vl["bfla"]["found"], vl["mass_assignment"]["found"]), want["verdict"])
66
+
67
+ def test_safe_marker_hits_are_the_documented_ones(self) -> None:
68
+ for run in RUNS:
69
+ with self.subTest(run=run):
70
+ report = score(run)
71
+ for cat, row in report["per_category"].items():
72
+ self.assertEqual(row["false_positive_on_safe_code"], SAFE_HITS[run].get(cat, []), cat)
73
+ for cat, row in report["verdict_level"].items():
74
+ self.assertEqual(row["flagged_safe"], [], cat)
75
+
76
+ def test_no_batch_lost_to_parse_or_call_errors(self) -> None:
77
+ for run in RUNS:
78
+ with self.subTest(run=run):
79
+ stats = json.loads((EVAL_RUN_6_DIR / run / "ai-run-stats.json").read_text(encoding="utf-8"))
80
+ self.assertEqual(stats["parse_failures"], {})
81
+ self.assertEqual(stats["call_errors"], {})
82
+ self.assertEqual(stats["anchoring"]["unanchored"], [])
83
+
84
+ def test_qwen_partial_score_matches_eval_results(self) -> None:
85
+ """The stopped qwen3.5:9b 16K run: indicative score of its unverified findings (EVAL-RESULTS, Run 6)."""
86
+ with tempfile.TemporaryDirectory() as tmp:
87
+ shutil.copy(QWEN_PARTIAL / "findings.partial.json", Path(tmp) / "findings.json")
88
+ proc = subprocess.run(
89
+ [sys.executable, str(TESTS_DIR / "score_findings.py"), str(GROUND_TRUTH), tmp],
90
+ capture_output=True, text=True, check=True,
91
+ )
92
+ report = json.loads(proc.stdout)
93
+ archived = json.loads((QWEN_PARTIAL / "score.partial.json").read_text(encoding="utf-8"))
94
+ self.assertEqual(report, archived)
95
+ pc = report["per_category"]
96
+ got = tuple(pc[c]["found"] for c in ("bola", "injection", "bfla", "mass_assignment"))
97
+ self.assertEqual(got, (12, 22, 0, 0))
98
+ self.assertEqual(report["precision"], 0.733)
99
+ for row in pc.values():
100
+ self.assertEqual(row["false_positive_on_safe_code"], [])
101
+
102
+
103
+ if __name__ == "__main__":
104
+ unittest.main()
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ """Laravel routes map to the controller method they name, not to their declaration (#380)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import shutil
7
+ import sys
8
+ import tempfile
9
+ import unittest
10
+ from pathlib import Path
11
+
12
+ TESTS_DIR = Path(__file__).resolve().parent
13
+ sys.path.insert(0, str(TESTS_DIR.parent / "scripts"))
14
+ from laravel_routes import route_action # noqa: E402
15
+ from lsa_common import load_config # noqa: E402
16
+ from map_routes import map_routes # noqa: E402
17
+
18
+ ROUTES = """<?php
19
+ use App\\Http\\Controllers\\OrderController;
20
+ use Illuminate\\Support\\Facades\\Route;
21
+
22
+ Route::get('/orders/{id}', [OrderController::class, 'show']);
23
+ Route::post('/orders', 'OrderController@store');
24
+ Route::get('/missing', [GoneController::class, 'index']);
25
+ Route::get('/ping', function () { return 'ok'; });
26
+ """
27
+
28
+ CONTROLLER = """<?php
29
+ namespace App\\Http\\Controllers;
30
+
31
+ use App\\Models\\Order;
32
+ use Illuminate\\Http\\Request;
33
+
34
+ class OrderController extends Controller
35
+ {
36
+ public function show($id)
37
+ {
38
+ return Order::find($id);
39
+ }
40
+
41
+ public function store(Request $request)
42
+ {
43
+ return Order::create(['customer_id' => $request->input('customer_id')]);
44
+ }
45
+ }
46
+ """
47
+
48
+
49
+ class TestRouteAction(unittest.TestCase):
50
+ def test_array_and_string_actions(self) -> None:
51
+ self.assertEqual(route_action("Route::get('/a', [OrderController::class, 'show'])"), "OrderController@show")
52
+ self.assertEqual(route_action("Route::get('/a', [\\App\\Http\\Controllers\\OrderController::class, 'show'])"),
53
+ "OrderController@show")
54
+ self.assertEqual(route_action("Route::post('/a', 'App\\Http\\Controllers\\OrderController@store')"),
55
+ "OrderController@store")
56
+ self.assertIsNone(route_action("Route::get('/a', function () { return 1; })"))
57
+
58
+
59
+ class TestLaravelHandlers(unittest.TestCase):
60
+ def setUp(self) -> None:
61
+ self.root = Path(tempfile.mkdtemp(prefix="lsa_laravel_"))
62
+ (self.root / "routes").mkdir()
63
+ (self.root / "app/Http/Controllers").mkdir(parents=True)
64
+ (self.root / "routes/web.php").write_text(ROUTES, encoding="utf-8")
65
+ (self.root / "app/Http/Controllers/OrderController.php").write_text(CONTROLLER, encoding="utf-8")
66
+
67
+ def tearDown(self) -> None:
68
+ shutil.rmtree(self.root, ignore_errors=True)
69
+
70
+ def routes(self) -> dict:
71
+ found = map_routes(self.root, load_config(None), {"php-laravel"}, workers=1)["candidates"]
72
+ return {r["path"]: r for r in found if r["stack"] == "php-laravel"}
73
+
74
+ def test_route_points_at_the_controller_method(self) -> None:
75
+ show = self.routes()["/orders/{id}"]
76
+ self.assertEqual(show["handler"], {"file": "app/Http/Controllers/OrderController.php", "line": 9,
77
+ "symbol": "OrderController@show"})
78
+ self.assertEqual(show["notes"], "declared in routes/web.php:5")
79
+ self.assertIn("id", [p["name"] for p in show["id_params"]])
80
+
81
+ def test_string_action_and_body_inputs(self) -> None:
82
+ store = self.routes()["/orders"]
83
+ self.assertEqual((store["handler"]["file"], store["handler"]["line"]),
84
+ ("app/Http/Controllers/OrderController.php", 14))
85
+ self.assertIn("customer_id", [p["name"] for p in store["id_params"]])
86
+
87
+ def test_unresolved_routes_keep_their_declaration(self) -> None:
88
+ routes = self.routes()
89
+ self.assertEqual(routes["/missing"]["handler"], {"file": "routes/web.php", "line": 7,
90
+ "symbol": "GoneController@index"})
91
+ self.assertEqual(routes["/ping"]["handler"]["file"], "routes/web.php")
92
+
93
+
94
+ if __name__ == "__main__":
95
+ unittest.main()
@@ -0,0 +1,75 @@
1
+ namespace MemberPortal.Backend.Auth;
2
+
3
+ using System.Security.Claims;
4
+ using System.Text.Encodings.Web;
5
+ using Microsoft.AspNetCore.Authentication;
6
+ using Microsoft.Extensions.Options;
7
+
8
+ /// <summary>
9
+ /// Flexible authentication handler supporting mock tokens for personas as well as JWT claims.
10
+ /// </summary>
11
+ public class DemoAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
12
+ {
13
+ public DemoAuthenticationHandler(
14
+ IOptionsMonitor<AuthenticationSchemeOptions> options,
15
+ ILoggerFactory logger,
16
+ UrlEncoder encoder)
17
+ : base(options, logger, encoder)
18
+ {
19
+ }
20
+
21
+ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
22
+ {
23
+ if (!Request.Headers.TryGetValue("Authorization", out var authHeaderValues))
24
+ {
25
+ // Anonymous fallback
26
+ return Task.FromResult(AuthenticateResult.NoResult());
27
+ }
28
+
29
+ var header = authHeaderValues.ToString();
30
+ var token = header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
31
+ ? header.Substring(7).Trim()
32
+ : header.Trim();
33
+
34
+ var claims = token switch
35
+ {
36
+ "token-user-a" => new[]
37
+ {
38
+ new Claim(ClaimTypes.NameIdentifier, "user-a"),
39
+ new Claim("sub", "user-a"),
40
+ new Claim("tenant_id", "tenant-alpha"),
41
+ new Claim("name", "Alice"),
42
+ new Claim(ClaimTypes.Role, "member")
43
+ },
44
+ "token-user-b" => new[]
45
+ {
46
+ new Claim(ClaimTypes.NameIdentifier, "user-b"),
47
+ new Claim("sub", "user-b"),
48
+ new Claim("tenant_id", "tenant-alpha"),
49
+ new Claim("name", "Bob"),
50
+ new Claim(ClaimTypes.Role, "member")
51
+ },
52
+ "token-user-c" => new[]
53
+ {
54
+ new Claim(ClaimTypes.NameIdentifier, "user-c"),
55
+ new Claim("sub", "user-c"),
56
+ new Claim("tenant_id", "tenant-beta"),
57
+ new Claim("name", "Charlie"),
58
+ new Claim(ClaimTypes.Role, "member")
59
+ },
60
+ _ => new[]
61
+ {
62
+ new Claim(ClaimTypes.NameIdentifier, token),
63
+ new Claim("sub", token),
64
+ new Claim("tenant_id", "tenant-alpha"),
65
+ new Claim(ClaimTypes.Role, "member")
66
+ }
67
+ };
68
+
69
+ var identity = new ClaimsIdentity(claims, Scheme.Name);
70
+ var principal = new ClaimsPrincipal(identity);
71
+ var ticket = new AuthenticationTicket(principal, Scheme.Name);
72
+
73
+ return Task.FromResult(AuthenticateResult.Success(ticket));
74
+ }
75
+ }
@@ -1,9 +1,7 @@
1
- using System.Security.Claims;
2
- using System.Text.Encodings.Web;
1
+ using MemberPortal.Backend.Auth;
3
2
  using MemberPortal.Backend.Data;
4
3
  using Microsoft.AspNetCore.Authentication;
5
4
  using Microsoft.EntityFrameworkCore;
6
- using Microsoft.Extensions.Options;
7
5
  using Komagu.Security.Authorization;
8
6
 
9
7
  var builder = WebApplication.CreateBuilder(args);
@@ -53,72 +51,3 @@ app.UseAuthorization();
53
51
  app.MapControllers();
54
52
 
55
53
  app.Run();
56
-
57
- /// <summary>
58
- /// Flexible authentication handler supporting mock tokens for personas as well as JWT claims.
59
- /// </summary>
60
- public class DemoAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
61
- {
62
- public DemoAuthenticationHandler(
63
- IOptionsMonitor<AuthenticationSchemeOptions> options,
64
- ILoggerFactory logger,
65
- UrlEncoder encoder)
66
- : base(options, logger, encoder)
67
- {
68
- }
69
-
70
- protected override Task<AuthenticateResult> HandleAuthenticateAsync()
71
- {
72
- if (!Request.Headers.TryGetValue("Authorization", out var authHeaderValues))
73
- {
74
- // Anonymous fallback
75
- return Task.FromResult(AuthenticateResult.NoResult());
76
- }
77
-
78
- var header = authHeaderValues.ToString();
79
- var token = header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
80
- ? header.Substring(7).Trim()
81
- : header.Trim();
82
-
83
- var claims = token switch
84
- {
85
- "token-user-a" => new[]
86
- {
87
- new Claim(ClaimTypes.NameIdentifier, "user-a"),
88
- new Claim("sub", "user-a"),
89
- new Claim("tenant_id", "tenant-alpha"),
90
- new Claim("name", "Alice"),
91
- new Claim(ClaimTypes.Role, "member")
92
- },
93
- "token-user-b" => new[]
94
- {
95
- new Claim(ClaimTypes.NameIdentifier, "user-b"),
96
- new Claim("sub", "user-b"),
97
- new Claim("tenant_id", "tenant-alpha"),
98
- new Claim("name", "Bob"),
99
- new Claim(ClaimTypes.Role, "member")
100
- },
101
- "token-user-c" => new[]
102
- {
103
- new Claim(ClaimTypes.NameIdentifier, "user-c"),
104
- new Claim("sub", "user-c"),
105
- new Claim("tenant_id", "tenant-beta"),
106
- new Claim("name", "Charlie"),
107
- new Claim(ClaimTypes.Role, "member")
108
- },
109
- _ => new[]
110
- {
111
- new Claim(ClaimTypes.NameIdentifier, token),
112
- new Claim("sub", token),
113
- new Claim("tenant_id", "tenant-alpha"),
114
- new Claim(ClaimTypes.Role, "member")
115
- }
116
- };
117
-
118
- var identity = new ClaimsIdentity(claims, Scheme.Name);
119
- var principal = new ClaimsPrincipal(identity);
120
- var ticket = new AuthenticationTicket(principal, Scheme.Name);
121
-
122
- return Task.FromResult(AuthenticateResult.Success(ticket));
123
- }
124
- }
@@ -34,6 +34,8 @@ import java.time.Instant
34
34
  // so the identity module's JWT decoder, which requires komagu.security.oidc.*, is left out.
35
35
  @SpringBootApplication(exclude = [KomaguIdentityAutoConfiguration::class])
36
36
  class Application {
37
+ // Demo seed data: one literal per row, read as a table.
38
+ @Suppress("LongMethod")
37
39
  @Bean
38
40
  fun seedDataRunner(
39
41
  orderRepository: OrderRepository,
@@ -107,6 +109,8 @@ class Application {
107
109
  }
108
110
  }
109
111
 
112
+ // Standard Spring Boot entry point; the copy of args happens once at startup.
113
+ @Suppress("SpreadOperator")
110
114
  fun main(args: Array<String>) {
111
115
  runApplication<Application>(*args)
112
116
  }
@@ -162,6 +162,9 @@ class DocumentController(
162
162
  }
163
163
 
164
164
  @RestController
165
+ // Each endpoint keeps the planted vulnerable branch and the remediated branch side by side,
166
+ // with early returns, so the demo shows the before and after in one place.
167
+ @Suppress("ReturnCount")
165
168
  @RequestMapping("/api/v1/test")
166
169
  class LegacyTestController(
167
170
  private val orderRepository: OrderRepository,
@@ -8,6 +8,8 @@ import java.math.BigDecimal
8
8
  import java.time.Instant
9
9
  import java.util.UUID
10
10
 
11
+ // JPA entity: one constructor parameter per column.
12
+ @Suppress("LongParameterList")
11
13
  @Entity
12
14
  @Table(name = "portal_orders")
13
15
  class OrderEntity(
@@ -21,6 +23,8 @@ class OrderEntity(
21
23
  var createdAt: Instant = Instant.now()
22
24
  ) : OwnedResource<String>
23
25
 
26
+ // JPA entity: one constructor parameter per column.
27
+ @Suppress("LongParameterList")
24
28
  @Entity
25
29
  @Table(name = "portal_documents")
26
30
  class DocumentEntity(
@@ -86,7 +86,7 @@ class TestDemoAppVerification(unittest.TestCase):
86
86
  health_url = f"{cls.base_url}/api/v1/auth/personas"
87
87
  for _ in range(50):
88
88
  try:
89
- with urllib.request.urlopen(health_url, timeout=1.0) as resp:
89
+ with urllib.request.urlopen(health_url, timeout=1.0) as resp: # nosec B310 # http://127.0.0.1 test server
90
90
  if resp.status == 200:
91
91
  ready = True
92
92
  break
@@ -124,7 +124,7 @@ class TestDemoAppVerification(unittest.TestCase):
124
124
  url,
125
125
  headers={"Authorization": "Bearer token-user-a"},
126
126
  )
127
- with urllib.request.urlopen(req, timeout=5) as resp:
127
+ with urllib.request.urlopen(req, timeout=5) as resp: # nosec B310 # http://127.0.0.1 test server
128
128
  self.assertEqual(resp.status, 200)
129
129
  data = json.loads(resp.read().decode("utf-8"))
130
130
  self.assertEqual(data["id"], "ord-001")
@@ -188,7 +188,7 @@ class TestDemoAppVerification(unittest.TestCase):
188
188
  headers={"Authorization": "Bearer token-user-c"},
189
189
  )
190
190
  with self.assertRaises(urllib.error.HTTPError) as ctx:
191
- urllib.request.urlopen(req, timeout=5)
191
+ urllib.request.urlopen(req, timeout=5) # nosec B310 # http://127.0.0.1 test server
192
192
  self.assertEqual(ctx.exception.code, 404)
193
193
 
194
194
  def test_05_safe_sort_protects_against_sqli(self) -> None:
@@ -199,7 +199,7 @@ class TestDemoAppVerification(unittest.TestCase):
199
199
  headers={"Authorization": "Bearer token-user-a"},
200
200
  )
201
201
  with self.assertRaises(urllib.error.HTTPError) as ctx:
202
- urllib.request.urlopen(req, timeout=5)
202
+ urllib.request.urlopen(req, timeout=5) # nosec B310 # http://127.0.0.1 test server
203
203
  # Server must reject with 400 or 500 SecurityException, not 200
204
204
  self.assertIn(ctx.exception.code, [400, 500])
205
205
 
@@ -88,7 +88,7 @@ class TestRemediationVerification(unittest.TestCase):
88
88
  health_url = f"{cls.base_url}/api/v1/auth/personas"
89
89
  for _ in range(50):
90
90
  try:
91
- with urllib.request.urlopen(health_url, timeout=1.0) as resp:
91
+ with urllib.request.urlopen(health_url, timeout=1.0) as resp: # nosec B310 # http://127.0.0.1 test server
92
92
  if resp.status == 200:
93
93
  ready = True
94
94
  break
@@ -126,7 +126,7 @@ class TestRemediationVerification(unittest.TestCase):
126
126
  url,
127
127
  headers={"Authorization": "Bearer token-user-a"},
128
128
  )
129
- with urllib.request.urlopen(req, timeout=5) as resp:
129
+ with urllib.request.urlopen(req, timeout=5) as resp: # nosec B310 # http://127.0.0.1 test server
130
130
  self.assertEqual(resp.status, 200)
131
131
  data = json.loads(resp.read().decode("utf-8"))
132
132
  self.assertEqual(data["id"], "ord-001")
@@ -169,7 +169,7 @@ class TestRemediationVerification(unittest.TestCase):
169
169
  headers={"Authorization": "Bearer token-user-b"},
170
170
  )
171
171
  with self.assertRaises(urllib.error.HTTPError) as ctx:
172
- urllib.request.urlopen(req, timeout=5)
172
+ urllib.request.urlopen(req, timeout=5) # nosec B310 # http://127.0.0.1 test server
173
173
  self.assertEqual(ctx.exception.code, 404)
174
174
 
175
175
  def test_04_attacker_direct_put_unauthorized_returns_404(self) -> None:
@@ -186,7 +186,7 @@ class TestRemediationVerification(unittest.TestCase):
186
186
  method="PUT",
187
187
  )
188
188
  with self.assertRaises(urllib.error.HTTPError) as ctx:
189
- urllib.request.urlopen(req, timeout=5)
189
+ urllib.request.urlopen(req, timeout=5) # nosec B310 # http://127.0.0.1 test server
190
190
  self.assertEqual(ctx.exception.code, 404)
191
191
 
192
192
  def test_05_attacker_direct_delete_unauthorized_returns_404(self) -> None:
@@ -198,7 +198,7 @@ class TestRemediationVerification(unittest.TestCase):
198
198
  method="DELETE",
199
199
  )
200
200
  with self.assertRaises(urllib.error.HTTPError) as ctx:
201
- urllib.request.urlopen(req, timeout=5)
201
+ urllib.request.urlopen(req, timeout=5) # nosec B310 # http://127.0.0.1 test server
202
202
  self.assertEqual(ctx.exception.code, 404)
203
203
 
204
204
  def test_06_parameterized_search_neutralizes_sqli(self) -> None:
@@ -210,7 +210,7 @@ class TestRemediationVerification(unittest.TestCase):
210
210
  url,
211
211
  headers={"Authorization": "Bearer token-user-a"},
212
212
  )
213
- with urllib.request.urlopen(req, timeout=5) as resp:
213
+ with urllib.request.urlopen(req, timeout=5) as resp: # nosec B310 # http://127.0.0.1 test server
214
214
  self.assertEqual(resp.status, 200)
215
215
  data = json.loads(resp.read().decode("utf-8"))
216
216
  # When safely parameterized as LIKE '%...%', it searches for the literal string and matches nothing