Add files via upload

This commit is contained in:
公明
2026-05-26 14:24:32 +08:00
committed by GitHub
parent 97834c162e
commit 44ced98863
13 changed files with 2641 additions and 33 deletions
+189 -7
View File
@@ -48,6 +48,7 @@ let currentVulnerabilityId = null;
let vulnerabilityFilters = {
q: '',
id: '',
project_id: '',
conversation_id: '',
task_id: '',
conversation_tag: '',
@@ -77,6 +78,7 @@ const VULN_FILTER_CHIP_FIELDS = [
{ key: 'id', labelKey: 'vulnerabilityPage.vulnId' },
{ key: 'status', labelKey: null, format: 'status' },
{ key: 'severity', labelKey: null, format: 'severity' },
{ key: 'project_id', labelKey: null },
{ key: 'conversation_id', labelKey: 'vulnerabilityPage.conversationId' },
{ key: 'task_id', labelKey: 'vulnerabilityPage.taskOrQueueId' },
{ key: 'conversation_tag', labelKey: 'vulnerabilityPage.conversationTag' },
@@ -98,13 +100,15 @@ function syncVulnerabilityFiltersFromLocationHash() {
const st = (params.get('status') || '').trim();
const convTag = (params.get('conversation_tag') || '').trim();
const taskTag = (params.get('task_tag') || '').trim();
const pid = (params.get('project_id') || '').trim();
const q = (params.get('q') || params.get('search') || '').trim();
if (!vid && !cid && !tid && !sev && !st && !convTag && !taskTag && !q) {
if (!vid && !cid && !tid && !sev && !st && !convTag && !taskTag && !q && !pid) {
return;
}
vulnerabilityFilters.q = '';
vulnerabilityFilters.id = '';
vulnerabilityFilters.project_id = '';
vulnerabilityFilters.conversation_id = '';
vulnerabilityFilters.task_id = '';
vulnerabilityFilters.conversation_tag = '';
@@ -117,6 +121,7 @@ function syncVulnerabilityFiltersFromLocationHash() {
const taskEl = document.getElementById('vulnerability-task-filter');
const convTagEl = document.getElementById('vulnerability-conversation-tag-filter');
const taskTagEl = document.getElementById('vulnerability-task-tag-filter');
const projEl = document.getElementById('vulnerability-project-filter');
const sevEl = document.getElementById('vulnerability-severity-filter');
const stEl = document.getElementById('vulnerability-status-filter');
if (searchEl) searchEl.value = '';
@@ -125,6 +130,7 @@ function syncVulnerabilityFiltersFromLocationHash() {
if (taskEl) taskEl.value = '';
if (convTagEl) convTagEl.value = '';
if (taskTagEl) taskTagEl.value = '';
if (projEl) projEl.value = '';
if (sevEl) sevEl.value = '';
if (stEl) stEl.value = '';
@@ -132,6 +138,10 @@ function syncVulnerabilityFiltersFromLocationHash() {
vulnerabilityFilters.q = q;
if (searchEl) searchEl.value = q;
}
if (pid) {
vulnerabilityFilters.project_id = pid;
if (projEl) projEl.value = pid;
}
if (vid) {
vulnerabilityFilters.id = vid;
if (exactIdEl) exactIdEl.value = vid;
@@ -167,12 +177,13 @@ function syncVulnerabilityFiltersFromLocationHash() {
}
// 初始化漏洞管理页面
function initVulnerabilityPage() {
async function initVulnerabilityPage() {
// 从localStorage加载每页条数设置
vulnerabilityPagination.pageSize = getVulnerabilityPageSize();
initVulnerabilityStatCards();
initVulnerabilityFilterPanel();
syncVulnerabilityFiltersFromLocationHash();
await refreshVulnerabilityProjectFilter();
updateVulnerabilityFilterPanelState();
renderVulnerabilityFilterChips();
loadVulnerabilityFilterOptions();
@@ -224,6 +235,7 @@ function applyVulnerabilitySeverityFilter(severity) {
function readVulnerabilityFiltersFromForm() {
vulnerabilityFilters.q = (document.getElementById('vulnerability-search-filter')?.value || '').trim();
vulnerabilityFilters.id = (document.getElementById('vulnerability-exact-id-filter')?.value || '').trim();
vulnerabilityFilters.project_id = (document.getElementById('vulnerability-project-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();
@@ -241,7 +253,7 @@ function hasVulnerabilityAdvancedFiltersActive() {
function hasAnyVulnerabilityFilterActive() {
const f = vulnerabilityFilters;
return Boolean(
f.q || f.id || f.conversation_id || f.task_id || f.conversation_tag || f.task_tag || f.severity || f.status
f.q || f.id || f.project_id || f.conversation_id || f.task_id || f.conversation_tag || f.task_tag || f.severity || f.status
);
}
@@ -265,6 +277,7 @@ function updateVulnerabilityLocationHashFromFilters() {
const pairs = [
['q', f.q],
['id', f.id],
['project_id', f.project_id],
['conversation_id', f.conversation_id],
['task_id', f.task_id],
['conversation_tag', f.conversation_tag],
@@ -476,6 +489,10 @@ function updateVulnerabilityFilterPanelState() {
function formatVulnerabilityFilterChipValue(key, value) {
if (key === 'severity') return vulnSeverityLabel(value);
if (key === 'status') return vulnStatusLabel(value);
if (key === 'project_id') {
const name = typeof getProjectName === 'function' ? getProjectName(value) : '';
return name && name !== value ? name : value;
}
return value;
}
@@ -489,7 +506,7 @@ function renderVulnerabilityFilterChips() {
VULN_FILTER_CHIP_FIELDS.forEach(function (field) {
const val = vulnerabilityFilters[field.key];
if (!val) return;
const label = field.labelKey ? vulnT(field.labelKey) : '';
const label = field.labelKey ? vulnT(field.labelKey) : (field.key === 'project_id' ? '项目' : '');
const displayVal = formatVulnerabilityFilterChipValue(field.key, val);
const text = label ? label + ': ' + displayVal : displayVal;
chips.push({ key: field.key, text: text });
@@ -529,6 +546,7 @@ function removeVulnerabilityFilterByKey(key) {
task_id: 'vulnerability-task-filter',
conversation_tag: 'vulnerability-conversation-tag-filter',
task_tag: 'vulnerability-task-tag-filter',
project_id: 'vulnerability-project-filter',
severity: 'vulnerability-severity-filter',
status: 'vulnerability-status-filter'
};
@@ -850,6 +868,12 @@ function renderVulnerabilities(vulnerabilities) {
const severityText = vulnSeverityLabel(vuln.severity);
const statusText = vulnStatusLabel(vuln.status);
const createdDate = new Date(vuln.created_at).toLocaleString(vulnDateLocale());
const projectLabel = vuln.project_id
? escapeHtml(typeof getProjectName === 'function' ? getProjectName(vuln.project_id) : vuln.project_id)
: escapeHtml(vulnT('vulnerabilityPage.projectUnbound'));
const projectBadge = vuln.project_id
? `<span class="vulnerability-project-badge" title="${escapeHtml(vuln.project_id)}">${escapeHtml(vulnT('vulnerabilityPage.detailProject'))}: ${projectLabel}</span>`
: `<span class="vulnerability-project-badge vulnerability-project-badge--unbound">${escapeHtml(vulnT('vulnerabilityPage.projectUnbound'))}</span>`;
const dlTitle = escapeHtml(vulnT('vulnerabilityPage.downloadMarkdownTitle'));
const editTitle = escapeHtml(vulnT('common.edit'));
const deleteTitle = escapeHtml(vulnT('common.delete'));
@@ -867,6 +891,7 @@ function renderVulnerabilities(vulnerabilities) {
<div class="vulnerability-meta">
<span class="severity-badge ${severityClass}">${severityText}</span>
<span class="status-badge status-${vuln.status}">${statusText}</span>
${projectBadge}
<span class="vulnerability-date">${createdDate}</span>
</div>
</div>
@@ -895,6 +920,7 @@ function renderVulnerabilities(vulnerabilities) {
${vuln.description ? `<div class="vulnerability-description">${escapeHtml(vuln.description)}</div>` : ''}
<div class="vulnerability-details">
${vulnDetailField(vulnT('vulnerabilityPage.detailVulnId'), vuln.id, true)}
${vulnDetailProjectField(vuln)}
${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)}
@@ -1005,11 +1031,50 @@ async function changeVulnerabilityPageSize() {
await loadVulnerabilities();
}
function buildVulnerabilityProjectOptionsHtml(selectedId) {
const sel = (selectedId || '').trim();
let html = `<option value="">${escapeHtml(vulnT('vulnerabilityModal.projectNone'))}</option>`;
const entries = typeof projectNameById !== 'undefined' ? Object.entries(projectNameById) : [];
entries.sort((a, b) => (a[1] || '').localeCompare(b[1] || '', undefined, { sensitivity: 'base' }));
entries.forEach(([id, name]) => {
if (!id) return;
const selected = id === sel ? ' selected' : '';
html += `<option value="${escapeHtml(id)}"${selected}>${escapeHtml(name || id)}</option>`;
});
if (sel && !entries.some(([id]) => id === sel)) {
html += `<option value="${escapeHtml(sel)}" selected>${escapeHtml(sel)}</option>`;
}
return html;
}
async function populateVulnerabilityModalProjectSelect(selectedId) {
const sel = document.getElementById('vulnerability-project-id');
if (!sel) return;
try {
const res = await apiFetch('/api/projects?limit=200');
if (res.ok) {
const list = await res.json();
if (typeof rebuildProjectNameMap === 'function') {
rebuildProjectNameMap(list);
} else if (typeof projectNameById !== 'undefined') {
(list || []).forEach((p) => { if (p.id) projectNameById[p.id] = p.name || p.id; });
}
}
} catch (e) {
console.warn('加载项目列表失败', e);
}
sel.innerHTML = buildVulnerabilityProjectOptionsHtml(selectedId || '');
sel.value = selectedId || '';
}
// 显示添加漏洞模态框
function showAddVulnerabilityModal() {
async function showAddVulnerabilityModal() {
currentVulnerabilityId = null;
document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.addVuln');
const defaultProject = vulnerabilityFilters.project_id || '';
await populateVulnerabilityModalProjectSelect(defaultProject);
// 清空表单
document.getElementById('vulnerability-conversation-id').value = '';
document.getElementById('vulnerability-conversation-tag').value = '';
@@ -1051,6 +1116,8 @@ async function editVulnerability(id) {
document.getElementById('vulnerability-impact').value = vuln.impact || '';
document.getElementById('vulnerability-recommendation').value = vuln.recommendation || '';
await populateVulnerabilityModalProjectSelect(vuln.project_id || '');
document.getElementById('vulnerability-modal').style.display = 'block';
} catch (error) {
console.error('加载漏洞失败:', error);
@@ -1069,8 +1136,11 @@ async function saveVulnerability() {
return;
}
const projectId = (document.getElementById('vulnerability-project-id')?.value || '').trim();
const data = {
conversation_id: conversationId,
project_id: projectId,
conversation_tag: document.getElementById('vulnerability-conversation-tag').value.trim(),
task_tag: document.getElementById('vulnerability-task-tag').value.trim(),
title: title,
@@ -1090,12 +1160,30 @@ async function saveVulnerability() {
: '/api/vulnerabilities';
const method = currentVulnerabilityId ? 'PUT' : 'POST';
let body = data;
if (currentVulnerabilityId) {
body = {
project_id: projectId,
conversation_tag: data.conversation_tag,
task_tag: data.task_tag,
title: data.title,
description: data.description,
severity: data.severity,
status: data.status,
type: data.type,
target: data.target,
proof: data.proof,
impact: data.impact,
recommendation: data.recommendation,
};
}
const response = await apiFetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
body: JSON.stringify(body)
});
if (!response.ok) {
@@ -1167,6 +1255,7 @@ function clearVulnerabilityFilters() {
'vulnerability-task-filter',
'vulnerability-conversation-tag-filter',
'vulnerability-task-tag-filter',
'vulnerability-project-filter',
'vulnerability-severity-filter',
'vulnerability-status-filter'
];
@@ -1178,6 +1267,7 @@ function clearVulnerabilityFilters() {
vulnerabilityFilters = {
q: '',
id: '',
project_id: '',
conversation_id: '',
task_id: '',
conversation_tag: '',
@@ -1272,6 +1362,21 @@ function vulnerabilityCopyEncoded(evt, encoded) {
}
}
function vulnDetailProjectField(vuln) {
const label = vulnT('vulnerabilityPage.detailProject');
const hint = escapeHtml(vulnT('vulnerabilityPage.projectBindHint'));
return `<div class="vuln-detail-field">
<div class="vuln-detail-field__label">${escapeHtml(label)}</div>
<div class="vuln-detail-field__row">
<select class="vuln-detail-field-select vulnerability-project-bind-select" data-vuln-id="${escapeHtml(vuln.id)}"
onchange="bindVulnerabilityProject(this.dataset.vulnId, this.value, true)" onclick="event.stopPropagation();"
title="${hint}" aria-label="${escapeHtml(label)}">
${buildVulnerabilityProjectOptionsHtml(vuln.project_id || '')}
</select>
</div>
</div>`;
}
function vulnDetailField(label, value, asCode) {
if (value === undefined || value === null || String(value) === '') {
return '';
@@ -1352,7 +1457,7 @@ function buildVulnerabilityFilterParams() {
if (vulnerabilityFilters.q) {
params.append('q', vulnerabilityFilters.q);
}
const keys = ['id', 'conversation_id', 'task_id', 'conversation_tag', 'task_tag', 'severity', 'status'];
const keys = ['id', 'project_id', 'conversation_id', 'task_id', 'conversation_tag', 'task_tag', 'severity', 'status'];
keys.forEach(function (k) {
if (vulnerabilityFilters[k]) {
params.append(k, vulnerabilityFilters[k]);
@@ -1470,3 +1575,80 @@ document.addEventListener('languagechange', function () {
}
});
async function bindVulnerabilityProject(vulnId, projectId, silent) {
if (!vulnId) return;
try {
const response = await apiFetch(`/api/vulnerabilities/${encodeURIComponent(vulnId)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project_id: projectId || '' }),
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || vulnT('vulnerabilityPage.projectBindFailed'));
}
if (!silent) {
alert(vulnT('vulnerabilityPage.projectBindOk'));
}
loadVulnerabilityStats();
loadVulnerabilities();
} catch (error) {
console.error('绑定项目失败:', error);
alert(vulnT('vulnerabilityPage.projectBindFailed') + ': ' + error.message);
loadVulnerabilities();
}
}
async function refreshVulnerabilityProjectFilter() {
const sel = document.getElementById('vulnerability-project-filter');
if (!sel) return;
try {
const res = await apiFetch('/api/projects?limit=200');
if (!res.ok) return;
const list = await res.json();
if (typeof rebuildProjectNameMap === 'function') {
rebuildProjectNameMap(list);
} else if (typeof projectNameById !== 'undefined') {
list.forEach((p) => { if (p.id) projectNameById[p.id] = p.name || p.id; });
}
const cur = vulnerabilityFilters.project_id || sel.value || '';
let html = '<option value="">全部项目</option>';
(list || []).forEach((p) => {
if (!p.id) return;
const selected = p.id === cur ? ' selected' : '';
const arch = p.status === 'archived' ? ' [归档]' : '';
html += `<option value="${escapeHtml(p.id)}"${selected}>${escapeHtml(p.name || p.id)}${arch}</option>`;
});
sel.innerHTML = html;
if (cur) sel.value = cur;
const modalSel = document.getElementById('vulnerability-project-id');
if (modalSel && document.getElementById('vulnerability-modal')?.style.display === 'block') {
const modalCur = modalSel.value || '';
modalSel.innerHTML = buildVulnerabilityProjectOptionsHtml(modalCur);
modalSel.value = modalCur;
}
} catch (e) {
console.warn('加载项目筛选列表失败', e);
}
}
function setVulnerabilityProjectFilter(projectId) {
vulnerabilityFilters.project_id = projectId || '';
const sel = document.getElementById('vulnerability-project-filter');
if (sel) sel.value = projectId || '';
applyVulnerabilityFilters();
}
function setVulnerabilityIdFilter(vulnId) {
vulnerabilityFilters.id = vulnId || '';
const el = document.getElementById('vulnerability-exact-id-filter');
if (el) el.value = vulnId || '';
applyVulnerabilityFilters();
}
window.refreshVulnerabilityProjectFilter = refreshVulnerabilityProjectFilter;
window.setVulnerabilityProjectFilter = setVulnerabilityProjectFilter;
window.setVulnerabilityIdFilter = setVulnerabilityIdFilter;
window.bindVulnerabilityProject = bindVulnerabilityProject;
window.buildVulnerabilityProjectOptionsHtml = buildVulnerabilityProjectOptionsHtml;