mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-18 14:04:52 +02:00
1398 lines
56 KiB
JavaScript
1398 lines
56 KiB
JavaScript
// 漏洞管理相关功能
|
||
|
||
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
|
||
};
|
||
|
||
const VULN_STAT_SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
|
||
let vulnerabilityStatCardsBound = false;
|
||
let vulnerabilityFilterPanelBound = false;
|
||
let vulnerabilityFilterOptionsCache = null;
|
||
const VULNERABILITY_ADVANCED_OPEN_KEY = 'vulnerabilityAdvancedFiltersOpen';
|
||
const VULNERABILITY_DATALIST_MAX = 8;
|
||
const VULNERABILITY_DATALIST_MIN_QUERY = 2;
|
||
|
||
const VULN_FILTER_CHIP_FIELDS = [
|
||
{ key: 'id', labelKey: 'vulnerabilityPage.vulnId' },
|
||
{ key: 'status', labelKey: null, format: 'status' },
|
||
{ key: 'severity', labelKey: null, format: 'severity' },
|
||
{ key: 'conversation_id', labelKey: 'vulnerabilityPage.conversationId' },
|
||
{ key: 'task_id', labelKey: 'vulnerabilityPage.taskOrQueueId' },
|
||
{ key: 'conversation_tag', labelKey: 'vulnerabilityPage.conversationTag' },
|
||
{ key: 'task_tag', labelKey: 'vulnerabilityPage.taskTag' }
|
||
];
|
||
|
||
// 从地址栏 #vulnerabilities?conversation_id= / ?task_id= / ?id= 同步筛选(通知/对话菜单/任务管理联动)
|
||
function syncVulnerabilityFiltersFromLocationHash() {
|
||
const hash = window.location.hash.slice(1);
|
||
const hashParts = hash.split('?');
|
||
if (hashParts[0] !== 'vulnerabilities' || hashParts.length < 2) {
|
||
return;
|
||
}
|
||
const params = new URLSearchParams(hashParts.slice(1).join('?'));
|
||
const vid = (params.get('id') || '').trim();
|
||
const cid = (params.get('conversation_id') || '').trim();
|
||
const tid = (params.get('task_id') || '').trim();
|
||
const sev = (params.get('severity') || '').trim();
|
||
const st = (params.get('status') || '').trim();
|
||
const convTag = (params.get('conversation_tag') || '').trim();
|
||
const taskTag = (params.get('task_tag') || '').trim();
|
||
if (!vid && !cid && !tid && !sev && !st && !convTag && !taskTag) {
|
||
return;
|
||
}
|
||
|
||
vulnerabilityFilters.id = '';
|
||
vulnerabilityFilters.conversation_id = '';
|
||
vulnerabilityFilters.task_id = '';
|
||
vulnerabilityFilters.conversation_tag = '';
|
||
vulnerabilityFilters.task_tag = '';
|
||
vulnerabilityFilters.severity = '';
|
||
vulnerabilityFilters.status = '';
|
||
const idEl = document.getElementById('vulnerability-id-filter');
|
||
const convEl = document.getElementById('vulnerability-conversation-filter');
|
||
const taskEl = document.getElementById('vulnerability-task-filter');
|
||
const convTagEl = document.getElementById('vulnerability-conversation-tag-filter');
|
||
const taskTagEl = document.getElementById('vulnerability-task-tag-filter');
|
||
const sevEl = document.getElementById('vulnerability-severity-filter');
|
||
const stEl = document.getElementById('vulnerability-status-filter');
|
||
if (idEl) idEl.value = '';
|
||
if (convEl) convEl.value = '';
|
||
if (taskEl) taskEl.value = '';
|
||
if (convTagEl) convTagEl.value = '';
|
||
if (taskTagEl) taskTagEl.value = '';
|
||
if (sevEl) sevEl.value = '';
|
||
if (stEl) stEl.value = '';
|
||
|
||
if (vid) {
|
||
vulnerabilityFilters.id = vid;
|
||
if (idEl) idEl.value = vid;
|
||
}
|
||
if (cid) {
|
||
vulnerabilityFilters.conversation_id = cid;
|
||
if (convEl) convEl.value = cid;
|
||
}
|
||
if (tid) {
|
||
vulnerabilityFilters.task_id = tid;
|
||
if (taskEl) taskEl.value = tid;
|
||
}
|
||
if (convTag) {
|
||
vulnerabilityFilters.conversation_tag = convTag;
|
||
if (convTagEl) convTagEl.value = convTag;
|
||
}
|
||
if (taskTag) {
|
||
vulnerabilityFilters.task_tag = taskTag;
|
||
if (taskTagEl) taskTagEl.value = taskTag;
|
||
}
|
||
if (sev) {
|
||
vulnerabilityFilters.severity = sev;
|
||
if (sevEl) sevEl.value = sev;
|
||
}
|
||
if (st) {
|
||
vulnerabilityFilters.status = st;
|
||
if (stEl) stEl.value = st;
|
||
}
|
||
vulnerabilityPagination.currentPage = 1;
|
||
if (hasVulnerabilityAdvancedFiltersActive()) {
|
||
setVulnerabilityAdvancedFiltersOpen(true, false);
|
||
}
|
||
syncVulnerabilityStatCardActiveState();
|
||
updateVulnerabilityFilterPanelState();
|
||
renderVulnerabilityFilterChips();
|
||
}
|
||
|
||
// 初始化漏洞管理页面
|
||
function initVulnerabilityPage() {
|
||
// 从localStorage加载每页条数设置
|
||
vulnerabilityPagination.pageSize = getVulnerabilityPageSize();
|
||
initVulnerabilityStatCards();
|
||
initVulnerabilityFilterPanel();
|
||
syncVulnerabilityFiltersFromLocationHash();
|
||
updateVulnerabilityFilterPanelState();
|
||
renderVulnerabilityFilterChips();
|
||
loadVulnerabilityFilterOptions();
|
||
loadVulnerabilityStats();
|
||
loadVulnerabilities();
|
||
}
|
||
|
||
function initVulnerabilityStatCards() {
|
||
if (vulnerabilityStatCardsBound) {
|
||
syncVulnerabilityStatCardActiveState();
|
||
return;
|
||
}
|
||
const root = document.getElementById('vulnerability-stat-cards');
|
||
if (!root) return;
|
||
vulnerabilityStatCardsBound = true;
|
||
root.addEventListener('click', onVulnerabilityStatCardClick);
|
||
root.addEventListener('keydown', onVulnerabilityStatCardKeydown);
|
||
}
|
||
|
||
function onVulnerabilityStatCardClick(ev) {
|
||
const totalCard = ev.target.closest('.stat-card.stat-card-total');
|
||
if (totalCard) {
|
||
applyVulnerabilitySeverityFilter('');
|
||
return;
|
||
}
|
||
const card = ev.target.closest('.stat-card.is-clickable[data-severity]');
|
||
if (!card) return;
|
||
const sev = card.getAttribute('data-severity');
|
||
if (!sev) return;
|
||
const sevEl = document.getElementById('vulnerability-severity-filter');
|
||
const current = sevEl ? sevEl.value : vulnerabilityFilters.severity;
|
||
applyVulnerabilitySeverityFilter(current === sev ? '' : sev);
|
||
}
|
||
|
||
function onVulnerabilityStatCardKeydown(ev) {
|
||
if (ev.key !== 'Enter' && ev.key !== ' ') return;
|
||
const card = ev.target.closest('.stat-card.is-clickable');
|
||
if (!card || !card.contains(ev.target)) return;
|
||
ev.preventDefault();
|
||
card.click();
|
||
}
|
||
|
||
function applyVulnerabilitySeverityFilter(severity) {
|
||
const sevEl = document.getElementById('vulnerability-severity-filter');
|
||
if (sevEl) sevEl.value = severity || '';
|
||
applyVulnerabilityFilters();
|
||
}
|
||
|
||
function readVulnerabilityFiltersFromForm() {
|
||
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 || '';
|
||
return vulnerabilityFilters;
|
||
}
|
||
|
||
function hasVulnerabilityAdvancedFiltersActive() {
|
||
const f = vulnerabilityFilters;
|
||
return Boolean(f.conversation_id || f.task_id || f.conversation_tag || f.task_tag);
|
||
}
|
||
|
||
function hasAnyVulnerabilityFilterActive() {
|
||
const f = vulnerabilityFilters;
|
||
return Boolean(
|
||
f.id || f.conversation_id || f.task_id || f.conversation_tag || f.task_tag || f.severity || f.status
|
||
);
|
||
}
|
||
|
||
function applyVulnerabilityFilters() {
|
||
readVulnerabilityFiltersFromForm();
|
||
vulnerabilityPagination.currentPage = 1;
|
||
syncVulnerabilityStatCardActiveState();
|
||
updateVulnerabilityLocationHashFromFilters();
|
||
updateVulnerabilityFilterPanelState();
|
||
renderVulnerabilityFilterChips();
|
||
loadVulnerabilityStats();
|
||
loadVulnerabilities();
|
||
}
|
||
|
||
function updateVulnerabilityLocationHashFromFilters() {
|
||
const hash = window.location.hash.slice(1);
|
||
const hashParts = hash.split('?');
|
||
if (hashParts[0] !== 'vulnerabilities') return;
|
||
const params = new URLSearchParams(hashParts.length >= 2 ? hashParts.slice(1).join('?') : '');
|
||
const f = vulnerabilityFilters;
|
||
const pairs = [
|
||
['id', f.id],
|
||
['conversation_id', f.conversation_id],
|
||
['task_id', f.task_id],
|
||
['conversation_tag', f.conversation_tag],
|
||
['task_tag', f.task_tag],
|
||
['severity', f.severity],
|
||
['status', f.status]
|
||
];
|
||
pairs.forEach(function (pair) {
|
||
if (pair[1]) {
|
||
params.set(pair[0], pair[1]);
|
||
} else {
|
||
params.delete(pair[0]);
|
||
}
|
||
});
|
||
const qs = params.toString();
|
||
const newHash = qs ? 'vulnerabilities?' + qs : 'vulnerabilities';
|
||
if (window.location.hash.slice(1) === newHash) return;
|
||
const newFull = '#' + newHash;
|
||
if (typeof history.replaceState === 'function') {
|
||
history.replaceState(null, '', window.location.pathname + window.location.search + newFull);
|
||
} else {
|
||
window.location.hash = newHash;
|
||
}
|
||
}
|
||
|
||
function toggleVulnerabilityAdvancedFilters(ev) {
|
||
if (ev) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
}
|
||
const toggleBtn = document.getElementById('vulnerability-advanced-toggle');
|
||
if (!toggleBtn) return;
|
||
const expanded = toggleBtn.getAttribute('aria-expanded') === 'true';
|
||
setVulnerabilityAdvancedFiltersOpen(!expanded, true);
|
||
}
|
||
window.toggleVulnerabilityAdvancedFilters = toggleVulnerabilityAdvancedFilters;
|
||
|
||
function initVulnerabilityFilterPanel() {
|
||
const panel = document.getElementById('vulnerability-filter-panel');
|
||
if (!panel) return;
|
||
|
||
if (vulnerabilityFilterPanelBound) {
|
||
updateVulnerabilityFilterPanelState();
|
||
return;
|
||
}
|
||
vulnerabilityFilterPanelBound = true;
|
||
|
||
let savedOpen = false;
|
||
try {
|
||
savedOpen = localStorage.getItem(VULNERABILITY_ADVANCED_OPEN_KEY) === 'true';
|
||
} catch (e) { /* ignore */ }
|
||
setVulnerabilityAdvancedFiltersOpen(savedOpen, false);
|
||
|
||
const stEl = document.getElementById('vulnerability-status-filter');
|
||
if (stEl) stEl.addEventListener('change', applyVulnerabilityFilters);
|
||
|
||
const textIds = [
|
||
'vulnerability-id-filter',
|
||
'vulnerability-conversation-filter',
|
||
'vulnerability-task-filter',
|
||
'vulnerability-conversation-tag-filter',
|
||
'vulnerability-task-tag-filter'
|
||
];
|
||
textIds.forEach(function (id) {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.addEventListener('keydown', function (ev) {
|
||
if (ev.key === 'Enter') {
|
||
ev.preventDefault();
|
||
applyVulnerabilityFilters();
|
||
}
|
||
});
|
||
});
|
||
|
||
bindVulnerabilityFilterTypeaheads();
|
||
}
|
||
|
||
function setVulnerabilityAdvancedFiltersOpen(open, persist) {
|
||
const toggleBtn = document.getElementById('vulnerability-advanced-toggle');
|
||
const advanced = document.getElementById('vulnerability-advanced-filters');
|
||
const wrap = document.querySelector('#vulnerability-filter-panel .vulnerability-filter-advanced-wrap');
|
||
if (!toggleBtn || !advanced) return;
|
||
toggleBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||
advanced.hidden = !open;
|
||
advanced.classList.toggle('is-open', open);
|
||
if (wrap) wrap.classList.toggle('is-expanded', open);
|
||
if (persist) {
|
||
try {
|
||
localStorage.setItem(VULNERABILITY_ADVANCED_OPEN_KEY, open ? 'true' : 'false');
|
||
} catch (e) { /* ignore */ }
|
||
}
|
||
}
|
||
|
||
function countVulnerabilityAdvancedFiltersActive() {
|
||
const f = vulnerabilityFilters;
|
||
let n = 0;
|
||
if (f.conversation_id) n++;
|
||
if (f.task_id) n++;
|
||
if (f.conversation_tag) n++;
|
||
if (f.task_tag) n++;
|
||
return n;
|
||
}
|
||
|
||
function updateVulnerabilityAdvancedBadge() {
|
||
const badge = document.getElementById('vulnerability-advanced-badge');
|
||
if (!badge) return;
|
||
readVulnerabilityFiltersFromForm();
|
||
const n = countVulnerabilityAdvancedFiltersActive();
|
||
if (n > 0) {
|
||
badge.hidden = false;
|
||
badge.textContent = '(' + n + ')';
|
||
badge.setAttribute('aria-label', String(n));
|
||
} else {
|
||
badge.hidden = true;
|
||
badge.textContent = '';
|
||
badge.removeAttribute('aria-label');
|
||
}
|
||
}
|
||
|
||
function updateVulnerabilityFilterPanelState() {
|
||
const panel = document.getElementById('vulnerability-filter-panel');
|
||
if (!panel) return;
|
||
readVulnerabilityFiltersFromForm();
|
||
panel.classList.toggle('is-filtered', hasAnyVulnerabilityFilterActive());
|
||
updateVulnerabilityAdvancedBadge();
|
||
}
|
||
|
||
function formatVulnerabilityFilterChipValue(key, value) {
|
||
if (key === 'severity') return vulnSeverityLabel(value);
|
||
if (key === 'status') return vulnStatusLabel(value);
|
||
return value;
|
||
}
|
||
|
||
function renderVulnerabilityFilterChips() {
|
||
const wrap = document.getElementById('vulnerability-filter-chips');
|
||
const list = document.getElementById('vulnerability-filter-chips-list');
|
||
if (!wrap || !list) return;
|
||
|
||
readVulnerabilityFiltersFromForm();
|
||
const chips = [];
|
||
VULN_FILTER_CHIP_FIELDS.forEach(function (field) {
|
||
const val = vulnerabilityFilters[field.key];
|
||
if (!val) return;
|
||
const label = field.labelKey ? vulnT(field.labelKey) : '';
|
||
const displayVal = formatVulnerabilityFilterChipValue(field.key, val);
|
||
const text = label ? label + ': ' + displayVal : displayVal;
|
||
chips.push({ key: field.key, text: text });
|
||
});
|
||
|
||
if (!chips.length) {
|
||
wrap.hidden = true;
|
||
list.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
wrap.hidden = false;
|
||
const removeLabel = vulnT('vulnerabilityPage.chipRemove');
|
||
list.innerHTML = chips.map(function (chip) {
|
||
return (
|
||
'<button type="button" class="vulnerability-filter-chip" role="listitem" data-filter-key="' +
|
||
escapeHtml(chip.key) + '" title="' + escapeHtml(removeLabel) + '">' +
|
||
'<span>' + escapeHtml(chip.text) + '</span>' +
|
||
'<span class="vulnerability-filter-chip-remove" aria-hidden="true">×</span>' +
|
||
'</button>'
|
||
);
|
||
}).join('');
|
||
|
||
list.querySelectorAll('.vulnerability-filter-chip').forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
const key = btn.getAttribute('data-filter-key');
|
||
if (key) removeVulnerabilityFilterByKey(key);
|
||
});
|
||
});
|
||
}
|
||
|
||
function removeVulnerabilityFilterByKey(key) {
|
||
const map = {
|
||
id: 'vulnerability-id-filter',
|
||
conversation_id: 'vulnerability-conversation-filter',
|
||
task_id: 'vulnerability-task-filter',
|
||
conversation_tag: 'vulnerability-conversation-tag-filter',
|
||
task_tag: 'vulnerability-task-tag-filter',
|
||
severity: 'vulnerability-severity-filter',
|
||
status: 'vulnerability-status-filter'
|
||
};
|
||
const elId = map[key];
|
||
if (elId) {
|
||
const el = document.getElementById(elId);
|
||
if (el) el.value = '';
|
||
}
|
||
if (Object.prototype.hasOwnProperty.call(vulnerabilityFilters, key)) {
|
||
vulnerabilityFilters[key] = '';
|
||
}
|
||
applyVulnerabilityFilters();
|
||
}
|
||
|
||
async function loadVulnerabilityFilterOptions() {
|
||
if (typeof apiFetch === 'undefined') return;
|
||
try {
|
||
const response = await apiFetch('/api/vulnerabilities/filter-options');
|
||
if (!response.ok) return;
|
||
vulnerabilityFilterOptionsCache = await response.json();
|
||
populateVulnerabilityDatalist(
|
||
'vulnerability-conversation-tag-suggestions',
|
||
vulnerabilityFilterOptionsCache.conversation_tags,
|
||
{ max: 20 }
|
||
);
|
||
populateVulnerabilityDatalist(
|
||
'vulnerability-task-tag-suggestions',
|
||
vulnerabilityFilterOptionsCache.task_tags,
|
||
{ max: 20 }
|
||
);
|
||
clearVulnerabilityDatalist('vulnerability-conversation-suggestions');
|
||
clearVulnerabilityDatalist('vulnerability-task-suggestions');
|
||
} catch (e) {
|
||
console.warn('加载漏洞筛选建议失败', e);
|
||
}
|
||
}
|
||
|
||
function clearVulnerabilityDatalist(listId) {
|
||
const list = document.getElementById(listId);
|
||
if (list) list.innerHTML = '';
|
||
}
|
||
|
||
function populateVulnerabilityDatalist(listId, values, opts) {
|
||
const list = document.getElementById(listId);
|
||
if (!list || !Array.isArray(values)) return;
|
||
const max = (opts && opts.max) || VULNERABILITY_DATALIST_MAX;
|
||
const seen = new Set();
|
||
const unique = [];
|
||
values.forEach(function (v) {
|
||
const s = String(v || '').trim();
|
||
if (!s || seen.has(s)) return;
|
||
seen.add(s);
|
||
unique.push(s);
|
||
if (unique.length >= max) return;
|
||
});
|
||
list.innerHTML = unique.slice(0, max).map(function (v) {
|
||
return '<option value="' + escapeHtml(v) + '"></option>';
|
||
}).join('');
|
||
}
|
||
|
||
function filterVulnerabilitySuggestionPool(pool, query) {
|
||
if (!Array.isArray(pool) || !query) return [];
|
||
const q = query.toLowerCase();
|
||
const out = [];
|
||
for (let i = 0; i < pool.length && out.length < VULNERABILITY_DATALIST_MAX; i++) {
|
||
const s = String(pool[i] || '').trim();
|
||
if (s && s.toLowerCase().indexOf(q) !== -1) out.push(s);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function updateVulnerabilityTypeaheadDatalist(inputId, listId, poolKey) {
|
||
const el = document.getElementById(inputId);
|
||
if (!el || !vulnerabilityFilterOptionsCache) return;
|
||
const q = el.value.trim();
|
||
if (q.length < VULNERABILITY_DATALIST_MIN_QUERY) {
|
||
clearVulnerabilityDatalist(listId);
|
||
return;
|
||
}
|
||
let pool = vulnerabilityFilterOptionsCache[poolKey] || [];
|
||
if (poolKey === 'task_ids') {
|
||
pool = (vulnerabilityFilterOptionsCache.task_ids || []).concat(vulnerabilityFilterOptionsCache.queue_ids || []);
|
||
}
|
||
populateVulnerabilityDatalist(listId, filterVulnerabilitySuggestionPool(pool, q));
|
||
}
|
||
|
||
function bindVulnerabilityFilterTypeaheads() {
|
||
const pairs = [
|
||
{ inputId: 'vulnerability-conversation-filter', listId: 'vulnerability-conversation-suggestions', poolKey: 'conversation_ids' },
|
||
{ inputId: 'vulnerability-task-filter', listId: 'vulnerability-task-suggestions', poolKey: 'task_ids' }
|
||
];
|
||
pairs.forEach(function (pair) {
|
||
const el = document.getElementById(pair.inputId);
|
||
if (!el) return;
|
||
el.addEventListener('input', function () {
|
||
updateVulnerabilityTypeaheadDatalist(pair.inputId, pair.listId, pair.poolKey);
|
||
});
|
||
el.addEventListener('blur', function () {
|
||
setTimeout(function () { clearVulnerabilityDatalist(pair.listId); }, 150);
|
||
});
|
||
});
|
||
|
||
['vulnerability-conversation-tag-filter', 'vulnerability-task-tag-filter'].forEach(function (inputId) {
|
||
const el = document.getElementById(inputId);
|
||
if (!el) return;
|
||
el.addEventListener('focus', function () {
|
||
if (!vulnerabilityFilterOptionsCache) return;
|
||
const listId = inputId === 'vulnerability-conversation-tag-filter'
|
||
? 'vulnerability-conversation-tag-suggestions'
|
||
: 'vulnerability-task-tag-suggestions';
|
||
const key = inputId === 'vulnerability-conversation-tag-filter' ? 'conversation_tags' : 'task_tags';
|
||
const q = el.value.trim();
|
||
if (q.length >= VULNERABILITY_DATALIST_MIN_QUERY) {
|
||
populateVulnerabilityDatalist(listId, filterVulnerabilitySuggestionPool(vulnerabilityFilterOptionsCache[key], q), { max: 20 });
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function syncVulnerabilityStatCardActiveState() {
|
||
const sevEl = document.getElementById('vulnerability-severity-filter');
|
||
const sev = (sevEl && sevEl.value) || vulnerabilityFilters.severity || '';
|
||
const root = document.getElementById('vulnerability-stat-cards');
|
||
if (!root) return;
|
||
root.querySelectorAll('.stat-card.is-clickable').forEach(function (card) {
|
||
if (card.classList.contains('stat-card-total')) {
|
||
card.classList.toggle('is-active', !sev);
|
||
card.setAttribute('aria-pressed', sev ? 'false' : 'true');
|
||
} else {
|
||
const cardSev = card.getAttribute('data-severity');
|
||
const active = Boolean(sev && cardSev === sev);
|
||
card.classList.toggle('is-active', active);
|
||
card.setAttribute('aria-pressed', active ? 'true' : 'false');
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateVulnerabilityStatStackedBar(bySeverity, total) {
|
||
const bar = document.getElementById('stat-stacked-bar');
|
||
if (!bar) return;
|
||
const segs = bar.querySelectorAll('.stat-stacked-seg');
|
||
if (!total) {
|
||
bar.classList.add('is-empty');
|
||
segs.forEach(function (seg) {
|
||
seg.style.flex = '0 0 0';
|
||
seg.style.display = 'none';
|
||
});
|
||
return;
|
||
}
|
||
bar.classList.remove('is-empty');
|
||
segs.forEach(function (seg) {
|
||
const sev = seg.getAttribute('data-sev');
|
||
const count = bySeverity[sev] || 0;
|
||
if (count <= 0) {
|
||
seg.style.display = 'none';
|
||
seg.style.flex = '0 0 0';
|
||
return;
|
||
}
|
||
seg.style.display = '';
|
||
const pct = Math.max((count / total) * 100, 0);
|
||
seg.style.flex = '1 1 ' + pct + '%';
|
||
});
|
||
}
|
||
|
||
// 加载漏洞统计
|
||
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);
|
||
}
|
||
if (vulnerabilityFilters.task_id) {
|
||
params.append('task_id', vulnerabilityFilters.task_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: {}
|
||
};
|
||
}
|
||
|
||
const total = stats.total || 0;
|
||
const bySeverity = stats.by_severity || {};
|
||
|
||
const totalEl = document.getElementById('stat-total');
|
||
if (totalEl) {
|
||
totalEl.textContent = String(total);
|
||
totalEl.classList.toggle('is-zero', total === 0);
|
||
}
|
||
|
||
VULN_STAT_SEVERITIES.forEach(function (sev) {
|
||
const count = bySeverity[sev] || 0;
|
||
const valEl = document.getElementById('stat-' + sev);
|
||
const pctEl = document.getElementById('stat-' + sev + '-pct');
|
||
if (valEl) {
|
||
valEl.textContent = String(count);
|
||
valEl.classList.toggle('is-zero', count === 0);
|
||
}
|
||
if (pctEl) {
|
||
const pct = total > 0 ? Math.round((count / total) * 100) : 0;
|
||
pctEl.textContent = pct + '%';
|
||
pctEl.setAttribute('aria-hidden', total === 0 ? 'true' : 'false');
|
||
}
|
||
});
|
||
|
||
updateVulnerabilityStatStackedBar(bySeverity, total);
|
||
syncVulnerabilityStatCardActiveState();
|
||
}
|
||
|
||
// 加载漏洞列表
|
||
async function loadVulnerabilities(page = null) {
|
||
const listContainer = document.getElementById('vulnerabilities-list');
|
||
listContainer.innerHTML = `<div class="loading-spinner">${escapeHtml(vulnT('vulnerabilityPage.loading'))}</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.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 = `<div class="error-message">${escapeHtml(vulnT('vulnerabilityPage.loadListFailed'))}: ${escapeHtml(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 = 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}">
|
||
<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="${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="${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="${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>
|
||
</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">
|
||
${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>${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);
|
||
}
|
||
|
||
// 如果通过漏洞ID筛选且只返回一条记录,自动展开详情(提升“点击查看”的用户体验)
|
||
if (vulnerabilities.length === 1 && vulnerabilityFilters.id && vulnerabilityFilters.id === vulnerabilities[0].id) {
|
||
setTimeout(() => {
|
||
toggleVulnerabilityDetails(vulnerabilities[0].id);
|
||
}, 300);
|
||
}
|
||
}
|
||
|
||
// 渲染分页控件
|
||
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>${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>
|
||
<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' : ''}>${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);
|
||
}
|
||
}
|
||
|
||
// 改变每页显示数量
|
||
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() {
|
||
applyVulnerabilityFilters();
|
||
}
|
||
|
||
// 清除筛选
|
||
function clearVulnerabilityFilters() {
|
||
const fields = [
|
||
'vulnerability-id-filter',
|
||
'vulnerability-conversation-filter',
|
||
'vulnerability-task-filter',
|
||
'vulnerability-conversation-tag-filter',
|
||
'vulnerability-task-tag-filter',
|
||
'vulnerability-severity-filter',
|
||
'vulnerability-status-filter'
|
||
];
|
||
fields.forEach(function (id) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.value = '';
|
||
});
|
||
|
||
vulnerabilityFilters = {
|
||
id: '',
|
||
conversation_id: '',
|
||
task_id: '',
|
||
conversation_tag: '',
|
||
task_tag: '',
|
||
severity: '',
|
||
status: ''
|
||
};
|
||
|
||
applyVulnerabilityFilters();
|
||
}
|
||
|
||
// 刷新漏洞
|
||
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
|
||
? `<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 = 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')) {
|
||
renderVulnerabilityFilterChips();
|
||
loadVulnerabilities();
|
||
}
|
||
});
|
||
|