Add reports/render_report.py
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user