From 3d4f90c547d2c1b8f12f9b430936a9c2d9796e74 Mon Sep 17 00:00:00 2001
From: lintsinghua
Date: Sat, 13 Dec 2025 21:38:11 +0800
Subject: [PATCH] feat: Add `marked` for improved Markdown to HTML report
generation and refined download handling.
---
backend/app/api/v1/endpoints/agent_tasks.py | 124 +++++----
frontend/package-lock.json | 35 ++-
frontend/package.json | 1 +
.../components/ReportExportDialog.tsx | 256 ++++++++++++------
4 files changed, 267 insertions(+), 149 deletions(-)
diff --git a/backend/app/api/v1/endpoints/agent_tasks.py b/backend/app/api/v1/endpoints/agent_tasks.py
index e5aafdb..0942ca1 100644
--- a/backend/app/api/v1/endpoints/agent_tasks.py
+++ b/backend/app/api/v1/endpoints/agent_tasks.py
@@ -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)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 8330cd7..f464cc6 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -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",
diff --git a/frontend/package.json b/frontend/package.json
index 854ef0b..c5a9e9a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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",
diff --git a/frontend/src/pages/AgentAudit/components/ReportExportDialog.tsx b/frontend/src/pages/AgentAudit/components/ReportExportDialog.tsx
index ee442c6..f57479d 100644
--- a/frontend/src/pages/AgentAudit/components/ReportExportDialog.tsx
+++ b/frontend/src/pages/AgentAudit/components/ReportExportDialog.tsx
@@ -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, '$1
')
- .replace(/^## (.+)$/gm, '$1
')
- .replace(/^### (.+)$/gm, '$1
')
- .replace(/\*\*(.+?)\*\*/g, '$1')
- .replace(/`([^`]+)`/g, '$1')
- .replace(/^- (.+)$/gm, '$1')
- .replace(/^---$/gm, '
')
- .replace(/\n\n/g, '
');
+ const generateHtmlReport = async (markdown: string, task: AgentTask): Promise => {
+ // Parse markdown to HTML using marked
+ const contentHtml = await marked.parse(markdown);
return `
-
+
- Security Audit Report - ${task.name || task.id}
+ 安全审计报告 - ${task.name || task.id}
-
-
- ${html}
-
+ ${contentHtml}
+
+
`;
};
@@ -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);
}