@hoanghainh/komagu 1.8.0 → 2.0.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 (210) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/assess/lsa/CHANGELOG.md +6 -0
  4. package/assess/lsa/VERSION +1 -1
  5. package/assess/lsa/adapters/build_adapters.py +16 -8
  6. package/assess/lsa/adapters/codex/README.md +1 -1
  7. package/assess/lsa/adapters/generic/README.md +1 -1
  8. package/assess/lsa/examples/eval-run-4/ground-truth.json +823 -0
  9. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/ai-run-stats.json +58 -0
  10. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/coverage.json +85 -0
  11. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/findings/priv-privilege-001.json +822 -0
  12. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/findings/priv-privilege-002.json +393 -0
  13. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/findings.json +4477 -0
  14. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/mass-assignment.candidates.json +232 -0
  15. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-001.json +425 -0
  16. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-002.json +333 -0
  17. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-003.json +400 -0
  18. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-004.json +403 -0
  19. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-005.json +398 -0
  20. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-006.json +63 -0
  21. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/score.json +120 -0
  22. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/ai-run-stats.json +55 -0
  23. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/coverage.json +50 -0
  24. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/findings/priv-privilege-001.json +613 -0
  25. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/findings/priv-privilege-002.json +287 -0
  26. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/findings.json +4870 -0
  27. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/mass-assignment.candidates.json +232 -0
  28. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-001.json +425 -0
  29. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-002.json +333 -0
  30. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-003.json +400 -0
  31. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-004.json +403 -0
  32. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-005.json +398 -0
  33. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-006.json +63 -0
  34. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/score.json +107 -0
  35. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/ai-run-stats.json +58 -0
  36. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/coverage.json +92 -0
  37. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/findings/priv-privilege-001.json +645 -0
  38. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/findings/priv-privilege-002.json +222 -0
  39. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/findings.json +4859 -0
  40. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/mass-assignment.candidates.json +232 -0
  41. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-001.json +425 -0
  42. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-002.json +333 -0
  43. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-003.json +400 -0
  44. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-004.json +403 -0
  45. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-005.json +398 -0
  46. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-006.json +63 -0
  47. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/score.json +121 -0
  48. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/coverage.json +111 -0
  49. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/findings/priv-privilege-001.json +6 -0
  50. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/findings/priv-privilege-002.json +344 -0
  51. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/findings.json +3544 -0
  52. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/mass-assignment.candidates.json +232 -0
  53. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-001.json +425 -0
  54. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-002.json +333 -0
  55. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-003.json +400 -0
  56. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-004.json +403 -0
  57. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-005.json +398 -0
  58. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-006.json +63 -0
  59. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/score.json +123 -0
  60. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/ai-run-stats.json +50 -0
  61. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/coverage.json +42 -0
  62. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings/priv-privilege-001.json +540 -0
  63. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings/priv-privilege-002.json +238 -0
  64. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings.json +4383 -0
  65. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings.sarif +8729 -0
  66. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/mass-assignment.candidates.json +232 -0
  67. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/report.ja.md +1911 -0
  68. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/report.md +1911 -0
  69. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-001.json +425 -0
  70. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-002.json +333 -0
  71. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-003.json +400 -0
  72. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-004.json +403 -0
  73. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-005.json +398 -0
  74. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-006.json +63 -0
  75. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/score.json +101 -0
  76. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/stack-profile.json +226 -0
  77. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/ai-run-stats.json +54 -0
  78. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/coverage.json +66 -0
  79. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/findings/priv-privilege-001.json +440 -0
  80. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/findings/priv-privilege-002.json +229 -0
  81. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/findings.json +3424 -0
  82. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/mass-assignment.candidates.json +232 -0
  83. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-001.json +425 -0
  84. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-002.json +333 -0
  85. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-003.json +400 -0
  86. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-004.json +403 -0
  87. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-005.json +398 -0
  88. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-006.json +63 -0
  89. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/score.json +120 -0
  90. package/assess/lsa/examples/eval-run-5/gptoss-a/ai-run-stats.json +88 -0
  91. package/assess/lsa/examples/eval-run-5/gptoss-a/coverage.json +36 -0
  92. package/assess/lsa/examples/eval-run-5/gptoss-a/findings/priv-privilege-001.json +530 -0
  93. package/assess/lsa/examples/eval-run-5/gptoss-a/findings/priv-privilege-002.json +248 -0
  94. package/assess/lsa/examples/eval-run-5/gptoss-a/findings.json +4766 -0
  95. package/assess/lsa/examples/eval-run-5/gptoss-a/findings.sarif +9326 -0
  96. package/assess/lsa/examples/eval-run-5/gptoss-a/mass-assignment.candidates.json +232 -0
  97. package/assess/lsa/examples/eval-run-5/gptoss-a/report.ja.md +2011 -0
  98. package/assess/lsa/examples/eval-run-5/gptoss-a/report.md +2011 -0
  99. package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-001.json +425 -0
  100. package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-002.json +333 -0
  101. package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-003.json +400 -0
  102. package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-004.json +403 -0
  103. package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-005.json +398 -0
  104. package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-006.json +63 -0
  105. package/assess/lsa/examples/eval-run-5/gptoss-a/score.json +80 -0
  106. package/assess/lsa/examples/eval-run-5/gptoss-a/stack-profile.json +226 -0
  107. package/assess/lsa/examples/eval-run-5/gptoss-b/ai-run-stats.json +83 -0
  108. package/assess/lsa/examples/eval-run-5/gptoss-b/coverage.json +44 -0
  109. package/assess/lsa/examples/eval-run-5/gptoss-b/findings/priv-privilege-001.json +579 -0
  110. package/assess/lsa/examples/eval-run-5/gptoss-b/findings/priv-privilege-002.json +268 -0
  111. package/assess/lsa/examples/eval-run-5/gptoss-b/findings.json +3913 -0
  112. package/assess/lsa/examples/eval-run-5/gptoss-b/mass-assignment.candidates.json +232 -0
  113. package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-001.json +425 -0
  114. package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-002.json +333 -0
  115. package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-003.json +400 -0
  116. package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-004.json +403 -0
  117. package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-005.json +398 -0
  118. package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-006.json +63 -0
  119. package/assess/lsa/examples/eval-run-5/gptoss-b/score.json +94 -0
  120. package/assess/lsa/examples/eval-run-5/ground-truth.json +823 -0
  121. package/assess/lsa/knowledge/severity-and-cwe.md +1 -1
  122. package/assess/lsa/prompts/35-privilege-auditor.md +3 -1
  123. package/assess/lsa/scripts/baseline.py +5 -2
  124. package/assess/lsa/scripts/findings_to_sarif.py +24 -1
  125. package/assess/lsa/tests/EVAL-RESULTS.md +156 -0
  126. package/assess/lsa/tests/score_findings.py +58 -0
  127. package/assess/lsa/tests/test_adapters.py +21 -2
  128. package/assess/lsa/tests/test_baseline.py +8 -0
  129. package/assess/lsa/tests/test_contract_groundwork.py +7 -7
  130. package/assess/lsa/tests/test_eval_run_4.py +73 -0
  131. package/assess/lsa/tests/test_eval_run_5.py +73 -0
  132. package/assess/lsa/tests/test_privilege_tier2.py +30 -0
  133. package/operate/ai/anchoring.py +130 -0
  134. package/operate/ai/client.py +13 -0
  135. package/operate/ai/context.py +105 -0
  136. package/operate/ai/runner.py +197 -69
  137. package/operate/cli/__init__.py +1 -1
  138. package/operate/cli/commands/scan.py +3 -8
  139. package/operate/cli/commands/test.py +1 -1
  140. package/operate/cli/env.py +4 -20
  141. package/operate/cross_user_tester/persona_matrix.py +5 -1
  142. package/operate/devsecops/pipeline.py +5 -5
  143. package/operate/exploit/generator.py +5 -1
  144. package/operate/lsa_sarif/lsa_to_sarif.py +24 -2
  145. package/operate/lsa_sarif/sarif_findings.py +96 -0
  146. package/operate/remediator/runner.py +1 -5
  147. package/operate/remediator/strategies.py +1 -1
  148. package/operate/tests/test_ai_anchoring.py +184 -0
  149. package/operate/tests/test_cli_baseline.py +1 -9
  150. package/operate/tests/test_cli_env.py +8 -29
  151. package/operate/tests/test_cross_user_pipeline.py +2 -2
  152. package/operate/tests/test_exploit_and_virtual_patch.py +3 -3
  153. package/operate/tests/test_headless_ai.py +148 -1
  154. package/operate/tests/test_npm_package.py +2 -16
  155. package/operate/tests/test_sarif_from_scan.py +156 -0
  156. package/package.json +2 -4
  157. package/remediate/authz/README.md +1 -1
  158. package/remediate/authz/dotnet/src/Komagu.Security.Authorization/DefaultKomaguAuthorizationService.cs +1 -1
  159. package/remediate/authz/dotnet/src/Komagu.Security.Authorization/SecurityAuditEvent.cs +2 -2
  160. package/remediate/authz/golang/authz.go +1 -1
  161. package/remediate/authz/nodejs/src/service.js +1 -1
  162. package/remediate/authz/nodejs/tests/test-authz.js +1 -1
  163. package/remediate/authz/patcher.py +3 -4
  164. package/remediate/authz/python/komagu_authz/models.py +2 -2
  165. package/remediate/authz/python/tests/test_fastapi_adapter.py +1 -1
  166. package/remediate/authz/python/tests/test_fastapi_d5_crosstest.py +1 -1
  167. package/remediate/authz/python/tests/test_python_authz.py +3 -3
  168. package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/DefaultKomaguSecurityManager.kt +1 -1
  169. package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/SecurityAuditEvent.kt +1 -2
  170. package/remediate/authz/tests/test_bola_patcher.py +1 -1
  171. package/remediate/edge/coraza/rules/coraza-crs-custom.conf +3 -4
  172. package/remediate/edge/coraza/virtual_patcher.py +4 -6
  173. package/remediate/edge/docker-compose.yml +1 -1
  174. package/remediate/ekyc/server/jpki_verifier.py +2 -8
  175. package/remediate/ekyc/server/tokenization.py +2 -8
  176. package/remediate/identity/README.md +5 -5
  177. package/remediate/identity/dotnet/src/Komagu.Security.Identity/KomaguOidcOptions.cs +2 -2
  178. package/remediate/identity/dotnet/tests/Komagu.Security.Identity.Tests/TestTokenGenerator.cs +1 -1
  179. package/remediate/identity/dotnet/tests/Komagu.Security.Identity.Tests/TokenValidationTests.cs +1 -1
  180. package/remediate/identity/python/komagu_identity/oidc_validator.py +2 -8
  181. package/remediate/identity/python/tests/test_oidc_validator.py +1 -1
  182. package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/KomaguOidcProperties.kt +2 -2
  183. package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/KomaguIdentityAutoConfigurationTest.kt +2 -2
  184. package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/TestJwtFactory.kt +1 -1
  185. package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/TokenValidationIntegrationTest.kt +1 -1
  186. package/remediate/identity/test-env/realm-komagu.json +1 -1
  187. package/remediate/legacy_shims/coldfusion/KomaguSecurityShim.cfc +5 -5
  188. package/remediate/query/README.md +1 -1
  189. package/remediate/query/dotnet/src/Komagu.Security.Query.Analyzers/DiagnosticIds.cs +1 -1
  190. package/remediate/query/dotnet/src/Komagu.Security.Query.Analyzers/Komagu.Security.Query.Analyzers.csproj +1 -1
  191. package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/detekt/KomaguRuleSetProvider.kt +1 -1
  192. package/remediate/resilience/dr/dr_manager.py +2 -8
  193. package/remediate/resilience/dr/runbook.md +2 -2
  194. package/remediate/resilience/terraform/variables.tf +1 -1
  195. package/remediate/tenancy/audit/audit_trail.py +2 -8
  196. package/remediate/tenancy/crypto/byok_manager.py +2 -8
  197. package/remediate/tenancy/middleware/tenant_context.py +2 -8
  198. package/remediate/tenancy/sql/rls_isolation.sql +11 -11
  199. package/remediate/tenancy/sql/schema_per_tenant.sql +7 -7
  200. package/remediate/tenancy/test_sql_postgres.py +1 -1
  201. package/remediate/tenancy/tests/test_tenancy.py +4 -4
  202. package/bin/komainu +0 -16
  203. package/bin/komainu-legacy.js +0 -16
  204. package/remediate/ekyc/tests/test_legacy_env.py +0 -33
  205. package/remediate/identity/python/tests/test_legacy_env.py +0 -33
  206. package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/LegacyKomainuPropertiesPostProcessor.kt +0 -60
  207. package/remediate/identity/spring/src/main/resources/META-INF/spring.factories +0 -2
  208. package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/LegacyKomainuPropertiesPostProcessorTest.kt +0 -86
  209. package/remediate/resilience/tests/test_legacy_env.py +0 -33
  210. package/remediate/tenancy/tests/test_legacy_env.py +0 -49
