@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
@@ -191,10 +191,6 @@ class RemediationRunner:
191
191
  self.target_dir / "out" / "findings.json",
192
192
  self.target_dir / ".komagu-devsecops" / "sca.sarif",
193
193
  self.target_dir / ".komagu-devsecops" / "komagu-devsecops.sarif",
194
- # Pre-rename output locations (#290)
195
- self.target_dir / "komainu-out" / "findings.json",
196
- self.target_dir / ".komainu-devsecops" / "sca.sarif",
197
- self.target_dir / ".komainu-devsecops" / "komainu-devsecops.sarif",
198
194
  ]
199
195
  for candidate in candidates:
200
196
  if candidate.is_file():
@@ -236,7 +232,7 @@ class RemediationRunner:
236
232
  rule_upper = rule_id.upper()
237
233
 
238
234
  if (
239
- rule_upper.startswith(("CVE-", "GHSA-", "OSV-", "KOMAINU-SCA-", "DEP-"))
235
+ rule_upper.startswith(("CVE-", "GHSA-", "OSV-", "KOMAGU-SCA-", "DEP-"))
240
236
  or base_name in self.MANIFEST_FILENAMES
241
237
  or base_name.endswith(".csproj")
242
238
  or any(k in driver_name for k in ("trivy", "sca", "dependency", "osv", "pip-audit", "govulncheck"))
@@ -139,7 +139,7 @@ class DependencyVulnerabilityRemediationStrategy(BaseRemediationStrategy):
139
139
  return True
140
140
  if any(code in cwe for code in ("CWE-1395", "CWE-1104", "CWE-937")):
141
141
  return True
142
- if finding_id.startswith(("CVE-", "GHSA-", "OSV-", "KOMAINU-SCA-", "DEP-")):
142
+ if finding_id.startswith(("CVE-", "GHSA-", "OSV-", "KOMAGU-SCA-", "DEP-")):
143
143
  return True
144
144
  if base_name in self.MANIFEST_FILES or base_name.endswith(".csproj"):
145
145
  return True
@@ -58,7 +58,7 @@ class TestScanBaseline(unittest.TestCase):
58
58
  def test_default_scan_adds_fingerprint_and_passes(self) -> None:
59
59
  res = self.scan()
60
60
  self.assertEqual(res.returncode, 0, res.stderr + res.stdout)
61
- self.assertTrue(self.sarif_result()["partialFingerprints"]["komainu/v1"].startswith("kfp1:"))
61
+ self.assertTrue(self.sarif_result()["partialFingerprints"]["komagu/v1"].startswith("kfp1:"))
62
62
 
63
63
  def test_fail_on_new_without_baseline_fails(self) -> None:
64
64
  res = self.scan("--fail-on", "new")
@@ -117,14 +117,6 @@ class TestScanSuppressions(unittest.TestCase):
117
117
  sarif = json.loads((self.out / "findings.sarif").read_text(encoding="utf-8"))
118
118
  self.assertEqual(sarif["runs"][0]["results"][0]["suppressions"][0]["status"], "accepted")
119
119
 
120
- def test_legacy_komainu_suppressions_dir_still_used(self) -> None:
121
- self.write_suppressions(self.target / ".komainu" / "suppressions.json", "2999-12-31")
122
- res = self.scan("--fail-on", "new")
123
- self.assertEqual(res.returncode, 0, res.stderr + res.stdout)
124
- self.assertIn(".komainu/suppressions.json is deprecated", res.stderr)
125
- sarif = json.loads((self.out / "findings.sarif").read_text(encoding="utf-8"))
126
- self.assertEqual(sarif["runs"][0]["results"][0]["suppressions"][0]["status"], "accepted")
127
-
128
120
  def test_expired_explicit_suppression_fails_gate_and_warns(self) -> None:
129
121
  path = self.out.parent / "suppressions.json"
130
122
  self.write_suppressions(path, "2020-01-01")
@@ -1,44 +1,23 @@
1
1
  #!/usr/bin/env python3
2
- """KOMAGU_* environment variables with the deprecated KOMAINU_* fallback (#290)."""
2
+ """KOMAGU_* environment variables; the pre-rename KOMAINU_* names are no longer read (#312)."""
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- import contextlib
7
- import io
8
6
  import unittest
9
7
 
10
8
  from operate.cli import env as cli_env
11
9
 
12
10
 
13
11
  class TestGetenv(unittest.TestCase):
14
- def setUp(self) -> None:
15
- cli_env._warned.clear()
16
-
17
- def lookup(self, environ, default=None):
18
- stderr = io.StringIO()
19
- with contextlib.redirect_stderr(stderr):
20
- value = cli_env.getenv("AI_MODEL", default, env=environ)
21
- return value, stderr.getvalue()
22
-
23
- def test_new_name_wins_without_warning(self) -> None:
24
- value, err = self.lookup({"KOMAGU_AI_MODEL": "new", "KOMAINU_AI_MODEL": "old"})
25
- self.assertEqual(value, "new")
26
- self.assertEqual(err, "")
27
-
28
- def test_legacy_name_is_read_with_warning(self) -> None:
29
- value, err = self.lookup({"KOMAINU_AI_MODEL": "old"})
30
- self.assertEqual(value, "old")
31
- self.assertIn("KOMAINU_AI_MODEL is deprecated, set KOMAGU_AI_MODEL", err)
32
-
33
- def test_legacy_warning_is_printed_once(self) -> None:
34
- self.lookup({"KOMAINU_AI_MODEL": "old"})
35
- _, err = self.lookup({"KOMAINU_AI_MODEL": "old"})
36
- self.assertEqual(err, "")
12
+ def test_reads_komagu_name(self) -> None:
13
+ self.assertEqual(cli_env.getenv("AI_MODEL", env={"KOMAGU_AI_MODEL": "new"}), "new")
14
+
15
+ def test_legacy_name_is_ignored(self) -> None:
16
+ self.assertEqual(cli_env.getenv("AI_MODEL", "llama3.1", env={"KOMAINU_AI_MODEL": "old"}), "llama3.1")
37
17
 
38
18
  def test_default_when_unset(self) -> None:
39
- value, err = self.lookup({}, default="llama3.1")
40
- self.assertEqual(value, "llama3.1")
41
- self.assertEqual(err, "")
19
+ self.assertEqual(cli_env.getenv("AI_MODEL", "llama3.1", env={}), "llama3.1")
20
+ self.assertIsNone(cli_env.getenv("AI_MODEL", env={}))
42
21
 
43
22
 
44
23
  if __name__ == "__main__":
@@ -208,7 +208,7 @@ class TestCrossUserPipeline(unittest.TestCase):
208
208
 
209
209
  # Check BOLA result mapping
210
210
  bola_res = run["results"][0]
211
- self.assertEqual(bola_res["ruleId"], "KOMAINU-BOLA-CWE639")
211
+ self.assertEqual(bola_res["ruleId"], "KOMAGU-BOLA-CWE639")
212
212
  self.assertEqual(bola_res["level"], "error")
213
213
  loc = bola_res["locations"][0]["physicalLocation"]
214
214
  self.assertEqual(loc["artifactLocation"]["uri"], "legacy/orders.php")
@@ -217,7 +217,7 @@ class TestCrossUserPipeline(unittest.TestCase):
217
217
 
218
218
  # Check SQLi result mapping
219
219
  sqli_res = run["results"][1]
220
- self.assertEqual(sqli_res["ruleId"], "KOMAINU-INJ-CWE89")
220
+ self.assertEqual(sqli_res["ruleId"], "KOMAGU-INJ-CWE89")
221
221
  self.assertEqual(sqli_res["level"], "error")
222
222
 
223
223
  def test_sarif_cli_execution(self) -> None:
@@ -111,9 +111,9 @@ class TestExploitAndVirtualPatcher(unittest.TestCase):
111
111
  self.assertIn("SecRule REQUEST_URI", conf_content)
112
112
  self.assertIn("id:1100001", conf_content)
113
113
  self.assertIn("id:1100002", conf_content)
114
- self.assertIn("tag:'komainu-virtual-patch'", conf_content)
115
- self.assertIn("tag:'komainu-bola'", conf_content)
116
- self.assertIn("tag:'komainu-sqli'", conf_content)
114
+ self.assertIn("tag:'komagu-virtual-patch'", conf_content)
115
+ self.assertIn("tag:'komagu-bola'", conf_content)
116
+ self.assertIn("tag:'komagu-sqli'", conf_content)
117
117
 
118
118
  def test_cli_protect_with_virtual_patch(self) -> None:
119
119
  """komagu protect --virtual-patch must synthesize rules via CLI."""
@@ -14,7 +14,6 @@ from pathlib import Path
14
14
  REPO_ROOT = Path(__file__).resolve().parents[2]
15
15
  PACKAGE_JSON = REPO_ROOT / "package.json"
16
16
  BIN_KOMAGU_JS = REPO_ROOT / "bin" / "komagu.js"
17
- BIN_LEGACY_JS = REPO_ROOT / "bin" / "komainu-legacy.js"
18
17
  WORKFLOW_FILE = REPO_ROOT / ".github" / "workflows" / "npm-publish.yml"
19
18
  RELEASE_PLEASE_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "release-please.yml"
20
19
  RELEASE_PLEASE_CONFIG = REPO_ROOT / "release-please-config.json"
@@ -35,9 +34,8 @@ class TestNPMPackage(unittest.TestCase):
35
34
  self.assertEqual(data.get("publishConfig", {}).get("access"), "public")
36
35
  self.assertIn("version", data)
37
36
  self.assertEqual(data.get("bin", {}).get("komagu"), "bin/komagu.js")
38
- # Deprecated pre-rename aliases (#290)
39
- self.assertEqual(data.get("bin", {}).get("komainu"), "bin/komainu-legacy.js")
40
- self.assertEqual(data.get("bin", {}).get("komainu-guard"), "bin/komainu-legacy.js")
37
+ # Pre-rename aliases were removed in 2.0 (#312)
38
+ self.assertEqual(set(data.get("bin", {})), {"komagu"})
41
39
  self.assertEqual(data.get("license"), "Apache-2.0")
42
40
 
43
41
  # Whitelisted files must include platform modules
@@ -72,18 +70,6 @@ class TestNPMPackage(unittest.TestCase):
72
70
  self.assertIn("test", res_h.stdout)
73
71
  self.assertIn("demo", res_h.stdout)
74
72
 
75
- def test_legacy_bin_alias_warns_and_delegates(self) -> None:
76
- """Deprecated komainu / komainu-guard bins must print a rename notice and still run (#290)."""
77
- node_bin = shutil.which("node")
78
- if not node_bin:
79
- self.skipTest("Node.js runtime not installed on host")
80
-
81
- self.assertTrue(BIN_LEGACY_JS.stat().st_mode & 0o111 != 0, "bin/komainu-legacy.js is not executable")
82
- res = subprocess.run([node_bin, str(BIN_LEGACY_JS), "--version"], capture_output=True, text=True)
83
- self.assertEqual(res.returncode, 0, res.stderr)
84
- self.assertIn("komagu v", res.stdout)
85
- self.assertIn("renamed to `komagu`", res.stderr)
86
-
87
73
  def test_node_launcher_error_on_missing_python(self) -> None:
88
74
  """Launcher must show friendly error with Docker fallback if Python is missing."""
89
75
  node_bin = shutil.which("node")
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env python3
2
+ """SARIF input for BFLA and mass assignment in `komagu test --from-scan` and `--export-poc` (#289)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import copy
7
+ import importlib.util
8
+ import io
9
+ import json
10
+ import os
11
+ import sys
12
+ import tempfile
13
+ import unittest
14
+ from contextlib import redirect_stdout
15
+ from pathlib import Path
16
+ from unittest import mock
17
+
18
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
19
+ if str(PROJECT_ROOT) not in sys.path:
20
+ sys.path.insert(0, str(PROJECT_ROOT))
21
+
22
+ from operate.cli.main import main as cli_main # noqa: E402
23
+ from operate.cross_user_tester.mock_test_server import start_mock_server # noqa: E402
24
+ from operate.cross_user_tester.persona_matrix import generate_matrix_cases_from_findings # noqa: E402
25
+ from operate.exploit.generator import ExploitGenerator # noqa: E402
26
+ from operate.lsa_sarif.lsa_to_sarif import convert_findings_to_sarif # noqa: E402
27
+ from operate.lsa_sarif.sarif_findings import findings_from_sarif # noqa: E402
28
+
29
+ _spec = importlib.util.spec_from_file_location(
30
+ "lsa_findings_to_sarif", PROJECT_ROOT / "assess" / "lsa" / "scripts" / "findings_to_sarif.py"
31
+ )
32
+ assert _spec and _spec.loader
33
+ lsa_findings_to_sarif = importlib.util.module_from_spec(_spec)
34
+ _spec.loader.exec_module(lsa_findings_to_sarif)
35
+
36
+ ENV = {"T_USER": "user-secret-token"}
37
+
38
+ FINDINGS = {
39
+ "findings": [
40
+ {"finding_id": "BFLA-R-0001-1", "category": "bfla", "title": "Any user deletes accounts",
41
+ "location": {"file": "app.py", "line": 10},
42
+ "endpoint": {"method": "DELETE", "path": "/api/admin/users/{id}"}, "status": "confirmed",
43
+ "consensus": {"score": 0.9}},
44
+ {"finding_id": "MASS-M-0001-1", "category": "mass_assignment", "title": "Profile binds body",
45
+ "location": {"file": "app.py", "line": 20},
46
+ "endpoint": {"method": "PUT", "path": "/api/profile"},
47
+ "attack_vector": {"fields": {"role": "admin"}, "readback": "GET /api/profile",
48
+ "suggested_payload": "{\"role\": \"admin\"}"},
49
+ "status": "unverified", "consensus": {"score": 0.6}},
50
+ {"finding_id": "BFLA-R-0002-1", "category": "bfla", "title": "Rejected one",
51
+ "location": {"file": "app.py", "line": 30},
52
+ "endpoint": {"method": "DELETE", "path": "/api/x"}, "status": "rejected"},
53
+ {"finding_id": "BOLA-R-0003-1", "category": "bola", "title": "Not ours",
54
+ "location": {"file": "app.py", "line": 40},
55
+ "endpoint": {"method": "GET", "path": "/api/orders/{id}"}},
56
+ ]
57
+ }
58
+
59
+
60
+ def case_key(c):
61
+ return (c.kind, c.finding_id, c.method, c.path, c.probe_fields, c.readback)
62
+
63
+
64
+ def sarif_from_assess(doc):
65
+ return lsa_findings_to_sarif.to_sarif(copy.deepcopy(doc), include_rejected=True)
66
+
67
+
68
+ def sarif_from_operate(doc):
69
+ return convert_findings_to_sarif(copy.deepcopy(doc["findings"]))
70
+
71
+
72
+ class TestSarifMatrixCases(unittest.TestCase):
73
+ def test_both_exporters_give_the_same_cases_as_findings_json(self):
74
+ expected = [case_key(c) for c in generate_matrix_cases_from_findings(FINDINGS, "T_USER", default_id="7")]
75
+ self.assertEqual([k[1] for k in expected], ["BFLA-R-0001-1", "MASS-M-0001-1"])
76
+ for export in (sarif_from_assess, sarif_from_operate):
77
+ with self.subTest(exporter=export.__name__):
78
+ cases = generate_matrix_cases_from_findings(export(FINDINGS), "T_USER", default_id="7")
79
+ self.assertEqual([case_key(c) for c in cases], expected)
80
+
81
+ def test_min_consensus_applies_to_sarif(self):
82
+ cases = generate_matrix_cases_from_findings(sarif_from_assess(FINDINGS), "T_USER", min_consensus=0.8)
83
+ self.assertEqual([c.finding_id for c in cases], ["BFLA-R-0001-1"])
84
+
85
+ def test_suggested_payload_is_not_written_to_sarif(self):
86
+ text = json.dumps(sarif_from_assess(FINDINGS)) + json.dumps(sarif_from_operate(FINDINGS))
87
+ self.assertNotIn("suggested_payload", text)
88
+
89
+ def test_sarif_without_replay_properties_falls_back_to_rule_and_message(self):
90
+ sarif = sarif_from_assess(FINDINGS)
91
+ for result in sarif["runs"][0]["results"]:
92
+ for key in ("finding_id", "category", "endpoint", "attack_vector", "consensus_score"):
93
+ result["properties"].pop(key, None)
94
+ findings = {f["finding_id"]: f for f in findings_from_sarif(sarif)}
95
+ self.assertEqual(findings["BFLA-R-0001-1"]["category"], "bfla")
96
+ self.assertEqual(findings["BFLA-R-0001-1"]["endpoint"], {"method": "DELETE", "path": "/api/admin/users/{id}"})
97
+ cases = generate_matrix_cases_from_findings(sarif, "T_USER", default_id="7")
98
+ mass = next(c for c in cases if c.kind == "mass_assignment")
99
+ self.assertIn("role", mass.probe_fields) # default probe fields without attack_vector
100
+ self.assertEqual(mass.readback, ("GET", "/api/profile"))
101
+
102
+ def test_owasp_tag_identifies_category_when_rule_id_does_not(self):
103
+ sarif = sarif_from_assess(FINDINGS)
104
+ run = sarif["runs"][0]
105
+ renamed = {}
106
+ for i, rule in enumerate(run["tool"]["driver"]["rules"]):
107
+ renamed[rule["id"]] = f"custom-{i}"
108
+ rule["id"] = f"custom-{i}"
109
+ for result in run["results"]:
110
+ result["ruleId"] = renamed[result["ruleId"]]
111
+ result["properties"].pop("category", None)
112
+ kinds = {f["finding_id"]: f["category"] for f in findings_from_sarif(sarif)}
113
+ self.assertEqual(kinds["BFLA-R-0001-1"], "bfla")
114
+ self.assertEqual(kinds["MASS-M-0001-1"], "mass_assignment")
115
+
116
+
117
+ class TestSarifPocExport(unittest.TestCase):
118
+ def test_pocs_from_sarif_match_findings_json(self):
119
+ gen = ExploitGenerator(base_url="http://127.0.0.1:1", default_resource_id="7")
120
+ expected = {a.finding_id: (a.category, a.method, a.target_path) for a in gen.generate_from_findings_data(FINDINGS)}
121
+ for export in (sarif_from_assess, sarif_from_operate):
122
+ with self.subTest(exporter=export.__name__):
123
+ arts = {a.finding_id: a for a in gen.generate_from_findings_data(export(FINDINGS))}
124
+ self.assertEqual({k: (a.category, a.method, a.target_path) for k, a in arts.items()}, expected)
125
+ self.assertIn('"role": "admin"', arts["MASS-M-0001-1"].python_script)
126
+ self.assertNotIn("BFLA-R-0002-1", arts)
127
+
128
+
129
+ class TestCliFromScanSarif(unittest.TestCase):
130
+ def setUp(self):
131
+ self.server, _, self.base_url = start_mock_server(port=0)
132
+ self.tmp = tempfile.TemporaryDirectory()
133
+ self.dir = Path(self.tmp.name)
134
+
135
+ def tearDown(self):
136
+ self.server.shutdown()
137
+ self.server.server_close()
138
+ self.tmp.cleanup()
139
+
140
+ def test_from_scan_sarif_runs_bfla_and_mass_cases(self):
141
+ scan = self.dir / "findings.sarif"
142
+ scan.write_text(json.dumps(sarif_from_assess(FINDINGS)), encoding="utf-8")
143
+ out = self.dir / "res.json"
144
+ buf = io.StringIO()
145
+ with mock.patch.dict(os.environ, ENV), redirect_stdout(buf):
146
+ code = cli_main(["test", "--url", self.base_url, "--from-scan", str(scan),
147
+ "--token-env", "T_USER", "--resource-id", "7", "--output", str(out)])
148
+ self.assertEqual(code, 1)
149
+ report = json.loads(out.read_text(encoding="utf-8"))
150
+ ids = {r["finding_id"] for r in report["matrix_results"] if r["is_breach"]}
151
+ self.assertEqual(ids, {"BFLA-R-0001-1", "MASS-M-0001-1"})
152
+ self.assertNotIn(ENV["T_USER"], out.read_text(encoding="utf-8") + buf.getvalue())
153
+
154
+
155
+ if __name__ == "__main__":
156
+ unittest.main()
package/package.json CHANGED
@@ -1,13 +1,11 @@
1
1
  {
2
2
  "name": "@hoanghainh/komagu",
3
- "version": "1.7.0",
3
+ "version": "2.0.0",
4
4
  "description": "Security-First Legacy Modernization Platform CLI (LSA Kit, IAM Perimeter, Cross-User Testing)",
5
5
  "main": "remediate/authz/nodejs/src/index.js",
6
6
  "types": "remediate/authz/nodejs/src/index.d.ts",
7
7
  "bin": {
8
- "komagu": "bin/komagu.js",
9
- "komainu": "bin/komainu-legacy.js",
10
- "komainu-guard": "bin/komainu-legacy.js"
8
+ "komagu": "bin/komagu.js"
11
9
  },
12
10
  "files": [
13
11
  "bin/",
@@ -16,7 +16,7 @@ All implementations strictly enforce these architectural guarantees:
16
16
  - **Imperative (Programmatic):** Explicit service calls (`AuthorizeAsync` / `requireOwnership`) for complex business workflows and multi-condition rules.
17
17
  - **Declarative (Metadata/Attribute/Annotation):** Endpoint filter (`[RequireOwnership]`) or AOP Aspect (`@RequireOwnership`) for clean, declarative controller boundaries.
18
18
  4. **Scoped Data-Access Helpers:** Query pipeline extensions (EF Core `WithUserScope`, Spring Data `ScopedRepository`) automatically appending tenant and owner constraints to SQL queries before execution.
19
- 5. **Structured Security Audit Logging:** Every denial emits a structured event (`Komainu.Authorization.Denied`) with `actorUserId`, `actorTenantId`, `resourceType`, `resourceId`, and failure rationale for SIEM and security monitoring.
19
+ 5. **Structured Security Audit Logging:** Every denial emits a structured event (`Komagu.Authorization.Denied`) with `actorUserId`, `actorTenantId`, `resourceType`, `resourceId`, and failure rationale for SIEM and security monitoring.
20
20
 
21
21
  ---
22
22
 
@@ -220,7 +220,7 @@ public sealed class DefaultKomaguAuthorizationService : IKomaguAuthorizationServ
220
220
  var auditEvent = new SecurityAuditEvent
221
221
  {
222
222
  Timestamp = DateTimeOffset.UtcNow,
223
- EventName = "Komainu.Authorization.Denied",
223
+ EventName = "Komagu.Authorization.Denied",
224
224
  ActorUserId = actorUserId,
225
225
  ActorTenantId = actorTenantId,
226
226
  ResourceType = typeof(T).Name,
@@ -11,9 +11,9 @@ public record SecurityAuditEvent
11
11
  public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
12
12
 
13
13
  /// <summary>
14
- /// Event name or category. Kept as "Komainu.*" after the rename so existing SIEM rules still match (#290).
14
+ /// Event name or category.
15
15
  /// </summary>
16
- public string EventName { get; init; } = "Komainu.Authorization.Denied";
16
+ public string EventName { get; init; } = "Komagu.Authorization.Denied";
17
17
 
18
18
  /// <summary>
19
19
  /// Distributed tracing identifier (W3C traceparent or correlation ID).
@@ -210,7 +210,7 @@ func (a *Authorizer) emitDenial(userId, tenantId string, resource IOwnedResource
210
210
 
211
211
  a.Sink.Emit(SecurityAuditEvent{
212
212
  Timestamp: time.Now().UTC(),
213
- EventName: "Komainu.Authorization.Denied",
213
+ EventName: "Komagu.Authorization.Denied",
214
214
  ActorUserID: userId,
215
215
  ActorTenantID: tenantId,
216
216
  ResourceType: resType,
@@ -140,7 +140,7 @@ class KomaguSecurityService {
140
140
 
141
141
  const event = {
142
142
  timestamp: new Date().toISOString(),
143
- eventName: 'Komainu.Authorization.Denied',
143
+ eventName: 'Komagu.Authorization.Denied',
144
144
  actorUserId: actorUserId || undefined,
145
145
  actorTenantId: actorTenantId || undefined,
146
146
  resourceType: resource && resource.constructor ? resource.constructor.name : 'Object',
@@ -59,7 +59,7 @@ test('KomaguSecurityService: denies attacker with 404 and emits audit', async ()
59
59
  // Verify audit event
60
60
  assert.strictEqual(auditSink.events.length, 2); // 1 from authorize + 1 from requireOwnership
61
61
  const event = auditSink.events[0];
62
- assert.strictEqual(event.eventName, 'Komainu.Authorization.Denied');
62
+ assert.strictEqual(event.eventName, 'Komagu.Authorization.Denied');
63
63
  assert.strictEqual(event.actorUserId, 'user-b');
64
64
  assert.strictEqual(event.actorTenantId, 'tenant-komagu');
65
65
  assert.strictEqual(event.resourceId, 'ord-01');
@@ -85,10 +85,8 @@ class BOLAOwnershipPatcher:
85
85
  def ensure_imports(self, content: str, lang: str) -> Tuple[str, bool]:
86
86
  """Ensure necessary Komagu authorization import is present at the top of the file."""
87
87
  if lang in ("typescript", "javascript"):
88
- # komainu-guard: package name before the rename (#290), already-patched files keep it
89
- for pkg in ("@hoanghainh/komagu", "komainu-guard"):
90
- if f"from '{pkg}'" in content or f'from "{pkg}"' in content:
91
- return content, False
88
+ if "from '@hoanghainh/komagu'" in content or 'from "@hoanghainh/komagu"' in content:
89
+ return content, False
92
90
  import_stmt = "import { RequireOwnership, OwnershipGuard, ResourceOperation } from '@hoanghainh/komagu';\n"
93
91
  return import_stmt + content, True
94
92
 
@@ -143,6 +141,7 @@ class BOLAOwnershipPatcher:
143
141
 
144
142
  # Check if guard is already present on or adjacent to this line
145
143
  surrounding = "".join(lines[max(0, idx - 3):min(len(lines), idx + 3)])
144
+ # [KOMAINU-FIX] is only read: nothing writes it since #216, but files patched before that still carry it (#312)
146
145
  if "RequireOwnership" in surrounding or "OwnershipGuard" in surrounding or "[KOMAINU-FIX]" in surrounding:
147
146
  return content, BOLARewriteResult(
148
147
  original_line=target_line,
@@ -48,7 +48,7 @@ class AuthorizationResult:
48
48
  class SecurityAuditEvent:
49
49
  """Structured security audit event for authorization failures."""
50
50
  timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
51
- event_name: str = "Komainu.Authorization.Denied"
51
+ event_name: str = "Komagu.Authorization.Denied"
52
52
  actor_user_id: Optional[str] = None
53
53
  actor_tenant_id: Optional[str] = None
54
54
  resource_type: str = "Unknown"
@@ -81,7 +81,7 @@ class ISecurityAuditSink(ABC):
81
81
  class LoggingSecurityAuditSink(ISecurityAuditSink):
82
82
  """Default logger-based audit sink emitting JSON to standard logging."""
83
83
 
84
- def __init__(self, logger_name: str = "komainu.security.audit") -> None:
84
+ def __init__(self, logger_name: str = "komagu.security.audit") -> None:
85
85
  import logging
86
86
  self.logger = logging.getLogger(logger_name)
87
87
 
@@ -67,7 +67,7 @@ class TestFastApiAdapter(unittest.TestCase):
67
67
  self.authz_service = KomaguAuthorizationService(audit_sink=self.audit_sink)
68
68
  self.validator = OidcTokenValidator(
69
69
  issuer="https://auth.komagu.test",
70
- audience="komainu-api",
70
+ audience="komagu-api",
71
71
  allow_dev_tokens=True,
72
72
  )
73
73
 
@@ -42,7 +42,7 @@ class MockFastApiServer(http.server.BaseHTTPRequestHandler):
42
42
  authz_service = KomaguAuthorizationService(audit_sink=InMemorySecurityAuditSink())
43
43
  validator = OidcTokenValidator(
44
44
  issuer="https://auth.komagu.test",
45
- audience="komainu-api",
45
+ audience="komagu-api",
46
46
  allow_dev_tokens=True,
47
47
  )
48
48
 
@@ -38,7 +38,7 @@ class TestPythonAuthz(unittest.TestCase):
38
38
  self.authz_service = KomaguAuthorizationService(audit_sink=self.audit_sink)
39
39
  self.validator = OidcTokenValidator(
40
40
  issuer="https://auth.komagu.test",
41
- audience="komainu-api",
41
+ audience="komagu-api",
42
42
  allow_dev_tokens=True,
43
43
  )
44
44
 
@@ -77,7 +77,7 @@ class TestPythonAuthz(unittest.TestCase):
77
77
  self.assertEqual(len(self.audit_sink.events), 0)
78
78
 
79
79
  def test_02_attacker_same_tenant_denied_with_audit(self) -> None:
80
- """Attacker Bob (same tenant) is denied access; emits Komainu.Authorization.Denied event."""
80
+ """Attacker Bob (same tenant) is denied access; emits Komagu.Authorization.Denied event."""
81
81
  result = self.authz_service.authorize(
82
82
  user=self.bob_attacker,
83
83
  resource=self.alice_order,
@@ -89,7 +89,7 @@ class TestPythonAuthz(unittest.TestCase):
89
89
  # Verify audit emission
90
90
  self.assertEqual(len(self.audit_sink.events), 1)
91
91
  event = self.audit_sink.events[0]
92
- self.assertEqual(event.event_name, "Komainu.Authorization.Denied")
92
+ self.assertEqual(event.event_name, "Komagu.Authorization.Denied")
93
93
  self.assertEqual(event.actor_user_id, "user-b")
94
94
  self.assertEqual(event.actor_tenant_id, "tenant-komagu")
95
95
  self.assertEqual(event.resource_id, "ord-001")
@@ -182,7 +182,7 @@ class DefaultKomaguSecurityManager(
182
182
 
183
183
  val auditEvent = SecurityAuditEvent(
184
184
  timestamp = Instant.now(),
185
- eventName = "Komainu.Authorization.Denied",
185
+ eventName = "Komagu.Authorization.Denied",
186
186
  actorUserId = actorUserId,
187
187
  actorTenantId = actorTenantId,
188
188
  resourceType = resource?.let { it::class.java.simpleName } ?: "Unknown",
@@ -7,8 +7,7 @@ import java.time.Instant
7
7
  */
8
8
  data class SecurityAuditEvent(
9
9
  val timestamp: Instant = Instant.now(),
10
- // Kept as "Komainu.*" after the rename so existing SIEM rules still match (#290).
11
- val eventName: String = "Komainu.Authorization.Denied",
10
+ val eventName: String = "Komagu.Authorization.Denied",
12
11
  val traceId: String? = null,
13
12
  val actorUserId: String? = null,
14
13
  val actorTenantId: String? = null,
@@ -157,7 +157,7 @@ class TestBOLAOwnershipPatcher(unittest.TestCase):
157
157
  """If file already contains ownership guards, patcher must skip to prevent duplicates."""
158
158
  controller_file = self.test_dir / "protected_order.ts"
159
159
  controller_file.write_text(
160
- "import { RequireOwnership } from 'komainu-guard';\n"
160
+ "import { RequireOwnership } from '@hoanghainh/komagu';\n"
161
161
  "export class OrderController {\n"
162
162
  " @UseGuards(OwnershipGuard)\n"
163
163
  " @RequireOwnership(ResourceOperation.READ)\n"
@@ -1,6 +1,5 @@
1
1
  # ==============================================================================
2
2
  # Komagu Custom OWASP CRS v4 Tuning & Rule Exclusions
3
- # Rule tags stay 'komainu-*' after the rename so existing WAF log alerts still match (#290).
4
3
  # ==============================================================================
5
4
 
6
5
  # Enable JSON request body inspection for application/json Content-Type
@@ -33,7 +32,7 @@ SecRule ARGS|REQUEST_URI|REQUEST_HEADERS "@rx (?i)(?:(?:\bunion\b[\s\S]*?\bselec
33
32
  msg:'Komagu WAF: SQL Injection Attack Detected',\
34
33
  logdata:'Matched Data: %{TX.0}',\
35
34
  severity:'CRITICAL',\
36
- tag:'komainu-sqli',\
35
+ tag:'komagu-sqli',\
37
36
  setvar:'tx.sql_injection_score=+5',\
38
37
  setvar:'tx.inbound_anomaly_score_exceeded=1'"
39
38
 
@@ -48,7 +47,7 @@ SecRule ARGS|REQUEST_URI "@rx (?i)(?:<script[^>]*>|javascript:[^\"']*|onerror\s*
48
47
  msg:'Komagu WAF: Cross-Site Scripting (XSS) Attack Detected',\
49
48
  logdata:'Matched Data: %{TX.0}',\
50
49
  severity:'CRITICAL',\
51
- tag:'komainu-xss',\
50
+ tag:'komagu-xss',\
52
51
  setvar:'tx.xss_score=+5',\
53
52
  setvar:'tx.inbound_anomaly_score_exceeded=1'"
54
53
 
@@ -62,4 +61,4 @@ SecRule REQUEST_URI|ARGS "@rx (?i)(?:\.\./|\.\.\\)" \
62
61
  msg:'Komagu WAF: Path Traversal Attack Detected',\
63
62
  logdata:'Matched Data: %{TX.0}',\
64
63
  severity:'CRITICAL',\
65
- tag:'komainu-path-traversal'"
64
+ tag:'komagu-path-traversal'"
@@ -3,8 +3,6 @@
3
3
 
4
4
  Translates confirmed LSA findings (BOLA & SQLi) into deterministic,
5
5
  production-ready SecRule configurations for instant Day-2 edge protection.
6
-
7
- Rule tags stay 'komainu-*' after the rename so existing WAF log alerts still match (#290).
8
6
  """
9
7
 
10
8
  from __future__ import annotations
@@ -74,8 +72,8 @@ SecRule REQUEST_URI "@rx {path_regex}" \\
74
72
  msg:'Komagu Virtual Patch: SQL Injection blocked on {path}',\\
75
73
  logdata:'Matched Data: %{{TX.0}}',\\
76
74
  severity:'CRITICAL',\\
77
- tag:'komainu-virtual-patch',\\
78
- tag:'komainu-sqli',\\
75
+ tag:'komagu-virtual-patch',\\
76
+ tag:'komagu-sqli',\\
79
77
  tag:'{finding_id}',\\
80
78
  setvar:'tx.sql_injection_score=+5',\\
81
79
  setvar:'tx.inbound_anomaly_score_exceeded=1',\\
@@ -115,8 +113,8 @@ SecRule REQUEST_URI "@rx {path_regex}" \\
115
113
  t:urlDecodeUni,\\
116
114
  msg:'Komagu Virtual Patch: Missing Authorization Token on BOLA Endpoint {path}',\\
117
115
  severity:'CRITICAL',\\
118
- tag:'komainu-virtual-patch',\\
119
- tag:'komainu-bola',\\
116
+ tag:'komagu-virtual-patch',\\
117
+ tag:'komagu-bola',\\
120
118
  tag:'{finding_id}',\\
121
119
  chain"
122
120
  SecRule &REQUEST_HEADERS:Authorization "@eq 0"
@@ -11,7 +11,7 @@ services:
11
11
  KONG_PROXY_ERROR_LOG: /dev/stderr
12
12
  KONG_ADMIN_ERROR_LOG: /dev/stderr
13
13
  KONG_ADMIN_LISTEN: 0.0.0.0:8001
14
- KOMAGU_JWT_SECRET: ${KOMAGU_JWT_SECRET:-${KOMAINU_JWT_SECRET:-komagu-default-test-jwt-secret-key-32chars}}
14
+ KOMAGU_JWT_SECRET: ${KOMAGU_JWT_SECRET:-komagu-default-test-jwt-secret-key-32chars}
15
15
  volumes:
16
16
  - ./kong/kong.yaml:/kong/declarative/kong.yaml:ro
17
17
  ports:
@@ -16,18 +16,12 @@ import os
16
16
  import secrets
17
17
  import threading
18
18
  import time
19
- import warnings
20
19
  from typing import Any, Callable, Dict, List, Optional, Tuple
21
20
 
22
21
 
23
22
  def _env(name: str, default: Optional[str] = None) -> Optional[str]:
24
- """Read KOMAGU_<name>, falling back to the deprecated KOMAINU_<name> (#290)."""
25
- value = os.environ.get("KOMAGU_" + name)
26
- if value is None:
27
- value = os.environ.get("KOMAINU_" + name)
28
- if value is not None:
29
- warnings.warn(f"KOMAINU_{name} is deprecated, set KOMAGU_{name} instead", DeprecationWarning, stacklevel=2)
30
- return default if value is None else value
23
+ """Read KOMAGU_<name>."""
24
+ return os.environ.get("KOMAGU_" + name, default)
31
25
 
32
26
 
33
27
  try: