#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
2520 漏洞报告生成器
读取崩溃/模糊测试数据并生成专业的安全公告级 HTML 报告
"""

import os
import json
import uuid
import argparse
from datetime import datetime


def read_crashes_from_dir(crash_dir="./crash_records"):
    """从本地崩溃记录目录读取 JSON 文件"""
    crashes = []
    if os.path.exists(crash_dir):
        for f in sorted(os.listdir(crash_dir)):
            if f.endswith(".json"):
                fp = os.path.join(crash_dir, f)
                try:
                    with open(fp, "r", encoding="utf-8") as fh:
                        crashes.append(json.load(fh))
                except (json.JSONDecodeError, IOError):
                    pass
    return crashes


def read_crashes_from_jetson(host, ssh_port=22, agent_port=7886, user="nvidia", password="nvidia"):
    """通过 paramiko SSH 连接 Jetson Agent API 获取崩溃数据"""
    import paramiko

    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(host, username=user, password=password, port=ssh_port, timeout=10)

    crashes = []

    # 获取主崩溃列表
    stdin, stdout, stderr = ssh.exec_command(
        "curl -s http://localhost:7886/api/crashes", timeout=15
    )
    raw = stdout.read().decode().strip()
    if raw:
        try:
            data = json.loads(raw)
            crashes.extend(data.get("crashes", data.get("records", [])))
        except json.JSONDecodeError:
            pass

    # 获取各模块结果中的崩溃记录
    modules = [
        "can_fuzz", "serial_fuzz", "camera_fuzz",
        "i2c_sensor_fuzz", "ollama_fuzz", "network_fuzz",
    ]
    for m in modules:
        cmd = f"curl -s http://localhost:7886/api/module/{m}/results"
        stdin, stdout, stderr = ssh.exec_command(cmd, timeout=10)
        raw = stdout.read().decode().strip()
        if not raw:
            continue
        try:
            mod_data = json.loads(raw)
            for c in mod_data.get("crashes", []):
                c.setdefault("module", m)
                c.setdefault("target", mod_data.get("module_name", m))
                crashes.append(c)
        except json.JSONDecodeError:
            pass

    ssh.close()
    return crashes


def _signal_label(sig):
    """返回信号的 CSS 类名和中文描述"""
    mapping = {
        "SIGSEGV": ("danger", "段错误 (SIGSEGV)"),
        "SIGABRT": ("danger", "中止 (SIGABRT)"),
        "SIGBUS":  ("danger", "总线错误 (SIGBUS)"),
        "SIGFPE":  ("warning", "浮点异常 (SIGFPE)"),
        "SIGILL":  ("warning", "非法指令 (SIGILL)"),
        "SIGKILL": ("warning", "异常终止 (SIGKILL)"),
        "TIMEOUT": ("info", "超时 (Timeout)"),
        "Timeout": ("info", "超时 (Timeout)"),
    }
    return mapping.get(sig, ("danger", str(sig)))


def generate_report(crashes, output_path="bug.html", title="2520 漏洞报告"):
    """生成完整的漏洞报告 HTML 文件"""

    report_id = uuid.uuid4().hex[:12].upper()
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    # 统计
    signal_counts = {}
    target_counts = {}
    for c in crashes:
        sig = c.get("signal", "UNKNOWN")
        signal_counts[sig] = signal_counts.get(sig, 0) + 1
        tgt = c.get("target", c.get("module", "unknown"))
        target_counts[tgt] = target_counts.get(tgt, 0) + 1

    total = len(crashes)
    unique_signals = len(signal_counts)
    affected_targets = len(target_counts)

    if total > 10:
        severity = "CRITICAL"
        severity_color = "#ff0040"
    elif total > 0:
        severity = "HIGH"
        severity_color = "#ffaa00"
    else:
        severity = "INFO"
        severity_color = "#00d4ff"

    # 构建表格行
    table_rows = ""
    detail_sections = ""
    for i, c in enumerate(crashes, 1):
        sig = c.get("signal", "UNKNOWN")
        cls, _ = _signal_label(sig)
        timestamp = c.get("timestamp", c.get("time", "-"))
        target = c.get("target", c.get("module", "-"))
        sample = c.get("sample", c.get("sample_name", "-"))
        exit_code = c.get("exit_code", "-")

        table_rows += f"""\
            <tr>
              <td>{i}</td>
              <td>{timestamp}</td>
              <td>{target}</td>
              <td><span class="signal-tag signal-{cls}">{sig}</span></td>
              <td>{sample}</td>
              <td>{exit_code}</td>
            </tr>"""

        detail_sections += f"""\
          <div class="crash-detail">
            <div class="detail-header">#{i} — {sig} @ {target}</div>
            <table class="detail-table">
              <tr><th>时间戳</th><td>{timestamp}</td></tr>
              <tr><th>目标</th><td>{target}</td></tr>
              <tr><th>样本</th><td>{sample}</td></tr>
              <tr><th>信号</th><td>{sig}</td></tr>
              <tr><th>退出码</th><td>{exit_code}</td></tr>
              <tr><th>样本路径</th><td>{c.get("sample_path", c.get("path", "-"))}</td></tr>
              <tr><th>耗时</th><td>{c.get("duration", c.get("elapsed", "-"))}</td></tr>
            </table>
            <div class="detail-output">{c.get("output", c.get("log", c.get("stderr", ""))) or "无输出日志"}</div>
          </div>"""

    # 信号统计行
    signal_stats = ""
    for sig, count in sorted(signal_counts.items(), key=lambda x: -x[1]):
        cls, label = _signal_label(sig)
        signal_stats += f"""\
          <div class="stat-card stat-{cls}">
            <div class="stat-value">{count}</div>
            <div class="stat-label">{label}</div>
          </div>"""

    # 信号图例
    signal_legend_items = [
        ("danger", "SIGSEGV", "段错误 — 访问无效内存地址"),
        ("danger", "SIGABRT", "中止 — 进程主动调用 abort()"),
        ("danger", "SIGBUS", "总线错误 — 未对齐内存访问"),
        ("warning", "SIGFPE", "浮点异常 — 除零等算术错误"),
        ("warning", "SIGILL", "非法指令 — CPU 无法识别的指令"),
    ]
    legend_html = ""
    for cls_name, sig, desc in signal_legend_items:
        legend_html += f"""\
          <div class="legend-item">
            <span class="signal-tag signal-{cls_name}">{sig}</span>
            <span class="legend-desc">{desc}</span>
          </div>"""

    # 受影响目标列表
    target_list = "".join(
        f'<span class="target-badge">{t}</span>'
        for t in sorted(target_counts.keys())
    )

    # 安全建议
    recommendations = ""
    if any(s.startswith("SIG") for s in signal_counts):
        recommendations += """\
          <li>对崩溃信号对应的代码路径进行深入审查，确认是否存在可被利用的内存破坏漏洞。</li>
          <li>启用地址消毒剂 (AddressSanitizer) 和未定义行为消毒剂 (UBSan) 进行回归测试。</li>"""
    if signal_counts.get("SIGSEGV", 0) > 0:
        recommendations += """\
          <li>SIGSEGV 段错误表明存在内存越界或空指针解引用风险，建议使用边界检查工具进行审计。</li>"""
    if signal_counts.get("SIGFPE", 0) > 0:
        recommendations += """\
          <li>SIGFPE 浮点异常可能由除零或无效运算导致，在嵌入式/具身智能场景中可能导致系统不稳定。</li>"""
    if signal_counts.get("TIMEOUT", 0) > 0 or signal_counts.get("Timeout", 0) > 0:
        recommendations += """\
          <li>超时异常表明程序在处理特定输入时陷入死循环或资源阻塞，建议添加看门狗机制。</li>"""
    if not recommendations:
        recommendations = "<li>未检测到严重崩溃，建议持续监控并增加覆盖深度。</li>"
    recommendations += """\
          <li>所有暴露给外部输入的接口应实施严格的输入验证和长度检查。</li>
          <li>建议将复现样本纳入回归测试套件，防止回归漏洞。</li>"""

    html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<style>
*{{margin:0;padding:0;box-sizing:border-box}}
:root{{
  --bg-primary:#0a0a0f;--bg-secondary:#0d0d14;--bg-deep:#08080e;
  --accent-green:#00ff41;--accent-cyan:#00d4ff;
  --danger:#ff0040;--warning:#ffaa00;--info:#00d4ff;
  --text-primary:#e0e0e0;--text-secondary:#888;--text-dim:#555;
  --border-color:#1a1a2e;
  --font-mono:Consolas,'Courier New',monospace;
}}
body{{font-family:var(--font-mono);background:var(--bg-primary);color:var(--text-primary);padding:0;min-height:100vh}}
.header{{
  background:linear-gradient(180deg,var(--bg-deep),var(--bg-secondary));
  border-bottom:1px solid var(--border-color);padding:20px 32px;position:relative;
}}
.header::after{{
  content:'';position:absolute;bottom:0;left:0;width:100%;height:1px;
  background:linear-gradient(90deg,transparent,var(--accent-green),var(--accent-cyan),transparent);
}}
.logo{{font-size:2rem;font-weight:900;color:var(--accent-green);text-shadow:0 0 10px var(--accent-green),0 0 20px rgba(0,255,65,0.3);letter-spacing:2px}}
.logo-sub{{font-size:0.8rem;color:var(--text-secondary);margin-top:4px}}
.meta-row{{display:flex;gap:24px;margin-top:6px;font-size:0.75rem;color:var(--text-dim);flex-wrap:wrap}}
.container{{max-width:1200px;margin:0 auto;padding:24px 32px}}
.section{{margin-bottom:28px}}
.section-title{{font-size:0.9rem;color:var(--accent-cyan);margin-bottom:14px;padding-bottom:6px;border-bottom:1px solid var(--border-color);letter-spacing:1px}}
.section-title::before{{content:'> ';color:var(--accent-green)}}
.summary-card{{
  background:linear-gradient(135deg,rgba(0,255,65,0.03),rgba(0,212,255,0.03));
  border:1px solid var(--border-color);border-radius:4px;padding:16px;position:relative;overflow:hidden;
}}
.summary-card::before{{
  content:'';position:absolute;top:0;left:0;width:100%;height:1px;
  background:linear-gradient(90deg,transparent,var(--accent-green)20%,transparent);
}}
.stats-grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:16px}}
.stat-card{{background:var(--bg-deep);border:1px solid var(--border-color);border-radius:2px;padding:12px;text-align:center;transition:border-color 0.3s}}
.stat-card:hover{{border-color:var(--accent-green)}}
.stat-card .stat-value{{font-size:1.6rem;font-weight:bold;color:var(--accent-green)}}
.stat-card .stat-label{{font-size:0.65rem;color:var(--text-secondary);margin-top:4px}}
.stat-card.stat-danger .stat-value{{color:var(--danger)}}
.stat-card.stat-warning .stat-value{{color:var(--warning)}}
.stat-card.stat-info .stat-value{{color:var(--accent-cyan)}}
.severity-badge{{
  display:inline-block;padding:4px 16px;border-radius:2px;font-size:0.85rem;font-weight:bold;
  letter-spacing:2px;margin-left:8px;
}}
.severity-critical{{background:rgba(255,0,64,0.2);color:#ff0040;border:1px solid #ff0040}}
.severity-high{{background:rgba(255,170,0,0.2);color:#ffaa00;border:1px solid #ffaa00}}
.severity-info{{background:rgba(0,212,255,0.2);color:#00d4ff;border:1px solid #00d4ff}}
.target-list{{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0}}
.target-badge{{padding:3px 12px;border:1px solid var(--border-color);border-radius:2px;font-size:0.75rem;color:var(--accent-cyan);background:rgba(0,212,255,0.05)}}
.table-wrap{{overflow-x:auto;border:1px solid var(--border-color);border-radius:2px}}
table{{width:100%;border-collapse:collapse;font-size:0.8rem}}
th{{background:var(--bg-deep);color:var(--accent-cyan);padding:8px 12px;text-align:left;border-bottom:1px solid var(--border-color);font-weight:normal;letter-spacing:0.5px;white-space:nowrap}}
td{{padding:7px 12px;border-bottom:1px solid rgba(26,26,46,0.5);color:var(--text-primary);white-space:nowrap}}
tr:hover td{{background:rgba(0,255,65,0.02)}}
.signal-tag{{display:inline-block;padding:2px 8px;border-radius:2px;font-size:0.7rem;font-weight:bold;letter-spacing:0.5px}}
.signal-danger{{background:rgba(255,0,64,0.2);color:var(--danger);border:1px solid var(--danger)}}
.signal-warning{{background:rgba(255,170,0,0.2);color:var(--warning);border:1px solid var(--warning)}}
.signal-info{{background:rgba(0,212,255,0.2);color:var(--accent-cyan);border:1px solid var(--accent-cyan)}}
.crash-detail{{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:2px;padding:14px;margin-bottom:12px;position:relative}}
.crash-detail::before{{content:'';position:absolute;top:0;left:0;width:100%;height:1px;background:linear-gradient(90deg,transparent,var(--accent-green),transparent)}}
.detail-header{{font-size:0.85rem;color:var(--accent-green);margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid var(--border-color)}}
.detail-table{{font-size:0.75rem;width:100%;margin-bottom:8px}}
.detail-table th{{padding:4px 10px;color:var(--text-secondary);width:100px;white-space:nowrap;border-bottom:1px solid rgba(26,26,46,0.3)}}
.detail-table td{{padding:4px 10px;color:var(--text-primary);border-bottom:1px solid rgba(26,26,46,0.3);word-break:break-all}}
.detail-output{{background:var(--bg-deep);border:1px solid var(--border-color);border-radius:2px;padding:8px;font-size:0.7rem;color:var(--text-secondary);white-space:pre-wrap;max-height:120px;overflow-y:auto;line-height:1.5;margin-top:6px}}
.legend-grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:8px}}
.legend-item{{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--bg-deep);border:1px solid var(--border-color);border-radius:2px;font-size:0.75rem}}
.legend-desc{{color:var(--text-secondary)}}
.recommendations{{padding-left:20px;font-size:0.8rem;line-height:2}}
.recommendations li{{color:var(--text-primary)}}
.recommendations li::marker{{color:var(--accent-green)}}
.footer{{border-top:1px solid var(--border-color);padding:16px 32px;font-size:0.7rem;color:var(--text-dim);text-align:center;letter-spacing:1px}}
.footer span{{color:var(--accent-green)}}
.separator{{border:none;border-top:1px solid var(--border-color);margin:20px 0}}
@media(max-width:768px){{
  .container{{padding:16px}}
  .stats-grid{{grid-template-columns:repeat(2,1fr)}}
  .header{{padding:14px 16px}}
  .logo{{font-size:1.5rem}}
}}
</style>
</head>
<body>

<div class="header">
  <div class="logo">[2520]FUZZ</div>
  <div class="logo-sub">{title}</div>
  <div class="meta-row">
    <span>报告编号: {report_id}</span>
    <span>生成时间: {now}</span>
    <span>崩溃总数: {total}</span>
  </div>
</div>

<div class="container">

  <!-- 执行摘要 -->
  <div class="section">
    <div class="section-title">执行摘要</div>
    <div class="summary-card">
      <div class="stats-grid">
        <div class="stat-card"><div class="stat-value">{total}</div><div class="stat-label">总崩溃数</div></div>
        <div class="stat-card stat-info"><div class="stat-value">{unique_signals}</div><div class="stat-label">信号种类</div></div>
        <div class="stat-card stat-info"><div class="stat-value">{affected_targets}</div><div class="stat-label">受影响目标</div></div>
        <div class="stat-card stat-{'danger' if severity=='CRITICAL' else 'warning' if severity=='HIGH' else 'info'}">
          <div class="stat-value">
            <span class="severity-badge severity-{severity.lower()}">{severity}</span>
          </div>
          <div class="stat-label">风险等级</div>
        </div>
      </div>
      <div><div class="section-title" style="margin-bottom:6px;font-size:0.8rem">受影响目标</div><div class="target-list">{target_list}</div></div>
    </div>
  </div>

  <!-- 崩溃详情表 -->
  <div class="section">
    <div class="section-title">崩溃详情</div>
    <div class="table-wrap">
      <table>
        <thead>
          <tr><th>#</th><th>时间戳</th><th>目标</th><th>信号</th><th>样本</th><th>退出码</th></tr>
        </thead>
        <tbody>
          {table_rows if table_rows else '<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--text-dim)">无崩溃数据</td></tr>'}
        </tbody>
      </table>
    </div>
  </div>

  <!-- 信号统计 -->
  <div class="section">
    <div class="section-title">信号统计</div>
    <div class="stats-grid">
      {signal_stats if signal_stats else '<div class="stat-card"><div class="stat-value">0</div><div class="stat-label">无数据</div></div>'}
    </div>
  </div>

  <!-- 逐崩溃详情 -->
  <div class="section">
    <div class="section-title">逐崩溃详情</div>
    {detail_sections if detail_sections else '<div style="text-align:center;padding:20px;color:var(--text-dim);font-size:0.8rem">无崩溃详情</div>'}
  </div>

  <!-- 信号释义 -->
  <div class="section">
    <div class="section-title">信号释义</div>
    <div class="legend-grid">
      {legend_html}
    </div>
  </div>

  <!-- 安全建议 -->
  <div class="section">
    <div class="section-title">安全建议</div>
    <ol class="recommendations">
      {recommendations}
    </ol>
  </div>

</div>

<div class="footer">
  2520 <span>·</span> 漏洞报告生成器 <span>·</span> 版本 1.0.0 <span>·</span> {now}
</div>

</body>
</html>"""

    os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(html)
    return output_path


