@hoanghainh/komagu 2.2.1 → 2.4.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 (44) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/assess/lsa/CHANGELOG.md +15 -0
  3. package/assess/lsa/scripts/attack_chains.py +573 -0
  4. package/assess/lsa/scripts/baseline.py +14 -0
  5. package/assess/lsa/scripts/detect_stack.py +2 -1
  6. package/assess/lsa/scripts/findings_to_sarif.py +11 -0
  7. package/assess/lsa/scripts/forecast.py +544 -0
  8. package/assess/lsa/scripts/lsa_common.py +1 -5
  9. package/assess/lsa/scripts/merge_findings.py +14 -0
  10. package/assess/lsa/scripts/redaction.py +118 -0
  11. package/assess/lsa/scripts/render_report.py +86 -0
  12. package/assess/lsa/tests/test_attack_chains.py +292 -0
  13. package/assess/lsa/tests/test_forecast.py +160 -0
  14. package/assess/lsa/tests/test_modern_stacks.py +13 -0
  15. package/contracts/schemas/api-inventory-diff.schema.json +213 -0
  16. package/contracts/schemas/attack-chain.schema.json +181 -0
  17. package/contracts/schemas/bas-scenario.schema.json +217 -0
  18. package/contracts/schemas/evidence-manifest.schema.json +117 -0
  19. package/contracts/schemas/finding.schema.json +7 -0
  20. package/contracts/schemas/forecast.schema.json +172 -0
  21. package/governance/scs/__init__.py +21 -0
  22. package/governance/scs/mapping.py +133 -0
  23. package/governance/scs/pack.py +354 -0
  24. package/operate/ai/context.py +22 -2
  25. package/operate/cli/__init__.py +1 -1
  26. package/operate/cli/commands/evidence.py +138 -0
  27. package/operate/cli/commands/inventory.py +187 -0
  28. package/operate/cli/commands/simulate.py +175 -58
  29. package/operate/cli/main.py +7 -3
  30. package/operate/inventory/__init__.py +22 -0
  31. package/operate/inventory/api_inventory.py +566 -0
  32. package/operate/simulate/__init__.py +16 -0
  33. package/operate/simulate/engine.py +370 -0
  34. package/operate/simulate/loader.py +158 -0
  35. package/operate/simulate/scenarios/idc-frontier-ransomware.json +47 -0
  36. package/operate/simulate/scenarios/metabase-cve-2026-72898.json +84 -0
  37. package/operate/simulate/scenarios/times-car-breach.json +74 -0
  38. package/operate/simulate/scenarios.py +52 -0
  39. package/operate/tests/test_ai_redaction.py +181 -0
  40. package/operate/tests/test_api_inventory.py +239 -0
  41. package/operate/tests/test_declarative_scenarios.py +169 -0
  42. package/operate/tests/test_evidence_pack.py +128 -0
  43. package/operate/tests/test_headless_ai.py +16 -1
  44. package/package.json +1 -1
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env python3
2
+ """Comprehensive unit tests for Declarative Incident Scenarios (#317).
3
+
4
+ Validates:
5
+ 1. bas-scenario.schema.json schema validation on built-in and custom scenarios.
6
+ 2. Discovery of scenarios from files, directories, and built-ins.
7
+ 3. DeclarativeScenario execution of HTTP and storage steps.
8
+ 4. Governance safety gates: draft scenarios blocked without --allow-draft.
9
+ 5. Destructive step safety gates: destructive steps blocked without --allow-destructive.
10
+ 6. Parameter substitutions and CLI --set overrides.
11
+ 7. METI SCS audit evidence generation (#271).
12
+ 8. CLI commands: --list, --validate, --scenario-file, --evidence.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import sys
20
+ import tempfile
21
+ import unittest
22
+ from pathlib import Path
23
+
24
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
25
+ if str(PROJECT_ROOT) not in sys.path:
26
+ sys.path.insert(0, str(PROJECT_ROOT))
27
+
28
+ from operate.cli.main import main as cli_main
29
+ from operate.cross_user_tester.mock_test_server import start_mock_server
30
+ from operate.simulate.engine import (
31
+ DeclarativeScenario,
32
+ generate_execution_evidence,
33
+ )
34
+ from operate.simulate.loader import (
35
+ BUILTIN_SCENARIOS_DIR,
36
+ discover_scenarios,
37
+ load_scenario_file,
38
+ load_validator,
39
+ validate_scenario,
40
+ )
41
+
42
+
43
+ class TestDeclarativeScenarios(unittest.TestCase):
44
+ @classmethod
45
+ def setUpClass(cls) -> None:
46
+ cls.server, cls.thread, cls.base_url = start_mock_server(port=0)
47
+
48
+ @classmethod
49
+ def tearDownClass(cls) -> None:
50
+ cls.server.server_close()
51
+
52
+ def test_schema_validates_builtin_scenarios(self) -> None:
53
+ """All 3 built-in scenario files must strictly conform to bas-scenario.schema.json."""
54
+ validator = load_validator("bas-scenario.schema.json")
55
+ for sc_name in ["times-car-breach.json", "metabase-cve-2026-72898.json", "idc-frontier-ransomware.json"]:
56
+ path = BUILTIN_SCENARIOS_DIR / sc_name
57
+ self.assertTrue(path.is_file(), f"Missing built-in scenario: {sc_name}")
58
+ data = load_scenario_file(path)
59
+ errs = validator(data)
60
+ self.assertEqual(errs, [], f"Validation errors for {sc_name}: {errs}")
61
+
62
+ def test_schema_validation_error_on_invalid_scenario(self) -> None:
63
+ """Validator must report missing required properties."""
64
+ invalid_data = {
65
+ "id": "invalid-scenario",
66
+ "title": "Invalid",
67
+ # missing status, version, incident, mappings, etc.
68
+ }
69
+ errs = validate_scenario(invalid_data)
70
+ self.assertTrue(len(errs) > 0)
71
+ self.assertTrue(any("required property missing" in e or "is a required property" in e for e in errs))
72
+
73
+ def test_discovery_scenarios(self) -> None:
74
+ """discover_scenarios must locate all built-in scenarios and custom ones."""
75
+ scenarios = discover_scenarios(include_builtins=True)
76
+ self.assertIn("times-car-breach", scenarios)
77
+ self.assertIn("metabase-cve-2026-72898", scenarios)
78
+ self.assertIn("idc-frontier-ransomware", scenarios)
79
+
80
+ # Test custom dir discovery
81
+ with tempfile.TemporaryDirectory() as tmp_dir:
82
+ custom_path = Path(tmp_dir) / "custom-sc.json"
83
+ base_sc = dict(scenarios["times-car-breach"])
84
+ base_sc["id"] = "custom-test-scenario"
85
+ custom_path.write_text(json.dumps(base_sc), encoding="utf-8")
86
+
87
+ discovered = discover_scenarios(scenario_dirs=[tmp_dir], include_builtins=False)
88
+ self.assertIn("custom-test-scenario", discovered)
89
+
90
+ def test_draft_scenario_blocked_without_override(self) -> None:
91
+ """Scenario with status != 'approved' must fail execution unless allow_draft is set."""
92
+ data = dict(load_scenario_file(BUILTIN_SCENARIOS_DIR / "times-car-breach.json"))
93
+ data["status"] = "draft"
94
+ runner = DeclarativeScenario(data, self.base_url, allow_draft=False)
95
+ with self.assertRaises(PermissionError):
96
+ runner.run()
97
+
98
+ # Overriding with allow_draft=True must succeed
99
+ runner_override = DeclarativeScenario(data, self.base_url, allow_draft=True)
100
+ res = runner_override.run()
101
+ self.assertIsNotNone(res)
102
+
103
+ def test_destructive_step_blocked_without_override(self) -> None:
104
+ """Destructive steps must be skipped unless allow_destructive is set."""
105
+ data = dict(load_scenario_file(BUILTIN_SCENARIOS_DIR / "times-car-breach.json"))
106
+ data["steps"] = [dict(data["steps"][0])]
107
+ data["steps"][0]["destructive"] = True
108
+
109
+ runner_safe = DeclarativeScenario(data, self.base_url, allow_destructive=False)
110
+ res_safe = runner_safe.run()
111
+ self.assertTrue(any(d.get("skipped") for d in res_safe.details))
112
+
113
+ runner_destr = DeclarativeScenario(data, self.base_url, allow_destructive=True)
114
+ res_destr = runner_destr.run()
115
+ self.assertFalse(any(d.get("skipped") for d in res_destr.details))
116
+
117
+ def test_parameter_override_and_substitution(self) -> None:
118
+ """Custom parameter overrides must take precedence over defaults."""
119
+ data = load_scenario_file(BUILTIN_SCENARIOS_DIR / "times-car-breach.json")
120
+ runner = DeclarativeScenario(
121
+ data,
122
+ self.base_url,
123
+ param_overrides={
124
+ "endpoint_pattern": "/api/orders/{id}",
125
+ "sample_ids": ["999"],
126
+ },
127
+ )
128
+ res = runner.run()
129
+ self.assertEqual(res.total_attempts, 1)
130
+ self.assertTrue(res.passed)
131
+
132
+ def test_evidence_generation(self) -> None:
133
+ """generate_execution_evidence produces complete audit record conforming to METI SCS."""
134
+ data = load_scenario_file(BUILTIN_SCENARIOS_DIR / "times-car-breach.json")
135
+ runner = DeclarativeScenario(data, self.base_url)
136
+ res = runner.run()
137
+
138
+ with tempfile.TemporaryDirectory() as tmp_dir:
139
+ ev_path = Path(tmp_dir) / "evidence.json"
140
+ ev = generate_execution_evidence([runner], [res], self.base_url, output_path=ev_path)
141
+ self.assertTrue(ev_path.is_file())
142
+ self.assertEqual(ev["evidence_type"], "breach_and_attack_simulation_audit")
143
+ self.assertEqual(len(ev["audit_records"]), 1)
144
+ self.assertTrue(ev["audit_records"][0]["scenario_hash"].startswith("sha256:"))
145
+
146
+ def test_cli_list_and_validate(self) -> None:
147
+ """Test CLI flags --list and --validate."""
148
+ # --list
149
+ exit_code = cli_main(["simulate", "--list"])
150
+ self.assertEqual(exit_code, 0)
151
+
152
+ # --validate valid
153
+ valid_file = str(BUILTIN_SCENARIOS_DIR / "times-car-breach.json")
154
+ exit_code = cli_main(["simulate", "--validate", valid_file])
155
+ self.assertEqual(exit_code, 0)
156
+
157
+ # --validate invalid
158
+ with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
159
+ f.write("{\"id\": \"incomplete\"}")
160
+ inv_file = f.name
161
+ try:
162
+ exit_code = cli_main(["simulate", "--validate", inv_file])
163
+ self.assertEqual(exit_code, 2)
164
+ finally:
165
+ os.unlink(inv_file)
166
+
167
+
168
+ if __name__ == "__main__":
169
+ unittest.main()
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+ """Comprehensive unit tests for METI SCS Evidence Pack and Gap Report (#271).
3
+
4
+ Validates:
5
+ 1. evidence-manifest.schema.json schema conformity.
6
+ 2. Control mapping evaluation and readiness rating logic (★3, ★4, GAP).
7
+ 3. ZIP archive bundling, SHA-256 cryptographic digests, and manifest structure.
8
+ 4. Bilingual gap report generation (gap-report.ja.md and gap-report.md).
9
+ 5. CLI execution `komagu evidence`, argument parsing, and quality gating (--require-tier).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import sys
17
+ import tempfile
18
+ import unittest
19
+ import zipfile
20
+ from pathlib import Path
21
+
22
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
23
+ if str(PROJECT_ROOT) not in sys.path:
24
+ sys.path.insert(0, str(PROJECT_ROOT))
25
+
26
+ from assess.lsa.scripts.merge_findings import load_validator
27
+ from governance.scs.mapping import SCS_CONTROLS
28
+ from governance.scs.pack import EvidencePackBuilder
29
+ from operate.cli.main import main as cli_main
30
+
31
+
32
+ class TestEvidencePack(unittest.TestCase):
33
+ def setUp(self) -> None:
34
+ self.validate_schema = load_validator("evidence-manifest.schema.json")
35
+
36
+ def test_schema_conformity_on_evaluated_manifest(self) -> None:
37
+ """Generated evidence manifest must strictly conform to evidence-manifest.schema.json."""
38
+ with tempfile.TemporaryDirectory() as tmp_dir:
39
+ # Create synthetic audit artifacts
40
+ audit_dir = Path(tmp_dir) / "audit"
41
+ audit_dir.mkdir()
42
+ (audit_dir / "findings.sarif").write_text("{}", encoding="utf-8")
43
+ (audit_dir / "sbom-cyclonedx.json").write_text("{}", encoding="utf-8")
44
+ (audit_dir / "api-inventory-diff.json").write_text("{}", encoding="utf-8")
45
+ (audit_dir / "coraza.conf").write_text("SecRuleEngine On", encoding="utf-8")
46
+ (audit_dir / "bas-evidence.json").write_text("{}", encoding="utf-8")
47
+
48
+ builder = EvidencePackBuilder(
49
+ audit_dirs=[audit_dir],
50
+ project_name="Test Enterprise Portal",
51
+ target_system="Production-Kubernetes",
52
+ )
53
+ arts = builder.discover_artifacts()
54
+ ctrls, readiness = builder.evaluate_controls(arts)
55
+ manifest = builder.generate_manifest(ctrls, readiness, arts)
56
+
57
+ errs = self.validate_schema(manifest)
58
+ self.assertEqual(errs, [], f"Schema validation errors: {errs}")
59
+ self.assertEqual(manifest["project_name"], "Test Enterprise Portal")
60
+ self.assertEqual(manifest["overall_readiness"]["rating"], "SCS-★4-READY")
61
+
62
+ def test_pack_builder_generates_zip_and_reports(self) -> None:
63
+ """build_pack must package evidence-pack.zip, manifest JSON, and bilingual reports."""
64
+ with tempfile.TemporaryDirectory() as tmp_dir:
65
+ audit_dir = Path(tmp_dir) / "audit"
66
+ audit_dir.mkdir()
67
+ (audit_dir / "findings.sarif").write_text('{"runs": []}', encoding="utf-8")
68
+ (audit_dir / "sbom-cyclonedx.json").write_text('{"bomFormat": "CycloneDX"}', encoding="utf-8")
69
+
70
+ out_dir = Path(tmp_dir) / "output"
71
+
72
+ builder = EvidencePackBuilder(
73
+ audit_dirs=[audit_dir],
74
+ project_name="Tokyo Logistics Monolith",
75
+ )
76
+ zip_p, manifest_p, ja_p, en_p = builder.build_pack(out_dir)
77
+
78
+ self.assertTrue(zip_p.is_file())
79
+ self.assertTrue(manifest_p.is_file())
80
+ self.assertTrue(ja_p.is_file())
81
+ self.assertTrue(en_p.is_file())
82
+
83
+ # Verify Japanese report content
84
+ ja_content = ja_p.read_text(encoding="utf-8")
85
+ self.assertIn("サプライチェーン・サイバーセキュリティ", ja_content)
86
+ self.assertIn("NIST CSF 2.0", ja_content)
87
+ self.assertIn("Tokyo Logistics Monolith", ja_content)
88
+
89
+ # Verify English report content
90
+ en_content = en_p.read_text(encoding="utf-8")
91
+ self.assertIn("METI Supply Chain Security", en_content)
92
+ self.assertIn("Tokyo Logistics Monolith", en_content)
93
+
94
+ # Verify ZIP contents
95
+ with zipfile.ZipFile(zip_p, "r") as zf:
96
+ names = zf.namelist()
97
+ self.assertIn("evidence-manifest.json", names)
98
+ self.assertIn("gap-report.ja.md", names)
99
+ self.assertIn("gap-report.md", names)
100
+ self.assertIn("artifacts/findings.sarif", names)
101
+ self.assertIn("artifacts/sbom-cyclonedx.json", names)
102
+
103
+ def test_cli_evidence_command_execution_and_gating(self) -> None:
104
+ """Test `komagu evidence` CLI invocation and tier requirements."""
105
+ with tempfile.TemporaryDirectory() as tmp_dir:
106
+ audit_dir = Path(tmp_dir) / "audit"
107
+ audit_dir.mkdir()
108
+ (audit_dir / "findings.sarif").write_text("{}", encoding="utf-8")
109
+
110
+ out_dir = Path(tmp_dir) / "pack"
111
+
112
+ # Execute without gate -> should succeed
113
+ code = cli_main(["evidence", "--audit-dir", str(audit_dir), "--output-dir", str(out_dir)])
114
+ self.assertEqual(code, 0)
115
+ self.assertTrue((out_dir / "evidence-pack.zip").is_file())
116
+
117
+ # Require tier 4 with only 1 artifact -> should fail quality gate
118
+ code_gate = cli_main([
119
+ "evidence",
120
+ "--audit-dir", str(audit_dir),
121
+ "--output-dir", str(out_dir),
122
+ "--require-tier", "4",
123
+ ])
124
+ self.assertEqual(code_gate, 1)
125
+
126
+
127
+ if __name__ == "__main__":
128
+ unittest.main()
@@ -19,7 +19,7 @@ if str(PROJECT_ROOT) not in sys.path:
19
19
  sys.path.insert(0, str(PROJECT_ROOT))
20
20
 
21
21
  from operate.ai.client import LLMClient
22
- from operate.ai.mock_server import start_mock_llm_server
22
+ from operate.ai.mock_server import MockLLMHandler, start_mock_llm_server
23
23
  from operate.ai.runner import HeadlessAIRunner
24
24
  from operate.cli.main import main as cli_main
25
25
 
@@ -295,6 +295,21 @@ module.exports = app;
295
295
  self.assertEqual(stats["parse_failures"], {})
296
296
  self.assertEqual(stats["anchoring"]["unanchored"], [])
297
297
 
298
+ def test_no_planted_secret_reaches_the_model(self) -> None:
299
+ planted = ("PlantedPg9", "planted-key-42", "PlantedOra3")
300
+ with (self.target_dir / "app.js").open("a", encoding="utf-8") as fh:
301
+ fh.write('const DB = "postgres://app:PlantedPg9@db/orders";\nconst apiKey = "planted-key-42";\n'
302
+ '// legacy: db.password=PlantedOra3\n')
303
+ self._prescan()
304
+ client = LLMClient(endpoint=self.base_url, model="mock-model", api_key="test")
305
+ HeadlessAIRunner(self.target_dir, self.out_dir, client).run()
306
+
307
+ sent = json.dumps(MockLLMHandler.received_requests)
308
+ self.assertIn("5-finding-verifier", json.loads((self.out_dir / "ai-run-stats.json").read_text())["phases"])
309
+ self.assertIn("[REDACTED]", sent)
310
+ for secret in planted:
311
+ self.assertNotIn(secret, sent)
312
+
298
313
  def test_unparseable_verifier_reply_leaves_findings_unverified(self) -> None:
299
314
  """A broken verifier reply must not confirm findings (#286)."""
300
315
  self.server.server_close()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoanghainh/komagu",
3
- "version": "2.2.1",
3
+ "version": "2.4.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",