From 1a121e5c003bb838bf319020028b0bae996bae31 Mon Sep 17 00:00:00 2001 From: oval Date: Tue, 7 Jul 2026 16:57:38 +0000 Subject: [PATCH] Add reports/render_report.py --- reports/render_report.py | 185 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 reports/render_report.py diff --git a/reports/render_report.py b/reports/render_report.py new file mode 100644 index 0000000..9c182ba --- /dev/null +++ b/reports/render_report.py @@ -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 + 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)