@hoanghainh/komagu 2.2.1 → 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 (36) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/assess/lsa/scripts/attack_chains.py +573 -0
  3. package/assess/lsa/scripts/baseline.py +14 -0
  4. package/assess/lsa/scripts/findings_to_sarif.py +11 -0
  5. package/assess/lsa/scripts/forecast.py +544 -0
  6. package/assess/lsa/scripts/merge_findings.py +14 -0
  7. package/assess/lsa/scripts/render_report.py +86 -0
  8. package/assess/lsa/tests/test_attack_chains.py +292 -0
  9. package/assess/lsa/tests/test_forecast.py +160 -0
  10. package/contracts/schemas/api-inventory-diff.schema.json +213 -0
  11. package/contracts/schemas/attack-chain.schema.json +181 -0
  12. package/contracts/schemas/bas-scenario.schema.json +217 -0
  13. package/contracts/schemas/evidence-manifest.schema.json +117 -0
  14. package/contracts/schemas/finding.schema.json +7 -0
  15. package/contracts/schemas/forecast.schema.json +172 -0
  16. package/governance/scs/__init__.py +21 -0
  17. package/governance/scs/mapping.py +133 -0
  18. package/governance/scs/pack.py +354 -0
  19. package/operate/cli/__init__.py +1 -1
  20. package/operate/cli/commands/evidence.py +138 -0
  21. package/operate/cli/commands/inventory.py +187 -0
  22. package/operate/cli/commands/simulate.py +175 -58
  23. package/operate/cli/main.py +7 -3
  24. package/operate/inventory/__init__.py +22 -0
  25. package/operate/inventory/api_inventory.py +566 -0
  26. package/operate/simulate/__init__.py +16 -0
  27. package/operate/simulate/engine.py +370 -0
  28. package/operate/simulate/loader.py +158 -0
  29. package/operate/simulate/scenarios/idc-frontier-ransomware.json +47 -0
  30. package/operate/simulate/scenarios/metabase-cve-2026-72898.json +84 -0
  31. package/operate/simulate/scenarios/times-car-breach.json +74 -0
  32. package/operate/simulate/scenarios.py +52 -0
  33. package/operate/tests/test_api_inventory.py +239 -0
  34. package/operate/tests/test_declarative_scenarios.py +169 -0
  35. package/operate/tests/test_evidence_pack.py +128 -0
  36. package/package.json +1 -1
