// 漏洞管理相关功能 function vulnT(key, opts) { if (typeof window.t === 'function') { return window.t(key, opts); } return key; } function vulnDateLocale() { try { const lang = (window.__locale || '').toLowerCase(); if (lang.indexOf('zh') === 0) { return 'zh-CN'; } } catch (e) { /* ignore */ } return 'en-US'; } function vulnSeverityLabel(code) { const m = { critical: 'dashboard.severityCritical', high: 'dashboard.severityHigh', medium: 'dashboard.severityMedium', low: 'dashboard.severityLow', info: 'dashboard.severityInfo' }; return m[code] ? vulnT(m[code]) : code; } function vulnStatusLabel(code) { const m = { open: 'vulnerabilityPage.statusOpen', confirmed: 'vulnerabilityPage.statusConfirmed', fixed: 'vulnerabilityPage.statusFixed', false_positive: 'vulnerabilityPage.statusFalsePositive' }; return m[code] ? vulnT(m[code]) : code; } // 从localStorage读取每页显示数量,默认为20 const getVulnerabilityPageSize = () => { const saved = localStorage.getItem('vulnerabilityPageSize'); return saved ? parseInt(saved, 10) : 20; }; let currentVulnerabilityId = null; let vulnerabilityFilters = { id: '', conversation_id: '', task_id: '', conversation_tag: '', task_tag: '', severity: '', status: '' }; let vulnerabilityPagination = { currentPage: 1, pageSize: getVulnerabilityPageSize(), total: 0, totalPages: 1 }; // 初始化漏洞管理页面 function initVulnerabilityPage() { // 从localStorage加载每页条数设置 vulnerabilityPagination.pageSize = getVulnerabilityPageSize(); loadVulnerabilityStats(); loadVulnerabilities(); } // 加载漏洞统计 async function loadVulnerabilityStats() { try { // 检查apiFetch是否可用 if (typeof apiFetch === 'undefined') { console.error('apiFetch未定义,请确保auth.js已加载'); throw new Error('apiFetch未定义'); } const params = new URLSearchParams(); if (vulnerabilityFilters.conversation_id) { params.append('conversation_id', vulnerabilityFilters.conversation_id); } const response = await apiFetch(`/api/vulnerabilities/stats?${params.toString()}`); if (!response.ok) { const errorText = await response.text(); console.error('获取统计失败:', response.status, errorText); throw new Error(`获取统计失败: ${response.status}`); } const stats = await response.json(); updateVulnerabilityStats(stats); } catch (error) { console.error('加载漏洞统计失败:', error); // 统计失败不影响列表显示,只重置统计为0 updateVulnerabilityStats(null); } } // 更新漏洞统计显示 function updateVulnerabilityStats(stats) { // 处理空值情况 if (!stats) { stats = { total: 0, by_severity: {}, by_status: {} }; } document.getElementById('stat-total').textContent = stats.total || 0; const bySeverity = stats.by_severity || {}; document.getElementById('stat-critical').textContent = bySeverity.critical || 0; document.getElementById('stat-high').textContent = bySeverity.high || 0; document.getElementById('stat-medium').textContent = bySeverity.medium || 0; document.getElementById('stat-low').textContent = bySeverity.low || 0; document.getElementById('stat-info').textContent = bySeverity.info || 0; } // 加载漏洞列表 async function loadVulnerabilities(page = null) { const listContainer = document.getElementById('vulnerabilities-list'); listContainer.innerHTML = `
${escapeHtml(vulnT('vulnerabilityPage.loading'))}
`; try { // 检查apiFetch是否可用 if (typeof apiFetch === 'undefined') { console.error('apiFetch未定义,请确保auth.js已加载'); throw new Error('apiFetch未定义'); } // 如果指定了页码,使用页码;否则使用当前页码 if (page !== null) { vulnerabilityPagination.currentPage = page; } const params = new URLSearchParams(); params.append('page', vulnerabilityPagination.currentPage.toString()); params.append('limit', vulnerabilityPagination.pageSize.toString()); if (vulnerabilityFilters.id) { params.append('id', vulnerabilityFilters.id); } if (vulnerabilityFilters.conversation_id) { params.append('conversation_id', vulnerabilityFilters.conversation_id); } if (vulnerabilityFilters.task_id) { params.append('task_id', vulnerabilityFilters.task_id); } if (vulnerabilityFilters.conversation_tag) { params.append('conversation_tag', vulnerabilityFilters.conversation_tag); } if (vulnerabilityFilters.task_tag) { params.append('task_tag', vulnerabilityFilters.task_tag); } if (vulnerabilityFilters.severity) { params.append('severity', vulnerabilityFilters.severity); } if (vulnerabilityFilters.status) { params.append('status', vulnerabilityFilters.status); } const response = await apiFetch(`/api/vulnerabilities?${params.toString()}`); if (!response.ok) { const errorText = await response.text(); console.error('获取漏洞列表失败:', response.status, errorText); throw new Error(`获取漏洞列表失败: ${response.status}`); } const data = await response.json(); // 判断响应格式:新格式(有total字段)还是旧格式(直接是数组) let vulnerabilities; if (Array.isArray(data)) { // 旧格式:直接是数组 vulnerabilities = data; // 使用数组长度作为总数(可能不准确,但至少能显示分页控件) vulnerabilityPagination.total = data.length; vulnerabilityPagination.totalPages = Math.max(1, Math.ceil(data.length / vulnerabilityPagination.pageSize)); console.warn('后端返回的是旧格式(数组),建议更新后端API以支持分页'); } else if ('vulnerabilities' in data) { // 新格式:包含分页信息的对象(vulnerabilities可能为null或数组) vulnerabilities = Array.isArray(data.vulnerabilities) ? data.vulnerabilities : []; vulnerabilityPagination.total = data.total || 0; vulnerabilityPagination.currentPage = data.page || vulnerabilityPagination.currentPage; vulnerabilityPagination.pageSize = data.page_size || vulnerabilityPagination.pageSize; vulnerabilityPagination.totalPages = data.total_pages || 1; } else { // 未知格式,尝试作为数组处理 vulnerabilities = []; console.error('未知的响应格式:', data); } renderVulnerabilities(vulnerabilities); renderVulnerabilityPagination(); } catch (error) { console.error('加载漏洞列表失败:', error); listContainer.innerHTML = `
${escapeHtml(vulnT('vulnerabilityPage.loadListFailed'))}: ${escapeHtml(error.message)}
`; } } // 渲染漏洞列表 function renderVulnerabilities(vulnerabilities) { const listContainer = document.getElementById('vulnerabilities-list'); // 处理空值情况(使用 data-i18n 以便语言切换时自动更新) if (!vulnerabilities || !Array.isArray(vulnerabilities)) { listContainer.innerHTML = '
暂无漏洞记录
'; if (typeof window.applyTranslations === 'function') { window.applyTranslations(listContainer); } return; } if (vulnerabilities.length === 0) { listContainer.innerHTML = '
暂无漏洞记录
'; if (typeof window.applyTranslations === 'function') { window.applyTranslations(listContainer); } // 清空分页信息 const paginationContainer = document.getElementById('vulnerability-pagination'); if (paginationContainer) { paginationContainer.innerHTML = ''; } return; } const html = vulnerabilities.map(vuln => { const severityClass = `severity-${vuln.severity}`; const severityText = vulnSeverityLabel(vuln.severity); const statusText = vulnStatusLabel(vuln.status); const createdDate = new Date(vuln.created_at).toLocaleString(vulnDateLocale()); const dlTitle = escapeHtml(vulnT('vulnerabilityPage.downloadMarkdownTitle')); const editTitle = escapeHtml(vulnT('common.edit')); const deleteTitle = escapeHtml(vulnT('common.delete')); return `