def main():
    parser = argparse.ArgumentParser(
        description="2520 漏洞报告生成器 — 生成安全公告级 HTML 报告"
    )
    source = parser.add_mutually_exclusive_group(required=True)
    source.add_argument(
        "--local", type=str, metavar="DIR",
        help="从本地 crash_records 目录读取崩溃数据"
    )
    source.add_argument(
        "--jetson", type=str, metavar="HOST",
        help="从 Jetson Agent API 读取崩溃数据 (SSH)"
    )
    parser.add_argument("-o", "--output", type=str, default="bug.html",
                        help="输出 HTML 文件路径 (默认: bug.html)")
    parser.add_argument("--title", type=str, default="2520 漏洞报告",
                        help="报告标题")
    parser.add_argument("--port", type=int, default=22,
                        help="Jetson SSH 端口 (默认: 22)")
    parser.add_argument("--agent-port", type=int, default=7886,
                        help="Jetson Agent HTTP 端口 (默认: 7886)")
    parser.add_argument("--user", type=str, default="nvidia",
                        help="Jetson SSH 用户名 (默认: nvidia)")
    parser.add_argument("--password", type=str, default="nvidia",
                        help="Jetson SSH 密码 (默认: nvidia)")

    args = parser.parse_args()

    if args.local:
        print(f"[*] 从本地目录读取: {args.local}")
        crashes = read_crashes_from_dir(args.local)
    else:
        print(f"[*] 连接 Jetson: {args.jetson} (SSH端口:{args.port})")
        crashes = read_crashes_from_jetson(
            args.jetson, ssh_port=args.port, agent_port=args.agent_port,
            user=args.user, password=args.password,
        )

    print(f"[+] 读取到 {len(crashes)} 条崩溃记录")
    if not crashes:
        print("[!] 警告: 无崩溃数据，将生成空报告")

    out = generate_report(crashes, output_path=args.output, title=args.title)
    print(f"[+] 报告已生成: {os.path.abspath(out)}")


if __name__ == "__main__":
    main()
