mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-01 23:35:18 +02:00
Add files via upload
This commit is contained in:
+68
-10
@@ -13575,29 +13575,87 @@ header {
|
||||
|
||||
.vulnerability-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px 16px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 6px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
/* 元数据条数为奇数时,最后一项占满一行,长 URL/队列 ID 更易读 */
|
||||
.vulnerability-details .vuln-detail-field:last-child:nth-child(odd) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.vulnerability-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.vulnerability-details .vuln-detail-field:last-child:nth-child(odd) {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 漏洞详情字段:标签与值分行,长 ID/URL 可换行、可选中复制 */
|
||||
.vuln-detail-field {
|
||||
min-width: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.detail-item strong {
|
||||
.vuln-detail-field__label {
|
||||
color: var(--text-secondary);
|
||||
margin-right: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 6px;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.detail-item code {
|
||||
.vuln-detail-field__row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vuln-detail-field-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
color: var(--text-primary);
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.vuln-detail-field__copy {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
padding: 6px;
|
||||
line-height: 0;
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vuln-detail-field__copy:hover {
|
||||
color: var(--accent-color);
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.vulnerability-proof,
|
||||
|
||||
@@ -1312,6 +1312,12 @@
|
||||
"clear": "Clear",
|
||||
"vulnId": "Vuln ID",
|
||||
"conversationId": "Conversation ID",
|
||||
"taskOrQueueId": "Task / queue ID",
|
||||
"filterTaskOrQueue": "Filter by task or queue ID",
|
||||
"conversationTag": "Conversation tag",
|
||||
"filterConversationTag": "Filter by conversation tag",
|
||||
"taskTag": "Task tag",
|
||||
"filterTaskTag": "Filter by task tag",
|
||||
"severity": "Severity",
|
||||
"status": "Status",
|
||||
"statusOpen": "Open",
|
||||
@@ -1321,7 +1327,31 @@
|
||||
"searchVulnId": "Search vuln ID",
|
||||
"filterConversation": "Filter by conversation",
|
||||
"loading": "Loading...",
|
||||
"noRecords": "No vulnerability records"
|
||||
"loadListFailed": "Failed to load",
|
||||
"noRecords": "No vulnerability records",
|
||||
"batchExport": "Batch export",
|
||||
"downloadMarkdownTitle": "Download Markdown",
|
||||
"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",
|
||||
"saveFailed": "Save failed",
|
||||
"fetchFailed": "Failed to fetch vulnerability",
|
||||
"deleteFailed": "Delete failed",
|
||||
"detailVulnId": "Vuln ID",
|
||||
"detailType": "Type",
|
||||
"detailTarget": "Target",
|
||||
"detailConversationId": "Conversation ID",
|
||||
"detailTaskId": "Task ID",
|
||||
"detailTaskQueueId": "Task queue ID",
|
||||
"detailConversationTag": "Conversation tag",
|
||||
"detailTaskTag": "Task tag",
|
||||
"detailProof": "Proof",
|
||||
"detailImpact": "Impact",
|
||||
"detailRecommendation": "Remediation",
|
||||
"downloadOkTitle": "Downloaded",
|
||||
"exportFailedMessage": "Export failed",
|
||||
"downloadFailed": "Download failed"
|
||||
},
|
||||
"tasksPage": {
|
||||
"statusFilter": "Status filter",
|
||||
@@ -1767,6 +1797,10 @@
|
||||
"vulnerabilityModal": {
|
||||
"conversationId": "Conversation ID",
|
||||
"conversationIdPlaceholder": "Enter conversation ID",
|
||||
"conversationTag": "Conversation tag",
|
||||
"conversationTagPlaceholder": "e.g. engagement A, weekly report",
|
||||
"taskTag": "Task tag",
|
||||
"taskTagPlaceholder": "e.g. batch scan Q2, retest",
|
||||
"title": "Title",
|
||||
"titlePlaceholder": "Vulnerability title",
|
||||
"description": "Description",
|
||||
@@ -1794,6 +1828,25 @@
|
||||
"recommendation": "Recommendation",
|
||||
"recommendationPlaceholder": "Remediation"
|
||||
},
|
||||
"vulnerabilityMd": {
|
||||
"headingBasic": "Basic information",
|
||||
"labelId": "Vulnerability ID",
|
||||
"labelSeverity": "Severity",
|
||||
"labelStatus": "Status",
|
||||
"labelType": "Type",
|
||||
"labelTarget": "Target",
|
||||
"labelConversationId": "Conversation ID",
|
||||
"labelTaskId": "Task ID",
|
||||
"labelTaskQueueId": "Task queue ID",
|
||||
"labelConversationTag": "Conversation tag",
|
||||
"labelTaskTag": "Task tag",
|
||||
"labelCreated": "Created at",
|
||||
"labelUpdated": "Updated at",
|
||||
"headingDescription": "Description",
|
||||
"headingProof": "Proof (POC)",
|
||||
"headingImpact": "Impact",
|
||||
"headingRecommendation": "Remediation"
|
||||
},
|
||||
"roleModal": {
|
||||
"addRole": "Add role",
|
||||
"editRole": "Edit role",
|
||||
|
||||
@@ -1312,6 +1312,12 @@
|
||||
"clear": "清除",
|
||||
"vulnId": "漏洞ID",
|
||||
"conversationId": "会话ID",
|
||||
"taskOrQueueId": "任务ID/队列ID",
|
||||
"filterTaskOrQueue": "筛选任务ID或队列ID",
|
||||
"conversationTag": "对话标签",
|
||||
"filterConversationTag": "筛选对话标签",
|
||||
"taskTag": "任务标签",
|
||||
"filterTaskTag": "筛选任务标签",
|
||||
"severity": "严重程度",
|
||||
"status": "状态",
|
||||
"statusOpen": "待处理",
|
||||
@@ -1321,7 +1327,31 @@
|
||||
"searchVulnId": "搜索漏洞ID",
|
||||
"filterConversation": "筛选特定会话",
|
||||
"loading": "加载中...",
|
||||
"noRecords": "暂无漏洞记录"
|
||||
"loadListFailed": "加载失败",
|
||||
"noRecords": "暂无漏洞记录",
|
||||
"batchExport": "批量导出",
|
||||
"downloadMarkdownTitle": "下载 Markdown",
|
||||
"exportNoResults": "当前筛选条件下无可导出漏洞",
|
||||
"exportStarted": "已开始下载 {{count}} 份报告",
|
||||
"exportFailed": "导出失败",
|
||||
"saveRequiredFields": "请填写必填字段:会话ID、标题和严重程度",
|
||||
"saveFailed": "保存失败",
|
||||
"fetchFailed": "获取漏洞失败",
|
||||
"deleteFailed": "删除失败",
|
||||
"detailVulnId": "漏洞ID",
|
||||
"detailType": "类型",
|
||||
"detailTarget": "目标",
|
||||
"detailConversationId": "会话ID",
|
||||
"detailTaskId": "任务ID",
|
||||
"detailTaskQueueId": "任务队列ID",
|
||||
"detailConversationTag": "对话标签",
|
||||
"detailTaskTag": "任务标签",
|
||||
"detailProof": "证明",
|
||||
"detailImpact": "影响",
|
||||
"detailRecommendation": "修复建议",
|
||||
"downloadOkTitle": "下载成功",
|
||||
"exportFailedMessage": "导出失败",
|
||||
"downloadFailed": "下载失败"
|
||||
},
|
||||
"tasksPage": {
|
||||
"statusFilter": "状态筛选",
|
||||
@@ -1767,6 +1797,10 @@
|
||||
"vulnerabilityModal": {
|
||||
"conversationId": "会话ID",
|
||||
"conversationIdPlaceholder": "输入会话ID",
|
||||
"conversationTag": "对话标签",
|
||||
"conversationTagPlaceholder": "如:红队演练A、客户A周报",
|
||||
"taskTag": "任务标签",
|
||||
"taskTagPlaceholder": "如:批量扫描Q2、专项复测",
|
||||
"title": "标题",
|
||||
"titlePlaceholder": "漏洞标题",
|
||||
"description": "描述",
|
||||
@@ -1794,6 +1828,25 @@
|
||||
"recommendation": "修复建议",
|
||||
"recommendationPlaceholder": "修复建议"
|
||||
},
|
||||
"vulnerabilityMd": {
|
||||
"headingBasic": "基本信息",
|
||||
"labelId": "漏洞ID",
|
||||
"labelSeverity": "严重程度",
|
||||
"labelStatus": "状态",
|
||||
"labelType": "类型",
|
||||
"labelTarget": "目标",
|
||||
"labelConversationId": "会话ID",
|
||||
"labelTaskId": "任务ID",
|
||||
"labelTaskQueueId": "任务队列ID",
|
||||
"labelConversationTag": "对话标签",
|
||||
"labelTaskTag": "任务标签",
|
||||
"labelCreated": "创建时间",
|
||||
"labelUpdated": "更新时间",
|
||||
"headingDescription": "描述",
|
||||
"headingProof": "证明(POC)",
|
||||
"headingImpact": "影响",
|
||||
"headingRecommendation": "修复建议"
|
||||
},
|
||||
"roleModal": {
|
||||
"addRole": "添加角色",
|
||||
"editRole": "编辑角色",
|
||||
|
||||
+198
-95
@@ -1,5 +1,43 @@
|
||||
// 漏洞管理相关功能
|
||||
|
||||
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');
|
||||
@@ -85,7 +123,7 @@ function updateVulnerabilityStats(stats) {
|
||||
// 加载漏洞列表
|
||||
async function loadVulnerabilities(page = null) {
|
||||
const listContainer = document.getElementById('vulnerabilities-list');
|
||||
listContainer.innerHTML = '<div class="loading-spinner">加载中...</div>';
|
||||
listContainer.innerHTML = `<div class="loading-spinner">${escapeHtml(vulnT('vulnerabilityPage.loading'))}</div>`;
|
||||
|
||||
try {
|
||||
// 检查apiFetch是否可用
|
||||
@@ -160,7 +198,7 @@ async function loadVulnerabilities(page = null) {
|
||||
renderVulnerabilityPagination();
|
||||
} catch (error) {
|
||||
console.error('加载漏洞列表失败:', error);
|
||||
listContainer.innerHTML = `<div class="error-message">加载失败: ${error.message}</div>`;
|
||||
listContainer.innerHTML = `<div class="error-message">${escapeHtml(vulnT('vulnerabilityPage.loadListFailed'))}: ${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,22 +230,12 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
|
||||
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');
|
||||
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 `
|
||||
<div class="vulnerability-card ${severityClass}">
|
||||
@@ -226,20 +254,20 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="vulnerability-actions" onclick="event.stopPropagation();">
|
||||
<button class="btn-ghost" onclick="downloadVulnerabilityAsMarkdown('${vuln.id}', event)" title="下载Markdown">
|
||||
<button class="btn-ghost" onclick="downloadVulnerabilityAsMarkdown('${vuln.id}', event)" title="${dlTitle}">
|
||||
<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="编辑">
|
||||
<button class="btn-ghost" onclick="editVulnerability('${vuln.id}')" title="${editTitle}">
|
||||
<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="删除">
|
||||
<button class="btn-ghost" onclick="deleteVulnerability('${vuln.id}')" title="${deleteTitle}">
|
||||
<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>
|
||||
@@ -249,24 +277,27 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
<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>
|
||||
${vuln.task_id ? `<div class="detail-item"><strong>任务ID:</strong> <code>${escapeHtml(vuln.task_id)}</code></div>` : ''}
|
||||
${vuln.task_queue_id ? `<div class="detail-item"><strong>任务队列ID:</strong> <code>${escapeHtml(vuln.task_queue_id)}</code></div>` : ''}
|
||||
${vuln.conversation_tag ? `<div class="detail-item"><strong>对话标签:</strong> ${escapeHtml(vuln.conversation_tag)}</div>` : ''}
|
||||
${vuln.task_tag ? `<div class="detail-item"><strong>任务标签:</strong> ${escapeHtml(vuln.task_tag)}</div>` : ''}
|
||||
${vulnDetailField(vulnT('vulnerabilityPage.detailVulnId'), vuln.id, true)}
|
||||
${vuln.type ? vulnDetailField(vulnT('vulnerabilityPage.detailType'), vuln.type, false) : ''}
|
||||
${vuln.target ? vulnDetailField(vulnT('vulnerabilityPage.detailTarget'), vuln.target, false) : ''}
|
||||
${vulnDetailField(vulnT('vulnerabilityPage.detailConversationId'), vuln.conversation_id, true)}
|
||||
${vuln.task_id ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskId'), vuln.task_id, true) : ''}
|
||||
${vuln.task_queue_id ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskQueueId'), vuln.task_queue_id, true) : ''}
|
||||
${vuln.conversation_tag ? vulnDetailField(vulnT('vulnerabilityPage.detailConversationTag'), vuln.conversation_tag, false) : ''}
|
||||
${vuln.task_tag ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskTag'), vuln.task_tag, false) : ''}
|
||||
</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>` : ''}
|
||||
${vuln.proof ? `<div class="vulnerability-proof"><strong>${escapeHtml(vulnT('vulnerabilityPage.detailProof'))}:</strong><pre>${escapeHtml(vuln.proof)}</pre></div>` : ''}
|
||||
${vuln.impact ? `<div class="vulnerability-impact"><strong>${escapeHtml(vulnT('vulnerabilityPage.detailImpact'))}:</strong> ${escapeHtml(vuln.impact)}</div>` : ''}
|
||||
${vuln.recommendation ? `<div class="vulnerability-recommendation"><strong>${escapeHtml(vulnT('vulnerabilityPage.detailRecommendation'))}:</strong> ${escapeHtml(vuln.recommendation)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
listContainer.innerHTML = html;
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(listContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染分页控件
|
||||
@@ -293,9 +324,9 @@ function renderVulnerabilityPagination() {
|
||||
// 左侧:显示范围信息和每页数量选择器(参考Skills样式)
|
||||
paginationHTML += `
|
||||
<div class="pagination-info">
|
||||
<span>显示 ${start}-${end} / 共 ${total} 条</span>
|
||||
<span>${escapeHtml(vulnT('skillsPage.paginationShow', { start, end, total }))}</span>
|
||||
<label class="pagination-page-size">
|
||||
每页显示
|
||||
${escapeHtml(vulnT('skillsPage.perPageLabel'))}
|
||||
<select id="vulnerability-page-size-pagination" onchange="changeVulnerabilityPageSize()">
|
||||
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
|
||||
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
|
||||
@@ -309,17 +340,20 @@ function renderVulnerabilityPagination() {
|
||||
// 右侧:分页按钮(参考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>
|
||||
<button class="btn-secondary" onclick="loadVulnerabilities(1)" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(vulnT('skillsPage.firstPage'))}</button>
|
||||
<button class="btn-secondary" onclick="loadVulnerabilities(${currentPage - 1})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(vulnT('skillsPage.prevPage'))}</button>
|
||||
<span class="pagination-page">${escapeHtml(vulnT('skillsPage.pageOf', { current: currentPage, total: totalPages || 1 }))}</span>
|
||||
<button class="btn-secondary" onclick="loadVulnerabilities(${currentPage + 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(vulnT('skillsPage.nextPage'))}</button>
|
||||
<button class="btn-secondary" onclick="loadVulnerabilities(${totalPages || 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(vulnT('skillsPage.lastPage'))}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
paginationHTML += '</div>';
|
||||
|
||||
paginationContainer.innerHTML = paginationHTML;
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(paginationContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// 改变每页显示数量
|
||||
@@ -350,7 +384,7 @@ async function changeVulnerabilityPageSize() {
|
||||
// 显示添加漏洞模态框
|
||||
function showAddVulnerabilityModal() {
|
||||
currentVulnerabilityId = null;
|
||||
document.getElementById('vulnerability-modal-title').textContent = (typeof window.t === 'function' ? window.t('vulnerability.addVuln') : '添加漏洞');
|
||||
document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.addVuln');
|
||||
|
||||
// 清空表单
|
||||
document.getElementById('vulnerability-conversation-id').value = '';
|
||||
@@ -373,11 +407,11 @@ function showAddVulnerabilityModal() {
|
||||
async function editVulnerability(id) {
|
||||
try {
|
||||
const response = await apiFetch(`/api/vulnerabilities/${id}`);
|
||||
if (!response.ok) throw new Error('获取漏洞失败');
|
||||
if (!response.ok) throw new Error(vulnT('vulnerabilityPage.fetchFailed'));
|
||||
|
||||
const vuln = await response.json();
|
||||
currentVulnerabilityId = id;
|
||||
document.getElementById('vulnerability-modal-title').textContent = (typeof window.t === 'function' ? window.t('vulnerability.editVuln') : '编辑漏洞');
|
||||
document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.editVuln');
|
||||
|
||||
// 填充表单
|
||||
document.getElementById('vulnerability-conversation-id').value = vuln.conversation_id || '';
|
||||
@@ -396,7 +430,7 @@ async function editVulnerability(id) {
|
||||
document.getElementById('vulnerability-modal').style.display = 'block';
|
||||
} catch (error) {
|
||||
console.error('加载漏洞失败:', error);
|
||||
alert('加载漏洞失败: ' + error.message);
|
||||
alert(vulnT('vulnerability.loadFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,7 +441,7 @@ async function saveVulnerability() {
|
||||
const severity = document.getElementById('vulnerability-severity').value;
|
||||
|
||||
if (!conversationId || !title || !severity) {
|
||||
alert('请填写必填字段:会话ID、标题和严重程度');
|
||||
alert(vulnT('vulnerabilityPage.saveRequiredFields'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -442,7 +476,7 @@ async function saveVulnerability() {
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || '保存失败');
|
||||
throw new Error(error.error || vulnT('vulnerabilityPage.saveFailed'));
|
||||
}
|
||||
|
||||
closeVulnerabilityModal();
|
||||
@@ -452,13 +486,13 @@ async function saveVulnerability() {
|
||||
loadVulnerabilities();
|
||||
} catch (error) {
|
||||
console.error('保存漏洞失败:', error);
|
||||
alert('保存漏洞失败: ' + error.message);
|
||||
alert(vulnT('vulnerabilityPage.saveFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除漏洞
|
||||
async function deleteVulnerability(id) {
|
||||
if (!confirm('确定要删除此漏洞吗?')) {
|
||||
if (!confirm(vulnT('vulnerability.deleteConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -467,7 +501,7 @@ async function deleteVulnerability(id) {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('删除失败');
|
||||
if (!response.ok) throw new Error(vulnT('vulnerabilityPage.deleteFailed'));
|
||||
|
||||
loadVulnerabilityStats();
|
||||
// 删除后,如果当前页没有数据了,回到上一页
|
||||
@@ -480,7 +514,7 @@ async function deleteVulnerability(id) {
|
||||
loadVulnerabilities();
|
||||
} catch (error) {
|
||||
console.error('删除漏洞失败:', error);
|
||||
alert('删除漏洞失败: ' + error.message);
|
||||
alert(vulnT('vulnerabilityPage.deleteFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,68 +597,130 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 将漏洞格式化为Markdown
|
||||
/** 复制详情字段(编码由 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
|
||||
? `<code class="vuln-detail-field-value">${escapeHtml(s)}</code>`
|
||||
: `<span class="vuln-detail-field-value">${escapeHtml(s)}</span>`;
|
||||
const copyBtn = `<button type="button" class="vuln-detail-field__copy" onclick="vulnerabilityCopyEncoded(event, '${enc}')" title="${copyTitle}" aria-label="${copyTitle}">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
</button>`;
|
||||
return `<div class="vuln-detail-field">
|
||||
<div class="vuln-detail-field__label">${escapeHtml(label)}</div>
|
||||
<div class="vuln-detail-field__row">${valueEl}${copyBtn}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 将漏洞格式化为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');
|
||||
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 += `## 基本信息\n\n`;
|
||||
markdown += `- **漏洞ID**: \`${vuln.id}\`\n`;
|
||||
markdown += `- **严重程度**: ${severityText}\n`;
|
||||
markdown += `- **状态**: ${statusText}\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 += `- **类型**: ${vuln.type}\n`;
|
||||
markdown += `- **${L('labelType')}**: ${vuln.type}\n`;
|
||||
}
|
||||
if (vuln.target) {
|
||||
markdown += `- **目标**: ${vuln.target}\n`;
|
||||
markdown += `- **${L('labelTarget')}**: ${vuln.target}\n`;
|
||||
}
|
||||
markdown += `- **会话ID**: \`${vuln.conversation_id}\`\n`;
|
||||
markdown += `- **${L('labelConversationId')}**: \`${vuln.conversation_id}\`\n`;
|
||||
if (vuln.task_id) {
|
||||
markdown += `- **任务ID**: \`${vuln.task_id}\`\n`;
|
||||
markdown += `- **${L('labelTaskId')}**: \`${vuln.task_id}\`\n`;
|
||||
}
|
||||
if (vuln.task_queue_id) {
|
||||
markdown += `- **任务队列ID**: \`${vuln.task_queue_id}\`\n`;
|
||||
markdown += `- **${L('labelTaskQueueId')}**: \`${vuln.task_queue_id}\`\n`;
|
||||
}
|
||||
if (vuln.conversation_tag) {
|
||||
markdown += `- **对话标签**: ${vuln.conversation_tag}\n`;
|
||||
markdown += `- **${L('labelConversationTag')}**: ${vuln.conversation_tag}\n`;
|
||||
}
|
||||
if (vuln.task_tag) {
|
||||
markdown += `- **任务标签**: ${vuln.task_tag}\n`;
|
||||
markdown += `- **${L('labelTaskTag')}**: ${vuln.task_tag}\n`;
|
||||
}
|
||||
markdown += `- **创建时间**: ${createdDate}\n`;
|
||||
markdown += `- **更新时间**: ${updatedDate}\n\n`;
|
||||
markdown += `- **${L('labelCreated')}**: ${createdDate}\n`;
|
||||
markdown += `- **${L('labelUpdated')}**: ${updatedDate}\n\n`;
|
||||
|
||||
if (vuln.description) {
|
||||
markdown += `## 描述\n\n${vuln.description}\n\n`;
|
||||
markdown += `## ${L('headingDescription')}\n\n${vuln.description}\n\n`;
|
||||
}
|
||||
|
||||
if (vuln.proof) {
|
||||
markdown += `## 证明(POC)\n\n\`\`\`\n${vuln.proof}\n\`\`\`\n\n`;
|
||||
markdown += `## ${L('headingProof')}\n\n\`\`\`\n${vuln.proof}\n\`\`\`\n\n`;
|
||||
}
|
||||
|
||||
if (vuln.impact) {
|
||||
markdown += `## 影响\n\n${vuln.impact}\n\n`;
|
||||
markdown += `## ${L('headingImpact')}\n\n${vuln.impact}\n\n`;
|
||||
}
|
||||
|
||||
if (vuln.recommendation) {
|
||||
markdown += `## 修复建议\n\n${vuln.recommendation}\n\n`;
|
||||
markdown += `## ${L('headingRecommendation')}\n\n${vuln.recommendation}\n\n`;
|
||||
}
|
||||
|
||||
return markdown;
|
||||
@@ -660,24 +756,24 @@ async function exportVulnerabilityReports() {
|
||||
params.set('group_by', 'conversation');
|
||||
const response = await apiFetch(`/api/vulnerabilities/export?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: '导出失败' }));
|
||||
throw new Error(error.error || '导出失败');
|
||||
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('当前筛选条件下无可导出漏洞');
|
||||
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(`已开始下载 ${files.length} 份报告`);
|
||||
alert(vulnT('vulnerabilityPage.exportStarted', { count: files.length }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导出漏洞报告失败:', error);
|
||||
alert('导出漏洞报告失败: ' + error.message);
|
||||
alert(vulnT('vulnerabilityPage.exportFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,7 +783,7 @@ async function downloadVulnerabilityAsMarkdown(id, event) {
|
||||
try {
|
||||
const response = await apiFetch(`/api/vulnerabilities/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('获取漏洞失败');
|
||||
throw new Error(vulnT('vulnerabilityPage.fetchFailed'));
|
||||
}
|
||||
|
||||
const vuln = await response.json();
|
||||
@@ -721,8 +817,8 @@ async function downloadVulnerabilityAsMarkdown(id, event) {
|
||||
if (event && event.target) {
|
||||
const button = event.target.closest('button');
|
||||
if (button) {
|
||||
const originalTitle = button.title || '下载Markdown';
|
||||
button.title = '下载成功!';
|
||||
const originalTitle = button.title || vulnT('vulnerabilityPage.downloadMarkdownTitle');
|
||||
button.title = vulnT('vulnerabilityPage.downloadOkTitle');
|
||||
setTimeout(() => {
|
||||
button.title = originalTitle;
|
||||
}, 2000);
|
||||
@@ -730,7 +826,7 @@ async function downloadVulnerabilityAsMarkdown(id, event) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error);
|
||||
alert('下载失败: ' + error.message);
|
||||
alert(vulnT('vulnerabilityPage.downloadFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,5 +836,12 @@ window.onclick = function(event) {
|
||||
if (event.target === modal) {
|
||||
closeVulnerabilityModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('languagechange', function () {
|
||||
const page = document.getElementById('page-vulnerabilities');
|
||||
if (page && page.classList.contains('active')) {
|
||||
loadVulnerabilities();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+12
-12
@@ -1098,16 +1098,16 @@
|
||||
<input type="text" id="vulnerability-conversation-filter" data-i18n="vulnerabilityPage.filterConversation" data-i18n-attr="placeholder" placeholder="筛选特定会话" />
|
||||
</label>
|
||||
<label>
|
||||
<span>任务ID/队列ID</span>
|
||||
<input type="text" id="vulnerability-task-filter" placeholder="筛选任务ID或队列ID" />
|
||||
<span data-i18n="vulnerabilityPage.taskOrQueueId">任务ID/队列ID</span>
|
||||
<input type="text" id="vulnerability-task-filter" data-i18n="vulnerabilityPage.filterTaskOrQueue" data-i18n-attr="placeholder" placeholder="筛选任务ID或队列ID" />
|
||||
</label>
|
||||
<label>
|
||||
<span>对话标签</span>
|
||||
<input type="text" id="vulnerability-conversation-tag-filter" placeholder="筛选对话标签" />
|
||||
<span data-i18n="vulnerabilityPage.conversationTag">对话标签</span>
|
||||
<input type="text" id="vulnerability-conversation-tag-filter" data-i18n="vulnerabilityPage.filterConversationTag" data-i18n-attr="placeholder" placeholder="筛选对话标签" />
|
||||
</label>
|
||||
<label>
|
||||
<span>任务标签</span>
|
||||
<input type="text" id="vulnerability-task-tag-filter" placeholder="筛选任务标签" />
|
||||
<span data-i18n="vulnerabilityPage.taskTag">任务标签</span>
|
||||
<input type="text" id="vulnerability-task-tag-filter" data-i18n="vulnerabilityPage.filterTaskTag" data-i18n-attr="placeholder" placeholder="筛选任务标签" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="vulnerabilityPage.severity">严重程度</span>
|
||||
@@ -1132,7 +1132,7 @@
|
||||
</label>
|
||||
<button class="btn-secondary" onclick="filterVulnerabilities()" data-i18n="vulnerabilityPage.filter">筛选</button>
|
||||
<button class="btn-secondary" onclick="clearVulnerabilityFilters()" data-i18n="vulnerabilityPage.clear">清除</button>
|
||||
<button class="btn-primary" onclick="exportVulnerabilityReports()">导出报告</button>
|
||||
<button class="btn-primary" onclick="exportVulnerabilityReports()" data-i18n="vulnerabilityPage.batchExport">批量导出</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2613,12 +2613,12 @@
|
||||
<input type="text" id="vulnerability-conversation-id" data-i18n="vulnerabilityModal.conversationIdPlaceholder" data-i18n-attr="placeholder" placeholder="输入会话ID" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vulnerability-conversation-tag">对话标签</label>
|
||||
<input type="text" id="vulnerability-conversation-tag" placeholder="如:红队演练A、客户A周报" />
|
||||
<label for="vulnerability-conversation-tag" data-i18n="vulnerabilityModal.conversationTag">对话标签</label>
|
||||
<input type="text" id="vulnerability-conversation-tag" data-i18n="vulnerabilityModal.conversationTagPlaceholder" data-i18n-attr="placeholder" placeholder="如:红队演练A、客户A周报" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vulnerability-task-tag">任务标签</label>
|
||||
<input type="text" id="vulnerability-task-tag" placeholder="如:批量扫描Q2、专项复测" />
|
||||
<label for="vulnerability-task-tag" data-i18n="vulnerabilityModal.taskTag">任务标签</label>
|
||||
<input type="text" id="vulnerability-task-tag" data-i18n="vulnerabilityModal.taskTagPlaceholder" data-i18n-attr="placeholder" placeholder="如:批量扫描Q2、专项复测" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vulnerability-title"><span data-i18n="vulnerabilityModal.title">标题</span> <span style="color: red;">*</span></label>
|
||||
@@ -2838,7 +2838,7 @@
|
||||
<script src="/static/js/terminal.js"></script>
|
||||
<script src="/static/js/knowledge.js"></script>
|
||||
<script src="/static/js/skills.js"></script>
|
||||
<script src="/static/js/vulnerability.js?v=4"></script>
|
||||
<script src="/static/js/vulnerability.js?v=7"></script>
|
||||
<script src="/static/js/webshell.js"></script>
|
||||
<script src="/static/js/chat-files.js"></script>
|
||||
<script src="/static/js/tasks.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user