@@ -0,0 +1,354 @@
1
+ #!/usr/bin/env python3
2
+ """METI SCS Evidence Pack Bundler and Gap Report Generator (#271).
3
+
4
+ Assembles technical audit artifacts into a timestamped, cryptographically
5
+ hashed ZIP archive with evidence-manifest.json and bilingual gap reports.
6
+
7
+ Standard library only (Python 3.8+) per ADR-0006.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import os
15
+ import shutil
16
+ import time
17
+ import zipfile
18
+ from pathlib import Path
19
+ from typing import Any, Dict, List, Optional, Tuple, Union
20
+
21
+ from governance.scs.mapping import (
22
+ LEGAL_DISCLAIMER_EN,
23
+ LEGAL_DISCLAIMER_JA,
24
+ SCS_CONTROLS,
25
+ )
26
+
27
+
28
+ def compute_file_sha256(path: Path) -> str:
29
+ """Compute standard SHA-256 hex digest of a file."""
30
+ h = hashlib.sha256()
31
+ with open(path, "rb") as f:
32
+ while chunk := f.read(65536):
33
+ h.update(chunk)
34
+ return h.hexdigest()
35
+
36
+
37
+ class EvidencePackBuilder:
38
+ """Evaluates audit directories and packages METI SCS evidence."""
39
+
40
+ def __init__(
41
+ self,
42
+ audit_dirs: List[Union[str, Path]],
43
+ project_name: str = "Komagu-Protected-Project",
44
+ target_system: str = "Production-Environment",
45
+ ) -> None:
46
+ self.audit_dirs = [Path(d).resolve() for d in audit_dirs]
47
+ self.project_name = project_name
48
+ self.target_system = target_system
49
+
50
+ def discover_artifacts(self) -> Dict[str, Path]:
51
+ """Discover available technical evidence artifacts across provided directories."""
52
+ discovered: Dict[str, Path] = {}
53
+ for d in self.audit_dirs:
54
+ if not d.is_dir():
55
+ continue
56
+ for root, _, files in os.walk(d):
57
+ for f in files:
58
+ fp = Path(root) / f
59
+ # Key by filename for mapping match
60
+ if f not in discovered:
61
+ discovered[f] = fp
62
+ return discovered
63
+
64
+ def evaluate_controls(self, artifacts: Dict[str, Path]) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
65
+ """Map discovered artifacts against METI SCS controls and compute readiness rating."""
66
+ evaluated_controls: List[Dict[str, Any]] = []
67
+ covered_count = 0
68
+ partial_count = 0
69
+
70
+ for ctrl in SCS_CONTROLS:
71
+ matched_files: List[str] = []
72
+ for exp in ctrl["expected_artifacts"]:
73
+ if exp in artifacts:
74
+ matched_files.append(exp)
75
+
76
+ if ctrl["control_id"] == "SCS-OG-01":
77
+ # Organizational governance requires manual policy attachment
78
+ status = "partially_covered" if matched_files else "out_of_scope_organizational"
79
+ elif matched_files:
80
+ status = "covered"
81
+ else:
82
+ status = "missing_evidence"
83
+
84
+ if status == "covered":
85
+ covered_count += 1
86
+ elif status == "partially_covered":
87
+ partial_count += 1
88
+
89
+ entry = dict(ctrl)
90
+ entry["status"] = status
91
+ entry["evidence_files"] = matched_files
92
+ evaluated_controls.append(entry)
93
+
94
+ total = len(SCS_CONTROLS)
95
+ compliance_pct = round(((covered_count + (0.5 * partial_count)) / total) * 100.0, 1)
96
+
97
+ if compliance_pct >= 85.0 and any(c["tier"] == "★4" and c["status"] == "covered" for c in evaluated_controls):
98
+ rating = "SCS-★4-READY"
99
+ elif compliance_pct >= 60.0:
100
+ rating = "SCS-★3-READY"
101
+ elif compliance_pct > 0:
102
+ rating = "GAP-IDENTIFIED"
103
+ else:
104
+ rating = "INSUFFICIENT-EVIDENCE"
105
+
106
+ readiness = {
107
+ "rating": rating,
108
+ "total_controls": total,
109
+ "covered_controls": covered_count,
110
+ "partial_controls": partial_count,
111
+ "compliance_percentage": compliance_pct,
112
+ }
113
+
114
+ return evaluated_controls, readiness
115
+
116
+ def generate_manifest(
117
+ self,
118
+ evaluated_controls: List[Dict[str, Any]],
119
+ readiness: Dict[str, Any],
120
+ artifacts: Dict[str, Path],
121
+ ) -> Dict[str, Any]:
122
+ """Generate evidence-manifest.json conforming to evidence-manifest.schema.json."""
123
+ bundled_records: List[Dict[str, Any]] = []
124
+
125
+ # Collect artifacts referenced in controls
126
+ used_files = set()
127
+ for ctrl in evaluated_controls:
128
+ for ef in ctrl["evidence_files"]:
129
+ used_files.add(ef)
130
+
131
+ for fname in sorted(used_files):
132
+ fp = artifacts.get(fname)
133
+ if fp and fp.is_file():
134
+ bundled_records.append({
135
+ "filename": fname,
136
+ "artifact_type": fp.suffix.lstrip(".") or "txt",
137
+ "sha256": compute_file_sha256(fp),
138
+ "size_bytes": fp.stat().st_size,
139
+ "description": f"Audit artifact: {fname}",
140
+ })
141
+
142
+ manifest = {
143
+ "$schema": "https://raw.githubusercontent.com/hoanghainh1188/komagu/main/contracts/schemas/evidence-manifest.schema.json",
144
+ "manifest_version": "1.0.0",
145
+ "project_name": self.project_name,
146
+ "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
147
+ "target_system": self.target_system,
148
+ "frameworks": [
149
+ "METI Supply Chain Security Evaluation Scheme (SCS ★3/★4)",
150
+ "NIST Cybersecurity Framework (NIST CSF 2.0)",
151
+ "Japan Active Cyber Defense Act (能動的サイバー防御 2025)",
152
+ ],
153
+ "overall_readiness": readiness,
154
+ "controls": evaluated_controls,
155
+ "bundled_artifacts": bundled_records,
156
+ "disclaimer": LEGAL_DISCLAIMER_JA + " " + LEGAL_DISCLAIMER_EN,
157
+ }
158
+
159
+ return manifest
160
+
161
+ def render_gap_report_ja(self, manifest: Dict[str, Any]) -> str:
162
+ """Render Japanese METI SCS Gap Analysis Report (gap-report.ja.md)."""
163
+ r = manifest["overall_readiness"]
164
+ lines = [
165
+ "# 経済産業省 サプライチェーン・サイバーセキュリティ (SCS ★3/★4) 適合性ギャップ分析レポート",
166
+ "",
167
+ "> **対象プロジェクト / システム:** `" + manifest["project_name"] + " (" + manifest["target_system"] + ")` ",
168
+ "> **評価実施日時:** `" + manifest["generated_at"] + "` ",
169
+ "> **総合適合性ステータス:** **`" + r["rating"] + "`** (適合率: **" + str(r["compliance_percentage"]) + "%**) ",
170
+ "",
171
+ "## 1. 概要及び評価結果サマリー",
172
+ "",
173
+ "本レポートは、2026年10月施行の**能動的サイバー防御法**および経済産業省**サプライチェーン・サイバーセキュリティ評価制度(SCS ★3/★4、NIST CSF 2.0準拠)**に基づき、"
174
+ "Komaguが検出・収集したセキュリティ監査成果物を自動照合した結果です。",
175
+ "",
176
+ "| 評価項目 | 結果 / 指標 |",
177
+ "|---|---|",
178
+ f"| **評価対象統制数** | {r['total_controls']} 項目 |",
179
+ f"| **技術的エビデンス充足 (Covered)** | {r['covered_controls']} 項目 |",
180
+ f"| **部分的充足 / 組織的対応要 (Partial)** | {r['partial_controls']} 項目 |",
181
+ f"| **総合適合率** | **{r['compliance_percentage']}%** |",
182
+ f"| **判定ランク** | **{r['rating']}** |",
183
+ "",
184
+ "## 2. METI SCS / NIST CSF 2.0 統制項目別 照合結果マトリクス",
185
+ "",
186
+ "| 統制ID | 統制名 / カテゴリ | 要求水準 | NIST CSF 2.0 | ステータス | 添付エビデンス |",
187
+ "|---|---|---|---|---|---|",
188
+ ]
189
+
190
+ status_ja = {
191
+ "covered": "✅ 適合 (Covered)",
192
+ "partially_covered": "⚠️ 部分的充足 (Partial)",
193
+ "out_of_scope_organizational": "ℹ️ 組織的対応 (Organizational)",
194
+ "missing_evidence": "❌ 証跡不足 (Missing)",
195
+ }
196
+
197
+ for c in manifest["controls"]:
198
+ st = status_ja.get(c["status"], c["status"])
199
+ ev_str = ", ".join(f"`{f}`" for f in c["evidence_files"]) if c["evidence_files"] else "*(なし)*"
200
+ csf = ", ".join(c["nist_csf_v2"])
201
+ lines.append(f"| **{c['control_id']}** | {c['name_ja']} | `{c['tier']}` | `{csf}` | {st} | {ev_str} |")
202
+
203
+ lines.extend([
204
+ "",
205
+ "## 3. 残存ギャップおよび是正アクション (Action Items)",
206
+ "",
207
+ ])
208
+
209
+ gap_found = False
210
+ for c in manifest["controls"]:
211
+ if c["status"] != "covered":
212
+ gap_found = True
213
+ lines.append(f"### 【{c['control_id']}】{c['name_ja']} ({c['tier']})")
214
+ lines.append(f"- **NIST CSF 2.0 カテゴリ:** `{', '.join(c['nist_csf_v2'])}`")
215
+ lines.append(f"- **現在の状態:** {status_ja.get(c['status'], c['status'])}")
216
+ lines.append(f"- **推奨是正アクション:** {c.get('action_required_ja', '社内規程の整備')}")
217
+ lines.append("")
218
+
219
+ if not gap_found:
220
+ lines.append("🎉 すべての統制項目に対して技術的エビデンスが充足しています。元請企業への提出が可能です。\n")
221
+
222
+ lines.extend([
223
+ "## 4. 添付エビデンスファイル一覧 (SHA-256 チェックサム)",
224
+ "",
225
+ "| ファイル名 | 種別 | SHA-256 チェックサム | サイズ |",
226
+ "|---|---|---|---|",
227
+ ])
228
+
229
+ for a in manifest["bundled_artifacts"]:
230
+ lines.append(f"| `{a['filename']}` | `{a['artifact_type']}` | `{a['sha256'][:16]}...` | {a['size_bytes']} bytes |")
231
+
232
+ lines.extend([
233
+ "",
234
+ "---",
235
+ "### 法的免責事項 (Legal Disclaimer)",
236
+ LEGAL_DISCLAIMER_JA,
237
+ "",
238
+ "*Report generated autonomously by Komagu Security-First Modernization Platform.*",
239
+ ])
240
+
241
+ return "\n".join(lines) + "\n"
242
+
243
+ def render_gap_report_en(self, manifest: Dict[str, Any]) -> str:
244
+ """Render English METI SCS Gap Analysis Report (gap-report.md)."""
245
+ r = manifest["overall_readiness"]
246
+ lines = [
247
+ "# METI Supply Chain Security (SCS ★3/★4) & NIST CSF 2.0 Gap Analysis Report",
248
+ "",
249
+ "> **Target Project / System:** `" + manifest["project_name"] + " (" + manifest["target_system"] + ")` ",
250
+ "> **Evaluation Timestamp:** `" + manifest["generated_at"] + "` ",
251
+ "> **Readiness Verdict:** **`" + r["rating"] + "`** (Compliance Score: **" + str(r["compliance_percentage"]) + "%**) ",
252
+ "",
253
+ "## 1. Executive Summary",
254
+ "",
255
+ "This report maps automated Komagu technical security audit artifacts against the Japanese Ministry of Economy, "
256
+ "Trade and Industry (METI) Supply Chain Security Evaluation Scheme (SCS ★3/★4) and NIST Cybersecurity Framework (CSF 2.0).",
257
+ "",
258
+ "| Metric | Value |",
259
+ "|---|---|",
260
+ f"| **Total Controls Evaluated** | {r['total_controls']} |",
261
+ f"| **Technically Covered** | {r['covered_controls']} |",
262
+ f"| **Partially Covered / Organizational** | {r['partial_controls']} |",
263
+ f"| **Overall Compliance Score** | **{r['compliance_percentage']}%** |",
264
+ f"| **Readiness Status** | **{r['rating']}** |",
265
+ "",
266
+ "## 2. Control Evaluation Matrix",
267
+ "",
268
+ "| Control ID | Name | Tier | NIST CSF 2.0 | Status | Evidence Artifacts |",
269
+ "|---|---|---|---|---|---|",
270
+ ]
271
+
272
+ status_en = {
273
+ "covered": "PASSED (Covered)",
274
+ "partially_covered": "PARTIAL (Action Req)",
275
+ "out_of_scope_organizational": "ORGANIZATIONAL",
276
+ "missing_evidence": "MISSING",
277
+ }
278
+
279
+ for c in manifest["controls"]:
280
+ st = status_en.get(c["status"], c["status"])
281
+ ev_str = ", ".join(f"`{f}`" for f in c["evidence_files"]) if c["evidence_files"] else "*(none)*"
282
+ csf = ", ".join(c["nist_csf_v2"])
283
+ lines.append(f"| **{c['control_id']}** | {c['name_en']} | `{c['tier']}` | `{csf}` | `{st}` | {ev_str} |")
284
+
285
+ lines.extend([
286
+ "",
287
+ "## 3. Gap Analysis & Remediation Action Plan",
288
+ "",
289
+ ])
290
+
291
+ for c in manifest["controls"]:
292
+ if c["status"] != "covered":
293
+ lines.append(f"### [{c['control_id']}] {c['name_en']} ({c['tier']})")
294
+ lines.append(f"- **NIST CSF 2.0 Category:** `{', '.join(c['nist_csf_v2'])}`")
295
+ lines.append(f"- **Current Status:** `{status_en.get(c['status'], c['status'])}`")
296
+ lines.append(f"- **Recommended Action:** {c.get('action_required_en', 'Document internal policy.')}")
297
+ lines.append("")
298
+
299
+ lines.extend([
300
+ "## 4. Bundled Artifact Manifest & Cryptographic Hashes",
301
+ "",
302
+ "| Filename | Type | SHA-256 Digest | Size (Bytes) |",
303
+ "|---|---|---|---|",
304
+ ])
305
+
306
+ for a in manifest["bundled_artifacts"]:
307
+ lines.append(f"| `{a['filename']}` | `{a['artifact_type']}` | `{a['sha256']}` | {a['size_bytes']} |")
308
+
309
+ lines.extend([
310
+ "",
311
+ "---",
312
+ "### Disclaimer",
313
+ LEGAL_DISCLAIMER_EN,
314
+ "",
315
+ "*Report generated autonomously by Komagu Security-First Modernization Platform.*",
316
+ ])
317
+
318
+ return "\n".join(lines) + "\n"
319
+
320
+ def build_pack(self, output_dir: Union[str, Path]) -> Tuple[Path, Path, Path, Path]:
321
+ """Build evidence pack ZIP, manifest JSON, and bilingual reports.
322
+
323
+ Returns:
324
+ (zip_path, manifest_path, gap_report_ja_path, gap_report_en_path)
325
+ """
326
+ out_p = Path(output_dir).resolve()
327
+ out_p.mkdir(parents=True, exist_ok=True)
328
+
329
+ artifacts = self.discover_artifacts()
330
+ evaluated_controls, readiness = self.evaluate_controls(artifacts)
331
+ manifest = self.generate_manifest(evaluated_controls, readiness, artifacts)
332
+
333
+ manifest_path = out_p / "evidence-manifest.json"
334
+ manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")
335
+
336
+ gap_report_ja_path = out_p / "gap-report.ja.md"
337
+ gap_report_ja_path.write_text(self.render_gap_report_ja(manifest), encoding="utf-8")
338
+
339
+ gap_report_en_path = out_p / "gap-report.md"
340
+ gap_report_en_path.write_text(self.render_gap_report_en(manifest), encoding="utf-8")
341
+
342
+ # Create evidence-pack.zip
343
+ zip_path = out_p / "evidence-pack.zip"
344
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
345
+ zf.write(manifest_path, arcname="evidence-manifest.json")
346
+ zf.write(gap_report_ja_path, arcname="gap-report.ja.md")
347
+ zf.write(gap_report_en_path, arcname="gap-report.md")
348
+
349
+ for item in manifest["bundled_artifacts"]:
350
+ fname = item["filename"]
351
+ if fname in artifacts and artifacts[fname].is_file():
352
+ zf.write(artifacts[fname], arcname=f"artifacts/{fname}")
353
+
354
+ return zip_path, manifest_path, gap_report_ja_path, gap_report_en_path
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "2.2.1" # x-release-please-version
5
+ __version__ = "2.3.0" # x-release-please-version
@@ -0,0 +1,138 @@
1
+ """Command: komagu evidence - METI SCS ★3/★4 & NIST CSF 2.0 Evidence Pack Bundler (#271)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import List
9
+
10
+ from governance.scs.pack import EvidencePackBuilder
11
+ from operate.cli.ui import (
12
+ BOLD,
13
+ CYAN,
14
+ GREEN,
15
+ RED,
16
+ YELLOW,
17
+ color,
18
+ print_summary_table,
19
+ step,
20
+ step_fail,
21
+ step_info,
22
+ step_ok,
23
+ step_warn,
24
+ )
25
+
26
+
27
+ def register_subcommand(subparsers: argparse._SubParsersAction) -> None:
28
+ """Register the evidence subcommand parser."""
29
+ parser = subparsers.add_parser(
30
+ "evidence",
31
+ help="Generate METI Supply Chain Security (SCS ★3/★4) evidence pack and gap report",
32
+ description="Bundles technical audit artifacts into a timestamped, cryptographically hashed evidence pack (ZIP + JSON manifest) with Japanese/English gap reports.",
33
+ formatter_class=argparse.RawDescriptionHelpFormatter,
34
+ epilog="""
35
+ Examples:
36
+ # Generate METI SCS evidence pack from default audit output directory (OUT/)
37
+ komagu evidence --audit-dir OUT --output-dir ./meti-evidence
38
+
39
+ # Generate evidence pack specifying project and system names
40
+ komagu evidence \
41
+ --audit-dir OUT \
42
+ --project "FinTech Core Banking" \
43
+ --system "Staging API Cluster" \
44
+ --output-dir ./audit-pack
45
+
46
+ # Enforce minimum readiness rating (e.g. fail CI if SCS-★3-READY is not met)
47
+ komagu evidence --audit-dir OUT --require-tier 3
48
+ """,
49
+ )
50
+ parser.add_argument(
51
+ "--audit-dir",
52
+ "-d",
53
+ action="append",
54
+ dest="audit_dirs",
55
+ metavar="DIR",
56
+ help="Path to directory containing audit outputs (e.g. OUT/, findings.sarif, sbom.json). Repeatable.",
57
+ )
58
+ parser.add_argument(
59
+ "--output-dir",
60
+ "-o",
61
+ default="./evidence-pack",
62
+ help="Directory to save evidence-pack.zip, evidence-manifest.json, and gap reports (default: ./evidence-pack)",
63
+ )
64
+ parser.add_argument(
65
+ "--project",
66
+ default="Target Modernization System",
67
+ help="Project or organization name (default: Target Modernization System)",
68
+ )
69
+ parser.add_argument(
70
+ "--system",
71
+ default="Production Architecture",
72
+ help="Target system or environment name (default: Production Architecture)",
73
+ )
74
+ parser.add_argument(
75
+ "--require-tier",
76
+ type=int,
77
+ choices=[3, 4],
78
+ help="Fail with exit code 1 if evaluated readiness does not meet the specified star tier (3 or 4)",
79
+ )
80
+ parser.set_defaults(func=execute_evidence)
81
+
82
+
83
+ def execute_evidence(args: argparse.Namespace) -> int:
84
+ """Execute evidence pack generation and gap analysis."""
85
+ audit_dirs = args.audit_dirs or ["OUT", "audit-out", "delivery/pilot/audit-out", "."]
86
+
87
+ step(f"Assembling METI SCS Evidence Pack for {color(args.project, BOLD)} ({args.system})")
88
+ step_info(f"Scanning audit source directories: {', '.join(audit_dirs)}")
89
+
90
+ builder = EvidencePackBuilder(
91
+ audit_dirs=audit_dirs,
92
+ project_name=args.project,
93
+ target_system=args.system,
94
+ )
95
+
96
+ artifacts = builder.discover_artifacts()
97
+ step_info(f"Discovered {len(artifacts)} audit artifacts across directories")
98
+
99
+ evaluated_controls, readiness = builder.evaluate_controls(artifacts)
100
+ rating = readiness["rating"]
101
+ score = readiness["compliance_percentage"]
102
+
103
+ # Build ZIP and reports
104
+ zip_path, manifest_path, gap_ja, gap_en = builder.build_pack(args.output_dir)
105
+
106
+ step_ok(f"Generated Evidence Manifest: {manifest_path}")
107
+ step_ok(f"Generated Japanese Gap Report: {gap_ja}")
108
+ step_ok(f"Generated English Gap Report: {gap_en}")
109
+ step_ok(f"Packaged Signed ZIP Archive: {zip_path}")
110
+
111
+ # Display Executive Summary Table
112
+ rating_color = GREEN if "READY" in rating else (YELLOW if rating == "GAP-IDENTIFIED" else RED)
113
+ summary_rows = [
114
+ ("Project Name", args.project),
115
+ ("Target System", args.system),
116
+ ("Total SCS Controls Evaluated", str(readiness["total_controls"])),
117
+ ("Technically Covered Controls", color(str(readiness["covered_controls"]), GREEN, BOLD)),
118
+ ("Partial / Organizational", color(str(readiness["partial_controls"]), YELLOW)),
119
+ ("Compliance Score", color(f"{score}%", rating_color, BOLD)),
120
+ ("METI SCS Readiness Rating", color(rating, rating_color, BOLD)),
121
+ ]
122
+
123
+ print_summary_table(
124
+ title="METI Supply Chain Security (SCS ★3/★4) Assessment Summary",
125
+ rows=summary_rows,
126
+ footer="Evidence pack ready for submission to prime contractor / accredited auditor."
127
+ )
128
+
129
+ # Gating enforcement if requested
130
+ if args.require_tier:
131
+ if args.require_tier == 4 and rating != "SCS-★4-READY":
132
+ step_fail(f"Quality Gate Failed: Required SCS-★4-READY, evaluated as {rating} ({score}%)")
133
+ return 1
134
+ elif args.require_tier == 3 and rating not in ("SCS-★3-READY", "SCS-★4-READY"):
135
+ step_fail(f"Quality Gate Failed: Required SCS-★3-READY, evaluated as {rating} ({score}%)")
136
+ return 1
137
+
138
+ return 0
@@ -0,0 +1,187 @@
1
+ """Inventory subcommand: compare code routes, OpenAPI spec, and gateway traffic to find Shadow/Zombie APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from operate.cli.ui import color, print_summary_table, step, step_fail, step_info, step_ok, step_warn
11
+ from operate.inventory.api_inventory import (
12
+ diff_api_inventory,
13
+ load_code_routes,
14
+ load_openapi_spec,
15
+ load_traffic_log,
16
+ )
17
+
18
+
19
+ def register_subcommand(subparsers: argparse._SubParsersAction) -> None:
20
+ """Register the inventory subcommand parser."""
21
+ parser = subparsers.add_parser(
22
+ "inventory",
23
+ help="Reconcile code routes, OpenAPI spec, and gateway traffic to detect Shadow and Zombie APIs",
24
+ description="Detects Shadow APIs (undocumented code/traffic) and Zombie APIs (deprecated routes still active).",
25
+ formatter_class=argparse.RawDescriptionHelpFormatter,
26
+ epilog="""
27
+ Examples:
28
+ # Reconcile LSA code routes against the published OpenAPI spec
29
+ komagu inventory --routes OUT/routes.json --openapi openapi.json
30
+
31
+ # Include Kong Gateway traffic log to spot Zombie APIs and active Shadow endpoints
32
+ komagu inventory --routes OUT/routes.json --openapi openapi.json --traffic-log /var/log/kong/access.log
33
+
34
+ # CI gate: fail if any undocumented Shadow API is detected
35
+ komagu inventory --routes OUT/routes.json --openapi openapi.json --fail-on-shadow
36
+
37
+ # Output machine-readable JSON diff to a custom path
38
+ komagu inventory --routes OUT/routes.json --openapi openapi.json --out OUT/api-diff.json
39
+ """,
40
+ )
41
+ parser.add_argument(
42
+ "--routes",
43
+ "-r",
44
+ type=Path,
45
+ required=True,
46
+ help="Path to code routes JSON (e.g. OUT/routes.json or routes.candidates.json from LSA)",
47
+ )
48
+ parser.add_argument(
49
+ "--openapi",
50
+ "-s",
51
+ type=Path,
52
+ required=True,
53
+ help="Path to OpenAPI / Swagger JSON or YAML specification",
54
+ )
55
+ parser.add_argument(
56
+ "--traffic-log",
57
+ "--kong-log",
58
+ "-t",
59
+ type=Path,
60
+ default=None,
61
+ help="Path to Kong HTTP-Log (JSON lines) or NGINX Combined access log",
62
+ )
63
+ parser.add_argument(
64
+ "--out",
65
+ "-o",
66
+ type=Path,
67
+ default=None,
68
+ help="Path to write the api-inventory-diff.json output (defaults to OUT/api-inventory-diff.json)",
69
+ )
70
+ parser.add_argument(
71
+ "--fail-on-shadow",
72
+ action="store_true",
73
+ help="Exit with non-zero code if any shadow API is detected",
74
+ )
75
+ parser.add_argument(
76
+ "--fail-on-zombie",
77
+ action="store_true",
78
+ help="Exit with non-zero code if any zombie API is detected",
79
+ )
80
+ parser.set_defaults(func=run_inventory)
81
+
82
+
83
+ def run_inventory(args: argparse.Namespace) -> int:
84
+ """Execute API inventory reconciliation."""
85
+ routes_file = Path(args.routes).resolve()
86
+ openapi_file = Path(args.openapi).resolve()
87
+ traffic_file = Path(args.traffic_log).resolve() if args.traffic_log else None
88
+
89
+ if not routes_file.is_file():
90
+ step_fail(f"Routes file not found: {routes_file}")
91
+ return 2
92
+
93
+ if not openapi_file.is_file():
94
+ step_fail(f"OpenAPI spec file not found: {openapi_file}")
95
+ return 2
96
+
97
+ step("Parsing routes extracted from source code")
98
+ code_routes = load_code_routes(routes_file)
99
+ step_ok(f"Loaded {len(code_routes)} endpoints from code routes")
100
+
101
+ step("Parsing OpenAPI specification")
102
+ spec_endpoints = load_openapi_spec(openapi_file)
103
+ step_ok(f"Loaded {len(spec_endpoints)} endpoints from OpenAPI specification")
104
+
105
+ traffic_data = {}
106
+ if traffic_file:
107
+ if not traffic_file.is_file():
108
+ step_warn(f"Traffic log file not found: {traffic_file} (skipping traffic analysis)")
109
+ else:
110
+ step("Analyzing gateway traffic log (Kong / NGINX)")
111
+ traffic_data = load_traffic_log(traffic_file)
112
+ total_hits = sum(t["traffic_hits"] for t in traffic_data.values())
113
+ step_ok(f"Parsed {len(traffic_data)} active endpoints ({total_hits} total requests) from traffic log")
114
+
115
+ step("Reconciling API inventories across 3 surfaces")
116
+ diff_doc = diff_api_inventory(
117
+ code_routes=code_routes,
118
+ spec_endpoints=spec_endpoints,
119
+ traffic_data=traffic_data,
120
+ routes_file_name=routes_file.name,
121
+ openapi_file_name=openapi_file.name,
122
+ traffic_file_name=traffic_file.name if traffic_file else None,
123
+ )
124
+
125
+ s = diff_doc["summary"]
126
+ shadow_count = s["shadow_count"]
127
+ zombie_count = s["zombie_count"]
128
+ missing_count = s["missing_code_count"]
129
+ dormant_count = s["dormant_count"]
130
+
131
+ # Determine output file path
132
+ out_file = args.out
133
+ if not out_file:
134
+ out_dir = routes_file.parent
135
+ out_file = out_dir / "api-inventory-diff.json"
136
+
137
+ out_file.parent.mkdir(parents=True, exist_ok=True)
138
+ with open(out_file, "w", encoding="utf-8") as fh:
139
+ json.dump(diff_doc, fh, ensure_ascii=False, indent=2)
140
+ fh.write("\n")
141
+ step_ok(f"Saved inventory diff to {out_file}")
142
+
143
+ # Display summary card
144
+ c_yellow_bold = "\033[33m\033[1m"
145
+ c_red_bold = "\033[31m\033[1m"
146
+ c_green = "\033[32m"
147
+ c_bold = "\033[1m"
148
+
149
+ summary_rows = [
150
+ ("Code Routes Discovered", str(s["total_code_routes"])),
151
+ ("OpenAPI Endpoints Documented", str(s["total_spec_endpoints"])),
152
+ ("Gateway Traffic Endpoints", str(s["total_traffic_endpoints"])),
153
+ ("Shadow APIs (Undocumented)", color(str(shadow_count), c_yellow_bold if shadow_count > 0 else c_green)),
154
+ ("Zombie APIs (Deprecated Active)", color(str(zombie_count), c_red_bold if zombie_count > 0 else c_green)),
155
+ ("Documented Missing in Code", str(missing_count)),
156
+ ("Dormant Code Endpoints", str(dormant_count)),
157
+ ]
158
+ print_summary_table(
159
+ "API Inventory Reconciliation Summary",
160
+ summary_rows,
161
+ footer="Shadow APIs expose undocumented attack surface; Zombie APIs must be retired."
162
+ )
163
+
164
+ if shadow_count > 0:
165
+ print(" " + color("⚠ Detected Shadow APIs (Top 5):", "\033[33m", c_bold))
166
+ for sh in diff_doc["shadow_apis"][:5]:
167
+ hits = f"({sh['traffic_hits']} traffic hits)" if sh["traffic_hits"] > 0 else ""
168
+ norm_str = color(sh["normalized"], c_bold)
169
+ disc_str = sh["discovered_in"]
170
+ print(f" • {norm_str} [{disc_str}] {hits}")
171
+
172
+ if zombie_count > 0:
173
+ print(" " + color("✖ Detected Zombie APIs (Top 5):", "\033[31m", c_bold))
174
+ for z in diff_doc["zombie_apis"][:5]:
175
+ norm_str = color(z["normalized"], c_bold)
176
+ hits_str = f"{z['traffic_hits']} active traffic hits!"
177
+ print(f" • {norm_str} ({hits_str})")
178
+
179
+ exit_code = 0
180
+ if args.fail_on_shadow and shadow_count > 0:
181
+ step_fail(f"Failing gate: {shadow_count} shadow APIs detected")
182
+ exit_code = 1
183
+ if args.fail_on_zombie and zombie_count > 0:
184
+ step_fail(f"Failing gate: {zombie_count} zombie APIs detected")
185
+ exit_code = 1
186
+
187
+ return exit_code