@@ -7,7 +7,7 @@ LLM endpoint (Ollama, vLLM, private OpenAI server):
7
7
  3. Audits candidate routes for BOLA / IDOR (BOLA Auditor).
8
8
  4. Audits candidate queries for SQL/NoSQL Injection (Injection Auditor).
9
9
  5. Adversarial Skeptic Verification (Finding Verifier Council).
10
- 6. Merges and schema-validates all findings with 0 false positives.
10
+ 6. Merges and schema-validates all findings; writes OUT/ai-run-stats.json.
11
11
 
12
12
  Standard-library only (Python 3.8+ compatible).
13
13
  """
@@ -18,30 +18,28 @@ import json
18
18
  from pathlib import Path
19
19
  import subprocess
20
20
  import sys
21
+ import time
21
22
  from typing import Any, Callable, Dict, List, Optional
22
23
 
24
+ from operate.ai.anchoring import Anchor, AnchorReport, anchor_findings, load_anchors
23
25
  from operate.ai.client import LLMClient
26
+ from operate.ai.context import _included_files, _model_source, _read_snippet, _verification_context # noqa: F401
24
27
 
25
28
  REPO_ROOT = Path(__file__).resolve().parents[2]
26
29
  LSA_PROMPTS = REPO_ROOT / "assess" / "lsa" / "prompts"
27
30
  DEFAULT_CWE = {"bola": ["CWE-284"], "bfla": ["CWE-285"], "mass_assignment": ["CWE-915"],
28
31
  "sql_injection": ["CWE-89"], "nosql_injection": ["CWE-943"]}
29
32
  LSA_SCRIPTS = REPO_ROOT / "assess" / "lsa" / "scripts"
33
+ # Valid subtypes per category (finding.schema.json); the first one is used when a model writes an invalid value
34
+ CATEGORY_SUBTYPES = {"bfla": ["missing_role_check", "role_from_request", "client_side_role_check"],
35
+ "mass_assignment": ["whole_object_binding", "privileged_field_writable"]}
36
+ FINDING_STATUSES = {"unverified", "confirmed", "likely", "rejected", "needs_review"}
30
37
 
31
38
 
32
- def _read_snippet(target_dir: Path, rel_file: str, line: int, radius: int = 15) -> str:
33
- """Read a window of code around target line from the audited repository."""
34
- full_path = target_dir / rel_file
35
- if not full_path.is_file():
36
- return f"[File not found: {rel_file}]"
37
- try:
38
- lines = full_path.read_text(encoding="utf-8", errors="replace").splitlines()
39
- start = max(0, line - radius - 1)
40
- end = min(len(lines), line + radius)
41
- numbered = [f"{i + 1:4d} | {lines[i]}" for i in range(start, end)]
42
- return "\n".join(numbered)
43
- except Exception as e:
44
- return f"[Error reading {rel_file}: {e}]"
39
+ VERIFY_BATCH_SIZE = 5
40
+ AUTH_MODEL_LIMIT = 6000
41
+ RETRY_REMINDER = ("Your previous reply was empty or not valid JSON. Answer again with the JSON object only, "
42
+ "exactly in the format requested above, with no other text.")
45
43
 
46
44
 
47
45
  class HeadlessAIRunner:
@@ -58,34 +56,121 @@ class HeadlessAIRunner:
58
56
  self.out_dir = Path(out_dir).resolve()
59
57
  self.client = client
60
58
  self.callback = step_callback or (lambda status, msg: None)
59
+ # Per-phase replies that could not be parsed as JSON; written to ai-run-stats.json
60
+ self.parse_failures: Dict[str, List[str]] = {}
61
+ # Per-phase batches whose model call failed (timeout, HTTP error); the run continues without them
62
+ self.call_errors: Dict[str, Dict[str, str]] = {}
63
+ # Per-phase batches whose first reply was empty or unparseable and were asked again
64
+ self.retries: Dict[str, List[str]] = {}
65
+ # Findings whose location was taken from the Tier 1 inventory (#319)
66
+ self.anchor_report = AnchorReport()
67
+ self._anchors: Optional[Dict[str, Anchor]] = None
68
+ self.phase_stats: Dict[str, Dict[str, Any]] = {}
61
69
 
62
70
  def _log(self, status: str, msg: str) -> None:
63
71
  self.callback(status, msg)
64
72
 
73
+ def _anchor(self, findings: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
74
+ if self._anchors is None:
75
+ self._anchors = load_anchors(self.out_dir, self.target_dir)
76
+ return anchor_findings(findings, self._anchors, self.target_dir, self.anchor_report)
77
+
78
+ def _chat(self, phase: str, batch_id: str, messages: List[Dict[str, str]]) -> Optional[str]:
79
+ """One batch call; a failed call is recorded and returns None instead of aborting the run.
80
+
81
+ An empty or unparseable reply is retried once with a reminder to answer with JSON only (#319).
82
+ """
83
+ try:
84
+ reply = self.client.chat(messages, temperature=0.1)
85
+ except Exception as e:
86
+ self.call_errors.setdefault(phase, {})[batch_id] = str(e)[:300]
87
+ self._log("warn", f"{phase}: model call failed for {batch_id} ({e}); batch left without verdicts")
88
+ return None
89
+ if self._parses(reply):
90
+ return reply
91
+ self.retries.setdefault(phase, []).append(batch_id)
92
+ self._log("warn", f"{phase}: reply for {batch_id} was {'empty' if not (reply or '').strip() else 'not valid JSON'}; retrying once")
93
+ retry_messages = messages + ([{"role": "assistant", "content": reply}] if (reply or "").strip() else []) + [
94
+ {"role": "user", "content": RETRY_REMINDER}]
95
+ try:
96
+ second = self.client.chat(retry_messages, temperature=0.1)
97
+ except Exception as e:
98
+ self._log("warn", f"{phase}: retry for {batch_id} failed ({e})")
99
+ return reply
100
+ return second if self._parses(second) or not (reply or "").strip() else reply
101
+
102
+ def _parses(self, reply: Optional[str]) -> bool:
103
+ if not (reply or "").strip():
104
+ return False
105
+ try:
106
+ self.client.extract_json(reply)
107
+ return True
108
+ except Exception:
109
+ return False
110
+
111
+ def _parse_failed(self, phase: str, batch_id: str, reply: Optional[str] = None) -> None:
112
+ if batch_id in self.call_errors.get(phase, {}):
113
+ return
114
+ self.parse_failures.setdefault(phase, []).append(batch_id)
115
+ if reply is not None:
116
+ raw_dir = self.out_dir / "ai-raw"
117
+ raw_dir.mkdir(parents=True, exist_ok=True)
118
+ (raw_dir / f"{phase}-{batch_id}.txt").write_text(reply, encoding="utf-8")
119
+ self._log("warn", f"{phase}: could not parse the model reply for {batch_id}; batch left without verdicts")
120
+
121
+ def _usage(self) -> Dict[str, Any]:
122
+ return dict(getattr(self.client, "usage", None) or {})
123
+
124
+ def _timed(self, phase: str, fn: Callable[[], Any]) -> None:
125
+ before, started = self._usage(), time.monotonic()
126
+ fn()
127
+ after = self._usage()
128
+ self.phase_stats[phase] = {
129
+ "seconds": round(time.monotonic() - started, 3),
130
+ **{k: after.get(k, 0) - before.get(k, 0) for k in ("calls", "prompt_tokens", "completion_tokens")},
131
+ }
132
+
133
+ def write_run_stats(self) -> Path:
134
+ """Write model, per-phase durations, token counts and parse failures to OUT/ai-run-stats.json."""
135
+ stats = {
136
+ "model": getattr(self.client, "model", None),
137
+ "endpoint": getattr(self.client, "endpoint", None),
138
+ "phases": self.phase_stats,
139
+ "totals": self._usage(),
140
+ "parse_failures": self.parse_failures,
141
+ "call_errors": self.call_errors,
142
+ "retries": self.retries,
143
+ "anchoring": {"moved": self.anchor_report.moved, "unanchored": self.anchor_report.unanchored},
144
+ }
145
+ path = self.out_dir / "ai-run-stats.json"
146
+ path.write_text(json.dumps(stats, indent=2), encoding="utf-8")
147
+ return path
148
+
65
149
  def run(self) -> Dict[str, Any]:
66
150
  """Execute all cognitive AI phases end-to-end."""
67
151
  self._log("step", "Starting Tier 2 Cognitive Multi-Agent Audit (Headless BYO-LLM)")
68
152
 
69
153
  # Phase 2: Stack & Auth Model Detector
70
- self.infer_auth_model()
154
+ self._timed("2-stack-detector", self.infer_auth_model)
71
155
 
72
156
  # Phase 3: Route Inventory Mapping
73
- self.map_routes_inventory()
157
+ self._timed("3-route-mapper", self.map_routes_inventory)
74
158
 
75
159
  # Phase 4a: BOLA / IDOR Cognitive Audit
76
- self.audit_bola_routes()
160
+ self._timed("4a-bola-auditor", self.audit_bola_routes)
77
161
 
78
162
  # Phase 4b: SQL / NoSQL Injection Cognitive Audit
79
- self.audit_injection_candidates()
163
+ self._timed("4b-injection-auditor", self.audit_injection_candidates)
80
164
 
81
165
  # Phase 4c: Privilege Audit (BFLA, mass assignment)
82
- self.audit_privilege()
166
+ self._timed("4c-privilege-auditor", self.audit_privilege)
83
167
 
84
168
  # Phase 5: Adversarial Skeptic Verifier
85
- self.verify_findings_skeptic()
169
+ self._timed("5-finding-verifier", self.verify_findings_skeptic)
86
170
 
87
171
  # Phase 6: Merge & Validate Outputs
88
172
  self.merge_and_validate()
173
+ self.write_run_stats()
89
174
 
90
175
  self._log("ok", "Tier 2 Cognitive Multi-Agent Audit completed successfully")
91
176
  return {"status": "success", "out_dir": str(self.out_dir)}
@@ -209,6 +294,20 @@ Output MUST be Markdown format adhering to Komagu auth-model.md specification.
209
294
  """Normalize finding fields to strictly comply with finding.schema.json."""
210
295
  if not isinstance(f, dict):
211
296
  return f
297
+ # impact must be a string; small models often return a list of consequences
298
+ if isinstance(f.get("impact"), list):
299
+ f["impact"] = "; ".join(str(i) for i in f["impact"])
300
+
301
+ # a verdict written into subtype belongs in status; fall back to the category's first subtype
302
+ allowed = CATEGORY_SUBTYPES.get(f.get("category"))
303
+ if allowed and f.get("subtype") not in allowed:
304
+ if f.get("subtype") in FINDING_STATUSES:
305
+ f["status"] = f["subtype"]
306
+ f["subtype"] = allowed[0]
307
+
308
+ if f.get("status") not in FINDING_STATUSES:
309
+ f["status"] = "unverified"
310
+
212
311
  # recommendation must be a string
213
312
  rec = f.get("recommendation")
214
313
  if isinstance(rec, dict):
@@ -227,8 +326,8 @@ Output MUST be Markdown format adhering to Komagu auth-model.md specification.
227
326
  else:
228
327
  for ev in evidence:
229
328
  if isinstance(ev, dict):
230
- if "line" not in ev:
231
- ev["line"] = ev.get("start_line", default_line)
329
+ if not isinstance(ev.get("line"), int):
330
+ ev["line"] = ev.get("start_line") if isinstance(ev.get("start_line"), int) else default_line
232
331
  if "file" not in ev:
233
332
  ev["file"] = default_file
234
333
  if "why" not in ev:
@@ -323,13 +422,14 @@ Respond STRICTLY with a valid JSON object matching:
323
422
  {"role": "user", "content": user_prompt},
324
423
  ]
325
424
 
326
- reply = self.client.chat(messages, temperature=0.1)
425
+ reply = self._chat("4a-bola-auditor", r_file.stem, messages)
327
426
  try:
328
427
  parsed = self.client.extract_json(reply)
329
428
  raw_findings = parsed.get("findings", [])
330
- findings = [self._sanitize_finding(f) for f in raw_findings]
429
+ findings = self._anchor([self._sanitize_finding(f) for f in raw_findings])
331
430
  route_verdicts = parsed.get("route_verdicts", [])
332
431
  except Exception:
432
+ self._parse_failed("4a-bola-auditor", r_file.stem, reply)
333
433
  findings = []
334
434
  route_verdicts = []
335
435
 
@@ -409,13 +509,14 @@ Respond STRICTLY with a valid JSON object matching:
409
509
  {"role": "user", "content": user_prompt},
410
510
  ]
411
511
 
412
- reply = self.client.chat(messages, temperature=0.1)
512
+ reply = self._chat("4b-injection-auditor", b_file.stem, messages)
413
513
  try:
414
514
  parsed = self.client.extract_json(reply)
415
515
  raw_findings = parsed.get("findings", [])
416
- findings = [self._sanitize_finding(f) for f in raw_findings]
516
+ findings = self._anchor([self._sanitize_finding(f) for f in raw_findings])
417
517
  candidate_verdicts = parsed.get("candidate_verdicts", [])
418
518
  except Exception:
519
+ self._parse_failed("4b-injection-auditor", b_file.stem, reply)
419
520
  findings = []
420
521
  candidate_verdicts = []
421
522
 
@@ -467,6 +568,7 @@ Respond STRICTLY with a valid JSON object matching:
467
568
  "allowlist": c.get("allowlist"),
468
569
  "sensitive_fields": c.get("sensitive_fields", []),
469
570
  "code_context": _read_snippet(self.target_dir, c.get("file", ""), c.get("line") or 1),
571
+ "model_definition": _model_source(self.target_dir, c.get("target"), c.get("file", ""), c.get("line") or 1),
470
572
  } for c in mass]
471
573
 
472
574
  user_prompt = f"""Auth Model:
@@ -480,20 +582,51 @@ Mass Assignment Candidates:
480
582
 
481
583
  Task:
482
584
  Judge every route for BFLA and every candidate for mass assignment as described in your role.
483
- Respond STRICTLY with a valid JSON object with keys "batch_id" ("{b_file.stem}"),
484
- "privilege_route_verdicts", "candidate_verdicts" and "findings" (category "bfla" or "mass_assignment").
585
+ Respond STRICTLY with a valid JSON object matching:
586
+ {{
587
+ "batch_id": "{b_file.stem}",
588
+ "privilege_route_verdicts": [
589
+ {{"route_id": "<route_id>", "verdict": "vulnerable|safe|needs_review"}}
590
+ ],
591
+ "candidate_verdicts": [
592
+ {{"candidate_id": "<candidate_id>", "verdict": "vulnerable|safe|needs_review"}}
593
+ ],
594
+ "findings": [
595
+ {{
596
+ "finding_id": "BFLA-<route_id>-1 or MASS-<candidate_id>-1",
597
+ "category": "bfla|mass_assignment",
598
+ "subtype": "missing_role_check|role_from_request|client_side_role_check (bfla) or whole_object_binding|privileged_field_writable (mass_assignment)",
599
+ "title": "<short title>",
600
+ "route_id": "<route_id, bfla only>",
601
+ "candidate_id": "<candidate_id, mass_assignment only>",
602
+ "location": {{"file": "<file>", "start_line": <line>}},
603
+ "evidence": [{{"file": "<file>", "line": <line>, "why": "<privileged action or binding, and the missing control>"}}],
604
+ "impact": "<one string naming the privileged action or fields>",
605
+ "severity": "critical|high|medium|low",
606
+ "confidence": "high|medium|low",
607
+ "cwe": ["CWE-285"],
608
+ "recommendation": "<one string>",
609
+ "status": "unverified|needs_review"
610
+ }}
611
+ ]
612
+ }}
613
+ Write one finding for every "vulnerable" or "needs_review" verdict. A "needs_review" verdict goes in
614
+ "status", never in "subtype".
615
+ For mass assignment, judge the fields from "model_definition". If it says the definition was not found
616
+ (or is null), you cannot know the fields: answer "needs_review", never "safe" for that reason alone.
485
617
  """
486
618
  messages = [
487
619
  {"role": "system", "content": auditor_prompt},
488
620
  {"role": "user", "content": user_prompt},
489
621
  ]
490
- reply = self.client.chat(messages, temperature=0.1)
622
+ reply = self._chat("4c-privilege-auditor", b_file.stem, messages)
491
623
  try:
492
624
  parsed = self.client.extract_json(reply)
493
- findings = [self._sanitize_finding(f) for f in parsed.get("findings", [])]
625
+ findings = self._anchor([self._sanitize_finding(f) for f in parsed.get("findings", [])])
494
626
  route_verdicts = parsed.get("privilege_route_verdicts", [])
495
627
  candidate_verdicts = parsed.get("candidate_verdicts", [])
496
628
  except Exception:
629
+ self._parse_failed("4c-privilege-auditor", b_file.stem, reply)
497
630
  findings, route_verdicts, candidate_verdicts = [], [], []
498
631
 
499
632
  out_doc = {
@@ -528,37 +661,46 @@ Respond STRICTLY with a valid JSON object with keys "batch_id" ("{b_file.stem}")
528
661
  return
529
662
 
530
663
  verifier_prompt = (LSA_PROMPTS / "50-finding-verifier.md").read_text(encoding="utf-8", errors="replace") if (LSA_PROMPTS / "50-finding-verifier.md").is_file() else ""
664
+ auth_model_path = self.out_dir / "auth-model.md"
665
+ auth_model = auth_model_path.read_text(encoding="utf-8", errors="replace")[:AUTH_MODEL_LIMIT] if auth_model_path.is_file() else ""
531
666
 
532
- # Batch up to 10 findings per verification
533
- batch_size = 10
667
+ batch_size = VERIFY_BATCH_SIZE
534
668
  for idx in range(0, len(all_findings), batch_size):
535
669
  chunk = all_findings[idx : idx + batch_size]
536
- user_prompt = f"""Findings to Verify & Challenge:
537
- {json.dumps(chunk, indent=2)}
670
+ # The headless verifier cannot open files, so it gets the code it must judge (#286)
671
+ to_verify = [{"finding": f, "code_context": _verification_context(self.target_dir, f)} for f in chunk]
672
+ user_prompt = f"""Auth Model:
673
+ {auth_model}
674
+
675
+ Findings to Verify & Challenge (each with the code you may rely on):
676
+ {json.dumps(to_verify, indent=2)}
538
677
 
539
678
  Task:
540
- You are the skeptical security verifier. For each finding:
541
- 1. Examine whether a compensating control (global filter, integer casting, ownership check, allow-list) neutralizes the vulnerability.
542
- 2. Confirm or refute:
543
- - "confirmed" (reason_code: "traced")
544
- - "rejected" (reason_code: "control_found" | "not_reachable" | "evidence_mismatch")
679
+ You are the skeptical security verifier. You cannot open files: judge only from the code_context given
680
+ with each finding and the Auth Model above.
681
+ For each finding:
682
+ 1. Look for a compensating control (role or ownership check, global filter, integer cast, allow-list,
683
+ parameter binding) in the code_context.
684
+ 2. Only use reason_code "control_found" when the control is visible in the code_context; quote its
685
+ file and line in "control_found". Never assume a control in a base class, include, config or
686
+ middleware you were not shown.
687
+ 3. If the code you would need is not shown, answer verdict "likely" with reason_code
688
+ "code_not_visible" or "config_not_visible".
689
+ Verdicts: "confirmed" | "likely" | "rejected".
690
+ reason_code is one of: "traced", "code_not_visible", "config_not_visible", "data_ownership_unclear",
691
+ "control_found", "evidence_mismatch", "not_executed", "not_reachable", "not_attacker_controlled".
545
692
  Respond STRICTLY with a valid JSON object matching verification.schema.json:
546
693
  {{
547
694
  "batch_id": "verified-{(idx // batch_size) + 1}",
548
695
  "verifications": [
549
696
  {{
550
697
  "finding_id": "<finding_id>",
551
- "verdict": "confirmed|rejected",
552
- "reason_code": "traced|control_found",
553
- "confidence": "high",
554
- "checked": ["Checked server-side session binding and sanitizers"],
555
- "notes": "<rationale for verdict>",
556
- "consensus": {{
557
- "score": 0.95,
558
- "agreement_level": "unanimous",
559
- "voted_models": ["{self.client.model}"],
560
- "rationale": "Independently audited and refuted false-alarm candidates."
561
- }}
698
+ "verdict": "confirmed|likely|rejected",
699
+ "reason_code": "<one of the reason codes above>",
700
+ "confidence": "high|medium|low",
701
+ "checked": ["<what you checked in the code_context>"],
702
+ "control_found": {{"file": "<file>", "line": <line>, "code": "<control>"}} or null,
703
+ "notes": "<rationale for verdict>"
562
704
  }}
563
705
  ]
564
706
  }}
@@ -568,29 +710,15 @@ Respond STRICTLY with a valid JSON object matching verification.schema.json:
568
710
  {"role": "user", "content": user_prompt},
569
711
  ]
570
712
 
571
- reply = self.client.chat(messages, temperature=0.1)
713
+ reply = self._chat("5-finding-verifier", f"verified-{(idx // batch_size) + 1}", messages)
572
714
  try:
573
715
  parsed = self.client.extract_json(reply)
574
716
  verifs = parsed.get("verifications", [])
575
717
  except Exception:
576
- # Default fallback verification for unverified items
577
- verifs = [
578
- {
579
- "finding_id": f.get("finding_id", ""),
580
- "verdict": "confirmed",
581
- "reason_code": "traced",
582
- "confidence": "high",
583
- "checked": ["Audited data-flow"],
584
- "notes": "Traced via headless cognitive consensus.",
585
- "consensus": {
586
- "score": 0.90,
587
- "agreement_level": "majority",
588
- "voted_models": [self.client.model],
589
- "rationale": "Verified in headless pipeline.",
590
- },
591
- }
592
- for f in chunk
593
- ]
718
+ # No verdicts: merge_findings.py lists these findings under
719
+ # coverage.findings_without_verification instead of confirming them
720
+ self._parse_failed("5-finding-verifier", f"verified-{(idx // batch_size) + 1}", reply)
721
+ verifs = []
594
722
 
595
723
  verif_doc = {
596
724
  "batch_id": f"verified-{(idx // batch_size) + 1}",
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "1.8.0" # x-release-please-version
5
+ __version__ = "2.0.1" # x-release-please-version
@@ -165,13 +165,8 @@ Examples:
165
165
 
166
166
 
167
167
  def _default_suppressions_file(target_path: Path) -> Path:
168
- """TARGET/.komagu/suppressions.json, or the pre-rename .komainu/ copy with a warning."""
169
- current = target_path / ".komagu" / "suppressions.json"
170
- legacy = target_path / ".komainu" / "suppressions.json"
171
- if not current.is_file() and legacy.is_file():
172
- print(f"warning: {legacy} is deprecated, move it to {current}", file=sys.stderr)
173
- return legacy
174
- return current
168
+ """TARGET/.komagu/suppressions.json."""
169
+ return target_path / ".komagu" / "suppressions.json"
175
170
 
176
171
 
177
172
  def execute_scan(args: argparse.Namespace) -> int:
@@ -460,7 +455,7 @@ def execute_scan(args: argparse.Namespace) -> int:
460
455
  f" {color('🚀 Mode: Tier 2 Cognitive Multi-Agent Engine', GREEN, BOLD)} "
461
456
  f"(Headless BYO-LLM: {args.headless_ai}, Model: {args.model})"
462
457
  )
463
- print(f" {color('✔ Skeptic Consensus Applied', GREEN)}: Verified zero-false-positive findings against compensative controls.\n")
458
+ print(f" {color('✔ Skeptic Consensus Applied', GREEN)}: Findings challenged against compensating controls.\n")
464
459
  else:
465
460
  print(f" {color('💡 Mode: Tier 1 Deterministic Engine', CYAN, BOLD)} (Regex & AST candidate extraction, no LLM required)")
466
461
  print(" To run Tier 2 Cognitive Multi-Agent Audit (BOLA semantic reasoning & Skeptic Verifier):")
@@ -297,7 +297,7 @@ def execute_test(args: argparse.Namespace) -> int:
297
297
 
298
298
 
299
299
  def _matrix_cases_from_scan(scan_file: Path, args: argparse.Namespace, min_consensus: float) -> List[MatrixCase]:
300
- """BFLA and mass assignment findings from findings.json (SARIF is not read here)."""
300
+ """BFLA and mass assignment findings from findings.json or SARIF."""
301
301
  try:
302
302
  data = json.loads(scan_file.read_text(encoding="utf-8"))
303
303
  except (OSError, ValueError):
@@ -1,30 +1,14 @@
1
- """Environment variable lookup with the pre-rename KOMAINU_* fallback (#290)."""
1
+ """Environment variable lookup for KOMAGU_* settings."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
- import sys
7
- from typing import Mapping, Optional, Set
6
+ from typing import Mapping, Optional
8
7
 
9
8
  PREFIX = "KOMAGU_"
10
- LEGACY_PREFIX = "KOMAINU_"
11
-
12
- _warned: Set[str] = set()
13
9
 
14
10
 
15
11
  def getenv(name: str, default: Optional[str] = None, env: Optional[Mapping[str, str]] = None) -> Optional[str]:
16
- """Read KOMAGU_<name>; fall back to the deprecated KOMAINU_<name> with a one-time warning."""
12
+ """Read KOMAGU_<name>."""
17
13
  env = os.environ if env is None else env
18
- value = env.get(PREFIX + name)
19
- if value is not None:
20
- return value
21
- legacy = env.get(LEGACY_PREFIX + name)
22
- if legacy is not None:
23
- if name not in _warned:
24
- _warned.add(name)
25
- print(
26
- f"warning: {LEGACY_PREFIX}{name} is deprecated, set {PREFIX}{name} instead",
27
- file=sys.stderr,
28
- )
29
- return legacy
30
- return default
14
+ return env.get(PREFIX + name, default)
@@ -27,6 +27,8 @@ import urllib.request
27
27
  from dataclasses import dataclass, field
28
28
  from typing import Any, Dict, List, Mapping, Optional, Tuple
29
29
 
30
+ from operate.lsa_sarif.sarif_findings import findings_from_sarif, is_sarif
31
+
30
32
  SUITE_VERSION = 2
31
33
  DENY_STATUS = (401, 403, 404)
32
34
  STRICT_DENY_STATUS = (404,)
@@ -278,12 +280,14 @@ def generate_matrix_cases_from_findings(
278
280
  default_id: str = "12345",
279
281
  min_consensus: float = 0.0,
280
282
  ) -> List[MatrixCase]:
281
- """Turn BFLA and mass assignment findings into matrix cases.
283
+ """Turn BFLA and mass assignment findings (findings.json or SARIF) into matrix cases.
282
284
 
283
285
  BFLA findings are replayed by a normal user (``token_env``) who must be
284
286
  refused. Mass assignment findings are probed by the same user with the
285
287
  privileged fields from ``attack_vector.fields`` (default: role / is_admin).
286
288
  """
289
+ if is_sarif(data):
290
+ data = findings_from_sarif(data)
287
291
  items = data if isinstance(data, list) else (data.get("findings", []) if isinstance(data, dict) else [])
288
292
  persona = Persona(name="user", token_env=token_env)
289
293
  cases: List[MatrixCase] = []
@@ -107,14 +107,14 @@ class DevSecOpsPipeline:
107
107
  results = []
108
108
  rules = [
109
109
  {
110
- "id": "KOMAINU-SECRET-001",
110
+ "id": "KOMAGU-SECRET-001",
111
111
  "name": "HardcodedCredentialDetected",
112
112
  "shortDescription": {"text": "Potential hardcoded secret or API credential detected."},
113
113
  "defaultConfiguration": {"level": "error"},
114
114
  }
115
115
  ]
116
116
 
117
- skip_dirs = {".git", "node_modules", "bin", "obj", ".komagu", ".komagu-devsecops", ".komainu", ".komainu-devsecops", "dist", "build"}
117
+ skip_dirs = {".git", "node_modules", "bin", "obj", ".komagu", ".komagu-devsecops", "dist", "build"}
118
118
  skip_exts = {".png", ".jpg", ".jpeg", ".gif", ".ico", ".pdf", ".zip", ".tar", ".gz", ".db", ".sqlite"}
119
119
 
120
120
  for dirpath, dirnames, filenames in os.walk(self.target_dir):
@@ -137,7 +137,7 @@ class DevSecOpsPipeline:
137
137
  if re.search(pattern, line):
138
138
  results.append(
139
139
  {
140
- "ruleId": "KOMAINU-SECRET-001",
140
+ "ruleId": "KOMAGU-SECRET-001",
141
141
  "level": "error",
142
142
  "message": {"text": f"{desc} found in {rel_path}"},
143
143
  "locations": [
@@ -324,7 +324,7 @@ class DevSecOpsPipeline:
324
324
  results = []
325
325
  rules = [
326
326
  {
327
- "id": "KOMAINU-CONTAINER-001",
327
+ "id": "KOMAGU-CONTAINER-001",
328
328
  "name": "DockerfileSecurityMisconfiguration",
329
329
  "shortDescription": {"text": "Container configuration deviates from production hardening standards."},
330
330
  "defaultConfiguration": {"level": "warning"},
@@ -343,7 +343,7 @@ class DevSecOpsPipeline:
343
343
  if re.search(pattern, line.strip()):
344
344
  results.append(
345
345
  {
346
- "ruleId": "KOMAINU-CONTAINER-001",
346
+ "ruleId": "KOMAGU-CONTAINER-001",
347
347
  "level": level,
348
348
  "message": {"text": f"{msg} at {rel_path}:{line_no}"},
349
349
  "locations": [
@@ -15,6 +15,8 @@ import re
15
15
  from pathlib import Path
16
16
  from typing import Any, Dict, List, Optional, Union
17
17
 
18
+ from operate.lsa_sarif.sarif_findings import findings_from_sarif, is_sarif
19
+
18
20
 
19
21
  class PoCArtifact:
20
22
  """Represents a generated Proof-of-Concept exploit artifact."""
@@ -317,7 +319,9 @@ curl -i -G "{self.base_url}{path}" \\
317
319
  self,
318
320
  findings_data: Union[Dict[str, Any], List[Dict[str, Any]]],
319
321
  ) -> List[PoCArtifact]:
320
- """Generate PoC artifacts for all confirmed/likely findings."""
322
+ """Generate PoC artifacts for all confirmed/likely findings (findings.json or SARIF)."""
323
+ if is_sarif(findings_data):
324
+ findings_data = findings_from_sarif(findings_data)
321
325
  items = findings_data if isinstance(findings_data, list) else findings_data.get("findings", [])
322
326
  artifacts: List[PoCArtifact] = []
323
327
 
@@ -69,7 +69,28 @@ RULE_ID_PREFIX = {"bola": "BOLA", "bfla": "BFLA", "mass_assignment": "MASS"}
69
69
 
70
70
  def _get_rule_id(category: str, cwe: int) -> str:
71
71
  cat_prefix = RULE_ID_PREFIX.get(category, "INJ")
72
- return f"KOMAINU-{cat_prefix}-CWE{cwe}"
72
+ return f"KOMAGU-{cat_prefix}-CWE{cwe}"
73
+
74
+
75
+ # Same replay properties as assess/lsa/scripts/findings_to_sarif.py, so `komagu test --from-scan`
76
+ # and `--export-poc` can read either exporter's SARIF. suggested_payload is left out on purpose.
77
+ _REPLAY_KEYS = ("method", "path", "fields", "readback", "target_param")
78
+
79
+
80
+ def _replay_properties(finding: Dict[str, Any]) -> Dict[str, Any]:
81
+ props: Dict[str, Any] = {}
82
+ ep = finding.get("endpoint")
83
+ if isinstance(ep, dict) and ep.get("path"):
84
+ props["endpoint"] = {k: ep[k] for k in ("method", "path") if ep.get(k)}
85
+ av = finding.get("attack_vector")
86
+ if isinstance(av, dict):
87
+ kept = {k: av[k] for k in _REPLAY_KEYS if av.get(k) not in (None, "", {})}
88
+ if kept:
89
+ props["attack_vector"] = kept
90
+ cons = finding.get("consensus")
91
+ if isinstance(cons, dict) and isinstance(cons.get("score"), (int, float)):
92
+ props["consensus_score"] = cons["score"]
93
+ return props
73
94
 
74
95
 
75
96
  def convert_findings_to_sarif(
@@ -172,6 +193,7 @@ def convert_findings_to_sarif(
172
193
  "confidence": finding.get("confidence"),
173
194
  "cwe": cwe,
174
195
  "status": finding.get("status", "new"),
196
+ **_replay_properties(finding),
175
197
  },
176
198
  }
177
199
 
@@ -179,7 +201,7 @@ def convert_findings_to_sarif(
179
201
  result_item["relatedLocations"] = related_locations
180
202
 
181
203
  if finding.get("fingerprint"):
182
- result_item["partialFingerprints"] = {"komainu/v1": finding["fingerprint"]}
204
+ result_item["partialFingerprints"] = {"komagu/v1": finding["fingerprint"]}
183
205
 
184
206
  results.append(result_item)
185
207