@hoanghainh/komagu 1.7.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/assess/lsa/CHANGELOG.md +6 -0
  3. package/assess/lsa/VERSION +1 -1
  4. package/assess/lsa/adapters/build_adapters.py +16 -8
  5. package/assess/lsa/adapters/codex/README.md +1 -1
  6. package/assess/lsa/adapters/generic/README.md +1 -1
  7. package/assess/lsa/knowledge/severity-and-cwe.md +1 -1
  8. package/assess/lsa/scripts/baseline.py +5 -2
  9. package/assess/lsa/scripts/findings_to_sarif.py +24 -1
  10. package/assess/lsa/tests/test_adapters.py +21 -2
  11. package/assess/lsa/tests/test_baseline.py +8 -0
  12. package/assess/lsa/tests/test_contract_groundwork.py +7 -7
  13. package/operate/cli/__init__.py +1 -1
  14. package/operate/cli/commands/scan.py +2 -7
  15. package/operate/cli/commands/test.py +1 -1
  16. package/operate/cli/env.py +4 -20
  17. package/operate/cross_user_tester/persona_matrix.py +5 -1
  18. package/operate/devsecops/pipeline.py +5 -5
  19. package/operate/exploit/generator.py +5 -1
  20. package/operate/lsa_sarif/lsa_to_sarif.py +24 -2
  21. package/operate/lsa_sarif/sarif_findings.py +96 -0
  22. package/operate/remediator/runner.py +1 -5
  23. package/operate/remediator/strategies.py +1 -1
  24. package/operate/tests/test_cli_baseline.py +1 -9
  25. package/operate/tests/test_cli_env.py +8 -29
  26. package/operate/tests/test_cross_user_pipeline.py +2 -2
  27. package/operate/tests/test_exploit_and_virtual_patch.py +3 -3
  28. package/operate/tests/test_npm_package.py +2 -16
  29. package/operate/tests/test_sarif_from_scan.py +156 -0
  30. package/package.json +2 -4
  31. package/remediate/authz/README.md +1 -1
  32. package/remediate/authz/dotnet/src/Komagu.Security.Authorization/DefaultKomaguAuthorizationService.cs +1 -1
  33. package/remediate/authz/dotnet/src/Komagu.Security.Authorization/SecurityAuditEvent.cs +2 -2
  34. package/remediate/authz/golang/authz.go +1 -1
  35. package/remediate/authz/nodejs/src/service.js +1 -1
  36. package/remediate/authz/nodejs/tests/test-authz.js +1 -1
  37. package/remediate/authz/patcher.py +3 -4
  38. package/remediate/authz/python/komagu_authz/models.py +2 -2
  39. package/remediate/authz/python/tests/test_fastapi_adapter.py +1 -1
  40. package/remediate/authz/python/tests/test_fastapi_d5_crosstest.py +1 -1
  41. package/remediate/authz/python/tests/test_python_authz.py +3 -3
  42. package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/DefaultKomaguSecurityManager.kt +1 -1
  43. package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/SecurityAuditEvent.kt +1 -2
  44. package/remediate/authz/tests/test_bola_patcher.py +1 -1
  45. package/remediate/edge/coraza/rules/coraza-crs-custom.conf +3 -4
  46. package/remediate/edge/coraza/virtual_patcher.py +4 -6
  47. package/remediate/edge/docker-compose.yml +1 -1
  48. package/remediate/ekyc/server/jpki_verifier.py +2 -8
  49. package/remediate/ekyc/server/tokenization.py +2 -8
  50. package/remediate/identity/README.md +5 -5
  51. package/remediate/identity/dotnet/src/Komagu.Security.Identity/KomaguOidcOptions.cs +2 -2
  52. package/remediate/identity/dotnet/tests/Komagu.Security.Identity.Tests/TestTokenGenerator.cs +1 -1
  53. package/remediate/identity/dotnet/tests/Komagu.Security.Identity.Tests/TokenValidationTests.cs +1 -1
  54. package/remediate/identity/python/komagu_identity/oidc_validator.py +2 -8
  55. package/remediate/identity/python/tests/test_oidc_validator.py +1 -1
  56. package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/KomaguOidcProperties.kt +2 -2
  57. package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/KomaguIdentityAutoConfigurationTest.kt +2 -2
  58. package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/TestJwtFactory.kt +1 -1
  59. package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/TokenValidationIntegrationTest.kt +1 -1
  60. package/remediate/identity/test-env/realm-komagu.json +1 -1
  61. package/remediate/legacy_shims/coldfusion/KomaguSecurityShim.cfc +5 -5
  62. package/remediate/query/README.md +1 -1
  63. package/remediate/query/dotnet/src/Komagu.Security.Query.Analyzers/DiagnosticIds.cs +1 -1
  64. package/remediate/query/dotnet/src/Komagu.Security.Query.Analyzers/Komagu.Security.Query.Analyzers.csproj +1 -1
  65. package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/detekt/KomaguRuleSetProvider.kt +1 -1
  66. package/remediate/resilience/dr/dr_manager.py +2 -8
  67. package/remediate/resilience/dr/runbook.md +2 -2
  68. package/remediate/resilience/terraform/variables.tf +1 -1
  69. package/remediate/tenancy/audit/audit_trail.py +2 -8
  70. package/remediate/tenancy/crypto/byok_manager.py +2 -8
  71. package/remediate/tenancy/middleware/tenant_context.py +2 -8
  72. package/remediate/tenancy/sql/rls_isolation.sql +11 -11
  73. package/remediate/tenancy/sql/schema_per_tenant.sql +7 -7
  74. package/remediate/tenancy/test_sql_postgres.py +1 -1
  75. package/remediate/tenancy/tests/test_tenancy.py +4 -4
  76. package/bin/komainu +0 -16
  77. package/bin/komainu-legacy.js +0 -16
  78. package/remediate/ekyc/tests/test_legacy_env.py +0 -33
  79. package/remediate/identity/python/tests/test_legacy_env.py +0 -33
  80. package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/LegacyKomainuPropertiesPostProcessor.kt +0 -60
  81. package/remediate/identity/spring/src/main/resources/META-INF/spring.factories +0 -2
  82. package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/LegacyKomainuPropertiesPostProcessorTest.kt +0 -86
  83. package/remediate/resilience/tests/test_legacy_env.py +0 -33
  84. package/remediate/tenancy/tests/test_legacy_env.py +0 -49
