diff --git a/web/static/css/vulnerability-list.css b/web/static/css/vulnerability-list.css
new file mode 100644
index 00000000..e1fab152
--- /dev/null
+++ b/web/static/css/vulnerability-list.css
@@ -0,0 +1,139 @@
+.vulnerability-record-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ min-width: 0;
+}
+
+.vulnerability-record-row {
+ display: grid;
+ grid-template-columns: 18px 52px minmax(0, 1fr) auto;
+ align-items: center;
+ column-gap: 12px;
+ min-height: 72px;
+ padding: 12px 16px;
+ border: 1px solid var(--border-color);
+ border-radius: 10px;
+ background: var(--bg-primary);
+}
+
+.vulnerability-record-row:hover {
+ border-color: color-mix(in srgb, var(--accent-color) 35%, var(--border-color));
+}
+
+.vulnerability-record-row:has(.vulnerability-select-record:checked) {
+ background: color-mix(in srgb, var(--accent-color) 5%, var(--bg-primary));
+}
+
+.vulnerability-record-select {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.vulnerability-record-select .vulnerability-select-record {
+ margin: 0;
+}
+
+.vulnerability-record-row > .severity-badge {
+ justify-self: start;
+ white-space: nowrap;
+}
+
+.vulnerability-record-main {
+ display: block;
+ min-width: 0;
+ width: 100%;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: var(--text-primary);
+ font: inherit;
+ text-align: start;
+ cursor: pointer;
+}
+
+.vulnerability-record-title-line {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+.vulnerability-record-title,
+.vulnerability-record-summary {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ line-height: 1.5;
+}
+
+.vulnerability-record-title {
+ min-width: 0;
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.vulnerability-record-summary {
+ margin-top: 3px;
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.vulnerability-record-title-line .vulnerability-case-badge {
+ margin: 0;
+ flex-shrink: 0;
+}
+
+.vulnerability-record-actions {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.vulnerability-record-view {
+ padding: 6px 0;
+ border: 0;
+ background: transparent;
+ color: var(--text-primary);
+ font: inherit;
+ font-size: 13px;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+.vulnerability-record-view:hover,
+.vulnerability-record-main:hover .vulnerability-record-title {
+ color: var(--accent-color);
+}
+
+.vulnerability-record-main:focus-visible,
+.vulnerability-record-view:focus-visible {
+ outline: 2px solid var(--accent-color);
+ outline-offset: 4px;
+}
+
+.vulnerability-inspector-actions .btn-delete {
+ margin-inline-start: auto;
+ color: var(--error-color);
+}
+
+@media (max-width: 600px) {
+ .vulnerability-record-row {
+ grid-template-columns: 18px 46px minmax(0, 1fr);
+ gap: 8px;
+ padding: 12px;
+ }
+ .vulnerability-record-main {
+ padding: 0 !important;
+ min-height: 0 !important;
+ }
+ .vulnerability-record-actions {
+ grid-column: 2 / -1;
+ justify-content: flex-end;
+ }
+}
diff --git a/web/static/i18n/ar-SA.json b/web/static/i18n/ar-SA.json
index dd672dd7..fb21ccc4 100644
--- a/web/static/i18n/ar-SA.json
+++ b/web/static/i18n/ar-SA.json
@@ -2720,6 +2720,7 @@
"errorGeneric": "حدث خطأ ما. يُرجى المحاولة مرة أخرى."
},
"vulnerabilityPage": {
+ "viewFinding": "عرض التفاصيل",
"taskSearch": "البحث في المهام أو قوائم الانتظار أو الوسوم…",
"allTasks": "جميع المهام",
"clearTaskSelection": "مسح مرشح المهام",
diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json
index 8f0d1980..ad94cb37 100644
--- a/web/static/i18n/en-US.json
+++ b/web/static/i18n/en-US.json
@@ -2862,6 +2862,7 @@
"errorGeneric": "Something went wrong. Please try again."
},
"vulnerabilityPage": {
+ "viewFinding": "View details",
"taskSearch": "Search tasks, queues or tags…",
"allTasks": "All tasks",
"clearTaskSelection": "Clear task filter",
diff --git a/web/static/i18n/ru-RU.json b/web/static/i18n/ru-RU.json
index 6edaa883..5bb8a202 100644
--- a/web/static/i18n/ru-RU.json
+++ b/web/static/i18n/ru-RU.json
@@ -2875,6 +2875,7 @@
"errorGeneric": "Что-то пошло не так. Повторите попытку."
},
"vulnerabilityPage": {
+ "viewFinding": "Подробнее",
"taskSearch": "Поиск задач, очередей или тегов…",
"allTasks": "Все задачи",
"clearTaskSelection": "Сбросить фильтр задач",
diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json
index 88bcbc58..8b93aa90 100644
--- a/web/static/i18n/zh-CN.json
+++ b/web/static/i18n/zh-CN.json
@@ -2850,6 +2850,7 @@
"errorGeneric": "操作失败,请稍后重试。"
},
"vulnerabilityPage": {
+ "viewFinding": "查看详情",
"taskSearch": "搜索任务、队列或标签…",
"allTasks": "全部任务",
"clearTaskSelection": "清除任务筛选",
diff --git a/web/static/js/vulnerability-workspace.js b/web/static/js/vulnerability-workspace.js
index 2b041075..a198d4d6 100644
--- a/web/static/js/vulnerability-workspace.js
+++ b/web/static/js/vulnerability-workspace.js
@@ -152,6 +152,7 @@ async function openVulnerabilityInspector(id) {
+
${['overview','evidence','history'].map(tab => `
`).join('')}
diff --git a/web/static/js/vulnerability.js b/web/static/js/vulnerability.js
index f853cb74..460acc7c 100644
--- a/web/static/js/vulnerability.js
+++ b/web/static/js/vulnerability.js
@@ -47,33 +47,6 @@ function getVulnerabilityScrollContainer() {
return page ? page.querySelector('.page-content') : null;
}
-function getExpandedVulnerabilityIds() {
- const ids = [];
- document.querySelectorAll('#vulnerabilities-list .vulnerability-content').forEach(function (el) {
- if (el.style.display !== 'none') {
- const id = (el.id || '').replace(/^content-/, '');
- if (id) ids.push(id);
- }
- });
- return ids;
-}
-
-function restoreExpandedVulnerabilityDetails(expandedIds) {
- if (!expandedIds || !expandedIds.length) return;
- expandedIds.forEach(function (id) {
- const content = document.getElementById('content-' + id);
- const icon = document.getElementById('expand-icon-' + id);
- if (!content || content.style.display !== 'none') return;
- content.style.display = 'block';
- const row = content.closest('.vulnerability-table-detail');
- if (row) row.hidden = false;
- const trigger = document.getElementById('vulnerability-row-toggle-' + id);
- if (trigger) trigger.setAttribute('aria-expanded', 'true');
- if (icon) icon.style.transform = 'rotate(90deg)';
- loadVulnerabilityRelatedFacts(id).catch(function (e) { console.warn(e); });
- });
-}
-
function buildVulnerabilityStatusPicker(vuln) {
const current = vuln.status || 'open';
const id = escapeHtml(vuln.id);
@@ -1304,13 +1277,9 @@ async function loadVulnerabilities(page = null, options = {}) {
const opts = options && typeof options === 'object' ? options : {};
const preserveScroll = !!opts.preserveScroll;
const silent = !!opts.silent;
- let expandedIds = opts.expandedIds;
const scrollEl = preserveScroll ? getVulnerabilityScrollContainer() : null;
const scrollTop = scrollEl ? scrollEl.scrollTop : 0;
- if (expandedIds === undefined && preserveScroll) {
- expandedIds = getExpandedVulnerabilityIds();
- }
const listContainer = document.getElementById('vulnerabilities-list');
const hostOpenStates = preserveScroll
@@ -1375,7 +1344,7 @@ async function loadVulnerabilities(page = null, options = {}) {
}
renderVulnerabilities(vulnerabilities, {
- view, expandedIds: expandedIds || [], hostOpenStates,
+ view, hostOpenStates,
preserveHostSelection: preserveScroll
});
renderVulnerabilityPagination();
@@ -1419,18 +1388,15 @@ function renderVulnerabilities(vulnerabilities, renderOptions) {
const exactMatch = vulnerabilities.length === 1 && vulnerabilityFilters.id === vulnerabilities[0].id
? vulnerabilities[0].id
: null;
- const expandedIds = exactMatch ? [exactMatch] : opts.expandedIds;
- listContainer.innerHTML = renderVulnerabilityTable(vulnerabilities);
+ listContainer.innerHTML = renderVulnerabilityList(vulnerabilities);
initVulnerabilityStatusPickers(listContainer);
initVulnerabilityProjectBindSelects(listContainer);
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(listContainer);
}
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(listContainer);
- if (exactMatch && vulnerabilities[0].summary_only && typeof openVulnerabilityInspector === 'function') {
+ if (exactMatch && typeof openVulnerabilityInspector === 'function') {
openVulnerabilityInspector(exactMatch);
- } else {
- restoreExpandedVulnerabilityDetails(expandedIds);
}
updateVulnerabilityListSummary();
}
@@ -1448,7 +1414,7 @@ function updateVulnerabilityListSummary() {
if (!list || !toolbar) return;
const count = ['host','task'].includes(vulnerabilityView) && list.querySelector('.vulnerability-host-browser')
? new Set(vulnerabilityBrowserRows.map(vuln => vuln.id)).size
- : list.querySelectorAll('.vulnerability-card, .vulnerability-table-item').length;
+ : list.querySelectorAll('.vulnerability-record-row').length;
document.getElementById('vulnerability-page-summary').textContent = count
? vulnT('vulnerabilityPage.pageFindings', { count }) : '';
}
@@ -1547,7 +1513,7 @@ function renderVulnerabilityBrowser(vulnerabilities, opts) {
!Array.from(list.querySelectorAll('[data-host-key]')).some(el => el.dataset.hostKey === vulnerabilityBrowserSelection)) {
vulnerabilityBrowserSelection = '';
}
- renderVulnerabilityBrowserTable(opts.expandedIds);
+ renderVulnerabilityBrowserTable();
if (activeSearch) {
search.focus({ preventScroll: true });
search.setSelectionRange(caret, caret);
@@ -1586,7 +1552,7 @@ function renderVulnerabilityHostRiskDot(items) {
return highest ? `
` : '';
}
-function renderVulnerabilityBrowserTable(expandedIds) {
+function renderVulnerabilityBrowserTable() {
const selection = vulnerabilityBrowserSelection;
const items = getVisibleVulnerabilityBrowserItems();
document.querySelectorAll('#vulnerabilities-list [data-host-key]').forEach(option => {
@@ -1613,100 +1579,41 @@ function renderVulnerabilityBrowserTable(expandedIds) {
).join('') + `
${escapeHtml(vulnT('vulnerabilityPage.groupPending', { count: pending }))}`;
document.getElementById('vulnerability-host-export').disabled = items.length === 0;
const container = document.getElementById('vulnerability-host-table');
- container.innerHTML = renderVulnerabilityTable(items);
+ container.innerHTML = renderVulnerabilityList(items);
initVulnerabilityStatusPickers(container);
initVulnerabilityProjectBindSelects(container);
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(container);
const exact = items.find(vuln => vulnerabilityFilters.id === vuln.id);
- if (exact?.summary_only && typeof openVulnerabilityInspector === 'function') openVulnerabilityInspector(exact.id);
- else restoreExpandedVulnerabilityDetails(exact ? [exact.id] : expandedIds);
+ if (exact && typeof openVulnerabilityInspector === 'function') openVulnerabilityInspector(exact.id);
updateVulnerabilityListSummary();
}
-function renderVulnerabilityTable(items) {
- return items.length ? `
` : `
${escapeHtml(vulnT('vulnerabilityPage.noRecords'))}
`;
+function renderVulnerabilityList(items) {
+ if (!items.length) {
+ return `
${escapeHtml(vulnT('vulnerabilityPage.noRecords'))}
`;
+ }
+ return `
${items.map(renderVulnerabilityListItem).join('')}
`;
}
-function renderVulnerabilityTableItem(vuln) {
+function renderVulnerabilityListItem(vuln) {
const id = escapeAttr(vuln.id);
const jsID = escapeJsStringAttr(vuln.id);
- const task = vuln.task_tag || vuln.task_id || vulnT('vulnerabilityPage.viewNoTask');
- const targetHosts = Array.from(new Map((vuln.targets || []).map(target => [target.host,target])).values());
- const created = new Date(vuln.created_at);
- return `
-
- | ${typeof vulnerabilitySelectionInput === 'function' ? vulnerabilitySelectionInput(vuln) : ''} |
- ${escapeHtml(vulnSeverityLabel(vuln.severity))} |
-
-
- ${escapeHtml((vuln.description || '').slice(0, 160))}
+ const summary = (vuln.description || vuln.target || '').replace(/\s+/g, ' ').trim();
+ return `
+ ${typeof vulnerabilitySelectionInput === 'function' ? vulnerabilitySelectionInput(vuln) : ''}
+ ${escapeHtml(vulnSeverityLabel(vuln.severity))}
+ |
- ${targetHosts.length ? targetHosts.slice(0,2).map(target =>
- `${escapeHtml(target.host)}`).join('')
- : `${escapeHtml(vuln.target_host || vuln.target || '—')}`}
- ${(vuln.targets || []).length > 1 ? `${escapeHtml(vulnT('vulnerabilityPage.targetCount', {count:vuln.targets.length}))}` : ''}
- |
- ${buildVulnerabilityStatusPicker(vuln)} |
- ${escapeHtml(task)} |
- ${escapeHtml(created.toLocaleString(vulnDateLocale(), {
- month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
- }))} |
-
-
-
-
-
- |
-
- ${vuln.summary_only ? '' : `| ${renderVulnerabilityDetailsContent(vuln)} |
`}
- `;
-}
-
-function renderVulnerabilityDetailsContent(vuln) {
- return `
-
- ${vuln.description ? `
${escapeHtml(vuln.description)}
` : ''}
-
- ${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) : ''}
- ${vulnDetailConversationField(vuln.conversation_id)}
- ${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) : ''}
-
-
- ${vulnNarrativeSection(vulnT('vulnerabilityPage.detailPreconditions'), vuln.preconditions)}
- ${vulnNarrativeSection(vulnT('vulnerabilityPage.detailReproductionSteps'), vuln.reproduction_steps)}
- ${vulnNarrativeSection(vulnT('vulnerabilityPage.detailEvidence'), vuln.evidence, { code: true })}
- ${vulnNarrativeSection(vulnT('vulnerabilityPage.detailImpact'), vuln.impact)}
- ${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRecommendation'), vuln.recommendation)}
- ${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRetestNotes'), vuln.retest_notes)}
- ${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRetestLog'), vuln.retest_log, { code: true })}
-
-
-
- `;
+
+ ${summary ? `
${escapeHtml(summary)}` : ''}
+
+
+ ${buildVulnerabilityStatusPicker(vuln)}
+
+
+ `;
}
// 渲染分页控件
@@ -1977,7 +1884,6 @@ async function saveVulnerability() {
if (typeof refreshVulnerabilityInspector === 'function') refreshVulnerabilityInspector(currentVulnerabilityId);
const editedId = currentVulnerabilityId;
const isEdit = !!editedId;
- const expandedIds = isEdit ? getExpandedVulnerabilityIds() : [];
closeVulnerabilityModal();
loadVulnerabilityStats();
@@ -1994,7 +1900,7 @@ async function saveVulnerability() {
return;
}
- await loadVulnerabilities(null, { preserveScroll: true, silent: true, expandedIds: expandedIds });
+ await loadVulnerabilities(null, { preserveScroll: true, silent: true });
} catch (error) {
console.error('保存漏洞失败:', error);
alert(vulnT('vulnerabilityPage.saveFailed') + ': ' + error.message);
@@ -2014,6 +1920,9 @@ async function deleteVulnerability(id) {
if (!response.ok) throw new Error(vulnT('vulnerabilityPage.deleteFailed'));
+ if (typeof vulnerabilityInspectorID !== 'undefined' && vulnerabilityInspectorID === id) {
+ closeAppModal('vulnerability-inspector');
+ }
loadVulnerabilityStats();
await removeVulnerabilityCard(id);
} catch (error) {
@@ -2151,28 +2060,6 @@ async function saveVulnerabilityAlertSubscription() {
}
}
-// 切换漏洞详情展开/折叠
-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)';
- loadVulnerabilityRelatedFacts(id).catch((e) => console.warn(e));
- } else {
- content.style.display = 'none';
- icon.style.transform = 'rotate(0deg)';
- }
- const expanded = content.style.display !== 'none';
- const row = content.closest('.vulnerability-table-detail');
- if (row) row.hidden = !expanded;
- const trigger = document.getElementById('vulnerability-row-toggle-' + id);
- if (trigger) trigger.setAttribute('aria-expanded', String(expanded));
-}
-
/** 展开漏洞详情时加载关联项目事实 */
async function loadVulnerabilityRelatedFacts(vulnId) {
const el = document.getElementById(`vuln-related-facts-${vulnId}`);
@@ -2658,11 +2545,7 @@ async function bindVulnerabilityProject(vulnId, projectId, silent) {
alert(vulnT('vulnerabilityPage.projectBindOk'));
}
loadVulnerabilityStats();
- const expandedIds = getExpandedVulnerabilityIds();
- if (!expandedIds.includes(vulnId)) {
- expandedIds.push(vulnId);
- }
- await loadVulnerabilities(null, { preserveScroll: true, silent: true, expandedIds: expandedIds });
+ await loadVulnerabilities(null, { preserveScroll: true, silent: true });
} catch (error) {
console.error('绑定项目失败:', error);
alert(vulnT('vulnerabilityPage.projectBindFailed') + ': ' + error.message);
diff --git a/web/templates/index.html b/web/templates/index.html
index 7aa83d35..f3069494 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -48,6 +48,7 @@
+
@@ -7096,8 +7097,8 @@
-
-
+
+