${escapeHtml(vuln.title)}

${severityText} ${statusText} ${createdDate}
`; }).join(''); listContainer.innerHTML = html; if (typeof window.applyTranslations === 'function') { window.applyTranslations(listContainer); } } // 渲染分页控件 function renderVulnerabilityPagination() { const paginationContainer = document.getElementById('vulnerability-pagination'); if (!paginationContainer) { return; } const { currentPage, totalPages, total, pageSize } = vulnerabilityPagination; // 如果没有数据,不显示分页控件 if (total === 0) { paginationContainer.innerHTML = ''; return; } // 计算显示范围 const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1; const end = total === 0 ? 0 : Math.min(currentPage * pageSize, total); let paginationHTML = ''; paginationContainer.innerHTML = paginationHTML; if (typeof window.applyTranslations === 'function') { window.applyTranslations(paginationContainer); } } // 改变每页显示数量 async function changeVulnerabilityPageSize() { const pageSizeSelect = document.getElementById('vulnerability-page-size-pagination'); if (!pageSizeSelect) return; const newPageSize = parseInt(pageSizeSelect.value, 10); if (isNaN(newPageSize) || newPageSize < 1) { return; } // 保存到localStorage localStorage.setItem('vulnerabilityPageSize', newPageSize.toString()); // 更新分页配置 vulnerabilityPagination.pageSize = newPageSize; // 重新计算当前页(保持显示的数据范围尽可能接近) const currentStartItem = (vulnerabilityPagination.currentPage - 1) * vulnerabilityPagination.pageSize + 1; const newPage = Math.max(1, Math.floor((currentStartItem - 1) / newPageSize) + 1); vulnerabilityPagination.currentPage = newPage; // 重新加载数据 await loadVulnerabilities(); } // 显示添加漏洞模态框 function showAddVulnerabilityModal() { currentVulnerabilityId = null; document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.addVuln'); // 清空表单 document.getElementById('vulnerability-conversation-id').value = ''; document.getElementById('vulnerability-conversation-tag').value = ''; document.getElementById('vulnerability-task-tag').value = ''; document.getElementById('vulnerability-title').value = ''; document.getElementById('vulnerability-description').value = ''; document.getElementById('vulnerability-severity').value = ''; document.getElementById('vulnerability-status').value = 'open'; document.getElementById('vulnerability-type').value = ''; document.getElementById('vulnerability-target').value = ''; document.getElementById('vulnerability-proof').value = ''; document.getElementById('vulnerability-impact').value = ''; document.getElementById('vulnerability-recommendation').value = ''; document.getElementById('vulnerability-modal').style.display = 'block'; } // 编辑漏洞 async function editVulnerability(id) { try { const response = await apiFetch(`/api/vulnerabilities/${id}`); if (!response.ok) throw new Error(vulnT('vulnerabilityPage.fetchFailed')); const vuln = await response.json(); currentVulnerabilityId = id; document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.editVuln'); // 填充表单 document.getElementById('vulnerability-conversation-id').value = vuln.conversation_id || ''; document.getElementById('vulnerability-conversation-tag').value = vuln.conversation_tag || ''; document.getElementById('vulnerability-task-tag').value = vuln.task_tag || ''; document.getElementById('vulnerability-title').value = vuln.title || ''; document.getElementById('vulnerability-description').value = vuln.description || ''; document.getElementById('vulnerability-severity').value = vuln.severity || ''; 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-impact').value = vuln.impact || ''; document.getElementById('vulnerability-recommendation').value = vuln.recommendation || ''; document.getElementById('vulnerability-modal').style.display = 'block'; } catch (error) { console.error('加载漏洞失败:', error); alert(vulnT('vulnerability.loadFailed') + ': ' + error.message); } } // 保存漏洞 async function saveVulnerability() { const conversationId = document.getElementById('vulnerability-conversation-id').value.trim(); const title = document.getElementById('vulnerability-title').value.trim(); const severity = document.getElementById('vulnerability-severity').value; if (!conversationId || !title || !severity) { alert(vulnT('vulnerabilityPage.saveRequiredFields')); return; } const data = { conversation_id: conversationId, 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(), 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() }; try { const url = currentVulnerabilityId ? `/api/vulnerabilities/${currentVulnerabilityId}` : '/api/vulnerabilities'; const method = currentVulnerabilityId ? 'PUT' : 'POST'; const response = await apiFetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || vulnT('vulnerabilityPage.saveFailed')); } closeVulnerabilityModal(); loadVulnerabilityStats(); // 保存/更新后,重置到第一页 vulnerabilityPagination.currentPage = 1; loadVulnerabilities(); } catch (error) { console.error('保存漏洞失败:', error); alert(vulnT('vulnerabilityPage.saveFailed') + ': ' + error.message); } } // 删除漏洞 async function deleteVulnerability(id) { if (!confirm(vulnT('vulnerability.deleteConfirm'))) { return; } try { const response = await apiFetch(`/api/vulnerabilities/${id}`, { method: 'DELETE' }); if (!response.ok) throw new Error(vulnT('vulnerabilityPage.deleteFailed')); loadVulnerabilityStats(); // 删除后,如果当前页没有数据了,回到上一页 if (vulnerabilityPagination.currentPage > 1 && vulnerabilityPagination.total > 0) { const itemsOnCurrentPage = vulnerabilityPagination.total - (vulnerabilityPagination.currentPage - 1) * vulnerabilityPagination.pageSize; if (itemsOnCurrentPage <= 1) { vulnerabilityPagination.currentPage--; } } loadVulnerabilities(); } catch (error) { console.error('删除漏洞失败:', error); alert(vulnT('vulnerabilityPage.deleteFailed') + ': ' + error.message); } } // 关闭漏洞模态框 function closeVulnerabilityModal() { document.getElementById('vulnerability-modal').style.display = 'none'; currentVulnerabilityId = null; } // 筛选漏洞 function filterVulnerabilities() { vulnerabilityFilters.id = document.getElementById('vulnerability-id-filter').value.trim(); vulnerabilityFilters.conversation_id = document.getElementById('vulnerability-conversation-filter').value.trim(); vulnerabilityFilters.task_id = document.getElementById('vulnerability-task-filter').value.trim(); vulnerabilityFilters.conversation_tag = document.getElementById('vulnerability-conversation-tag-filter').value.trim(); vulnerabilityFilters.task_tag = document.getElementById('vulnerability-task-tag-filter').value.trim(); vulnerabilityFilters.severity = document.getElementById('vulnerability-severity-filter').value; vulnerabilityFilters.status = document.getElementById('vulnerability-status-filter').value; // 重置到第一页 vulnerabilityPagination.currentPage = 1; loadVulnerabilityStats(); loadVulnerabilities(); } // 清除筛选 function clearVulnerabilityFilters() { document.getElementById('vulnerability-id-filter').value = ''; document.getElementById('vulnerability-conversation-filter').value = ''; document.getElementById('vulnerability-task-filter').value = ''; document.getElementById('vulnerability-conversation-tag-filter').value = ''; document.getElementById('vulnerability-task-tag-filter').value = ''; document.getElementById('vulnerability-severity-filter').value = ''; document.getElementById('vulnerability-status-filter').value = ''; vulnerabilityFilters = { id: '', conversation_id: '', task_id: '', conversation_tag: '', task_tag: '', severity: '', status: '' }; // 重置到第一页 vulnerabilityPagination.currentPage = 1; loadVulnerabilityStats(); loadVulnerabilities(); } // 刷新漏洞 function refreshVulnerabilities() { loadVulnerabilityStats(); loadVulnerabilities(); } // 切换漏洞详情展开/折叠 function toggleVulnerabilityDetails(id) { const content = document.getElementById(`content-${id}`); const icon = document.getElementById(`expand-icon-${id}`); if (!content || !icon) return; if (content.style.display === 'none') { content.style.display = 'block'; icon.style.transform = 'rotate(90deg)'; } else { content.style.display = 'none'; icon.style.transform = 'rotate(0deg)'; } } // HTML转义 function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** 复制详情字段(编码由 encodeURIComponent 传入,避免引号截断) */ function vulnerabilityCopyEncoded(evt, encoded) { if (evt && evt.stopPropagation) { evt.stopPropagation(); } let text = ''; try { text = decodeURIComponent(encoded); } catch (e) { return; } const done = () => { if (evt && evt.target && evt.target.closest) { const btn = evt.target.closest('.vuln-detail-field__copy'); if (btn) { const t0 = btn.getAttribute('title') || ''; btn.setAttribute('title', vulnT('common.copied')); setTimeout(() => btn.setAttribute('title', t0), 1600); } } }; if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { navigator.clipboard.writeText(text).then(done).catch(() => { try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); done(); } catch (err) { console.error('copy failed', err); } }); } else { try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); done(); } catch (err) { console.error('copy failed', err); } } } function vulnDetailField(label, value, asCode) { if (value === undefined || value === null || String(value) === '') { return ''; } const s = String(value); const enc = encodeURIComponent(s); const copyTitle = escapeHtml(vulnT('common.copy')); const valueEl = asCode ? `${escapeHtml(s)}` : `${escapeHtml(s)}`; const copyBtn = ``; return `
${escapeHtml(label)}
${valueEl}${copyBtn}
`; } // 将漏洞格式化为Markdown(章节标题随界面语言) function formatVulnerabilityAsMarkdown(vuln) { const severityText = vulnSeverityLabel(vuln.severity); const statusText = vulnStatusLabel(vuln.status); const loc = vulnDateLocale(); const createdDate = new Date(vuln.created_at).toLocaleString(loc); const updatedDate = new Date(vuln.updated_at).toLocaleString(loc); const L = (k) => vulnT('vulnerabilityMd.' + k); let markdown = `# ${vuln.title}\n\n`; markdown += `## ${L('headingBasic')}\n\n`; markdown += `- **${L('labelId')}**: \`${vuln.id}\`\n`; markdown += `- **${L('labelSeverity')}**: ${severityText}\n`; markdown += `- **${L('labelStatus')}**: ${statusText}\n`; if (vuln.type) { markdown += `- **${L('labelType')}**: ${vuln.type}\n`; } if (vuln.target) { markdown += `- **${L('labelTarget')}**: ${vuln.target}\n`; } markdown += `- **${L('labelConversationId')}**: \`${vuln.conversation_id}\`\n`; if (vuln.task_id) { markdown += `- **${L('labelTaskId')}**: \`${vuln.task_id}\`\n`; } if (vuln.task_queue_id) { markdown += `- **${L('labelTaskQueueId')}**: \`${vuln.task_queue_id}\`\n`; } if (vuln.conversation_tag) { markdown += `- **${L('labelConversationTag')}**: ${vuln.conversation_tag}\n`; } if (vuln.task_tag) { markdown += `- **${L('labelTaskTag')}**: ${vuln.task_tag}\n`; } markdown += `- **${L('labelCreated')}**: ${createdDate}\n`; markdown += `- **${L('labelUpdated')}**: ${updatedDate}\n\n`; if (vuln.description) { 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.impact) { markdown += `## ${L('headingImpact')}\n\n${vuln.impact}\n\n`; } if (vuln.recommendation) { markdown += `## ${L('headingRecommendation')}\n\n${vuln.recommendation}\n\n`; } return markdown; } function buildVulnerabilityFilterParams() { const params = new URLSearchParams(); const keys = ['id', 'conversation_id', 'task_id', 'conversation_tag', 'task_tag', 'severity', 'status']; keys.forEach((k) => { if (vulnerabilityFilters[k]) { params.append(k, vulnerabilityFilters[k]); } }); return params; } function triggerTextDownload(fileName, content) { const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } async function exportVulnerabilityReports() { try { const params = buildVulnerabilityFilterParams(); params.set('mode', 'summary'); params.set('group_by', 'conversation'); const response = await apiFetch(`/api/vulnerabilities/export?${params.toString()}`); if (!response.ok) { const error = await response.json().catch(() => ({ error: vulnT('vulnerabilityPage.exportFailedMessage') })); throw new Error(error.error || vulnT('vulnerabilityPage.exportFailedMessage')); } const data = await response.json(); const files = Array.isArray(data.files) ? data.files : []; if (!files.length) { alert(vulnT('vulnerabilityPage.exportNoResults')); return; } files.forEach((file, idx) => { setTimeout(() => triggerTextDownload(file.filename || `vulnerability-export-${idx + 1}.md`, file.content || ''), idx * 120); }); if (files.length > 1) { alert(vulnT('vulnerabilityPage.exportStarted', { count: files.length })); } } catch (error) { console.error('导出漏洞报告失败:', error); alert(vulnT('vulnerabilityPage.exportFailed') + ': ' + error.message); } } // 下载漏洞为Markdown格式 async function downloadVulnerabilityAsMarkdown(id, event) { try { const response = await apiFetch(`/api/vulnerabilities/${id}`); if (!response.ok) { throw new Error(vulnT('vulnerabilityPage.fetchFailed')); } const vuln = await response.json(); const markdown = formatVulnerabilityAsMarkdown(vuln); // 创建Blob对象 const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' }); // 创建下载链接 const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; // 生成文件名(使用漏洞标题,清理特殊字符,保留中文) const cleanTitle = vuln.title .replace(/[<>:"/\\|?*]/g, '') // 移除Windows不允许的字符 .replace(/\s+/g, '_') // 空格替换为下划线 .substring(0, 50); // 限制长度 const fileName = `${cleanTitle}_${vuln.id.substring(0, 8)}.md`; link.download = fileName; // 触发下载 document.body.appendChild(link); link.click(); // 清理 document.body.removeChild(link); URL.revokeObjectURL(url); // 显示成功提示 if (event && event.target) { const button = event.target.closest('button'); if (button) { const originalTitle = button.title || vulnT('vulnerabilityPage.downloadMarkdownTitle'); button.title = vulnT('vulnerabilityPage.downloadOkTitle'); setTimeout(() => { button.title = originalTitle; }, 2000); } } } catch (error) { console.error('下载失败:', error); alert(vulnT('vulnerabilityPage.downloadFailed') + ': ' + error.message); } } // 点击模态框外部关闭 window.onclick = function(event) { const modal = document.getElementById('vulnerability-modal'); if (event.target === modal) { closeVulnerabilityModal(); } }; document.addEventListener('languagechange', function () { const page = document.getElementById('page-vulnerabilities'); if (page && page.classList.contains('active')) { loadVulnerabilities(); } });