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} -

    -

    DEEPAUDIT Security Report

    -

    Generated ${new Date().toLocaleString()}

    -
    -
    -

    ${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); }