@hoanghainh/komagu 1.8.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.
- package/CHANGELOG.md +13 -0
- package/assess/lsa/CHANGELOG.md +6 -0
- package/assess/lsa/VERSION +1 -1
- package/assess/lsa/adapters/build_adapters.py +16 -8
- package/assess/lsa/adapters/codex/README.md +1 -1
- package/assess/lsa/adapters/generic/README.md +1 -1
- package/assess/lsa/knowledge/severity-and-cwe.md +1 -1
- package/assess/lsa/scripts/baseline.py +5 -2
- package/assess/lsa/scripts/findings_to_sarif.py +24 -1
- package/assess/lsa/tests/test_adapters.py +21 -2
- package/assess/lsa/tests/test_baseline.py +8 -0
- package/assess/lsa/tests/test_contract_groundwork.py +7 -7
- package/operate/cli/__init__.py +1 -1
- package/operate/cli/commands/scan.py +2 -7
- package/operate/cli/commands/test.py +1 -1
- package/operate/cli/env.py +4 -20
- package/operate/cross_user_tester/persona_matrix.py +5 -1
- package/operate/devsecops/pipeline.py +5 -5
- package/operate/exploit/generator.py +5 -1
- package/operate/lsa_sarif/lsa_to_sarif.py +24 -2
- package/operate/lsa_sarif/sarif_findings.py +96 -0
- package/operate/remediator/runner.py +1 -5
- package/operate/remediator/strategies.py +1 -1
- package/operate/tests/test_cli_baseline.py +1 -9
- package/operate/tests/test_cli_env.py +8 -29
- package/operate/tests/test_cross_user_pipeline.py +2 -2
- package/operate/tests/test_exploit_and_virtual_patch.py +3 -3
- package/operate/tests/test_npm_package.py +2 -16
- package/operate/tests/test_sarif_from_scan.py +156 -0
- package/package.json +2 -4
- package/remediate/authz/README.md +1 -1
- package/remediate/authz/dotnet/src/Komagu.Security.Authorization/DefaultKomaguAuthorizationService.cs +1 -1
- package/remediate/authz/dotnet/src/Komagu.Security.Authorization/SecurityAuditEvent.cs +2 -2
- package/remediate/authz/golang/authz.go +1 -1
- package/remediate/authz/nodejs/src/service.js +1 -1
- package/remediate/authz/nodejs/tests/test-authz.js +1 -1
- package/remediate/authz/patcher.py +3 -4
- package/remediate/authz/python/komagu_authz/models.py +2 -2
- package/remediate/authz/python/tests/test_fastapi_adapter.py +1 -1
- package/remediate/authz/python/tests/test_fastapi_d5_crosstest.py +1 -1
- package/remediate/authz/python/tests/test_python_authz.py +3 -3
- package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/DefaultKomaguSecurityManager.kt +1 -1
- package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/SecurityAuditEvent.kt +1 -2
- package/remediate/authz/tests/test_bola_patcher.py +1 -1
- package/remediate/edge/coraza/rules/coraza-crs-custom.conf +3 -4
- package/remediate/edge/coraza/virtual_patcher.py +4 -6
- package/remediate/edge/docker-compose.yml +1 -1
- package/remediate/ekyc/server/jpki_verifier.py +2 -8
- package/remediate/ekyc/server/tokenization.py +2 -8
- package/remediate/identity/README.md +5 -5
- package/remediate/identity/dotnet/src/Komagu.Security.Identity/KomaguOidcOptions.cs +2 -2
- package/remediate/identity/dotnet/tests/Komagu.Security.Identity.Tests/TestTokenGenerator.cs +1 -1
- package/remediate/identity/dotnet/tests/Komagu.Security.Identity.Tests/TokenValidationTests.cs +1 -1
- package/remediate/identity/python/komagu_identity/oidc_validator.py +2 -8
- package/remediate/identity/python/tests/test_oidc_validator.py +1 -1
- package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/KomaguOidcProperties.kt +2 -2
- package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/KomaguIdentityAutoConfigurationTest.kt +2 -2
- package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/TestJwtFactory.kt +1 -1
- package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/TokenValidationIntegrationTest.kt +1 -1
- package/remediate/identity/test-env/realm-komagu.json +1 -1
- package/remediate/legacy_shims/coldfusion/KomaguSecurityShim.cfc +5 -5
- package/remediate/query/README.md +1 -1
- package/remediate/query/dotnet/src/Komagu.Security.Query.Analyzers/DiagnosticIds.cs +1 -1
- package/remediate/query/dotnet/src/Komagu.Security.Query.Analyzers/Komagu.Security.Query.Analyzers.csproj +1 -1
- package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/detekt/KomaguRuleSetProvider.kt +1 -1
- package/remediate/resilience/dr/dr_manager.py +2 -8
- package/remediate/resilience/dr/runbook.md +2 -2
- package/remediate/resilience/terraform/variables.tf +1 -1
- package/remediate/tenancy/audit/audit_trail.py +2 -8
- package/remediate/tenancy/crypto/byok_manager.py +2 -8
- package/remediate/tenancy/middleware/tenant_context.py +2 -8
- package/remediate/tenancy/sql/rls_isolation.sql +11 -11
- package/remediate/tenancy/sql/schema_per_tenant.sql +7 -7
- package/remediate/tenancy/test_sql_postgres.py +1 -1
- package/remediate/tenancy/tests/test_tenancy.py +4 -4
- package/bin/komainu +0 -16
- package/bin/komainu-legacy.js +0 -16
- package/remediate/ekyc/tests/test_legacy_env.py +0 -33
- package/remediate/identity/python/tests/test_legacy_env.py +0 -33
- package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/LegacyKomainuPropertiesPostProcessor.kt +0 -60
- package/remediate/identity/spring/src/main/resources/META-INF/spring.factories +0 -2
- package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/LegacyKomainuPropertiesPostProcessorTest.kt +0 -86
- package/remediate/resilience/tests/test_legacy_env.py +0 -33
- package/remediate/tenancy/tests/test_legacy_env.py +0 -49
|
@@ -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"]["
|
|
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
|
|
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
|
|
15
|
-
cli_env.
|
|
16
|
-
|
|
17
|
-
def
|
|
18
|
-
|
|
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
|
-
|
|
40
|
-
self.
|
|
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"], "
|
|
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"], "
|
|
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:'
|
|
115
|
-
self.assertIn("tag:'
|
|
116
|
-
self.assertIn("tag:'
|
|
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
|
-
#
|
|
39
|
-
self.assertEqual(data.get("bin", {})
|
|
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": "
|
|
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 (`
|
|
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 = "
|
|
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.
|
|
14
|
+
/// Event name or category.
|
|
15
15
|
/// </summary>
|
|
16
|
-
public string EventName { get; init; } = "
|
|
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: "
|
|
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: '
|
|
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, '
|
|
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
|
-
|
|
89
|
-
|
|
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 = "
|
|
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 = "
|
|
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="
|
|
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="
|
|
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="
|
|
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
|
|
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, "
|
|
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 = "
|
|
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
|
-
|
|
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 '
|
|
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:'
|
|
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:'
|
|
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:'
|
|
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:'
|
|
78
|
-
tag:'
|
|
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:'
|
|
119
|
-
tag:'
|
|
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
|
|
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
|
|
25
|
-
|
|
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:
|
|
@@ -22,18 +22,12 @@ import re
|
|
|
22
22
|
import secrets
|
|
23
23
|
import sys
|
|
24
24
|
import time
|
|
25
|
-
import warnings
|
|
26
25
|
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
def _env(name: str, default: Optional[str] = None) -> Optional[str]:
|
|
30
|
-
"""Read KOMAGU_<name
|
|
31
|
-
|
|
32
|
-
if value is None:
|
|
33
|
-
value = os.environ.get("KOMAINU_" + name)
|
|
34
|
-
if value is not None:
|
|
35
|
-
warnings.warn(f"KOMAINU_{name} is deprecated, set KOMAGU_{name} instead", DeprecationWarning, stacklevel=2)
|
|
36
|
-
return default if value is None else value
|
|
29
|
+
"""Read KOMAGU_<name>."""
|
|
30
|
+
return os.environ.get("KOMAGU_" + name, default)
|
|
37
31
|
|
|
38
32
|
|
|
39
33
|
# Allowed fields in API requests under Zero-PII contract
|