package/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  All notable changes to `@hoanghainh/komagu` (published as `komainu-guard` up to v1.7.0) are documented here. From v1.2.2 on, entries are generated by [release-please](https://github.com/googleapis/release-please) from [Conventional Commits](https://www.conventionalcommits.org/). Earlier entries are copied from the [GitHub Releases](https://github.com/hoanghainh1188/komainu/releases).
4
4
 
5
+ ## [2.0.0](https://github.com/hoanghainh1188/komagu/compare/v1.8.0...v2.0.0) (2026-10-30)
6
+
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * rename identifiers kept as "komainu" and remove 1.x shims ([#315](https://github.com/hoanghainh1188/komagu/issues/315))
11
+ Rule IDs, the SARIF fingerprint key, audit event and logger names, WAF tags, the default token audience, the PostgreSQL role and Terraform resource names change; `KOMAINU_*` env vars, `komainu.security.*` properties and the `komainu` npm bins are removed. Read [Migrating to Komagu 2.0](https://github.com/hoanghainh1188/komagu/blob/main/docs/migration-2.0.md) before upgrading.
12
+
13
+ ### Features
14
+
15
+ * rename identifiers kept as "komainu" and remove 1.x shims ([#315](https://github.com/hoanghainh1188/komagu/issues/315)) ([cbc63de](https://github.com/hoanghainh1188/komagu/commit/cbc63de3c4e1f90985420b74ba5211740ef4fe25))
16
+ * **test:** read BFLA and mass assignment findings from SARIF in --from-scan and --export-poc ([#313](https://github.com/hoanghainh1188/komagu/issues/313)) ([f91c2fa](https://github.com/hoanghainh1188/komagu/commit/f91c2fa345972bb8da91c503537cafaec7e4ed7b))
17
+
18
+ ## [1.8.0](https://github.com/hoanghainh1188/komagu/compare/v1.7.0...v1.8.0) (2026-10-30)
19
+
20
+
21
+ ### Features
22
+
23
+ * **cli:** rename CLI and npm package to komagu (@hoanghainh/komagu) ([#296](https://github.com/hoanghainh1188/komagu/issues/296)) ([a37e8ec](https://github.com/hoanghainh1188/komagu/commit/a37e8ecae013b851dc8d69ba6c9ad7ddc600ae78))
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * **cli:** show "Komagu" in the ASCII banner; point npm links to @hoanghainh/komagu ([#309](https://github.com/hoanghainh1188/komagu/issues/309)) ([533d9b0](https://github.com/hoanghainh1188/komagu/commit/533d9b0de0511825a0704c1f9b2eb5f6546632f7))
29
+ * **spring:** make the member-portal Spring example compile and start ([#305](https://github.com/hoanghainh1188/komagu/issues/305)) ([578197d](https://github.com/hoanghainh1188/komagu/commit/578197d891b2905e03996cd3dbfa9c86445966e0))
30
+
31
+
32
+ ### Code Refactoring
33
+
34
+ * **dotnet:** rename .NET modules to Komagu.Security.* ([#298](https://github.com/hoanghainh1188/komagu/issues/298)) ([e8ca9d8](https://github.com/hoanghainh1188/komagu/commit/e8ca9d8feffdd6393f37aea8fa1234fe46e73505))
35
+ * **remediate:** rename Python, Go, Node.js, shims and edge modules to Komagu ([#300](https://github.com/hoanghainh1188/komagu/issues/300)) ([54e3daf](https://github.com/hoanghainh1188/komagu/commit/54e3daf8fec80e40626eabb39d9679e834729615))
36
+ * **spring:** rename Spring/Kotlin modules to io.komagu ([#299](https://github.com/hoanghainh1188/komagu/issues/299)) ([7202f6e](https://github.com/hoanghainh1188/komagu/commit/7202f6eef289124761b5f87420c27383be347c92))
37
+
38
+
39
+ ### Documentation
40
+
41
+ * **changelog:** name the package @hoanghainh/komagu in the header ([#307](https://github.com/hoanghainh1188/komagu/issues/307)) ([c773ba9](https://github.com/hoanghainh1188/komagu/commit/c773ba9b8e871b8b7da04c6c2d4df2c6d2eb31cc))
42
+ * rename LSA, contracts, docs and demo app to Komagu; add ADR-0007 ([#303](https://github.com/hoanghainh1188/komagu/issues/303)) ([9d193d9](https://github.com/hoanghainh1188/komagu/commit/9d193d94f607afb9917fedbe9d4593ca9f8f57c0))
43
+
5
44
  ## [1.7.0](https://github.com/hoanghainh1188/komainu/compare/v1.6.0...v1.7.0) (2026-10-30)
6
45
 
7
46
 
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1 — 2026-10-30
4
+
5
+ SARIF replay properties (#289).
6
+
7
+ - **`findings_to_sarif.py`:** each SARIF result's `properties` now carries `finding_id`, `category`, `endpoint`, `attack_vector` (`method`, `path`, `fields`, `readback`, `target_param`) and `consensus_score`, so `komagu test --from-scan` and `--export-poc` can replay BFLA and mass assignment findings from SARIF. `suggested_payload` is not exported, to keep attack strings out of code scanning.
8
+
3
9
  ## 0.2.3 — 2026-10-26
4
10
 
5
11
  Runtime adapters (#244).
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.3.1
@@ -118,8 +118,11 @@ Scripts: `python3 {ref}/scripts/<script>.py` (Python 3.8+, standard library only
118
118
  print("Next step: In your Claude Code workspace, prompt: 'Run the legacy-security-audit skill. TARGET=src, OUT=lsa-out'")
119
119
 
120
120
 
121
- AGENTS_MD_BEGIN = "<!-- komainu-lsa:begin (generated by adapters/build_adapters.py; edits inside this block are overwritten) -->"
122
- AGENTS_MD_END = "<!-- komainu-lsa:end -->"
121
+ AGENTS_MD_BEGIN = "<!-- komagu-lsa:begin (generated by adapters/build_adapters.py; edits inside this block are overwritten) -->"
122
+ AGENTS_MD_END = "<!-- komagu-lsa:end -->"
123
+ # Pre-2.0 markers, still replaced in place for one major release (#312)
124
+ LEGACY_AGENTS_MD_BEGIN = "<!-- komainu-lsa:begin (generated by adapters/build_adapters.py; edits inside this block are overwritten) -->"
125
+ LEGACY_AGENTS_MD_END = "<!-- komainu-lsa:end -->"
123
126
 
124
127
 
125
128
  def toml_multiline(text: str) -> str:
@@ -156,13 +159,16 @@ def agents_md_with_block(path: Path, block: str) -> str:
156
159
  raise ValueError(f"{path} is not UTF-8 ({e}); convert it, then re-run") from e
157
160
  nl = "\r\n" if "\r\n" in current else "\n"
158
161
  section = f"{AGENTS_MD_BEGIN}\n{block.strip()}\n{AGENTS_MD_END}\n".replace("\n", nl)
159
- begins, ends = current.count(AGENTS_MD_BEGIN), current.count(AGENTS_MD_END)
162
+ begin_marker, end_marker = AGENTS_MD_BEGIN, AGENTS_MD_END
163
+ if begin_marker not in current and end_marker not in current:
164
+ begin_marker, end_marker = LEGACY_AGENTS_MD_BEGIN, LEGACY_AGENTS_MD_END
165
+ begins, ends = current.count(begin_marker), current.count(end_marker)
160
166
  if begins == 0 and ends == 0:
161
167
  return section if not current.strip() else current.rstrip("\r\n") + nl + nl + section
162
- start, stop = current.find(AGENTS_MD_BEGIN), current.find(AGENTS_MD_END)
168
+ start, stop = current.find(begin_marker), current.find(end_marker)
163
169
  if begins != 1 or ends != 1 or stop < start:
164
- raise ValueError(f"{path} has unbalanced komainu-lsa markers; remove them by hand, then re-run")
165
- stop += len(AGENTS_MD_END)
170
+ raise ValueError(f"{path} has unbalanced komagu-lsa markers; remove them by hand, then re-run")
171
+ stop += len(end_marker)
166
172
  if current.startswith(nl, stop):
167
173
  stop += len(nl)
168
174
  return current[:start] + section + current[stop:]
@@ -252,7 +258,9 @@ def build_cursor(dest: Path) -> None:
252
258
  print("Next step: In Cursor, open a new chat and invoke: '@lsa-orchestrator run security audit on TARGET=src, OUT=lsa-out'")
253
259
 
254
260
 
255
- GENERATOR = "komainu-lsa"
261
+ GENERATOR = "komagu-lsa"
262
+ # contract.json files written before 2.0 carry the old value; still recognised for one major release (#312)
263
+ KNOWN_GENERATORS = (GENERATOR, "komainu-lsa")
256
264
 
257
265
 
258
266
  def check_generic_dest(dest: Path) -> None:
@@ -266,7 +274,7 @@ def check_generic_dest(dest: Path) -> None:
266
274
  data = json.loads(contract.read_text(encoding="utf-8"))
267
275
  except (UnicodeDecodeError, ValueError):
268
276
  data = None
269
- if not isinstance(data, dict) or data.get("generator") != GENERATOR:
277
+ if not isinstance(data, dict) or data.get("generator") not in KNOWN_GENERATORS:
270
278
  raise ValueError(f"{contract} was not generated by Komagu LSA (no \"generator\": \"{GENERATOR}\"); "
271
279
  "choose an empty DEST such as ./lsa-bundle, or move that file away")
272
280
  return
@@ -15,7 +15,7 @@ This creates:
15
15
  directory up to the repository root.
16
16
  - `.codex/agents/lsa-*.toml` — one project custom agent per role
17
17
  (`name`, `description`, `developer_instructions`).
18
- - `AGENTS.md` — an LSA section between `komainu-lsa:begin` / `komainu-lsa:end`
18
+ - `AGENTS.md` — an LSA section between `komagu-lsa:begin` / `komagu-lsa:end`
19
19
  markers. Anything you write outside the markers is kept; re-running the
20
20
  generator only replaces the section.
21
21
 
@@ -8,7 +8,7 @@ komagu init --ai generic ./lsa-bundle
8
8
  ```
9
9
 
10
10
  It writes `system-prompts/`, `kit/` and `contract.json` into that folder.
11
- The contract carries `"generator": "komainu-lsa"`. Re-running refreshes a
11
+ The contract carries `"generator": "komagu-lsa"`. Re-running refreshes a
12
12
  bundle that has this marker. The generator refuses, before writing anything,
13
13
  when the folder has a `contract.json` without the marker, or a `kit/` or
14
14
  `system-prompts/` folder but no `contract.json`. This keeps a project's own
@@ -43,7 +43,7 @@ Finding id prefixes: `BOLA-`, `INJ-`, `BFLA-`, `MASS-`.
43
43
 
44
44
  `fingerprint` is optional. When present it has the form `kfp1:<64 lowercase hex>`
45
45
  and must not depend on line numbers, so the same issue keeps the same value
46
- across scans. Exporters copy it to SARIF `partialFingerprints["komainu/v1"]`.
46
+ across scans. Exporters copy it to SARIF `partialFingerprints["komagu/v1"]`.
47
47
 
48
48
  ## SARIF level mapping
49
49
 
@@ -35,7 +35,9 @@ from trend import append_history, mttr_by_severity, snapshot # noqa: E402
35
35
 
36
36
  SEVERITY_RANK = {"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}
37
37
  FAIL_ON = ("none", "new", "any")
38
- SARIF_FINGERPRINT_KEY = "komainu/v1"
38
+ SARIF_FINGERPRINT_KEY = "komagu/v1"
39
+ # SARIF baselines written before 2.0 use the old key; still read for one major release (#312)
40
+ LEGACY_SARIF_FINGERPRINT_KEY = "komainu/v1"
39
41
 
40
42
 
41
43
  class BaselineError(Exception):
@@ -57,7 +59,8 @@ def _entries_from_sarif(doc: Dict) -> Dict[str, Dict]:
57
59
  entries: Dict[str, Dict] = {}
58
60
  for run in doc.get("runs") or []:
59
61
  for result in run.get("results") or []:
60
- fp = (result.get("partialFingerprints") or {}).get(SARIF_FINGERPRINT_KEY)
62
+ fingerprints = result.get("partialFingerprints") or {}
63
+ fp = fingerprints.get(SARIF_FINGERPRINT_KEY) or fingerprints.get(LEGACY_SARIF_FINGERPRINT_KEY)
61
64
  if not fp:
62
65
  continue
63
66
  props = result.get("properties") or {}
@@ -132,6 +132,28 @@ OWASP_TAGS = {
132
132
  }
133
133
 
134
134
 
135
+ # Attack-vector keys that `komagu test --from-scan` and `--export-poc` need to replay a
136
+ # finding from SARIF. suggested_payload is left out so attack strings stay out of code scanning.
137
+ REPLAY_KEYS = ("method", "path", "fields", "readback", "target_param")
138
+
139
+
140
+ def replay_properties(f: Dict[str, Any]) -> Dict[str, Any]:
141
+ """Result properties that let a SARIF result be replayed like a findings.json entry."""
142
+ props: Dict[str, Any] = {"finding_id": f.get("finding_id"), "category": f.get("category", "bola")}
143
+ ep = f.get("endpoint")
144
+ if isinstance(ep, dict) and ep.get("path"):
145
+ props["endpoint"] = {k: ep[k] for k in ("method", "path") if ep.get(k)}
146
+ av = f.get("attack_vector")
147
+ if isinstance(av, dict):
148
+ kept = {k: av[k] for k in REPLAY_KEYS if av.get(k) not in (None, "", {})}
149
+ if kept:
150
+ props["attack_vector"] = kept
151
+ cons = f.get("consensus")
152
+ if isinstance(cons, dict) and isinstance(cons.get("score"), (int, float)):
153
+ props["consensus_score"] = cons["score"]
154
+ return props
155
+
156
+
135
157
  def normalize_cwe_id(cwe_val: Any) -> str:
136
158
  """Normalize numeric or prefixed CWE ID to standard 'CWE-XXX' format."""
137
159
  s = str(cwe_val).strip()
@@ -319,7 +341,7 @@ def to_sarif(
319
341
 
320
342
  fingerprints = {"lsaFindingId": f.get("finding_id", f"LSA-{len(results)+1}")}
321
343
  if f.get("fingerprint"):
322
- fingerprints["komainu/v1"] = f["fingerprint"]
344
+ fingerprints["komagu/v1"] = f["fingerprint"]
323
345
 
324
346
  result_taxa = []
325
347
  for c in norm_cwes:
@@ -352,6 +374,7 @@ def to_sarif(
352
374
  "confidence": f.get("confidence", "high"),
353
375
  "status": f.get("status", "unverified"),
354
376
  "cwe": norm_cwes,
377
+ **replay_properties(f),
355
378
  },
356
379
  }
357
380
  state = f.get("baseline_state")
@@ -93,6 +93,17 @@ class TestCodexAdapter(unittest.TestCase):
93
93
  self.assertEqual(text.count(AGENTS_MD_BEGIN), 1)
94
94
  self.assertEqual(text.count(AGENTS_MD_END), 1)
95
95
 
96
+ def test_pre_2_0_markers_are_replaced_in_place(self) -> None:
97
+ old = (f"# Notes\n\n{build_adapters.LEGACY_AGENTS_MD_BEGIN}\nold block\n"
98
+ f"{build_adapters.LEGACY_AGENTS_MD_END}\n\n# Footer\n")
99
+ (self.dest / "AGENTS.md").write_text(old, encoding="utf-8")
100
+ build("codex", self.dest)
101
+ text = (self.dest / "AGENTS.md").read_text(encoding="utf-8")
102
+ self.assertNotIn("komainu-lsa", text)
103
+ self.assertNotIn("old block", text)
104
+ self.assertEqual(text.count(AGENTS_MD_BEGIN), 1)
105
+ self.assertTrue(text.startswith("# Notes\n\n") and text.endswith("# Footer\n"))
106
+
96
107
  def test_unbalanced_markers_fail_before_touching_workspace(self) -> None:
97
108
  user = f"# Notes\n{AGENTS_MD_BEGIN}\nimportant user text\n"
98
109
  (self.dest / "AGENTS.md").write_text(user, encoding="utf-8")
@@ -187,7 +198,7 @@ class TestGenericAdapterGuard(unittest.TestCase):
187
198
 
188
199
  def test_contract_carries_generator_marker(self) -> None:
189
200
  build("generic", self.dest)
190
- self.assertEqual(json.loads(self.contract.read_text(encoding="utf-8"))["generator"], "komainu-lsa")
201
+ self.assertEqual(json.loads(self.contract.read_text(encoding="utf-8"))["generator"], "komagu-lsa")
191
202
 
192
203
  def test_rebuild_refreshes_generated_contract(self) -> None:
193
204
  build("generic", self.dest)
@@ -197,6 +208,14 @@ class TestGenericAdapterGuard(unittest.TestCase):
197
208
  build("generic", self.dest)
198
209
  self.assertEqual(json.loads(self.contract.read_text(encoding="utf-8"))["kit_version"], build_adapters.VERSION)
199
210
 
211
+ def test_rebuild_accepts_pre_2_0_generator(self) -> None:
212
+ build("generic", self.dest)
213
+ data = json.loads(self.contract.read_text(encoding="utf-8"))
214
+ data["generator"] = "komainu-lsa"
215
+ self.contract.write_text(json.dumps(data), encoding="utf-8")
216
+ build("generic", self.dest)
217
+ self.assertEqual(json.loads(self.contract.read_text(encoding="utf-8"))["generator"], "komagu-lsa")
218
+
200
219
  def test_foreign_contract_fails_before_touching_workspace(self) -> None:
201
220
  self.contract.write_text('{"name": "my-project-contract"}', encoding="utf-8")
202
221
  with self.assertRaisesRegex(ValueError, "contract.json"):
@@ -211,7 +230,7 @@ class TestGenericAdapterGuard(unittest.TestCase):
211
230
  self.assert_untouched("contract.json")
212
231
 
213
232
  def test_non_object_contract_fails(self) -> None:
214
- self.contract.write_text('["komainu-lsa"]', encoding="utf-8")
233
+ self.contract.write_text('["komagu-lsa"]', encoding="utf-8")
215
234
  with self.assertRaises(ValueError):
216
235
  build("generic", self.dest)
217
236
 
@@ -198,6 +198,14 @@ class TestLoadBaseline(unittest.TestCase):
198
198
  entries = load_baseline(self.dir / "prev.sarif")
199
199
  self.assertEqual(entries["kfp1:" + "e" * 64]["state"], "open")
200
200
 
201
+ def test_loads_pre_2_0_sarif_fingerprint_key(self) -> None:
202
+ sarif = to_sarif({"findings": [{**_finding(), "fingerprint": "kfp1:" + "f" * 64}]})
203
+ for result in sarif["runs"][0]["results"]:
204
+ fps = result["partialFingerprints"]
205
+ fps["komainu/v1"] = fps.pop("komagu/v1")
206
+ write_json(self.dir / "old.sarif", sarif)
207
+ self.assertIn("kfp1:" + "f" * 64, load_baseline(self.dir / "old.sarif"))
208
+
201
209
  def test_invalid_file_raises(self) -> None:
202
210
  (self.dir / "bad.json").write_text("{not json", encoding="utf-8")
203
211
  with self.assertRaises(BaselineError):
@@ -156,9 +156,9 @@ class TestLsaSarifExport(unittest.TestCase):
156
156
  by_rule = {r["ruleId"]: r for r in self.run["results"]}
157
157
  mass_fp = by_rule["lsa/mass_assignment/whole_object_binding"]["partialFingerprints"]
158
158
  bfla_fp = by_rule["lsa/bfla/missing_role_check"]["partialFingerprints"]
159
- self.assertEqual(mass_fp["komainu/v1"], FINGERPRINT)
159
+ self.assertEqual(mass_fp["komagu/v1"], FINGERPRINT)
160
160
  self.assertEqual(mass_fp["lsaFindingId"], "MASS-R-0002-1")
161
- self.assertNotIn("komainu/v1", bfla_fp)
161
+ self.assertNotIn("komagu/v1", bfla_fp)
162
162
 
163
163
 
164
164
  class TestOperateSarifExport(unittest.TestCase):
@@ -166,16 +166,16 @@ class TestOperateSarifExport(unittest.TestCase):
166
166
  sarif = convert_findings_to_sarif([_finding(cwe=285), {**_mass_assignment(), "cwe": 915}])
167
167
  run = sarif["runs"][0]
168
168
  rule_ids = {r["id"] for r in run["tool"]["driver"]["rules"]}
169
- self.assertEqual(rule_ids, {"KOMAINU-BFLA-CWE285", "KOMAINU-MASS-CWE915"})
170
- mass = next(r for r in run["results"] if r["ruleId"] == "KOMAINU-MASS-CWE915")
171
- self.assertEqual(mass["partialFingerprints"], {"komainu/v1": FINGERPRINT})
172
- bfla = next(r for r in run["results"] if r["ruleId"] == "KOMAINU-BFLA-CWE285")
169
+ self.assertEqual(rule_ids, {"KOMAGU-BFLA-CWE285", "KOMAGU-MASS-CWE915"})
170
+ mass = next(r for r in run["results"] if r["ruleId"] == "KOMAGU-MASS-CWE915")
171
+ self.assertEqual(mass["partialFingerprints"], {"komagu/v1": FINGERPRINT})
172
+ bfla = next(r for r in run["results"] if r["ruleId"] == "KOMAGU-BFLA-CWE285")
173
173
  self.assertNotIn("partialFingerprints", bfla)
174
174
 
175
175
  def test_default_cwe_for_new_categories(self) -> None:
176
176
  sarif = convert_findings_to_sarif([_finding(cwe=None), {**_mass_assignment(), "cwe": None}])
177
177
  rule_ids = {r["id"] for r in sarif["runs"][0]["tool"]["driver"]["rules"]}
178
- self.assertEqual(rule_ids, {"KOMAINU-BFLA-CWE285", "KOMAINU-MASS-CWE915"})
178
+ self.assertEqual(rule_ids, {"KOMAGU-BFLA-CWE285", "KOMAGU-MASS-CWE915"})
179
179
 
180
180
 
181
181
  class TestSummaryAndReports(unittest.TestCase):
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "1.7.0" # x-release-please-version
5
+ __version__ = "2.0.0" # 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:
@@ -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
 
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env python3
2
+ """Read LSA SARIF results back as findings.json-style entries (#289).
3
+
4
+ `komagu test --from-scan` and `--export-poc` work on findings.json entries. This lets them take
5
+ the SARIF that `komagu scan` writes instead. Both LSA exporters (assess/lsa/scripts/findings_to_sarif.py
6
+ and operate/lsa_sarif/lsa_to_sarif.py) put finding_id, category, endpoint, attack_vector and
7
+ consensus_score in each result's properties; older SARIF without them falls back to the rule id,
8
+ the OWASP rule tags and the "[METHOD /path]" part of the message.
9
+
10
+ Python standard library only.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ # OWASP API Top 10 rule tags written by findings_to_sarif.py, for SARIF whose ruleId has no category
19
+ _TAG_CATEGORY = {
20
+ "owasp-api1:2023-bola": "bola",
21
+ "owasp-api5:2023-bfla": "bfla",
22
+ "owasp-api3:2023-bopla": "mass_assignment",
23
+ }
24
+ # Rule id prefixes written by operate/lsa_sarif/lsa_to_sarif.py (KOMAGU-<PREFIX>-CWE<n>;
25
+ # 1.x SARIF files use KOMAINU-, still read for one major release, #312)
26
+ _LEGACY_PREFIX_CATEGORY = {"BOLA": "bola", "BFLA": "bfla", "MASS": "mass_assignment", "INJ": "sql_injection"}
27
+ _ENDPOINT_IN_MESSAGE = re.compile(r"\[(GET|POST|PUT|PATCH|DELETE)(?:\|[A-Z]+)*\s+(/[^\]\s]*)\]")
28
+
29
+
30
+ def is_sarif(data: Any) -> bool:
31
+ return isinstance(data, dict) and isinstance(data.get("runs"), list)
32
+
33
+
34
+ def _rule_tags(run: Dict[str, Any]) -> Dict[str, List[str]]:
35
+ rules = ((run.get("tool") or {}).get("driver") or {}).get("rules") or []
36
+ return {
37
+ str(r.get("id", "")): [str(t).lower() for t in ((r.get("properties") or {}).get("tags") or [])]
38
+ for r in rules
39
+ if isinstance(r, dict)
40
+ }
41
+
42
+
43
+ def _category(rule_id: str, props: Dict[str, Any], tags: List[str]) -> Optional[str]:
44
+ if props.get("category"):
45
+ return str(props["category"]).lower()
46
+ if rule_id.startswith("lsa/"):
47
+ parts = rule_id.split("/")
48
+ if len(parts) > 1 and parts[1]:
49
+ return parts[1].lower()
50
+ m = re.match(r"(?:KOMAINU|KOMAGU)-([A-Z]+)-", rule_id)
51
+ if m and m.group(1) in _LEGACY_PREFIX_CATEGORY:
52
+ return _LEGACY_PREFIX_CATEGORY[m.group(1)]
53
+ for tag in tags:
54
+ if tag in _TAG_CATEGORY:
55
+ return _TAG_CATEGORY[tag]
56
+ return None
57
+
58
+
59
+ def findings_from_sarif(data: Dict[str, Any]) -> List[Dict[str, Any]]:
60
+ """Return one findings.json-style dict per SARIF result that names an endpoint."""
61
+ findings: List[Dict[str, Any]] = []
62
+ for run in data.get("runs") or []:
63
+ if not isinstance(run, dict):
64
+ continue
65
+ tags_by_rule = _rule_tags(run)
66
+ for result in run.get("results") or []:
67
+ if not isinstance(result, dict):
68
+ continue
69
+ rule_id = str(result.get("ruleId", ""))
70
+ props = result.get("properties") or {}
71
+ category = _category(rule_id, props, tags_by_rule.get(rule_id, []))
72
+ if category is None:
73
+ continue
74
+ message = str((result.get("message") or {}).get("text", ""))
75
+ endpoint = props.get("endpoint") if isinstance(props.get("endpoint"), dict) else None
76
+ if not endpoint:
77
+ m = _ENDPOINT_IN_MESSAGE.search(message)
78
+ endpoint = {"method": m.group(1), "path": m.group(2)} if m else None
79
+ attack_vector = props.get("attack_vector") if isinstance(props.get("attack_vector"), dict) else None
80
+ if not endpoint and not (attack_vector and attack_vector.get("path")):
81
+ continue
82
+ finding_id = props.get("finding_id") or (result.get("partialFingerprints") or {}).get("lsaFindingId") or rule_id
83
+ finding: Dict[str, Any] = {
84
+ "finding_id": str(finding_id),
85
+ "category": category,
86
+ "title": message.split(" [", 1)[0] or rule_id,
87
+ "status": props.get("status", "confirmed"),
88
+ }
89
+ if endpoint:
90
+ finding["endpoint"] = dict(endpoint)
91
+ if attack_vector:
92
+ finding["attack_vector"] = dict(attack_vector)
93
+ if isinstance(props.get("consensus_score"), (int, float)):
94
+ finding["consensus"] = {"score": props["consensus_score"]}
95
+ findings.append(finding)
96
+ return findings