CYBER-0 initial concept ready
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render a normalized test report as Markdown, HTML, and PDF."""
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from fpdf import FPDF
|
||||
|
||||
|
||||
def pdf_text(value: object) -> str:
|
||||
return str(value).encode("latin-1", errors="replace").decode("latin-1")
|
||||
|
||||
|
||||
def markdown(report: dict) -> str:
|
||||
project = report["project"]
|
||||
tool = report["tool"]
|
||||
target = report["target"]
|
||||
summary = report["summary"]
|
||||
lines = [
|
||||
f"# {project['name']} Test Report",
|
||||
"",
|
||||
"| Field | Value |",
|
||||
"|---|---|",
|
||||
f"| Project ID | {project['id']} |",
|
||||
f"| Environment | {project['environment']} |",
|
||||
f"| Tool | {tool['name']} |",
|
||||
f"| Target | {target['id']} |",
|
||||
f"| Run | {report['run']['id']} |",
|
||||
f"| Started | {report['run']['started_at']} |",
|
||||
"",
|
||||
"## Summary",
|
||||
"",
|
||||
"| Total | Passed | Failed | Errors | Skipped | Review | Score |",
|
||||
"|---:|---:|---:|---:|---:|---:|---:|",
|
||||
f"| {summary['total']} | {summary['passed']} | {summary['failed']} | {summary['errors']} | {summary['skipped']} | {summary['review']} | {summary.get('score', 0):.2f}% |",
|
||||
"",
|
||||
"## Results",
|
||||
"",
|
||||
"| Status | Severity | ID | Title |",
|
||||
"|---|---|---|---|",
|
||||
]
|
||||
for result in report["results"]:
|
||||
title = str(result["title"]).replace("|", "\\|")
|
||||
lines.append(f"| {result['status']} | {result['severity']} | {result['id']} | {title} |")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def html_document(markdown_text: str, report: dict) -> str:
|
||||
rows = "".join(
|
||||
"<tr>"
|
||||
f"<td>{html.escape(result['status'])}</td>"
|
||||
f"<td>{html.escape(result['severity'])}</td>"
|
||||
f"<td>{html.escape(result['id'])}</td>"
|
||||
f"<td>{html.escape(result['title'])}</td>"
|
||||
"</tr>"
|
||||
for result in report["results"]
|
||||
)
|
||||
summary = report["summary"]
|
||||
return f"""<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><title>{html.escape(report['project']['name'])} test report</title>
|
||||
<style>body{{font:14px sans-serif;max-width:1100px;margin:40px auto;color:#17202a}}table{{border-collapse:collapse;width:100%}}th,td{{border:1px solid #ccd1d1;padding:8px;text-align:left}}th{{background:#eaecee}}.summary{{display:flex;gap:24px;margin:24px 0}}.summary strong{{font-size:24px;display:block}}</style></head>
|
||||
<body><h1>{html.escape(report['project']['name'])} Test Report</h1>
|
||||
<p>{html.escape(report['tool']['name'])} against {html.escape(report['target']['id'])} at {html.escape(report['run']['started_at'])}</p>
|
||||
<div class="summary"><span><strong>{summary['total']}</strong>Total</span><span><strong>{summary['passed']}</strong>Passed</span><span><strong>{summary['failed']}</strong>Failed</span><span><strong>{summary.get('score', 0):.2f}%</strong>Score</span></div>
|
||||
<table><thead><tr><th>Status</th><th>Severity</th><th>ID</th><th>Title</th></tr></thead><tbody>{rows}</tbody></table>
|
||||
<details><summary>Markdown source</summary><pre>{html.escape(markdown_text)}</pre></details></body></html>"""
|
||||
|
||||
|
||||
def pdf_document(report: dict, output: Path) -> None:
|
||||
pdf = FPDF()
|
||||
pdf.set_auto_page_break(auto=True, margin=15)
|
||||
pdf.add_page()
|
||||
pdf.set_font("Helvetica", "B", 18)
|
||||
pdf.multi_cell(0, 10, pdf_text(f"{report['project']['name']} Test Report"), new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.set_font("Helvetica", size=10)
|
||||
pdf.multi_cell(
|
||||
0,
|
||||
6,
|
||||
pdf_text(f"Tool: {report['tool']['name']}\nTarget: {report['target']['id']}\nStarted: {report['run']['started_at']}"),
|
||||
new_x="LMARGIN",
|
||||
new_y="NEXT",
|
||||
)
|
||||
summary = report["summary"]
|
||||
pdf.ln(3)
|
||||
pdf.set_font("Helvetica", "B", 12)
|
||||
pdf.cell(0, 8, f"Total {summary['total']} Passed {summary['passed']} Failed {summary['failed']} Score {summary.get('score', 0):.2f}%", new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.set_font("Helvetica", size=9)
|
||||
for result in report["results"]:
|
||||
pdf.multi_cell(
|
||||
0,
|
||||
5,
|
||||
pdf_text(f"[{result['status'].upper()}] [{result['severity']}] {result['id']}: {result['title']}"),
|
||||
new_x="LMARGIN",
|
||||
new_y="NEXT",
|
||||
)
|
||||
pdf.output(output)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("input", type=Path)
|
||||
parser.add_argument("--output-dir", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
report = json.loads(args.input.read_text(encoding="utf-8"))
|
||||
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
stem = f"{report['tool']['id']}-{report['target']['id']}"
|
||||
markdown_text = markdown(report)
|
||||
(args.output_dir / f"{stem}.md").write_text(markdown_text, encoding="utf-8")
|
||||
(args.output_dir / f"{stem}.html").write_text(html_document(markdown_text, report), encoding="utf-8")
|
||||
pdf_document(report, args.output_dir / f"{stem}.pdf")
|
||||
print(f"Rendered Markdown, HTML, and PDF reports in {args.output_dir}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user