Add files via upload

This commit is contained in:
公明
2026-05-01 01:28:19 +08:00
committed by GitHub
parent 9b1e493023
commit e0753fd03e
5 changed files with 369 additions and 6 deletions
+220 -3
View File
@@ -14180,19 +14180,236 @@ header {
}
}
/* 漏洞严重程度分布:半环形图(浅色风格) */
/* 漏洞严重程度分布半环形图浅色风格
三列布局[风险概览卡(结论)] [donut(分布图)] [legend(明细)]
左列的风险概览填补原来 donut 左侧的留白"多危险 / 还有几个紧急项 / 多久前更新"这类
结论性信息前置与右列的分类明细互补 */
.dashboard-severity-wrap {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(200px, 260px);
gap: 32px;
grid-template-columns: minmax(160px, 180px) minmax(0, 1fr) minmax(200px, 260px);
gap: 24px;
align-items: center;
}
@media (max-width: 1100px) {
.dashboard-severity-wrap {
grid-template-columns: minmax(0, 1fr) minmax(200px, 260px);
gap: 24px;
}
.dashboard-severity-insights {
grid-column: 1 / -1;
flex-direction: row;
gap: 16px;
}
.dashboard-severity-insights > * {
flex: 1 1 0;
min-width: 0;
}
}
@media (max-width: 820px) {
.dashboard-severity-wrap {
grid-template-columns: minmax(0, 1fr);
gap: 20px;
}
.dashboard-severity-insights {
flex-direction: column;
gap: 12px;
}
}
/* 风险概览卡:竖向堆叠三块小模块(风险等级/待处理/最新时间) */
.dashboard-severity-insights {
display: flex;
flex-direction: column;
gap: 14px;
align-self: stretch;
justify-content: center;
padding: 4px 0;
}
/* —— 风险等级模块 —— */
.dashboard-severity-insight-risk {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px 14px;
border-radius: 12px;
background: #fafbfc;
border: 1px solid rgba(0, 0, 0, 0.05);
transition: background 0.2s, border-color 0.2s;
}
.dashboard-severity-insight-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.dashboard-severity-insight-label {
font-size: 0.75rem;
color: var(--text-secondary);
font-weight: 500;
letter-spacing: 0.02em;
white-space: nowrap;
}
.dashboard-severity-insight-risk-badge {
font-size: 0.8125rem;
font-weight: 700;
padding: 2px 8px;
border-radius: 999px;
letter-spacing: 0.02em;
line-height: 1.4;
white-space: nowrap;
flex-shrink: 0;
}
/* 风险等级着色:安全/低/中/高/极高 */
.dashboard-severity-insight-risk[data-level="safe"] { border-color: rgba(34, 197, 94, 0.20); background: rgba(34, 197, 94, 0.04); }
.dashboard-severity-insight-risk[data-level="safe"] .dashboard-severity-insight-risk-badge { background: rgba(34, 197, 94, 0.12); color: #16a34a; }
.dashboard-severity-insight-risk[data-level="low"] { border-color: rgba(59, 130, 246, 0.22); background: rgba(59, 130, 246, 0.04); }
.dashboard-severity-insight-risk[data-level="low"] .dashboard-severity-insight-risk-badge { background: rgba(59, 130, 246, 0.12); color: #2563eb; }
.dashboard-severity-insight-risk[data-level="medium"] { border-color: rgba(234, 179, 8, 0.25); background: rgba(234, 179, 8, 0.05); }
.dashboard-severity-insight-risk[data-level="medium"] .dashboard-severity-insight-risk-badge { background: rgba(234, 179, 8, 0.15); color: #b45309; }
.dashboard-severity-insight-risk[data-level="high"] { border-color: rgba(249, 115, 22, 0.28); background: rgba(249, 115, 22, 0.05); }
.dashboard-severity-insight-risk[data-level="high"] .dashboard-severity-insight-risk-badge { background: rgba(249, 115, 22, 0.15); color: #c2410c; }
.dashboard-severity-insight-risk[data-level="severe"] { border-color: rgba(239, 68, 68, 0.30); background: rgba(239, 68, 68, 0.06); }
.dashboard-severity-insight-risk[data-level="severe"] .dashboard-severity-insight-risk-badge { background: rgba(239, 68, 68, 0.15); color: #dc2626; }
/* 风险分进度条 */
.dashboard-severity-insight-score-track {
width: 100%;
height: 5px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.dashboard-severity-insight-score-fill {
height: 100%;
border-radius: 999px;
transition: width 0.4s ease, background 0.2s;
background: #94a3b8;
}
.dashboard-severity-insight-risk[data-level="safe"] .dashboard-severity-insight-score-fill { background: linear-gradient(90deg, #4ade80, #16a34a); }
.dashboard-severity-insight-risk[data-level="low"] .dashboard-severity-insight-score-fill { background: linear-gradient(90deg, #60a5fa, #2563eb); }
.dashboard-severity-insight-risk[data-level="medium"] .dashboard-severity-insight-score-fill { background: linear-gradient(90deg, #facc15, #ca8a04); }
.dashboard-severity-insight-risk[data-level="high"] .dashboard-severity-insight-score-fill { background: linear-gradient(90deg, #fb923c, #ea580c); }
.dashboard-severity-insight-risk[data-level="severe"] .dashboard-severity-insight-score-fill { background: linear-gradient(90deg, #f87171, #dc2626); }
.dashboard-severity-insight-score-meta {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.dashboard-severity-insight-score-label {
font-size: 0.6875rem;
color: var(--text-secondary);
}
.dashboard-severity-insight-score-value {
font-size: 0.9375rem;
font-weight: 800;
color: var(--text-primary);
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
/* —— 待处理紧急项:分组(标题 + 两个小徽章) —— */
.dashboard-severity-insight-urgent-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.dashboard-severity-insight-urgent {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.dashboard-severity-insight-urgent-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
padding: 10px 6px;
border-radius: 10px;
background: #fafbfc;
border: 1px solid rgba(0, 0, 0, 0.05);
cursor: pointer;
transition: background 0.15s, border-color 0.15s, transform 0.15s, box-shadow 0.15s;
text-decoration: none;
color: inherit;
min-width: 0;
}
.dashboard-severity-insight-urgent-item:hover {
transform: translateY(-1px);
background: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04);
}
.dashboard-severity-insight-urgent-item:focus-visible {
outline: 2px solid rgba(0, 102, 255, 0.5);
outline-offset: 2px;
}
.dashboard-severity-insight-urgent-item.u-critical { border-color: rgba(239, 68, 68, 0.22); }
.dashboard-severity-insight-urgent-item.u-critical:hover { border-color: rgba(239, 68, 68, 0.40); }
.dashboard-severity-insight-urgent-item.u-high { border-color: rgba(249, 115, 22, 0.22); }
.dashboard-severity-insight-urgent-item.u-high:hover { border-color: rgba(249, 115, 22, 0.40); }
.dashboard-severity-insight-urgent-value {
font-size: 1.25rem;
font-weight: 800;
line-height: 1.1;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
.dashboard-severity-insight-urgent-item.u-critical .dashboard-severity-insight-urgent-value { color: #dc2626; }
.dashboard-severity-insight-urgent-item.u-high .dashboard-severity-insight-urgent-value { color: #ea580c; }
/* 当数量为 0 时,数值变灰,避免在无紧急项时仍然引人注目 */
.dashboard-severity-insight-urgent-item.is-zero .dashboard-severity-insight-urgent-value {
color: var(--text-secondary);
opacity: 0.7;
}
.dashboard-severity-insight-urgent-label {
font-size: 0.6875rem;
color: var(--text-secondary);
font-weight: 500;
letter-spacing: 0.02em;
white-space: nowrap;
}
/* —— 最近发现 —— */
.dashboard-severity-insight-latest {
display: flex;
flex-direction: column;
gap: 2px;
padding: 2px 4px;
}
.dashboard-severity-insight-time {
font-size: 0.875rem;
font-weight: 700;
color: var(--text-primary);
font-variant-numeric: tabular-nums;
}
.dashboard-severity-insight-time.is-empty {
color: var(--text-secondary);
font-weight: 500;
opacity: 0.75;
}
.dashboard-severity-chart {
+9
View File
@@ -95,6 +95,15 @@
"severityLow": "Low",
"severityInfo": "Info",
"totalVulns": "Total vulnerabilities",
"riskLevel": "Risk level",
"riskScore": "Weighted risk score",
"riskSafe": "Safe",
"riskLow": "Low",
"riskMedium": "Medium",
"riskHigh": "High",
"riskSevere": "Severe",
"latestFound": "Latest found",
"noneYet": "None yet",
"runOverview": "Run overview",
"batchQueues": "Batch task queues",
"pending": "Pending",
+9
View File
@@ -95,6 +95,15 @@
"severityLow": "低危",
"severityInfo": "信息",
"totalVulns": "总漏洞数",
"riskLevel": "风险等级",
"riskScore": "加权风险分",
"riskSafe": "安全",
"riskLow": "低",
"riskMedium": "中",
"riskHigh": "高",
"riskSevere": "极高",
"latestFound": "最近发现",
"noneYet": "暂无",
"runOverview": "运行概览",
"batchQueues": "批量任务队列",
"pending": "待执行",
+97 -3
View File
@@ -46,6 +46,7 @@ async function refreshDashboard() {
if (severityTotalEl) severityTotalEl.textContent = '0';
renderSeverityDonut({}, 0);
renderVulnStatusPanel(null, 0);
renderSeverityInsights(null, 0, null);
setDashboardOverviewPlaceholder('…');
setEl('dashboard-kpi-tools-calls', '…');
setEl('dashboard-kpi-success-rate', '…');
@@ -91,15 +92,16 @@ async function refreshDashboard() {
try {
// /api/vulnerabilities/stats 只给出 by_severity 与 by_status 两个独立维度,
// 无法得到「严重 × 待处理」的交叉计数。这里额外拉两次(limit=1,仅取 total),
// 用真实的「待处理严重 / 待处理高危」数量驱动告警条 KPI 副标,避免修复后仍报警。
// 无法得到「严重 × 待处理」的交叉计数。这里按四档各拉一次(limit=1,仅取 total),
// 用真实的「待处理 × 各严重度」数量驱动告警条 / KPI 副标 / 风险概览卡的加权分,
// 避免「全部修复后风险等级仍显示极高」这类语义冲突。
var openVulnQuery = function (sev) {
return fetchJson('/api/vulnerabilities?severity=' + sev + '&status=open&limit=1');
};
const [
tasksRes, vulnRes, batchRes, monitorRes, knowledgeRes, skillsRes,
recentVulnsRes, rolesRes, agentsRes,
openCriticalRes, openHighRes, toolsConfigRes,
openCriticalRes, openHighRes, openMediumRes, openLowRes, toolsConfigRes,
hitlPendingRes, notificationsRes, externalMcpStatsRes,
webshellRes
] = await Promise.all([
@@ -114,6 +116,9 @@ async function refreshDashboard() {
fetchJson('/api/multi-agent/markdown-agents'),
openVulnQuery('critical'),
openVulnQuery('high'),
// 中/低危的「待处理」计数:用于风险概览卡的加权风险分,使其反映"当前未处理风险"
openVulnQuery('medium'),
openVulnQuery('low'),
// 拉取 MCP 工具的「配置总数」用于「能力总览」(区别于 monitor/stats 的「有调用记录」)。
// 仅取 total 字段,page_size=1 减少传输;total 已涵盖内部 + 外部 MCP + 直接注册的工具。
fetchJson('/api/config/tools?page=1&page_size=1'),
@@ -173,17 +178,25 @@ async function refreshDashboard() {
let criticalCount = 0;
let highCount = 0;
let mediumCount = 0;
let lowCount = 0;
let openCriticalCount = 0;
let openHighCount = 0;
let openMediumCount = 0;
let openLowCount = 0;
if (vulnRes && typeof vulnRes.total === 'number') {
if (vulnTotalEl) vulnTotalEl.textContent = String(vulnRes.total);
const bySeverity = vulnRes.by_severity || {};
const total = vulnRes.total || 0;
criticalCount = bySeverity.critical || 0;
highCount = bySeverity.high || 0;
mediumCount = bySeverity.medium || 0;
lowCount = bySeverity.low || 0;
// 优先用专门拉的「待处理」计数;若专项接口失败,则退回 by_severity(宁可误报,不可漏报)
openCriticalCount = pickOpenCount(openCriticalRes, criticalCount);
openHighCount = pickOpenCount(openHighRes, highCount);
openMediumCount = pickOpenCount(openMediumRes, mediumCount);
openLowCount = pickOpenCount(openLowRes, lowCount);
if (severityTotalEl) severityTotalEl.textContent = String(total);
severityIds.forEach(sev => {
const count = bySeverity[sev] || 0;
@@ -197,6 +210,11 @@ async function refreshDashboard() {
});
renderSeverityDonut(bySeverity, total);
renderVulnStatusPanel(vulnRes.by_status || {}, total);
renderSeverityInsights(
{ critical: openCriticalCount, high: openHighCount, medium: openMediumCount, low: openLowCount },
openCriticalCount + openHighCount + openMediumCount + openLowCount,
recentVulnsRes
);
// 漏洞 KPI 副标:徽章/文案均使用「待处理」口径
const critBadge = document.getElementById('dashboard-kpi-vuln-critical-badge');
@@ -225,6 +243,7 @@ async function refreshDashboard() {
});
renderSeverityDonut({}, 0);
renderVulnStatusPanel(null, 0);
renderSeverityInsights(null, 0, null);
hideEl('dashboard-kpi-vuln-critical-badge');
setKpiSubText('dashboard-kpi-vuln-sub-text', '-');
}
@@ -1133,6 +1152,81 @@ function renderVulnStatusPanel(byStatus, total) {
if (confirmedBar) confirmedBar.style.width = confirmedPct.toFixed(2) + '%';
}
// 风险概览卡:基于「待处理(open)」口径的严重度分布计算加权风险分 + 紧急徽章
//
// 为什么用 open 口径而不是全量:
// 如果用全量,全部漏洞修复后 by_severity 不变,风险分仍然居高,
// 但紧急徽章(待严重/待高危)已经归零——视觉上会出现「极高 + 0 待处理」的语义冲突。
// 改成 open 口径后,修复即卸掉风险,风险等级与紧急计数完全同步。
//
// bySeverityOpen: { critical, high, medium, low }(只统计 status=open 的漏洞;info 不计入)
// totalOpen: 待处理漏洞总数(= critical + high + medium + low),仅用于"全无待处理 → safe"判断
// recentVulnsRes: /api/vulnerabilities?limit=5 响应(用于"最近发现"时间,口径是全量,与处置状态无关)
function renderSeverityInsights(bySeverityOpen, totalOpen, recentVulnsRes) {
var riskBox = document.querySelector('.dashboard-severity-insight-risk');
var levelEl = document.getElementById('dashboard-severity-risk-level');
var fillEl = document.getElementById('dashboard-severity-risk-fill');
var scoreEl = document.getElementById('dashboard-severity-risk-score');
var urgentCriticalEl = document.getElementById('dashboard-severity-urgent-critical');
var urgentHighEl = document.getElementById('dashboard-severity-urgent-high');
var urgentCriticalCell = urgentCriticalEl ? urgentCriticalEl.closest('.dashboard-severity-insight-urgent-item') : null;
var urgentHighCell = urgentHighEl ? urgentHighEl.closest('.dashboard-severity-insight-urgent-item') : null;
var latestEl = document.getElementById('dashboard-severity-latest-time');
var sev = bySeverityOpen && typeof bySeverityOpen === 'object' ? bySeverityOpen : {};
var c = Number(sev.critical || 0) || 0;
var h = Number(sev.high || 0) || 0;
var m = Number(sev.medium || 0) || 0;
var l = Number(sev.low || 0) || 0;
// 加权分:严重 ×10、高危 ×5、中危 ×2、低危 ×0.5;信息忽略
// 阈值设计偏"保守"1 个待处理严重就进"中"2 个进"高",≥4 个进"极高"
var score = c * 10 + h * 5 + m * 2 + l * 0.5;
var level, levelKey, levelFallback;
var t = Number(totalOpen || 0) || 0;
if (t === 0 || score === 0) {
level = 'safe'; levelKey = 'dashboard.riskSafe'; levelFallback = '安全';
} else if (score <= 3) {
level = 'low'; levelKey = 'dashboard.riskLow'; levelFallback = '低';
} else if (score <= 10) {
level = 'medium'; levelKey = 'dashboard.riskMedium'; levelFallback = '中';
} else if (score <= 30) {
level = 'high'; levelKey = 'dashboard.riskHigh'; levelFallback = '高';
} else {
level = 'severe'; levelKey = 'dashboard.riskSevere'; levelFallback = '极高';
}
if (riskBox) riskBox.setAttribute('data-level', level);
if (levelEl) levelEl.textContent = dt(levelKey, null, levelFallback);
// 进度条用 0-100 线性映射:>=100 直接满格
var pct = Math.max(0, Math.min(100, score));
if (fillEl) fillEl.style.width = pct.toFixed(1) + '%';
if (scoreEl) {
// 分数保留一位小数(低危 0.5 权重可能出现非整数);整数直接显示
var displayScore = Math.round(score) === score ? String(score) : score.toFixed(1);
scoreEl.textContent = score >= 100 ? displayScore + '+' : displayScore;
}
// 紧急徽章直接复用 open 口径的 critical / high(与加权分完全同源,不会出现"风险极高 + 0 待处理"的矛盾)
if (urgentCriticalEl) urgentCriticalEl.textContent = formatNumber(c);
if (urgentHighEl) urgentHighEl.textContent = formatNumber(h);
if (urgentCriticalCell) urgentCriticalCell.classList.toggle('is-zero', c === 0);
if (urgentHighCell) urgentHighCell.classList.toggle('is-zero', h === 0);
if (latestEl) {
var list = recentVulnsRes && Array.isArray(recentVulnsRes.vulnerabilities) ? recentVulnsRes.vulnerabilities : [];
var latestIso = list.length > 0 ? list[0].created_at : null;
var timeStr = latestIso ? timeAgoStr(latestIso) : '';
if (timeStr) {
latestEl.textContent = timeStr;
latestEl.classList.remove('is-empty');
} else {
latestEl.textContent = dt('dashboard.noneYet', null, '暂无');
latestEl.classList.add('is-empty');
}
}
}
function renderDashboardToolsBar(monitorRes) {
const placeholder = document.getElementById('dashboard-tools-pie-placeholder');
const barChartEl = document.getElementById('dashboard-tools-bar-chart');
+34
View File
@@ -388,6 +388,40 @@
<a class="dashboard-section-link" onclick="switchPage('vulnerabilities')" data-i18n="dashboard.viewAll">查看全部 →</a>
</div>
<div class="dashboard-severity-wrap">
<!-- 风险概览卡:填充 donut 左侧留白;提供「结论性」洞察(风险等级/加权分/待处理计数/最新时间),
与右侧 legend 的「明细」形成互补,避免和下方「最近漏洞」列表重复 -->
<aside class="dashboard-severity-insights" aria-label="风险概览">
<div class="dashboard-severity-insight-risk" data-level="safe">
<div class="dashboard-severity-insight-head">
<span class="dashboard-severity-insight-label" data-i18n="dashboard.riskLevel">风险等级</span>
<span class="dashboard-severity-insight-risk-badge" id="dashboard-severity-risk-level" data-i18n="dashboard.riskSafe">安全</span>
</div>
<div class="dashboard-severity-insight-score-track" aria-hidden="true">
<div class="dashboard-severity-insight-score-fill" id="dashboard-severity-risk-fill" style="width: 0%"></div>
</div>
<div class="dashboard-severity-insight-score-meta">
<span class="dashboard-severity-insight-score-label" data-i18n="dashboard.riskScore">加权风险分</span>
<span class="dashboard-severity-insight-score-value" id="dashboard-severity-risk-score">0</span>
</div>
</div>
<div class="dashboard-severity-insight-urgent-group">
<span class="dashboard-severity-insight-label" data-i18n="dashboard.statusOpen">待处理</span>
<div class="dashboard-severity-insight-urgent">
<div class="dashboard-severity-insight-urgent-item u-critical" role="button" tabindex="0" onclick="switchPage('vulnerabilities')" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); switchPage('vulnerabilities'); }" title="查看待处理严重漏洞">
<span class="dashboard-severity-insight-urgent-value" id="dashboard-severity-urgent-critical">0</span>
<span class="dashboard-severity-insight-urgent-label" data-i18n="dashboard.severityCritical">严重</span>
</div>
<div class="dashboard-severity-insight-urgent-item u-high" role="button" tabindex="0" onclick="switchPage('vulnerabilities')" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); switchPage('vulnerabilities'); }" title="查看待处理高危漏洞">
<span class="dashboard-severity-insight-urgent-value" id="dashboard-severity-urgent-high">0</span>
<span class="dashboard-severity-insight-urgent-label" data-i18n="dashboard.severityHigh">高危</span>
</div>
</div>
</div>
<div class="dashboard-severity-insight-latest">
<span class="dashboard-severity-insight-label" data-i18n="dashboard.latestFound">最近发现</span>
<span class="dashboard-severity-insight-time" id="dashboard-severity-latest-time" data-i18n="dashboard.noneYet">暂无</span>
</div>
</aside>
<div class="dashboard-severity-chart">
<svg class="dashboard-severity-donut" id="dashboard-severity-donut" viewBox="0 0 480 260" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
<g id="dashboard-severity-donut-track"></g>