Files
CyberStrikeAI/web/static/js/vulnerability.js
2026-03-09 02:06:39 +08:00

650 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 漏洞管理相关功能
// 从localStorage读取每页显示数量默认为20
const getVulnerabilityPageSize = () => {
const saved = localStorage.getItem('vulnerabilityPageSize');
return saved ? parseInt(saved, 10) : 20;
};
let currentVulnerabilityId = null;
let vulnerabilityFilters = {
id: '',
conversation_id: '',
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 = '<div class="loading-spinner">加载中...</div>';
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.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 = `<div class="error-message">加载失败: ${error.message}</div>`;
}
}
// 渲染漏洞列表
function renderVulnerabilities(vulnerabilities) {
const listContainer = document.getElementById('vulnerabilities-list');
// 处理空值情况(使用 data-i18n 以便语言切换时自动更新)
if (!vulnerabilities || !Array.isArray(vulnerabilities)) {
listContainer.innerHTML = '<div class="empty-state" data-i18n="vulnerabilityPage.noRecords">暂无漏洞记录</div>';
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(listContainer);
}
return;
}
if (vulnerabilities.length === 0) {
listContainer.innerHTML = '<div class="empty-state" data-i18n="vulnerabilityPage.noRecords">暂无漏洞记录</div>';
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 = {
'critical': '严重',
'high': '高危',
'medium': '中危',
'low': '低危',
'info': '信息'
}[vuln.severity] || vuln.severity;
const statusText = {
'open': '待处理',
'confirmed': '已确认',
'fixed': '已修复',
'false_positive': '误报'
}[vuln.status] || vuln.status;
const createdDate = new Date(vuln.created_at).toLocaleString('zh-CN');
return `
<div class="vulnerability-card ${severityClass}">
<div class="vulnerability-header" onclick="toggleVulnerabilityDetails('${vuln.id}')" style="cursor: pointer;">
<div class="vulnerability-title-section">
<div style="display: flex; align-items: center; gap: 8px;">
<svg class="vulnerability-expand-icon" id="expand-icon-${vuln.id}" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="transition: transform 0.2s ease; flex-shrink: 0;">
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<h3 class="vulnerability-title">${escapeHtml(vuln.title)}</h3>
</div>
<div class="vulnerability-meta">
<span class="severity-badge ${severityClass}">${severityText}</span>
<span class="status-badge status-${vuln.status}">${statusText}</span>
<span class="vulnerability-date">${createdDate}</span>
</div>
</div>
<div class="vulnerability-actions" onclick="event.stopPropagation();">
<button class="btn-ghost" onclick="downloadVulnerabilityAsMarkdown('${vuln.id}', event)" title="下载Markdown">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="7 10 12 15 17 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="15" x2="12" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button class="btn-ghost" onclick="editVulnerability('${vuln.id}')" title="编辑">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button class="btn-ghost" onclick="deleteVulnerability('${vuln.id}')" title="删除">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
<div class="vulnerability-content" id="content-${vuln.id}" style="display: none;">
${vuln.description ? `<div class="vulnerability-description">${escapeHtml(vuln.description)}</div>` : ''}
<div class="vulnerability-details">
<div class="detail-item"><strong>漏洞ID:</strong> <code>${escapeHtml(vuln.id)}</code></div>
${vuln.type ? `<div class="detail-item"><strong>类型:</strong> ${escapeHtml(vuln.type)}</div>` : ''}
${vuln.target ? `<div class="detail-item"><strong>目标:</strong> ${escapeHtml(vuln.target)}</div>` : ''}
<div class="detail-item"><strong>会话ID:</strong> <code>${escapeHtml(vuln.conversation_id)}</code></div>
</div>
${vuln.proof ? `<div class="vulnerability-proof"><strong>证明:</strong><pre>${escapeHtml(vuln.proof)}</pre></div>` : ''}
${vuln.impact ? `<div class="vulnerability-impact"><strong>影响:</strong> ${escapeHtml(vuln.impact)}</div>` : ''}
${vuln.recommendation ? `<div class="vulnerability-recommendation"><strong>修复建议:</strong> ${escapeHtml(vuln.recommendation)}</div>` : ''}
</div>
</div>
`;
}).join('');
listContainer.innerHTML = html;
}
// 渲染分页控件
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 = '<div class="pagination">';
// 左侧显示范围信息和每页数量选择器参考Skills样式
paginationHTML += `
<div class="pagination-info">
<span>显示 ${start}-${end} / 共 ${total} 条</span>
<label class="pagination-page-size">
每页显示
<select id="vulnerability-page-size-pagination" onchange="changeVulnerabilityPageSize()">
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
<option value="50" ${pageSize === 50 ? 'selected' : ''}>50</option>
<option value="100" ${pageSize === 100 ? 'selected' : ''}>100</option>
</select>
</label>
</div>
`;
// 右侧分页按钮参考Skills样式首页、上一页、第X/Y页、下一页、末页
paginationHTML += `
<div class="pagination-controls">
<button class="btn-secondary" onclick="loadVulnerabilities(1)" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>首页</button>
<button class="btn-secondary" onclick="loadVulnerabilities(${currentPage - 1})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>上一页</button>
<span class="pagination-page">第 ${currentPage} / ${totalPages || 1} 页</span>
<button class="btn-secondary" onclick="loadVulnerabilities(${currentPage + 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>下一页</button>
<button class="btn-secondary" onclick="loadVulnerabilities(${totalPages || 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>末页</button>
</div>
`;
paginationHTML += '</div>';
paginationContainer.innerHTML = paginationHTML;
}
// 改变每页显示数量
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 = (typeof window.t === 'function' ? window.t('vulnerability.addVuln') : '添加漏洞');
// 清空表单
document.getElementById('vulnerability-conversation-id').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('获取漏洞失败');
const vuln = await response.json();
currentVulnerabilityId = id;
document.getElementById('vulnerability-modal-title').textContent = (typeof window.t === 'function' ? window.t('vulnerability.editVuln') : '编辑漏洞');
// 填充表单
document.getElementById('vulnerability-conversation-id').value = vuln.conversation_id || '';
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('加载漏洞失败: ' + 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('请填写必填字段会话ID、标题和严重程度');
return;
}
const data = {
conversation_id: conversationId,
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 || '保存失败');
}
closeVulnerabilityModal();
loadVulnerabilityStats();
// 保存/更新后,重置到第一页
vulnerabilityPagination.currentPage = 1;
loadVulnerabilities();
} catch (error) {
console.error('保存漏洞失败:', error);
alert('保存漏洞失败: ' + error.message);
}
}
// 删除漏洞
async function deleteVulnerability(id) {
if (!confirm('确定要删除此漏洞吗?')) {
return;
}
try {
const response = await apiFetch(`/api/vulnerabilities/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('删除失败');
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('删除漏洞失败: ' + 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.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-severity-filter').value = '';
document.getElementById('vulnerability-status-filter').value = '';
vulnerabilityFilters = {
id: '',
conversation_id: '',
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;
}
// 将漏洞格式化为Markdown
function formatVulnerabilityAsMarkdown(vuln) {
const severityText = {
'critical': '严重',
'high': '高危',
'medium': '中危',
'low': '低危',
'info': '信息'
}[vuln.severity] || vuln.severity;
const statusText = {
'open': '待处理',
'confirmed': '已确认',
'fixed': '已修复',
'false_positive': '误报'
}[vuln.status] || vuln.status;
const createdDate = new Date(vuln.created_at).toLocaleString('zh-CN');
const updatedDate = new Date(vuln.updated_at).toLocaleString('zh-CN');
let markdown = `# ${vuln.title}\n\n`;
markdown += `## 基本信息\n\n`;
markdown += `- **漏洞ID**: \`${vuln.id}\`\n`;
markdown += `- **严重程度**: ${severityText}\n`;
markdown += `- **状态**: ${statusText}\n`;
if (vuln.type) {
markdown += `- **类型**: ${vuln.type}\n`;
}
if (vuln.target) {
markdown += `- **目标**: ${vuln.target}\n`;
}
markdown += `- **会话ID**: \`${vuln.conversation_id}\`\n`;
markdown += `- **创建时间**: ${createdDate}\n`;
markdown += `- **更新时间**: ${updatedDate}\n\n`;
if (vuln.description) {
markdown += `## 描述\n\n${vuln.description}\n\n`;
}
if (vuln.proof) {
markdown += `## 证明POC\n\n\`\`\`\n${vuln.proof}\n\`\`\`\n\n`;
}
if (vuln.impact) {
markdown += `## 影响\n\n${vuln.impact}\n\n`;
}
if (vuln.recommendation) {
markdown += `## 修复建议\n\n${vuln.recommendation}\n\n`;
}
return markdown;
}
// 下载漏洞为Markdown格式
async function downloadVulnerabilityAsMarkdown(id, event) {
try {
const response = await apiFetch(`/api/vulnerabilities/${id}`);
if (!response.ok) {
throw new Error('获取漏洞失败');
}
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 || '下载Markdown';
button.title = '下载成功!';
setTimeout(() => {
button.title = originalTitle;
}, 2000);
}
}
} catch (error) {
console.error('下载失败:', error);
alert('下载失败: ' + error.message);
}
}
// 点击模态框外部关闭
window.onclick = function(event) {
const modal = document.getElementById('vulnerability-modal');
if (event.target === modal) {
closeVulnerabilityModal();
}
}