feat: Add `marked` for improved Markdown to HTML report generation and refined download handling.
This commit is contained in:
parent
a9a22b91c7
commit
3d4f90c547
|
|
@ -2298,103 +2298,110 @@ async def generate_audit_report(
|
|||
if task.completed_at and task.started_at:
|
||||
duration = (task.completed_at - task.started_at).total_seconds()
|
||||
if duration >= 3600:
|
||||
duration_str = f"{duration / 3600:.1f} hours"
|
||||
duration_str = f"{duration / 3600:.1f} 小时"
|
||||
elif duration >= 60:
|
||||
duration_str = f"{duration / 60:.1f} minutes"
|
||||
duration_str = f"{duration / 60:.1f} 分钟"
|
||||
else:
|
||||
duration_str = f"{int(duration)} seconds"
|
||||
duration_str = f"{int(duration)} 秒"
|
||||
|
||||
md_lines = []
|
||||
|
||||
# Header
|
||||
md_lines.append("# DeepAudit Security Audit Report")
|
||||
md_lines.append("# DeepAudit 安全审计报告")
|
||||
md_lines.append("")
|
||||
md_lines.append("---")
|
||||
md_lines.append("")
|
||||
|
||||
# Report Info
|
||||
md_lines.append("## Report Information")
|
||||
md_lines.append("## 报告信息")
|
||||
md_lines.append("")
|
||||
md_lines.append(f"| Property | Value |")
|
||||
md_lines.append(f"| 属性 | 内容 |")
|
||||
md_lines.append(f"|----------|-------|")
|
||||
md_lines.append(f"| **Project** | {project.name} |")
|
||||
md_lines.append(f"| **Task ID** | `{task.id[:8]}...` |")
|
||||
md_lines.append(f"| **Generated** | {timestamp} |")
|
||||
md_lines.append(f"| **Status** | {task.status.upper()} |")
|
||||
md_lines.append(f"| **Duration** | {duration_str} |")
|
||||
md_lines.append(f"| **项目名称** | {project.name} |")
|
||||
md_lines.append(f"| **任务 ID** | `{task.id[:8]}...` |")
|
||||
md_lines.append(f"| **生成时间** | {timestamp} |")
|
||||
md_lines.append(f"| **任务状态** | {task.status.upper()} |")
|
||||
md_lines.append(f"| **耗时** | {duration_str} |")
|
||||
md_lines.append("")
|
||||
|
||||
# Executive Summary
|
||||
md_lines.append("## Executive Summary")
|
||||
md_lines.append("## 执行摘要")
|
||||
md_lines.append("")
|
||||
|
||||
score = task.security_score
|
||||
if score is not None:
|
||||
if score >= 80:
|
||||
score_assessment = "Good - Minor improvements recommended"
|
||||
score_icon = "PASS"
|
||||
score_assessment = "良好 - 建议进行少量优化"
|
||||
score_icon = "通过"
|
||||
elif score >= 60:
|
||||
score_assessment = "Moderate - Several issues require attention"
|
||||
score_icon = "WARN"
|
||||
score_assessment = "中等 - 存在若干问题需要关注"
|
||||
score_icon = "警告"
|
||||
else:
|
||||
score_assessment = "Critical - Immediate remediation required"
|
||||
score_icon = "FAIL"
|
||||
md_lines.append(f"**Security Score: {int(score)}/100** [{score_icon}]")
|
||||
score_assessment = "严重 - 需要立即进行修复"
|
||||
score_icon = "未通过"
|
||||
md_lines.append(f"**安全评分: {int(score)}/100** [{score_icon}]")
|
||||
md_lines.append(f"*{score_assessment}*")
|
||||
else:
|
||||
md_lines.append("**Security Score:** Not calculated")
|
||||
md_lines.append("**安全评分:** 未计算")
|
||||
md_lines.append("")
|
||||
|
||||
# Findings Summary
|
||||
md_lines.append("### Findings Overview")
|
||||
md_lines.append("### 漏洞发现概览")
|
||||
md_lines.append("")
|
||||
md_lines.append(f"| Severity | Count | Verified |")
|
||||
md_lines.append(f"| 严重程度 | 数量 | 已验证 |")
|
||||
md_lines.append(f"|----------|-------|----------|")
|
||||
if critical > 0:
|
||||
md_lines.append(f"| **CRITICAL** | {critical} | {sum(1 for f in findings if normalize_severity(f.severity) == 'critical' and f.is_verified)} |")
|
||||
md_lines.append(f"| **严重 (CRITICAL)** | {critical} | {sum(1 for f in findings if normalize_severity(f.severity) == 'critical' and f.is_verified)} |")
|
||||
if high > 0:
|
||||
md_lines.append(f"| **HIGH** | {high} | {sum(1 for f in findings if normalize_severity(f.severity) == 'high' and f.is_verified)} |")
|
||||
md_lines.append(f"| **高危 (HIGH)** | {high} | {sum(1 for f in findings if normalize_severity(f.severity) == 'high' and f.is_verified)} |")
|
||||
if medium > 0:
|
||||
md_lines.append(f"| **MEDIUM** | {medium} | {sum(1 for f in findings if normalize_severity(f.severity) == 'medium' and f.is_verified)} |")
|
||||
md_lines.append(f"| **中危 (MEDIUM)** | {medium} | {sum(1 for f in findings if normalize_severity(f.severity) == 'medium' and f.is_verified)} |")
|
||||
if low > 0:
|
||||
md_lines.append(f"| **LOW** | {low} | {sum(1 for f in findings if normalize_severity(f.severity) == 'low' and f.is_verified)} |")
|
||||
md_lines.append(f"| **Total** | {total} | {verified} |")
|
||||
md_lines.append(f"| **低危 (LOW)** | {low} | {sum(1 for f in findings if normalize_severity(f.severity) == 'low' and f.is_verified)} |")
|
||||
md_lines.append(f"| **总计** | {total} | {verified} |")
|
||||
md_lines.append("")
|
||||
|
||||
# Audit Metrics
|
||||
md_lines.append("### Audit Metrics")
|
||||
md_lines.append("### 审计指标")
|
||||
md_lines.append("")
|
||||
md_lines.append(f"- **Files Analyzed:** {task.analyzed_files} / {task.total_files}")
|
||||
md_lines.append(f"- **Agent Iterations:** {task.total_iterations}")
|
||||
md_lines.append(f"- **Tool Invocations:** {task.tool_calls_count}")
|
||||
md_lines.append(f"- **Tokens Used:** {task.tokens_used:,}")
|
||||
md_lines.append(f"- **分析文件数:** {task.analyzed_files} / {task.total_files}")
|
||||
md_lines.append(f"- **Agent 迭代次数:** {task.total_iterations}")
|
||||
md_lines.append(f"- **工具调用次数:** {task.tool_calls_count}")
|
||||
md_lines.append(f"- **Token 消耗:** {task.tokens_used:,}")
|
||||
if with_poc > 0:
|
||||
md_lines.append(f"- **PoC Generated:** {with_poc}")
|
||||
md_lines.append(f"- **生成的 PoC:** {with_poc}")
|
||||
md_lines.append("")
|
||||
|
||||
# Detailed Findings
|
||||
if not findings:
|
||||
md_lines.append("## Findings")
|
||||
md_lines.append("## 漏洞详情")
|
||||
md_lines.append("")
|
||||
md_lines.append("*No security vulnerabilities were detected during this audit.*")
|
||||
md_lines.append("*本次审计未发现安全漏洞。*")
|
||||
md_lines.append("")
|
||||
else:
|
||||
# Group findings by severity
|
||||
for severity_level, severity_name in [('critical', 'Critical'), ('high', 'High'), ('medium', 'Medium'), ('low', 'Low')]:
|
||||
severity_map = {
|
||||
'critical': '严重 (Critical)',
|
||||
'high': '高危 (High)',
|
||||
'medium': '中危 (Medium)',
|
||||
'low': '低危 (Low)'
|
||||
}
|
||||
|
||||
for severity_level, severity_name in severity_map.items():
|
||||
severity_findings = [f for f in findings if normalize_severity(f.severity) == severity_level]
|
||||
if not severity_findings:
|
||||
continue
|
||||
|
||||
md_lines.append(f"## {severity_name} Severity Findings")
|
||||
md_lines.append(f"## {severity_name} 漏洞")
|
||||
md_lines.append("")
|
||||
|
||||
for i, f in enumerate(severity_findings, 1):
|
||||
verified_badge = "[Verified]" if f.is_verified else "[Unverified]"
|
||||
poc_badge = " [PoC]" if f.has_poc else ""
|
||||
verified_badge = "[已验证]" if f.is_verified else "[未验证]"
|
||||
poc_badge = " [含 PoC]" if f.has_poc else ""
|
||||
|
||||
md_lines.append(f"### {severity_level.upper()}-{i}: {f.title}")
|
||||
md_lines.append("")
|
||||
md_lines.append(f"**{verified_badge}**{poc_badge} | Type: `{f.vulnerability_type}`")
|
||||
md_lines.append(f"**{verified_badge}**{poc_badge} | 类型: `{f.vulnerability_type}`")
|
||||
md_lines.append("")
|
||||
|
||||
if f.file_path:
|
||||
|
|
@ -2404,15 +2411,15 @@ async def generate_audit_report(
|
|||
if f.line_end and f.line_end != f.line_start:
|
||||
location += f"-{f.line_end}"
|
||||
location += "`"
|
||||
md_lines.append(f"**Location:** {location}")
|
||||
md_lines.append(f"**位置:** {location}")
|
||||
md_lines.append("")
|
||||
|
||||
if f.ai_confidence:
|
||||
md_lines.append(f"**AI Confidence:** {int(f.ai_confidence * 100)}%")
|
||||
md_lines.append(f"**AI 置信度:** {int(f.ai_confidence * 100)}%")
|
||||
md_lines.append("")
|
||||
|
||||
if f.description:
|
||||
md_lines.append("**Description:**")
|
||||
md_lines.append("**漏洞描述:**")
|
||||
md_lines.append("")
|
||||
md_lines.append(f.description)
|
||||
md_lines.append("")
|
||||
|
|
@ -2429,7 +2436,7 @@ async def generate_audit_report(
|
|||
'cpp': 'cpp', 'cs': 'csharp', 'sol': 'solidity'
|
||||
}
|
||||
lang = lang_map.get(ext, 'text')
|
||||
md_lines.append("**Vulnerable Code:**")
|
||||
md_lines.append("**漏洞代码:**")
|
||||
md_lines.append("")
|
||||
md_lines.append(f"```{lang}")
|
||||
md_lines.append(f.code_snippet.strip())
|
||||
|
|
@ -2437,13 +2444,13 @@ async def generate_audit_report(
|
|||
md_lines.append("")
|
||||
|
||||
if f.suggestion:
|
||||
md_lines.append("**Recommendation:**")
|
||||
md_lines.append("**修复建议:**")
|
||||
md_lines.append("")
|
||||
md_lines.append(f.suggestion)
|
||||
md_lines.append("")
|
||||
|
||||
if f.fix_code:
|
||||
md_lines.append("**Suggested Fix:**")
|
||||
md_lines.append("**参考修复代码:**")
|
||||
md_lines.append("")
|
||||
md_lines.append(f"```{lang if f.file_path else 'text'}")
|
||||
md_lines.append(f.fix_code.strip())
|
||||
|
|
@ -2452,7 +2459,7 @@ async def generate_audit_report(
|
|||
|
||||
# 🔥 添加 PoC 详情
|
||||
if f.has_poc:
|
||||
md_lines.append("**Proof of Concept (PoC):**")
|
||||
md_lines.append("**概念验证 (PoC):**")
|
||||
md_lines.append("")
|
||||
|
||||
if f.poc_description:
|
||||
|
|
@ -2460,14 +2467,14 @@ async def generate_audit_report(
|
|||
md_lines.append("")
|
||||
|
||||
if f.poc_steps:
|
||||
md_lines.append("**Reproduction Steps:**")
|
||||
md_lines.append("**复现步骤:**")
|
||||
md_lines.append("")
|
||||
for step_idx, step in enumerate(f.poc_steps, 1):
|
||||
md_lines.append(f"{step_idx}. {step}")
|
||||
md_lines.append("")
|
||||
|
||||
if f.poc_code:
|
||||
md_lines.append("**PoC Payload:**")
|
||||
md_lines.append("**PoC 代码:**")
|
||||
md_lines.append("")
|
||||
md_lines.append("```")
|
||||
md_lines.append(f.poc_code.strip())
|
||||
|
|
@ -2479,24 +2486,29 @@ async def generate_audit_report(
|
|||
|
||||
# Remediation Priority
|
||||
if critical > 0 or high > 0:
|
||||
md_lines.append("## Remediation Priority")
|
||||
md_lines.append("## 修复优先级建议")
|
||||
md_lines.append("")
|
||||
md_lines.append("Based on the findings, we recommend the following remediation priority:")
|
||||
md_lines.append("基于已发现的漏洞,我们建议按以下优先级进行修复:")
|
||||
md_lines.append("")
|
||||
priority_idx = 1
|
||||
if critical > 0:
|
||||
md_lines.append(f"1. **IMMEDIATE:** Address {critical} critical finding(s) - potential for severe impact")
|
||||
md_lines.append(f"{priority_idx}. **立即修复:** 处理 {critical} 个严重漏洞 - 可能造成严重影响")
|
||||
priority_idx += 1
|
||||
if high > 0:
|
||||
md_lines.append(f"2. **HIGH PRIORITY:** Resolve {high} high severity finding(s) within 1 week")
|
||||
md_lines.append(f"{priority_idx}. **高优先级:** 在 1 周内修复 {high} 个高危漏洞")
|
||||
priority_idx += 1
|
||||
if medium > 0:
|
||||
md_lines.append(f"3. **MEDIUM PRIORITY:** Fix {medium} medium severity finding(s) within 2-4 weeks")
|
||||
md_lines.append(f"{priority_idx}. **中优先级:** 在 2-4 周内修复 {medium} 个中危漏洞")
|
||||
priority_idx += 1
|
||||
if low > 0:
|
||||
md_lines.append(f"4. **LOW PRIORITY:** Address {low} low severity finding(s) in regular maintenance")
|
||||
md_lines.append(f"{priority_idx}. **低优先级:** 在日常维护中处理 {low} 个低危漏洞")
|
||||
priority_idx += 1
|
||||
md_lines.append("")
|
||||
|
||||
# Footer
|
||||
md_lines.append("---")
|
||||
md_lines.append("")
|
||||
md_lines.append("*This report was generated by DeepAudit - AI-Powered Security Analysis*")
|
||||
md_lines.append("*本报告由 DeepAudit - AI 驱动的安全分析系统生成*")
|
||||
md_lines.append("")
|
||||
content = "\n".join(md_lines)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "deep-audit",
|
||||
"version": "1.3.4",
|
||||
"version": "2.0.0-beta.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "deep-audit",
|
||||
"version": "1.3.4",
|
||||
"version": "2.0.0-beta.7",
|
||||
"dependencies": {
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@radix-ui/react-accordion": "^1.2.8",
|
||||
|
|
@ -50,6 +50,7 @@
|
|||
"input-otp": "^1.4.2",
|
||||
"ky": "^1.9.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
"marked": "^17.0.1",
|
||||
"miaoda-auth-react": "^2.0.0",
|
||||
"miaoda-sc-plugin": "^1.0.11",
|
||||
"next-themes": "^0.4.6",
|
||||
|
|
@ -6912,9 +6913,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "16.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/marked/-/marked-16.4.2.tgz",
|
||||
"integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/marked/-/marked-17.0.1.tgz",
|
||||
"integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
|
|
@ -7258,6 +7259,18 @@
|
|||
"uuid": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid/node_modules/marked": {
|
||||
"version": "16.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/marked/-/marked-16.4.2.tgz",
|
||||
"integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/miaoda-auth-react": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/miaoda-auth-react/-/miaoda-auth-react-2.0.6.tgz",
|
||||
|
|
@ -9429,6 +9442,18 @@
|
|||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/streamdown/node_modules/marked": {
|
||||
"version": "16.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/marked/-/marked-16.4.2.tgz",
|
||||
"integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
"input-otp": "^1.4.2",
|
||||
"ky": "^1.9.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
"marked": "^17.0.1",
|
||||
"miaoda-auth-react": "^2.0.0",
|
||||
"miaoda-sc-plugin": "^1.0.11",
|
||||
"next-themes": "^0.4.6",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, memo } from "react";
|
||||
import { marked } from "marked";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -372,7 +373,7 @@ export const ReportExportDialog = memo(function ReportExportDialog({
|
|||
responseType: "text",
|
||||
});
|
||||
|
||||
const htmlContent = generateHtmlReport(mdResponse.data, task);
|
||||
const htmlContent = await generateHtmlReport(mdResponse.data, task);
|
||||
setPreview({
|
||||
content: htmlContent,
|
||||
format: "html",
|
||||
|
|
@ -405,34 +406,29 @@ export const ReportExportDialog = memo(function ReportExportDialog({
|
|||
}, [task, findings]);
|
||||
|
||||
// Generate HTML report from markdown
|
||||
const generateHtmlReport = (markdown: string, task: AgentTask): string => {
|
||||
// Convert markdown to HTML with styling
|
||||
let html = markdown
|
||||
.replace(/^# (.+)$/gm, '<h1 class="report-h1">$1</h1>')
|
||||
.replace(/^## (.+)$/gm, '<h2 class="report-h2">$1</h2>')
|
||||
.replace(/^### (.+)$/gm, '<h3 class="report-h3">$1</h3>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/`([^`]+)`/g, '<code class="report-code">$1</code>')
|
||||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/^---$/gm, '<hr class="report-hr">')
|
||||
.replace(/\n\n/g, '</p><p class="report-p">');
|
||||
const generateHtmlReport = async (markdown: string, task: AgentTask): Promise<string> => {
|
||||
// Parse markdown to HTML using marked
|
||||
const contentHtml = await marked.parse(markdown);
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Security Audit Report - ${task.name || task.id}</title>
|
||||
<title>安全审计报告 - ${task.name || task.id}</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-primary: #0a0a0f;
|
||||
--bg-secondary: #0d0d12;
|
||||
--bg-tertiary: #1a1a24;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #94a3b8;
|
||||
--accent: #FF6B2C;
|
||||
--border: #2d2d3d;
|
||||
--success: #34d399;
|
||||
--warning: #fbbf24;
|
||||
--error: #fb7185;
|
||||
--code-bg: #1e1e2e;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
|
|
@ -444,78 +440,124 @@ export const ReportExportDialog = memo(function ReportExportDialog({
|
|||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.report-h1 {
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--text-primary);
|
||||
font-size: 1.75rem;
|
||||
margin: 2rem 0 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.report-h2 {
|
||||
color: var(--text-primary);
|
||||
font-size: 1.25rem;
|
||||
margin: 1.5rem 0 0.75rem;
|
||||
}
|
||||
.report-h3 {
|
||||
color: var(--accent);
|
||||
font-size: 1rem;
|
||||
margin: 1rem 0 0.5rem;
|
||||
}
|
||||
.report-p { margin: 0.75rem 0; }
|
||||
.report-code {
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
.report-hr {
|
||||
border: none;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
h1 { font-size: 2rem; border-bottom: 2px solid var(--accent); padding-bottom: 0.5rem; }
|
||||
h2 { font-size: 1.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
|
||||
h3 { font-size: 1.25rem; color: var(--text-primary); margin-top: 1.5rem; }
|
||||
|
||||
p { margin-bottom: 1rem; }
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
li {
|
||||
margin: 0.25rem 0;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
pre {
|
||||
background: var(--bg-secondary);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 0.75rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
th, td {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.header h1 {
|
||||
th {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.header .subtitle {
|
||||
color: var(--accent);
|
||||
font-size: 0.875rem;
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr:hover td { background: rgba(255, 255, 255, 0.02); }
|
||||
|
||||
/* Code Blocks */
|
||||
pre {
|
||||
background: var(--code-bg);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
code {
|
||||
font-family: 'SF Mono', 'Menlo', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
p code, li code, td code {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ul, ol { margin-left: 1.5rem; margin-bottom: 1rem; }
|
||||
li { margin-bottom: 0.5rem; }
|
||||
|
||||
/* Blockquotes */
|
||||
blockquote {
|
||||
border-left: 4px solid var(--accent);
|
||||
padding-left: 1rem;
|
||||
margin: 1rem 0;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
background: rgba(255, 107, 44, 0.05);
|
||||
padding: 0.5rem 0.5rem 0.5rem 1rem;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
/* Horizontal Rule */
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
strong { color: var(--text-primary); font-weight: 600; }
|
||||
em { color: var(--text-secondary); }
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
h1, h2, h3, strong { color: black; }
|
||||
pre, code { background: #f1f5f9; color: black; border-color: #e2e8f0; }
|
||||
table { border-color: #e2e8f0; }
|
||||
th { background: #f8fafc; color: black; border-bottom-color: #cbd5e1; }
|
||||
td { border-bottom-color: #e2e8f0; }
|
||||
p code, li code, td code { background: #f1f5f9; color: #0f172a; }
|
||||
a { color: #2563eb; text-decoration: underline; }
|
||||
}
|
||||
.severity-critical { color: #fb7185; }
|
||||
.severity-high { color: #fb923c; }
|
||||
.severity-medium { color: #fbbf24; }
|
||||
.severity-low { color: #38bdf8; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>DEEPAUDIT Security Report</h1>
|
||||
<p class="subtitle">Generated ${new Date().toLocaleString()}</p>
|
||||
</div>
|
||||
<main>
|
||||
<p class="report-p">${html}</p>
|
||||
</main>
|
||||
${contentHtml}
|
||||
|
||||
<footer style="margin-top: 4rem; padding-top: 2rem; border-top: 1px solid var(--border); text-align: center; font-size: 0.875rem; color: var(--text-secondary);">
|
||||
<p>生成于 ${new Date().toLocaleString('zh-CN')} • DeepAudit 安全审计系统</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>`;
|
||||
};
|
||||
|
|
@ -540,26 +582,64 @@ export const ReportExportDialog = memo(function ReportExportDialog({
|
|||
|
||||
// Handle download
|
||||
const handleDownload = async () => {
|
||||
if (!task) return;
|
||||
if (!task || !findings && activeFormat !== "json") return; // Allow json even if findings empty? Actually backend handles it.
|
||||
|
||||
setDownloading(true);
|
||||
try {
|
||||
if (activeFormat === "markdown" || activeFormat === "json") {
|
||||
await downloadAgentReport(task.id, activeFormat);
|
||||
} else {
|
||||
// HTML download
|
||||
const blob = new Blob([preview.content], { type: "text/html" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `audit-report-${task.id.slice(0, 8)}.html`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
let content = "";
|
||||
let filename = `audit_report_${task.id.substring(0, 8)}`;
|
||||
let mimeType = "text/plain";
|
||||
|
||||
if (activeFormat === "markdown") {
|
||||
content = preview.content; // Use cached preview
|
||||
// Fallback if preview failed? Re-fetch.
|
||||
if (!content) {
|
||||
const response = await apiClient.get(`/agent-tasks/${task.id}/report`, {
|
||||
params: { format: "markdown" },
|
||||
responseType: "text",
|
||||
});
|
||||
content = response.data;
|
||||
}
|
||||
filename += ".md";
|
||||
} else if (activeFormat === "json") {
|
||||
// Fetch JSON directly from backend
|
||||
const response = await apiClient.get(`/agent-tasks/${task.id}/report`, {
|
||||
params: { format: "json" },
|
||||
responseType: "json", // Important: axios parses JSON
|
||||
});
|
||||
// Pretty print JSON
|
||||
content = JSON.stringify(response.data, null, 2);
|
||||
filename += ".json";
|
||||
mimeType = "application/json";
|
||||
} else if (activeFormat === "html") { // HTML
|
||||
if (preview.content && preview.format === "html") {
|
||||
content = preview.content;
|
||||
} else {
|
||||
const response = await apiClient.get(`/agent-tasks/${task.id}/report`, {
|
||||
params: { format: "markdown" },
|
||||
responseType: "text",
|
||||
});
|
||||
content = await generateHtmlReport(response.data, task);
|
||||
}
|
||||
filename += ".html";
|
||||
mimeType = "text/html";
|
||||
}
|
||||
|
||||
// Create download trigger
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
console.error("Download failed:", err);
|
||||
// Ideally show toast error here
|
||||
} finally {
|
||||
setDownloading(false);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue