@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.
- package/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/assess/lsa/scripts/attack_chains.py +573 -0
- package/assess/lsa/scripts/baseline.py +14 -0
- package/assess/lsa/scripts/findings_to_sarif.py +11 -0
- package/assess/lsa/scripts/forecast.py +544 -0
- package/assess/lsa/scripts/merge_findings.py +14 -0
- package/assess/lsa/scripts/render_report.py +86 -0
- package/assess/lsa/tests/test_attack_chains.py +292 -0
- package/assess/lsa/tests/test_forecast.py +160 -0
- package/contracts/schemas/api-inventory-diff.schema.json +213 -0
- package/contracts/schemas/attack-chain.schema.json +181 -0
- package/contracts/schemas/bas-scenario.schema.json +217 -0
- package/contracts/schemas/evidence-manifest.schema.json +117 -0
- package/contracts/schemas/finding.schema.json +7 -0
- package/contracts/schemas/forecast.schema.json +172 -0
- package/examples/member-portal/backend-dotnet/MemberPortal.Backend.csproj +2 -2
- package/governance/scs/__init__.py +21 -0
- package/governance/scs/mapping.py +133 -0
- package/governance/scs/pack.py +354 -0
- package/operate/cli/__init__.py +1 -1
- package/operate/cli/commands/evidence.py +138 -0
- package/operate/cli/commands/inventory.py +187 -0
- package/operate/cli/commands/simulate.py +175 -58
- package/operate/cli/main.py +7 -3
- package/operate/inventory/__init__.py +22 -0
- package/operate/inventory/api_inventory.py +566 -0
- package/operate/simulate/__init__.py +16 -0
- package/operate/simulate/engine.py +370 -0
- package/operate/simulate/loader.py +158 -0
- package/operate/simulate/scenarios/idc-frontier-ransomware.json +47 -0
- package/operate/simulate/scenarios/metabase-cve-2026-72898.json +84 -0
- package/operate/simulate/scenarios/times-car-breach.json +74 -0
- package/operate/simulate/scenarios.py +52 -0
- package/operate/tests/test_api_inventory.py +239 -0
- package/operate/tests/test_declarative_scenarios.py +169 -0
- package/operate/tests/test_evidence_pack.py +128 -0
- package/package.json +1 -1
- package/remediate/authz/dotnet/src/Komagu.Security.EntityFrameworkCore/Komagu.Security.EntityFrameworkCore.csproj +1 -1
- package/remediate/authz/dotnet/tests/Komagu.Security.Authorization.Tests/Komagu.Security.Authorization.Tests.csproj +1 -1
- package/remediate/authz/spring/build.gradle.kts +4 -4
- package/remediate/identity/dotnet/src/Komagu.Security.Identity/Komagu.Security.Identity.csproj +1 -1
- package/remediate/identity/dotnet/tests/Komagu.Security.Identity.Tests/Komagu.Security.Identity.Tests.csproj +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
|
package/operate/cli/__init__.py
CHANGED
|
@@ -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
|