#!/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 python3 reports/render_report.py --format md python3 reports/render_report.py --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]} [--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)