diff --git a/web/static/css/style.css b/web/static/css/style.css index 31bcd3b7..d48d02e8 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -21370,6 +21370,13 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { flex: 1; } +.vulnerability-repro { + display: flex; + flex-direction: column; + gap: 12px; +} + +.vulnerability-section, .vulnerability-proof, .vulnerability-impact, .vulnerability-recommendation { @@ -21381,6 +21388,10 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { line-height: 1.6; } +.vulnerability-section:first-child { + margin-top: 0; +} + .vulnerability-proof pre { margin-top: 8px; padding: 12px; @@ -21394,6 +21405,27 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { word-wrap: break-word; } +.vulnerability-section-body { + margin-top: 8px; + color: var(--text-primary); + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.vulnerability-section-body--code { + margin-top: 8px; + padding: 12px; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 4px; + overflow-x: auto; + font-size: 0.8rem; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + white-space: pre-wrap; + word-wrap: break-word; +} + +.vulnerability-section strong, .vulnerability-proof strong, .vulnerability-impact strong, .vulnerability-recommendation strong { diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 5c2496e0..7903b27e 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -2027,7 +2027,7 @@ "exportNoResults": "No vulnerabilities match the current filters", "exportStarted": "Started downloading {{count}} file(s)", "exportFailed": "Export failed", - "saveRequiredFields": "Please fill in conversation ID, title, and severity", + "saveRequiredFields": "Please fill in conversation ID, title, description, severity, type, target, reproduction steps, evidence/POC, impact, and remediation", "saveFailed": "Save failed", "fetchFailed": "Failed to fetch vulnerability", "deleteFailed": "Delete failed", @@ -2046,9 +2046,12 @@ "detailTaskQueueId": "Task queue ID", "detailConversationTag": "Conversation tag", "detailTaskTag": "Task tag", - "detailProof": "Proof", + "detailPreconditions": "Preconditions", + "detailReproductionSteps": "Reproduction steps", + "detailEvidence": "Evidence / POC", "detailImpact": "Impact", "detailRecommendation": "Remediation", + "detailRetestNotes": "Retest method", "downloadOkTitle": "Downloaded", "exportFailedMessage": "Export failed", "downloadFailed": "Download failed" @@ -2800,9 +2803,9 @@ "taskTag": "Task tag", "taskTagPlaceholder": "e.g. batch scan Q2, retest", "title": "Title", - "titlePlaceholder": "Vulnerability title", + "titlePlaceholder": "/api/login is vulnerable to SQL injection", "description": "Description", - "descriptionPlaceholder": "Detailed description", + "descriptionPlaceholder": "Describe the summary, trigger point, observed abnormal behavior, and why it is exploitable.", "severity": "Severity", "pleaseSelect": "Please select", "severityCritical": "Critical", @@ -2819,13 +2822,19 @@ "type": "Vulnerability type", "typePlaceholder": "e.g. SQL injection, XSS, CSRF", "target": "Target", - "targetPlaceholder": "Affected target (URL, IP, etc.)", - "proof": "Proof (POC)", - "proofPlaceholder": "Proof: request/response, screenshots, etc.", + "targetPlaceholder": "Be specific: URL, IP:port, endpoint path, and parameter name.", + "preconditions": "Preconditions", + "preconditionsPlaceholder": "Login state, permissions, account, headers/cookies, required data, environment/version; write none if not needed.", + "reproductionSteps": "Reproduction steps", + "reproductionStepsPlaceholder": "Number the steps and include entry point, parameter, payload, command, and observation point.", + "evidence": "Evidence / POC", + "evidencePlaceholder": "Raw request/response, curl/tool command, screenshot notes, logs, DNSLog/callback records, database results, file paths, timestamps, etc.", "impact": "Impact", - "impactPlaceholder": "Impact description", + "impactPlaceholder": "Describe the verified real-world impact, such as which data can be read or changed.", "recommendation": "Recommendation", - "recommendationPlaceholder": "Remediation" + "recommendationPlaceholder": "Write the concrete fix and retest criteria.", + "retestNotes": "Retest method", + "retestNotesPlaceholder": "How to verify the fix, including expected status code, error message, or access-control result." }, "vulnerabilityMd": { "headingBasic": "Basic information", @@ -2842,9 +2851,12 @@ "labelCreated": "Created at", "labelUpdated": "Updated at", "headingDescription": "Description", - "headingProof": "Proof (POC)", + "headingPreconditions": "Preconditions", + "headingReproductionSteps": "Reproduction steps", + "headingEvidence": "Evidence / POC", "headingImpact": "Impact", - "headingRecommendation": "Remediation" + "headingRecommendation": "Remediation", + "headingRetestNotes": "Retest method" }, "roleModal": { "addRole": "Add role", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 1765967c..9df82203 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2015,7 +2015,7 @@ "exportNoResults": "当前筛选条件下无可导出漏洞", "exportStarted": "已开始下载 {{count}} 份报告", "exportFailed": "导出失败", - "saveRequiredFields": "请填写必填字段:会话ID、标题和严重程度", + "saveRequiredFields": "请填写必填字段:会话ID、标题、描述、严重程度、漏洞类型、目标、复现步骤、证据/POC、影响和修复建议", "saveFailed": "保存失败", "fetchFailed": "获取漏洞失败", "deleteFailed": "删除失败", @@ -2034,9 +2034,12 @@ "detailTaskQueueId": "任务队列ID", "detailConversationTag": "对话标签", "detailTaskTag": "任务标签", - "detailProof": "证明", + "detailPreconditions": "前置条件", + "detailReproductionSteps": "复现步骤", + "detailEvidence": "证据 / POC", "detailImpact": "影响", "detailRecommendation": "修复建议", + "detailRetestNotes": "复测方式", "downloadOkTitle": "下载成功", "exportFailedMessage": "导出失败", "downloadFailed": "下载失败" @@ -2788,9 +2791,9 @@ "taskTag": "任务标签", "taskTagPlaceholder": "如:批量扫描Q2、专项复测", "title": "标题", - "titlePlaceholder": "漏洞标题", + "titlePlaceholder": "/api/login 存在 SQL 注入", "description": "描述", - "descriptionPlaceholder": "漏洞详细描述", + "descriptionPlaceholder": "说明漏洞摘要、触发点、异常现象和为什么可被利用。", "severity": "严重程度", "pleaseSelect": "请选择", "severityCritical": "严重", @@ -2807,13 +2810,19 @@ "type": "漏洞类型", "typePlaceholder": "如:SQL注入、XSS、CSRF等", "target": "目标", - "targetPlaceholder": "受影响的目标(URL、IP地址等)", - "proof": "证明(POC)", - "proofPlaceholder": "漏洞证明,如请求/响应、截图等", + "targetPlaceholder": "精确到 URL/IP:端口/接口路径/参数名", + "preconditions": "前置条件", + "preconditionsPlaceholder": "登录状态、权限、账号、Header/Cookie、特定数据、环境/版本;无则写无。", + "reproductionSteps": "复现步骤", + "reproductionStepsPlaceholder": "按 1/2/3 编号,写清入口、参数、payload、执行命令、观察点。", + "evidence": "证据 / POC", + "evidencePlaceholder": "原始请求/响应、curl/工具命令、截图说明、日志、DNSLog/回连记录、数据库结果、文件路径、时间戳等。", "impact": "影响", - "impactPlaceholder": "漏洞影响说明", + "impactPlaceholder": "结合已验证事实说明实际影响,例如越权读取哪些数据。", "recommendation": "修复建议", - "recommendationPlaceholder": "修复建议" + "recommendationPlaceholder": "写具体修复点和复测标准。", + "retestNotes": "复测方式", + "retestNotesPlaceholder": "修复后如何验证漏洞已关闭,包括应返回的状态码、错误信息或访问控制结果。" }, "vulnerabilityMd": { "headingBasic": "基本信息", @@ -2830,9 +2839,12 @@ "labelCreated": "创建时间", "labelUpdated": "更新时间", "headingDescription": "描述", - "headingProof": "证明(POC)", + "headingPreconditions": "前置条件", + "headingReproductionSteps": "复现步骤", + "headingEvidence": "证据 / POC", "headingImpact": "影响", - "headingRecommendation": "修复建议" + "headingRecommendation": "修复建议", + "headingRetestNotes": "复测方式" }, "roleModal": { "addRole": "添加角色", diff --git a/web/static/js/vulnerability.js b/web/static/js/vulnerability.js index 45470539..402682e3 100644 --- a/web/static/js/vulnerability.js +++ b/web/static/js/vulnerability.js @@ -1303,9 +1303,14 @@ function renderVulnerabilities(vulnerabilities, renderOptions) { ${vuln.conversation_tag ? vulnDetailField(vulnT('vulnerabilityPage.detailConversationTag'), vuln.conversation_tag, false) : ''} ${vuln.task_tag ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskTag'), vuln.task_tag, false) : ''} - ${vuln.proof ? `
${escapeHtml(vulnT('vulnerabilityPage.detailProof'))}:
${escapeHtml(vuln.proof)}
` : ''} - ${vuln.impact ? `
${escapeHtml(vulnT('vulnerabilityPage.detailImpact'))}: ${escapeHtml(vuln.impact)}
` : ''} - ${vuln.recommendation ? `
${escapeHtml(vulnT('vulnerabilityPage.detailRecommendation'))}: ${escapeHtml(vuln.recommendation)}
` : ''} +
+ ${vulnNarrativeSection(vulnT('vulnerabilityPage.detailPreconditions'), vuln.preconditions)} + ${vulnNarrativeSection(vulnT('vulnerabilityPage.detailReproductionSteps'), vuln.reproduction_steps)} + ${vulnNarrativeSection(vulnT('vulnerabilityPage.detailEvidence'), vuln.evidence, { code: true })} + ${vulnNarrativeSection(vulnT('vulnerabilityPage.detailImpact'), vuln.impact)} + ${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRecommendation'), vuln.recommendation)} + ${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRetestNotes'), vuln.retest_notes)} +
@@ -1467,9 +1472,12 @@ async function showAddVulnerabilityModal() { document.getElementById('vulnerability-status').value = 'open'; document.getElementById('vulnerability-type').value = ''; document.getElementById('vulnerability-target').value = ''; - document.getElementById('vulnerability-proof').value = ''; + document.getElementById('vulnerability-preconditions').value = ''; + document.getElementById('vulnerability-reproduction-steps').value = ''; + document.getElementById('vulnerability-evidence').value = ''; document.getElementById('vulnerability-impact').value = ''; document.getElementById('vulnerability-recommendation').value = ''; + document.getElementById('vulnerability-retest-notes').value = ''; openAppModal('vulnerability-modal'); } @@ -1493,9 +1501,12 @@ async function editVulnerability(id) { document.getElementById('vulnerability-status').value = vuln.status || 'open'; document.getElementById('vulnerability-type').value = vuln.type || ''; document.getElementById('vulnerability-target').value = vuln.target || ''; - document.getElementById('vulnerability-proof').value = vuln.proof || ''; + document.getElementById('vulnerability-preconditions').value = vuln.preconditions || ''; + document.getElementById('vulnerability-reproduction-steps').value = vuln.reproduction_steps || ''; + document.getElementById('vulnerability-evidence').value = vuln.evidence || ''; document.getElementById('vulnerability-impact').value = vuln.impact || ''; document.getElementById('vulnerability-recommendation').value = vuln.recommendation || ''; + document.getElementById('vulnerability-retest-notes').value = vuln.retest_notes || ''; await populateVulnerabilityModalProjectSelect(vuln.project_id || ''); document.getElementById('vulnerability-title')?.focus(); }); @@ -1510,9 +1521,16 @@ async function editVulnerability(id) { async function saveVulnerability() { const conversationId = document.getElementById('vulnerability-conversation-id').value.trim(); const title = document.getElementById('vulnerability-title').value.trim(); + const description = document.getElementById('vulnerability-description').value.trim(); const severity = document.getElementById('vulnerability-severity').value; + const type = document.getElementById('vulnerability-type').value.trim(); + const target = document.getElementById('vulnerability-target').value.trim(); + const reproductionSteps = document.getElementById('vulnerability-reproduction-steps').value.trim(); + const evidence = document.getElementById('vulnerability-evidence').value.trim(); + const impact = document.getElementById('vulnerability-impact').value.trim(); + const recommendation = document.getElementById('vulnerability-recommendation').value.trim(); - if (!conversationId || !title || !severity) { + if (!conversationId || !title || !description || !severity || !type || !target || !reproductionSteps || !evidence || !impact || !recommendation) { alert(vulnT('vulnerabilityPage.saveRequiredFields')); return; } @@ -1525,14 +1543,17 @@ async function saveVulnerability() { conversation_tag: document.getElementById('vulnerability-conversation-tag').value.trim(), task_tag: document.getElementById('vulnerability-task-tag').value.trim(), title: title, - description: document.getElementById('vulnerability-description').value.trim(), + description: description, severity: severity, status: document.getElementById('vulnerability-status').value, - type: document.getElementById('vulnerability-type').value.trim(), - target: document.getElementById('vulnerability-target').value.trim(), - proof: document.getElementById('vulnerability-proof').value.trim(), - impact: document.getElementById('vulnerability-impact').value.trim(), - recommendation: document.getElementById('vulnerability-recommendation').value.trim() + type: type, + target: target, + preconditions: document.getElementById('vulnerability-preconditions').value.trim(), + reproduction_steps: reproductionSteps, + evidence: evidence, + impact: impact, + recommendation: recommendation, + retest_notes: document.getElementById('vulnerability-retest-notes').value.trim() }; try { @@ -1553,9 +1574,12 @@ async function saveVulnerability() { status: data.status, type: data.type, target: data.target, - proof: data.proof, + preconditions: data.preconditions, + reproduction_steps: data.reproduction_steps, + evidence: data.evidence, impact: data.impact, recommendation: data.recommendation, + retest_notes: data.retest_notes, }; } @@ -1864,6 +1888,17 @@ function vulnDetailField(label, value, asCode) { `; } +function vulnNarrativeSection(label, value, options) { + if (value === undefined || value === null || String(value).trim() === '') return ''; + const opts = options || {}; + const tag = opts.code ? 'pre' : 'div'; + const cls = opts.code ? 'vulnerability-section-body vulnerability-section-body--code' : 'vulnerability-section-body'; + return `
+${escapeHtml(label)} +<${tag} class="${cls}">${escapeHtml(String(value))} +
`; +} + // 将漏洞格式化为Markdown(章节标题随界面语言) function formatVulnerabilityAsMarkdown(vuln) { const severityText = vulnSeverityLabel(vuln.severity); @@ -1905,8 +1940,16 @@ function formatVulnerabilityAsMarkdown(vuln) { markdown += `## ${L('headingDescription')}\n\n${vuln.description}\n\n`; } - if (vuln.proof) { - markdown += `## ${L('headingProof')}\n\n\`\`\`\n${vuln.proof}\n\`\`\`\n\n`; + if (vuln.preconditions) { + markdown += `## ${L('headingPreconditions')}\n\n${vuln.preconditions}\n\n`; + } + + if (vuln.reproduction_steps) { + markdown += `## ${L('headingReproductionSteps')}\n\n${vuln.reproduction_steps}\n\n`; + } + + if (vuln.evidence) { + markdown += `## ${L('headingEvidence')}\n\n\`\`\`\n${vuln.evidence}\n\`\`\`\n\n`; } if (vuln.impact) { @@ -1917,6 +1960,10 @@ function formatVulnerabilityAsMarkdown(vuln) { markdown += `## ${L('headingRecommendation')}\n\n${vuln.recommendation}\n\n`; } + if (vuln.retest_notes) { + markdown += `## ${L('headingRetestNotes')}\n\n${vuln.retest_notes}\n\n`; + } + return markdown; } @@ -2194,4 +2241,3 @@ window.setVulnerabilityIdFilter = setVulnerabilityIdFilter; window.bindVulnerabilityProject = bindVulnerabilityProject; window.buildVulnerabilityProjectOptionsHtml = buildVulnerabilityProjectOptionsHtml; window.changeVulnerabilityStatus = changeVulnerabilityStatus; - diff --git a/web/templates/index.html b/web/templates/index.html index d1b75915..a21497c4 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -4385,11 +4385,11 @@
- +
- - + +
@@ -4413,24 +4413,36 @@
- +
- - + +
- - + +
- - + +
- - + + +
+
+ + +
+
+ + +
+
+ +