186 lines
8.5 KiB
Python
186 lines
8.5 KiB
Python
#!/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)
|