125 lines
5.0 KiB
Python
125 lines
5.0 KiB
Python
"""Markdown report renderer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from ..engine import AnalysisResult
|
|
from ..findings import Severity
|
|
from ._common import (
|
|
SEVERITY_LABEL, fmt_ts, affected_line, evidence_line, artifact_line, sources_list,
|
|
inventory_lines,
|
|
)
|
|
|
|
|
|
def render(result: AnalysisResult, extra: dict | None = None) -> str:
|
|
m = result.meta
|
|
s = result.summary
|
|
out: list[str] = []
|
|
|
|
out.append("# IoT PreTester 4.6 Demo — Network Capture Security Analysis")
|
|
out.append("")
|
|
out.append(f"- **Capture:** `{m.get('capture_path', '—')}`")
|
|
out.append(f"- **Packets:** {m.get('packet_count', 0)} · "
|
|
f"**Duration:** {m.get('duration_seconds', 0)} s · "
|
|
f"**Findings:** {m.get('finding_count', 0)}")
|
|
if m.get("device_ip"):
|
|
out.append(f"- **Device under test:** `{m['device_ip']}`"
|
|
+ (f" (MAC {', '.join(m.get('device_macs') or [])})" if m.get("device_macs") else "")
|
|
+ f" · device-related findings: {s.get('device_related', 0)}, other hosts: {s.get('other_hosts', 0)}")
|
|
if m.get("truncated"):
|
|
out.append(f"- **⚠️ Partial analysis:** {m.get('truncation_reason')}")
|
|
out.append("")
|
|
|
|
bysev = s.get("by_severity", {})
|
|
out.append("## Summary")
|
|
out.append("")
|
|
out.append("| Severity | Count |")
|
|
out.append("| ----------- | ------ |")
|
|
for sev in Severity:
|
|
out.append(f"| {SEVERITY_LABEL[sev]} | {bysev.get(sev.value, 0)} |")
|
|
out.append("")
|
|
|
|
highlights = s.get("highlights", [])
|
|
if highlights:
|
|
out.append("## ⚠️ Prioritized Highlights")
|
|
out.append("")
|
|
out.append("Highest-impact credential, token, C2, and exfiltration findings:")
|
|
out.append("")
|
|
for f in highlights:
|
|
out.append(f"- **[{SEVERITY_LABEL[f.severity]}/{f.confidence.value}]** "
|
|
f"{f.title} — {affected_line(f)}")
|
|
out.append("")
|
|
|
|
out.append("## Findings")
|
|
out.append("")
|
|
if not result.findings:
|
|
out.append("_No security-relevant findings detected._")
|
|
for i, f in enumerate(result.findings, 1):
|
|
out.append(f"### {i}. {f.title}")
|
|
out.append("")
|
|
out.append(f"- **Severity:** {SEVERITY_LABEL[f.severity]} "
|
|
f"· **Confidence:** {f.confidence.value} "
|
|
f"· **Category:** {f.category.value} "
|
|
f"· **Detector:** {f.detector}")
|
|
art = artifact_line(f)
|
|
if art and art != "—":
|
|
out.append(f"- **🔎 Observed artifact:** `{art}`")
|
|
srcs = sources_list(f)
|
|
if srcs:
|
|
out.append("- **📍 Sources:**")
|
|
for source in srcs:
|
|
out.append(f" - {source}")
|
|
out.append(f"- **Risk:** {f.risk_description}")
|
|
out.append(f"- **Affected:** {affected_line(f)}"
|
|
+ (" · **involves device under test**" if f.evidence.get("device_related") else ""))
|
|
out.append(f"- **Additional evidence:** {evidence_line(f)}")
|
|
out.append(f"- **Recommendation:** {f.recommendation}")
|
|
out.append(f"- **Finding ID:** `{f.finding_id}`")
|
|
out.append("")
|
|
|
|
inv_lines = inventory_lines(result.inventory, md=True)
|
|
if inv_lines:
|
|
out.append("")
|
|
out.extend(inv_lines)
|
|
|
|
# Optional URL and hash enrichment results.
|
|
if extra:
|
|
hashes = extra.get("hashes") or []
|
|
urls = extra.get("urls") or []
|
|
if hashes:
|
|
out.append("")
|
|
out.append(f"## Hashes ({len(hashes)})")
|
|
out.append("")
|
|
for h in hashes:
|
|
if h.get("cracked"):
|
|
state = f"**CRACKED** → `{h.get('cracked_value')}`"
|
|
if h.get("crack_time"):
|
|
state += f" ({h['crack_time']})"
|
|
elif h.get("analyzed"):
|
|
state = "not cracked"
|
|
else:
|
|
state = "analysis pending"
|
|
htype = ", ".join(h.get("hash_type") or [])
|
|
out.append(f"- `{h.get('hash_value')}`"
|
|
f"{f' [{htype}]' if htype else ''} — {state}")
|
|
if urls:
|
|
out.append("")
|
|
out.append(f"## Cloud URLs and Endpoints ({len(urls)})")
|
|
out.append("")
|
|
for u in urls:
|
|
if u.get("analyzed"):
|
|
ports = ", ".join(map(str, u.get("ports") or [])) or "—"
|
|
details = f"Ports: {ports}"
|
|
if u.get("tls_versions"):
|
|
details += f"; TLS: {', '.join(u['tls_versions'])}"
|
|
if u.get("cipher_suites"):
|
|
details += f"; Cipher: {', '.join(u['cipher_suites'])}"
|
|
else:
|
|
details = "analysis pending"
|
|
out.append(f"- `{u.get('url')}` — {details}")
|
|
|
|
out.append("")
|
|
out.append("---")
|
|
out.append(f"_Generated by IoT PreTester 4.6 Demo · Detectors: "
|
|
f"{', '.join(m.get('detectors_run', []))}_")
|
|
return "\n".join(out)
|