Add reports/render_report.py

This commit is contained in:
2026-07-07 16:57:38 +00:00
parent 91baea3252
commit 1a121e5c00
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""
render_report.py — Render IEC 62443-3-3 SL2 compliance JSON to terminal or Markdown.
Zero dependencies beyond Python 3 stdlib (json, sys, datetime).
Output formats:
terminal (default) — Unicode box-drawing report with:
- Executive summary (total/passed/failed/compliance rate)
- Per-category results grouped by FR section
- Failure details with expected/actual/remediation for each
md — GitHub-flavored Markdown with:
- Metadata table
- Summary table
- Results table with icons
- Per-failure sections with remediation instructions
Usage:
python3 reports/render_report.py <report.json>
python3 reports/render_report.py <report.json> --format md
python3 reports/render_report.py <report.json> --format md > REPORT.md
Icon mapping:
passed == true → ✅
passed == false → ❌
passed == "review" → 🔍
passed == "skipped" → ⏭️
Severity mapping:
critical → 🔴, high → 🟠, medium → 🟡, low → 🟢
"""
import json
import sys
from datetime import datetime
ICONS = {
True: "",
False: "",
"review": "🔍",
"skipped": "⏭️",
}
SEVERITY_COLORS = {
"critical": "🔴",
"high": "🟠",
"medium": "🟡",
"low": "🟢",
}
def pass_icon(v):
if isinstance(v, bool):
return ICONS[v]
return ICONS.get(v, "")
def severity_icon(s):
return SEVERITY_COLORS.get(s, "")
def render_terminal(report):
"""Rich terminal box-drawing report."""
meta = report["meta"]
summary = report["summary"]
results = report["results"]
failures = report.get("failures", [])
total = summary["total"]
passed = summary["passed"]
failed = summary["failed"]
skipped = summary.get("skipped", 0)
rate = (passed / total * 100) if total > 0 else 0
# Header
print("╔══════════════════════════════════════════════════════════════════════════╗")
print("║ IEC 62443-3-3 SECURITY LEVEL 2 — COMPLIANCE REPORT ║")
print("╠══════════════════════════════════════════════════════════════════════════╣")
print(f"║ Target: {meta['target']:<56s}")
print(f"║ Standard: {meta['standard']:<56s}")
print(f"║ Level: {meta['security_level']:<56s}")
print(f"║ Timestamp: {meta['timestamp']:<56s}")
print(f"║ Executed: {meta['executed_by']:<56s}")
print("╠══════════════════════════════════════════════════════════════════════════╣")
print("║ EXECUTIVE SUMMARY ║")
print("╠══════════════════════════════════════════════════════════════════════════╣")
print(f"║ TOTAL: {total:<4d} ✅ PASS: {passed:<4d} ❌ FAIL: {failed:<4d} 🔍 REVIEW: {skipped:<4d}")
print(f"║ COMPLIANCE RATE: {rate:.1f}% ║")
print("╠══════════════════════════════════════════════════════════════════════════╣")
print("║ RESULTS BY TEST CASE ║")
print("╠══════════════════════════════════════════════════════════════════════════╣")
# By category
current_cat = None
for r in results:
if r["category"] != current_cat:
current_cat = r["category"]
print(f"║ ║")
print(f"║ ▸ {current_cat:<68s}")
print(f"║ ║")
icon = pass_icon(r["passed"])
sev = severity_icon(r["severity"])
print(f"{sev} [{r['test_id']}] {icon} {r['description'][:60]:<60s}")
# Failures detail
print("╠══════════════════════════════════════════════════════════════════════════╣")
print("║ FAILURE DETAILS ║")
print("╠══════════════════════════════════════════════════════════════════════════╣")
if failures:
for f in failures:
print(f"║ ║")
print(f"║ ❌ [{f['test_id']}] {f['description'][:56]:<56s}")
print(f"║ Severity: {f['severity']:<52s}")
print(f"║ Expected: {f['expected'][:52]:<52s}")
print(f"║ Actual: {f['actual'][:52]:<52s}")
print(f"║ Remediation: {f['remediation'][:52]:<52s}")
else:
print("║ ✅ ALL CONTROLS PASSED ║")
print("╚══════════════════════════════════════════════════════════════════════════╝")
def render_markdown(report):
"""GitHub-flavored markdown report."""
meta = report["meta"]
summary = report["summary"]
results = report["results"]
failures = report.get("failures", [])
total = summary["total"]
passed = summary["passed"]
failed = summary["failed"]
rate = (passed / total * 100) if total > 0 else 0
print(f"# IEC 62443-3-3 SL2 Compliance Report")
print()
print(f"| Field | Value |")
print(f"|-------|-------|")
print(f"| Target | `{meta['target']}` |")
print(f"| Standard | {meta['standard']} |")
print(f"| Security Level | **{meta['security_level']}** |")
print(f"| Timestamp | {meta['timestamp']} |")
print(f"| Executed by | {meta['executed_by']} |")
print()
print(f"## Summary")
print()
print(f"| Total | Passed | Failed | Review | Compliance Rate |")
print(f"|-------|--------|--------|--------|-----------------|")
print(f"| {total} | {passed} | {failed} | {summary.get('skipped', 0)} | **{rate:.1f}%** |")
print()
print(f"## Results")
print()
print(f"| | ID | Requirement | Description | Expected | Actual | Severity |")
print(f"|---|----|-------------|-------------|----------|--------|----------|")
for r in results:
icon = pass_icon(r["passed"])
sev = severity_icon(r["severity"]) + " " + r["severity"]
print(f"| {icon} | {r['test_id']} | {r['requirement']} | {r['description']} | {r['expected']} | {r['actual']} | {sev} |")
if failures:
print()
print(f"## Failures ({len(failures)})")
print()
for f in failures:
print(f"### ❌ {f['test_id']}: {f['description']}")
print()
print(f"- **Severity:** {f['severity']}")
print(f"- **Expected:** {f['expected']}")
print(f"- **Actual:** {f['actual']}")
print(f"- **Remediation:** {f['remediation']}")
print()
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <report.json> [--format md|terminal]", file=sys.stderr)
sys.exit(1)
path = sys.argv[1]
fmt = "terminal"
if len(sys.argv) > 2 and sys.argv[2] == "--format":
fmt = sys.argv[3] if len(sys.argv) > 3 else "terminal"
with open(path) as f:
report = json.load(f)
if fmt == "md":
render_markdown(report)
else:
render_terminal(report)