@hoanghainh/komagu 2.2.0 → 2.3.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 (43) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +1 -1
  3. package/assess/lsa/scripts/attack_chains.py +573 -0
  4. package/assess/lsa/scripts/baseline.py +14 -0
  5. package/assess/lsa/scripts/findings_to_sarif.py +11 -0
  6. package/assess/lsa/scripts/forecast.py +544 -0
  7. package/assess/lsa/scripts/merge_findings.py +14 -0
  8. package/assess/lsa/scripts/render_report.py +86 -0
  9. package/assess/lsa/tests/test_attack_chains.py +292 -0
  10. package/assess/lsa/tests/test_forecast.py +160 -0
  11. package/contracts/schemas/api-inventory-diff.schema.json +213 -0
  12. package/contracts/schemas/attack-chain.schema.json +181 -0
  13. package/contracts/schemas/bas-scenario.schema.json +217 -0
  14. package/contracts/schemas/evidence-manifest.schema.json +117 -0
  15. package/contracts/schemas/finding.schema.json +7 -0
  16. package/contracts/schemas/forecast.schema.json +172 -0
  17. package/examples/member-portal/backend-dotnet/MemberPortal.Backend.csproj +2 -2
  18. package/governance/scs/__init__.py +21 -0
  19. package/governance/scs/mapping.py +133 -0
  20. package/governance/scs/pack.py +354 -0
  21. package/operate/cli/__init__.py +1 -1
  22. package/operate/cli/commands/evidence.py +138 -0
  23. package/operate/cli/commands/inventory.py +187 -0
  24. package/operate/cli/commands/simulate.py +175 -58
  25. package/operate/cli/main.py +7 -3
  26. package/operate/inventory/__init__.py +22 -0
  27. package/operate/inventory/api_inventory.py +566 -0
  28. package/operate/simulate/__init__.py +16 -0
  29. package/operate/simulate/engine.py +370 -0
  30. package/operate/simulate/loader.py +158 -0
  31. package/operate/simulate/scenarios/idc-frontier-ransomware.json +47 -0
  32. package/operate/simulate/scenarios/metabase-cve-2026-72898.json +84 -0
  33. package/operate/simulate/scenarios/times-car-breach.json +74 -0
  34. package/operate/simulate/scenarios.py +52 -0
  35. package/operate/tests/test_api_inventory.py +239 -0
  36. package/operate/tests/test_declarative_scenarios.py +169 -0
  37. package/operate/tests/test_evidence_pack.py +128 -0
  38. package/package.json +1 -1
  39. package/remediate/authz/dotnet/src/Komagu.Security.EntityFrameworkCore/Komagu.Security.EntityFrameworkCore.csproj +1 -1
  40. package/remediate/authz/dotnet/tests/Komagu.Security.Authorization.Tests/Komagu.Security.Authorization.Tests.csproj +1 -1
  41. package/remediate/authz/spring/build.gradle.kts +4 -4
  42. package/remediate/identity/dotnet/src/Komagu.Security.Identity/Komagu.Security.Identity.csproj +1 -1
  43. package/remediate/identity/dotnet/tests/Komagu.Security.Identity.Tests/Komagu.Security.Identity.Tests.csproj +1 -1
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env python3
2
+ """Declarative Simulation Engine for Komagu BAS (#317).
3
+
4
+ Executes declarative scenario JSON definitions using stdlib urllib and filesystem assertions.
5
+ Enforces governance safeguards:
6
+ - Only status == "approved" runs without --allow-draft.
7
+ - destructive == True steps are blocked without --allow-destructive.
8
+ - Signed execution evidence generation for METI SCS evidence (#271).
9
+
10
+ Standard library only (Python 3.8+) per ADR-0006.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import json
17
+ import os
18
+ import re
19
+ import stat
20
+ import time
21
+ import urllib.error
22
+ import urllib.parse
23
+ import urllib.request
24
+ from pathlib import Path
25
+ from typing import Any, Dict, List, Optional, Tuple, Union
26
+
27
+ from operate.simulate.loader import validate_scenario
28
+ from operate.simulate.scenarios import BaseScenario, ScenarioResult
29
+
30
+
31
+ class DeclarativeScenario(BaseScenario):
32
+ """Executes a declarative scenario loaded from bas-scenario.schema.json."""
33
+
34
+ def __init__(
35
+ self,
36
+ scenario_data: Dict[str, Any],
37
+ target: str,
38
+ param_overrides: Optional[Dict[str, Any]] = None,
39
+ allow_draft: bool = False,
40
+ allow_destructive: bool = False,
41
+ timeout: float = 10.0,
42
+ ) -> None:
43
+ super().__init__(target, timeout)
44
+ self.raw_data = scenario_data
45
+ self.scenario_id = scenario_data["id"]
46
+ self.scenario_name = scenario_data["title"]
47
+ self.version = scenario_data.get("version", "1.0.0")
48
+ self.status = scenario_data.get("status", "draft")
49
+ self.description = scenario_data.get("incident", {}).get("summary", "")
50
+ self.incident_reference = (
51
+ f"{scenario_data.get('incident', {}).get('name', '')}: "
52
+ f"{' '.join(scenario_data.get('incident', {}).get('references', []))}"
53
+ )
54
+ self.param_overrides = param_overrides or {}
55
+ self.allow_draft = allow_draft
56
+ self.allow_destructive = allow_destructive
57
+
58
+ # Initialize parameters from defaults merged with overrides
59
+ self.params: Dict[str, Any] = {}
60
+ for p_name, p_def in scenario_data.get("parameters", {}).items():
61
+ self.params[p_name] = p_def.get("default")
62
+ self.params.update(self.param_overrides)
63
+
64
+ # Compute scenario definition content hash for audit evidence
65
+ scenario_bytes = json.dumps(scenario_data, sort_keys=True).encode("utf-8")
66
+ self.file_hash = f"sha256:{hashlib.sha256(scenario_bytes).hexdigest()}"
67
+
68
+ def run(self) -> ScenarioResult:
69
+ # Governance check: only approved scenarios can run unless allow_draft is set
70
+ if self.status != "approved" and not self.allow_draft:
71
+ raise PermissionError(
72
+ f"Scenario '{self.scenario_id}' has status '{self.status}'. "
73
+ "Only 'approved' scenarios can execute (use --allow-draft to override)."
74
+ )
75
+
76
+ details: List[Dict[str, Any]] = []
77
+ total_attempts = 0
78
+ breaches_blocked = 0
79
+ breaches_leaked = 0
80
+
81
+ steps = self.raw_data.get("steps", [])
82
+ for step_idx, step in enumerate(steps):
83
+ is_destructive = step.get("destructive", False)
84
+ if is_destructive and not self.allow_destructive:
85
+ details.append({
86
+ "step_name": step.get("name", f"Step {step_idx + 1}"),
87
+ "blocked": True,
88
+ "skipped": True,
89
+ "note": "Destructive step skipped (requires --allow-destructive)",
90
+ })
91
+ continue
92
+
93
+ step_type = step.get("type")
94
+ repeat_cfg = step.get("repeat")
95
+
96
+ if repeat_cfg:
97
+ param_name = repeat_cfg["param_name"]
98
+ as_var = repeat_cfg["as_var"]
99
+ items = self.params.get(param_name, [])
100
+ if not isinstance(items, list):
101
+ items = [items]
102
+
103
+ for it in items:
104
+ iter_vars = dict(self.params)
105
+ iter_vars[as_var] = str(it)
106
+ attempts, blocked, leaked, det = self._execute_single_step(step, iter_vars)
107
+ total_attempts += attempts
108
+ breaches_blocked += blocked
109
+ breaches_leaked += leaked
110
+ details.extend(det)
111
+ else:
112
+ attempts, blocked, leaked, det = self._execute_single_step(step, self.params)
113
+ total_attempts += attempts
114
+ breaches_blocked += blocked
115
+ breaches_leaked += leaked
116
+ details.extend(det)
117
+
118
+ scoring_cfg = self.raw_data.get("scoring", {})
119
+ recs = scoring_cfg.get("recommendations", {})
120
+ strategy = scoring_cfg.get("strategy", "ratio")
121
+
122
+ if total_attempts > 0:
123
+ if strategy == "all_or_nothing":
124
+ resilience_score = 100.0 if breaches_leaked == 0 else 0.0
125
+ else:
126
+ resilience_score = (breaches_blocked / total_attempts) * 100.0
127
+ else:
128
+ resilience_score = 0.0
129
+
130
+ passed = (breaches_leaked == 0) and (total_attempts > 0)
131
+ recommendation = recs.get("passed", "Secure.") if passed else recs.get("remediation", "Remediate vulnerabilities.")
132
+
133
+ return ScenarioResult(
134
+ scenario_id=self.scenario_id,
135
+ scenario_name=self.scenario_name,
136
+ description=self.description,
137
+ incident_reference=self.incident_reference,
138
+ total_attempts=total_attempts,
139
+ breaches_blocked=breaches_blocked,
140
+ breaches_leaked=breaches_leaked,
141
+ resilience_score=resilience_score,
142
+ passed=passed,
143
+ details=details,
144
+ recommendation=recommendation,
145
+ )
146
+
147
+ def _substitute(self, template: str, context: Dict[str, Any]) -> str:
148
+ """Replace placeholders {var} with values from context."""
149
+ res = template
150
+ for k, v in context.items():
151
+ res = res.replace(f"{{{k}}}", str(v))
152
+ return res
153
+
154
+ def _execute_single_step(
155
+ self, step: Dict[str, Any], context: Dict[str, Any]
156
+ ) -> Tuple[int, int, int, List[Dict[str, Any]]]:
157
+ step_type = step.get("type")
158
+ if step_type == "http_request":
159
+ return self._execute_http_step(step, context)
160
+ elif step_type == "storage_worm_check":
161
+ return self._execute_storage_worm_step(step, context)
162
+ else:
163
+ return 0, 0, 0, [{
164
+ "step_name": step.get("name"),
165
+ "blocked": False,
166
+ "note": f"Unsupported step type: {step_type}",
167
+ }]
168
+
169
+ def _execute_http_step(
170
+ self, step: Dict[str, Any], context: Dict[str, Any]
171
+ ) -> Tuple[int, int, int, List[Dict[str, Any]]]:
172
+ req_cfg = step.get("request", {})
173
+ method = req_cfg.get("method", "GET").upper()
174
+ raw_path = self._substitute(req_cfg.get("path", "/"), context)
175
+ # Substitute any residual path tokens
176
+ raw_path = self._substitute(raw_path, context)
177
+
178
+ # Build query string
179
+ query_map = req_cfg.get("query", {})
180
+ if query_map:
181
+ sub_query = {}
182
+ for qk, qv in query_map.items():
183
+ qk_sub = self._substitute(qk, context)
184
+ qv_sub = self._substitute(qv, context)
185
+ sub_query[qk_sub] = qv_sub
186
+ sep = "&" if "?" in raw_path else "?"
187
+ full_path = f"{raw_path}{sep}{urllib.parse.urlencode(sub_query)}"
188
+ else:
189
+ full_path = raw_path
190
+
191
+ url = f"{self.target}{full_path}"
192
+
193
+ headers = {}
194
+ for hk, hv in req_cfg.get("headers", {}).items():
195
+ headers[self._substitute(hk, context)] = self._substitute(hv, context)
196
+
197
+ body_bytes = None
198
+ if "body" in req_cfg:
199
+ body_bytes = self._substitute(req_cfg["body"], context).encode("utf-8")
200
+
201
+ req = urllib.request.Request(url, data=body_bytes, headers=headers, method=method)
202
+
203
+ status_code = 0
204
+ is_leaked = False
205
+ response_sample = ""
206
+
207
+ # Audit URL scheme to only permit HTTP/HTTPS per Bandit B310
208
+ parsed_url = urllib.parse.urlparse(url)
209
+ if parsed_url.scheme.lower() not in ("http", "https"):
210
+ raise ValueError(f"Unsupported URL scheme: {parsed_url.scheme}. Only http and https are permitted.")
211
+
212
+ try:
213
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp: # nosec B310
214
+ status_code = resp.status
215
+ body_text = resp.read().decode("utf-8", errors="replace")[:300]
216
+ response_sample = body_text
217
+ except urllib.error.HTTPError as err:
218
+ status_code = err.code
219
+ body_text = err.read().decode("utf-8", errors="replace")[:300]
220
+ response_sample = body_text
221
+ except Exception as ex:
222
+ status_code = 0
223
+ body_text = str(ex)
224
+ response_sample = body_text
225
+
226
+ # Evaluate assertions
227
+ expect_vulnerable = step.get("expect_vulnerable", {})
228
+ expect_secure = step.get("expect_secure", {})
229
+
230
+ # Check for vulnerability indicators
231
+ vuln_status_codes = expect_vulnerable.get("status_codes", [])
232
+ vuln_body_contains = expect_vulnerable.get("body_contains", [])
233
+ vuln_regex = expect_vulnerable.get("body_matches_regex")
234
+
235
+ if vuln_status_codes and status_code in vuln_status_codes:
236
+ is_leaked = True
237
+ elif vuln_body_contains and any(c in body_text for c in vuln_body_contains):
238
+ is_leaked = True
239
+ elif vuln_regex and re.search(vuln_regex, body_text, re.IGNORECASE):
240
+ is_leaked = True
241
+ else:
242
+ # Check secure indicators
243
+ sec_status_codes = expect_secure.get("status_codes", [])
244
+ sec_not_contains = expect_secure.get("body_not_contains", [])
245
+ if sec_status_codes and status_code in sec_status_codes:
246
+ is_leaked = False
247
+ elif sec_not_contains and any(c in body_text for c in sec_not_contains):
248
+ is_leaked = True
249
+ else:
250
+ is_leaked = False
251
+
252
+ blocked = 0 if is_leaked else 1
253
+ leaked = 1 if is_leaked else 0
254
+
255
+ detail_entry: Dict[str, Any] = {
256
+ "step": step.get("name"),
257
+ "url": url,
258
+ "status_code": status_code,
259
+ "leaked": is_leaked,
260
+ "response_sample": response_sample,
261
+ }
262
+ for k in ("target_id", "id", "payload"):
263
+ if k in context:
264
+ detail_entry[k] = context[k]
265
+
266
+ return 1, blocked, leaked, [detail_entry]
267
+
268
+ def _execute_storage_worm_step(
269
+ self, step: Dict[str, Any], context: Dict[str, Any]
270
+ ) -> Tuple[int, int, int, List[Dict[str, Any]]]:
271
+ target_dir = Path(context.get("target_dir", self.target)).resolve()
272
+ ops = step.get("storage_check", {}).get("operations", ["overwrite", "truncate", "delete"])
273
+
274
+ total = len(ops)
275
+ blocked = 0
276
+ leaked = 0
277
+ details: List[Dict[str, Any]] = []
278
+
279
+ test_file = target_dir / "audit_archive_sample.bak"
280
+ try:
281
+ target_dir.mkdir(parents=True, exist_ok=True)
282
+ test_file.write_text("Komagu Immutable WORM Backup - Read Only", encoding="utf-8")
283
+ # Mark file read-only for owner (simulating local WORM compliance mode without world permissions)
284
+ os.chmod(str(test_file), stat.S_IRUSR)
285
+ except Exception:
286
+ pass
287
+
288
+ for op in ops:
289
+ if op == "overwrite":
290
+ try:
291
+ with open(test_file, "w", encoding="utf-8") as f:
292
+ f.write("ENCRYPTED_RANSOMWARE_PAYLOAD")
293
+ leaked += 1
294
+ details.append({"operation": "Overwrite Archive", "blocked": False, "note": "File was writable without WORM lock"})
295
+ except (PermissionError, OSError):
296
+ blocked += 1
297
+ details.append({"operation": "Overwrite Archive", "blocked": True, "note": "Protected by write-protection / WORM lock"})
298
+ elif op == "truncate":
299
+ try:
300
+ with open(test_file, "a", encoding="utf-8") as f:
301
+ f.truncate(0)
302
+ leaked += 1
303
+ details.append({"operation": "Truncate File", "blocked": False, "note": "Truncation allowed"})
304
+ except (PermissionError, OSError):
305
+ blocked += 1
306
+ details.append({"operation": "Truncate File", "blocked": True, "note": "Truncation rejected by lock"})
307
+ elif op == "delete":
308
+ try:
309
+ test_file.unlink()
310
+ leaked += 1
311
+ details.append({"operation": "Delete File", "blocked": False, "note": "Deletion succeeded"})
312
+ except (PermissionError, OSError):
313
+ blocked += 1
314
+ details.append({"operation": "Delete File", "blocked": True, "note": "Deletion prevented by filesystem / WORM policy"})
315
+
316
+ # Restore owner write permission before cleanup
317
+ try:
318
+ os.chmod(str(test_file), stat.S_IRUSR | stat.S_IWUSR)
319
+ test_file.unlink(missing_ok=True)
320
+ except Exception:
321
+ pass
322
+
323
+ return total, blocked, leaked, details
324
+
325
+
326
+ def generate_execution_evidence(
327
+ scenarios: List[DeclarativeScenario],
328
+ results: List[ScenarioResult],
329
+ target: str,
330
+ output_path: Optional[Union[str, Path]] = None,
331
+ ) -> Dict[str, Any]:
332
+ """Generate signed audit evidence record conforming to METI SCS evidence requirements (#271)."""
333
+ evidence_doc = {
334
+ "$schema": "https://raw.githubusercontent.com/hoanghainh1188/komagu/main/contracts/schemas/bas-scenario.schema.json",
335
+ "evidence_type": "breach_and_attack_simulation_audit",
336
+ "target": target,
337
+ "executed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
338
+ "overall_resilience_score": round(
339
+ sum(r.resilience_score for r in results) / len(results) if results else 0.0, 1
340
+ ),
341
+ "scenarios_executed": len(results),
342
+ "audit_records": [],
343
+ }
344
+
345
+ sc_map = {s.scenario_id: s for s in scenarios}
346
+
347
+ for r in results:
348
+ sc = sc_map.get(r.scenario_id)
349
+ record = {
350
+ "scenario_id": r.scenario_id,
351
+ "scenario_name": r.scenario_name,
352
+ "version": sc.version if sc else "1.0.0",
353
+ "scenario_hash": sc.file_hash if sc else "sha256:unknown",
354
+ "status": sc.status if sc else "unknown",
355
+ "parameters": sc.params if sc else {},
356
+ "total_attempts": r.total_attempts,
357
+ "breaches_blocked": r.breaches_blocked,
358
+ "breaches_leaked": r.breaches_leaked,
359
+ "resilience_score": r.resilience_score,
360
+ "passed": r.passed,
361
+ "recommendation": r.recommendation,
362
+ }
363
+ evidence_doc["audit_records"].append(record)
364
+
365
+ if output_path:
366
+ p = Path(output_path).resolve()
367
+ p.parent.mkdir(parents=True, exist_ok=True)
368
+ p.write_text(json.dumps(evidence_doc, indent=2), encoding="utf-8")
369
+
370
+ return evidence_doc
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env python3
2
+ """Declarative Scenario Loader and Validator for Komagu BAS (#317).
3
+
4
+ Standard library only (Python 3.8+) per ADR-0006.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import re
12
+ from pathlib import Path
13
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
14
+
15
+ SCHEMAS_DIR = Path(__file__).resolve().parent.parent.parent / "contracts" / "schemas"
16
+ BUILTIN_SCENARIOS_DIR = Path(__file__).resolve().parent / "scenarios"
17
+
18
+
19
+ def load_validator(schema_name: str = "bas-scenario.schema.json") -> Callable[[Any], List[str]]:
20
+ """Loads a JSON schema validator for bas-scenario schema."""
21
+ schema_path = SCHEMAS_DIR / schema_name
22
+ if not schema_path.is_file():
23
+ return lambda obj: [f"Schema file not found: {schema_path}"]
24
+
25
+ schema = json.loads(schema_path.read_text(encoding="utf-8"))
26
+ try:
27
+ import jsonschema
28
+ cls = getattr(jsonschema, "Draft202012Validator", None) or getattr(jsonschema, "Draft7Validator")
29
+ schema_clean = schema if cls.__name__ == "Draft202012Validator" else {k: v for k, v in schema.items() if k != "$schema"}
30
+ v = cls(schema_clean)
31
+ return lambda obj: [f"{'/'.join(map(str, e.path))}: {e.message}" for e in v.iter_errors(obj)]
32
+ except (ImportError, AttributeError):
33
+ def fallback(obj: Any) -> List[str]:
34
+ errs: List[str] = []
35
+ _validate_node(obj, schema, schema, "", errs)
36
+ return errs
37
+ return fallback
38
+
39
+
40
+ def _validate_node(node: Any, spec: Dict[str, Any], root: Dict[str, Any], path: str, errs: List[str]) -> None:
41
+ """Recursive stdlib fallback schema validation."""
42
+ if not isinstance(spec, dict):
43
+ return
44
+
45
+ expected_type = spec.get("type")
46
+ if expected_type == "object":
47
+ if not isinstance(node, dict):
48
+ errs.append(f"{path or '/'}: expected object, got {type(node).__name__}")
49
+ return
50
+ for req in spec.get("required", []):
51
+ if req not in node:
52
+ errs.append(f"{path}/{req}: required property missing")
53
+ props = spec.get("properties", {})
54
+ for k, v in node.items():
55
+ if k in props:
56
+ _validate_node(v, props[k], root, f"{path}/{k}", errs)
57
+ elif spec.get("additionalProperties") is False:
58
+ errs.append(f"{path}/{k}: unexpected additional property")
59
+ elif isinstance(spec.get("additionalProperties"), dict):
60
+ _validate_node(v, spec["additionalProperties"], root, f"{path}/{k}", errs)
61
+
62
+ elif expected_type == "array":
63
+ if not isinstance(node, list):
64
+ errs.append(f"{path or '/'}: expected array, got {type(node).__name__}")
65
+ return
66
+ min_items = spec.get("minItems")
67
+ if min_items is not None and len(node) < min_items:
68
+ errs.append(f"{path}: array has {len(node)} items, expected at least {min_items}")
69
+ items_spec = spec.get("items")
70
+ if items_spec:
71
+ for idx, item in enumerate(node):
72
+ _validate_node(item, items_spec, root, f"{path}/{idx}", errs)
73
+
74
+ elif expected_type == "string":
75
+ if not isinstance(node, str):
76
+ errs.append(f"{path}: expected string, got {type(node).__name__}")
77
+ return
78
+ pattern = spec.get("pattern")
79
+ if pattern and not re.search(pattern, node):
80
+ errs.append(f"{path}: '{node}' does not match pattern '{pattern}'")
81
+ enum = spec.get("enum")
82
+ if enum and node not in enum:
83
+ errs.append(f"{path}: '{node}' is not one of {enum}")
84
+ min_len = spec.get("minLength")
85
+ if min_len is not None and len(node) < min_len:
86
+ errs.append(f"{path}: string length {len(node)} is less than minLength {min_len}")
87
+
88
+ elif expected_type == "integer":
89
+ if not isinstance(node, int) or isinstance(node, bool):
90
+ errs.append(f"{path}: expected integer, got {type(node).__name__}")
91
+
92
+ elif expected_type == "boolean":
93
+ if not isinstance(node, bool):
94
+ errs.append(f"{path}: expected boolean, got {type(node).__name__}")
95
+
96
+
97
+ def validate_scenario(scenario_data: Dict[str, Any]) -> List[str]:
98
+ """Validate a scenario dictionary against bas-scenario.schema.json."""
99
+ validator = load_validator("bas-scenario.schema.json")
100
+ return validator(scenario_data)
101
+
102
+
103
+ def load_scenario_file(file_path: Union[str, Path]) -> Dict[str, Any]:
104
+ """Read and parse a JSON scenario file."""
105
+ p = Path(file_path).resolve()
106
+ if not p.is_file():
107
+ raise FileNotFoundError(f"Scenario file not found: {p}")
108
+ content = p.read_text(encoding="utf-8")
109
+ data = json.loads(content)
110
+ return data
111
+
112
+
113
+ def discover_scenarios(
114
+ scenario_files: Optional[List[str]] = None,
115
+ scenario_dirs: Optional[List[str]] = None,
116
+ include_builtins: bool = True,
117
+ ) -> Dict[str, Dict[str, Any]]:
118
+ """Discover all scenarios from built-in directory, custom files, and custom directories.
119
+
120
+ Returns a dict mapping scenario_id -> scenario_dict.
121
+ """
122
+ discovered: Dict[str, Dict[str, Any]] = {}
123
+
124
+ # 1. Built-in scenarios
125
+ if include_builtins and BUILTIN_SCENARIOS_DIR.is_dir():
126
+ for f in BUILTIN_SCENARIOS_DIR.glob("*.json"):
127
+ try:
128
+ data = load_scenario_file(f)
129
+ sid = data.get("id")
130
+ if sid:
131
+ discovered[sid] = data
132
+ except Exception:
133
+ pass
134
+
135
+ # 2. Custom directories
136
+ if scenario_dirs:
137
+ for d in scenario_dirs:
138
+ dp = Path(d).resolve()
139
+ if dp.is_dir():
140
+ for f in sorted(dp.glob("*.json")):
141
+ try:
142
+ data = load_scenario_file(f)
143
+ sid = data.get("id")
144
+ if sid:
145
+ discovered[sid] = data
146
+ except Exception:
147
+ pass
148
+
149
+ # 3. Explicit scenario files
150
+ if scenario_files:
151
+ for f in scenario_files:
152
+ fp = Path(f).resolve()
153
+ data = load_scenario_file(fp)
154
+ sid = data.get("id")
155
+ if sid:
156
+ discovered[sid] = data
157
+
158
+ return discovered
@@ -0,0 +1,47 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/hoanghainh1188/komagu/main/contracts/schemas/bas-scenario.schema.json",
3
+ "id": "idc-frontier-ransomware",
4
+ "version": "1.0.0",
5
+ "title": "IDC Frontier Cloud Storage Ransomware Simulation",
6
+ "status": "approved",
7
+ "approved_by": "sec-team@komagu.io",
8
+ "approved_at": "2026-03-15T00:00:00Z",
9
+ "incident": {
10
+ "name": "IDC Frontier Cloud Storage Ransomware Disaster",
11
+ "date": "2024",
12
+ "summary": "Simulates in-flight malicious tamper/deletion attempts against storage archives to verify Write-Once-Read-Many (WORM) and S3 Object Lock compliance.",
13
+ "references": [
14
+ "IDC Frontier East Japan ransomware disaster: 495 enterprise tenants suffered unrecoverable cloud data loss."
15
+ ]
16
+ },
17
+ "mappings": {
18
+ "cwe": ["CWE-284"],
19
+ "owasp_api": ["API1:2023"],
20
+ "mitre_attack": ["T1485", "T1486"]
21
+ },
22
+ "parameters": {
23
+ "target_dir": {
24
+ "type": "string",
25
+ "default": "/tmp/backup-dir",
26
+ "description": "Path to local directory or storage mount point being evaluated for immutable WORM protection."
27
+ }
28
+ },
29
+ "steps": [
30
+ {
31
+ "name": "WORM Tamper & Deletion Compliance Check",
32
+ "description": "Performs overwrite, truncate, and delete operations against test archive file.",
33
+ "type": "storage_worm_check",
34
+ "destructive": false,
35
+ "storage_check": {
36
+ "operations": ["overwrite", "truncate", "delete"]
37
+ }
38
+ }
39
+ ],
40
+ "scoring": {
41
+ "strategy": "ratio",
42
+ "recommendations": {
43
+ "remediation": "Deploy Komagu Boilerplate 3 (Immutable Backup & DR) with AWS S3 Object Lock in COMPLIANCE mode and cross-region replication to prevent unrecoverable cloud ransomware wiping.",
44
+ "passed": "Storage assets strictly adhere to immutable WORM policy."
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,84 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/hoanghainh1188/komagu/main/contracts/schemas/bas-scenario.schema.json",
3
+ "id": "metabase-cve-2026-72898",
4
+ "version": "1.0.0",
5
+ "title": "Metabase CVE-2026-72898 Zero-Day SQLi Simulation",
6
+ "status": "approved",
7
+ "approved_by": "sec-team@komagu.io",
8
+ "approved_at": "2026-03-15T00:00:00Z",
9
+ "incident": {
10
+ "name": "Metabase Zero-Day Remote SQL Injection",
11
+ "date": "2024",
12
+ "summary": "Simulates unauthenticated remote SQL injection probes targeting query and search endpoints to measure WAF and AST parameterization defenses.",
13
+ "references": [
14
+ "CVE-2026-72898 (CVSS 10.0 unauthenticated SQLi)",
15
+ "Metabase zero-day leading to JGC Digital breach."
16
+ ]
17
+ },
18
+ "mappings": {
19
+ "cwe": ["CWE-89"],
20
+ "owasp_api": ["API8:2023"],
21
+ "mitre_attack": ["T1190"]
22
+ },
23
+ "parameters": {
24
+ "query_path": {
25
+ "type": "string",
26
+ "default": "/api/products",
27
+ "description": "Path to the query/search API endpoint."
28
+ },
29
+ "target_param": {
30
+ "type": "string",
31
+ "default": "q",
32
+ "description": "Query parameter name vulnerable to injection."
33
+ },
34
+ "payloads": {
35
+ "type": "list",
36
+ "default": [
37
+ "' OR '1'='1",
38
+ "1 OR 1=1 --",
39
+ "' UNION SELECT 1, 'komagu-cve-probe', 3 --",
40
+ "admin' --",
41
+ "1; SELECT pg_sleep(0) --"
42
+ ],
43
+ "description": "SQL injection payloads to probe."
44
+ }
45
+ },
46
+ "steps": [
47
+ {
48
+ "name": "Probe Unauthenticated SQL Injection",
49
+ "description": "Tests query endpoint with SQL injection payloads, checking for SQL error reflection or bypass.",
50
+ "type": "http_request",
51
+ "destructive": false,
52
+ "repeat": {
53
+ "param_name": "payloads",
54
+ "as_var": "payload"
55
+ },
56
+ "request": {
57
+ "method": "GET",
58
+ "path": "{query_path}",
59
+ "query": {
60
+ "{target_param}": "{payload}"
61
+ },
62
+ "headers": {
63
+ "User-Agent": "Komagu-BAS-MetabaseSimulator/1.0",
64
+ "Accept": "application/json"
65
+ }
66
+ },
67
+ "expect_secure": {
68
+ "status_codes": [400, 403],
69
+ "body_not_contains": ["komagu-cve-probe"]
70
+ },
71
+ "expect_vulnerable": {
72
+ "body_contains": ["komagu-cve-probe"],
73
+ "body_matches_regex": "(syntax error|sql|mysql|sqlite|postgresql|ora-)"
74
+ }
75
+ }
76
+ ],
77
+ "scoring": {
78
+ "strategy": "ratio",
79
+ "recommendations": {
80
+ "remediation": "Deploy Komagu Coraza WAF virtual patches (SecRule id:1100001+) and execute 'komagu fix' to rewrite dynamic SQL concatenations into parameterized prepared statements.",
81
+ "passed": "Perimeter WAF and parameterized queries successfully mitigated all injection payloads."
82
+ }
83
+ }
84
+ }