Add files via upload

This commit is contained in:
公明
2026-06-09 20:44:41 +08:00
committed by GitHub
parent 6bacac2e6a
commit 9e0525abc1
6 changed files with 322 additions and 109 deletions
+95 -17
View File
@@ -15287,39 +15287,64 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
.dashboard-kpi-row { grid-template-columns: 1fr; }
}
/* C2 概览:主列内卡片,外层样式复用 .dashboard-grid .dashboard-section */
/* 接入概览(C2 / WebShell Tab:主列内卡片,Tab 样式复用 .dashboard-feed-tabs */
.dashboard-c2-strip {
.dashboard-access-strip {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
@media (max-width: 720px) {
.dashboard-c2-strip { grid-template-columns: 1fr; }
.dashboard-access-strip--webshell {
grid-template-columns: repeat(3, 1fr);
max-width: 100%;
}
.dashboard-c2-stat {
background: linear-gradient(145deg, #f8fafc 0%, #eef2ff 100%);
@media (max-width: 720px) {
.dashboard-access-strip { grid-template-columns: 1fr; }
.dashboard-access-strip--webshell { grid-template-columns: 1fr; }
}
.dashboard-access-stat {
border-radius: 12px;
padding: 14px 16px;
cursor: pointer;
border: 1px solid rgba(99, 102, 241, 0.14);
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.dashboard-c2-stat:hover {
.dashboard-access-stat--c2 {
background: linear-gradient(145deg, #f8fafc 0%, #eef2ff 100%);
border: 1px solid rgba(99, 102, 241, 0.14);
}
.dashboard-access-stat--c2:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.12);
border-color: rgba(99, 102, 241, 0.28);
}
.dashboard-c2-stat:focus-visible {
.dashboard-access-stat--c2:focus-visible {
outline: 2px solid rgba(99, 102, 241, 0.45);
outline-offset: 2px;
}
.dashboard-c2-stat-value {
.dashboard-access-stat--webshell {
background: linear-gradient(145deg, #f8fafc 0%, #ecfdf5 100%);
border: 1px solid rgba(16, 185, 129, 0.16);
}
.dashboard-access-stat--webshell:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(16, 185, 129, 0.12);
border-color: rgba(16, 185, 129, 0.3);
}
.dashboard-access-stat--webshell:focus-visible {
outline: 2px solid rgba(16, 185, 129, 0.45);
outline-offset: 2px;
}
.dashboard-access-stat-value {
font-size: 1.625rem;
font-weight: 800;
color: var(--text-primary);
@@ -15328,7 +15353,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
font-variant-numeric: tabular-nums;
}
.dashboard-c2-stat-label {
.dashboard-access-stat-label {
display: block;
margin-top: 6px;
font-size: 0.8125rem;
@@ -15336,6 +15361,64 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
font-weight: 500;
}
.dashboard-webshell-recent {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 12px;
}
.dashboard-webshell-recent-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border-radius: 8px;
background: rgba(16, 185, 129, 0.05);
border: 1px solid rgba(16, 185, 129, 0.1);
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
min-width: 0;
}
.dashboard-webshell-recent-item:hover {
background: rgba(16, 185, 129, 0.1);
border-color: rgba(16, 185, 129, 0.22);
}
.dashboard-webshell-recent-item:focus-visible {
outline: 2px solid rgba(16, 185, 129, 0.4);
outline-offset: 2px;
}
.dashboard-webshell-recent-type {
flex-shrink: 0;
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
padding: 2px 7px;
border-radius: 4px;
background: rgba(16, 185, 129, 0.14);
color: #047857;
}
.dashboard-webshell-recent-label {
flex: 1;
min-width: 0;
font-size: 0.8125rem;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dashboard-webshell-recent-empty {
margin-top: 10px;
font-size: 0.8125rem;
color: var(--text-secondary);
}
.dashboard-kpi-card {
background: #fff;
border-radius: 14px;
@@ -15834,7 +15917,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
color: #b91c1c;
}
/* 升级版「最近漏洞」空状态:图标 + 标题 + 描述 + 行动按钮 */
/* 升级版「最近漏洞」空状态:标题 + 描述 + 行动按钮 */
.dashboard-recent-vulns-empty.is-rich {
display: flex;
flex-direction: column;
@@ -15845,11 +15928,6 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
gap: 8px;
}
.dashboard-empty-icon {
color: #c7d2fe;
margin-bottom: 4px;
}
.dashboard-empty-title {
font-size: 0.9375rem;
font-weight: 600;
+6 -1
View File
@@ -97,8 +97,13 @@
"clickToViewTasks": "Click to view tasks",
"clickToViewVuln": "Click to view vulnerabilities",
"clickToViewMCP": "Click to view MCP monitor",
"accessOverviewTitle": "Access overview",
"accessTabsAria": "C2 and WebShell",
"c2OverviewTitle": "C2 overview",
"c2GoManage": "Open C2 →",
"webshellGoManage": "Open WebShell →",
"webshellConnections": "Active connections",
"webshellClickConnections": "View connections",
"c2ListenersRunning": "Listeners running",
"c2SessionsOnline": "Sessions online",
"c2TasksPending": "Pending / queued tasks",
@@ -155,7 +160,7 @@
"recentFacts": "Recent facts",
"noVulnYet": "No recent vulnerabilities",
"noFactsYet": "No recent facts",
"noFactsDesc": "In project-bound chats, the agent records targets, findings, and attack chains; new facts appear here",
"noFactsDesc": "In project-bound chats, the agent records targets, findings, and attack chains",
"createFirstProjectBtn": "Create first project",
"factProjectMeta": "{{project}} · {{key}}",
"factsAcrossProjects_one": "{{count}} active project · {{facts}} facts",
+6 -1
View File
@@ -97,8 +97,13 @@
"clickToViewTasks": "点击查看任务管理",
"clickToViewVuln": "点击查看漏洞管理",
"clickToViewMCP": "点击查看 MCP 监控",
"accessOverviewTitle": "接入概览",
"accessTabsAria": "C2 与 WebShell",
"c2OverviewTitle": "C2 概览",
"c2GoManage": "进入 C2 →",
"webshellGoManage": "进入 WebShell →",
"webshellConnections": "活跃连接",
"webshellClickConnections": "查看连接",
"c2ListenersRunning": "运行中监听器",
"c2SessionsOnline": "在线会话",
"c2TasksPending": "待审 / 排队任务",
@@ -155,7 +160,7 @@
"recentFacts": "近期事实",
"noVulnYet": "暂无最近漏洞",
"noFactsYet": "暂无近期事实",
"noFactsDesc": "在绑定项目的对话中,Agent 会自动记录目标、漏洞、攻击链等事实;新事实会出现在这里",
"noFactsDesc": "在绑定项目的对话中,Agent 会自动记录目标、漏洞、攻击链等事实",
"createFirstProjectBtn": "创建第一个项目",
"factProjectMeta": "{{project}} · {{key}}",
"factsAcrossProjects": "{{count}} 个活跃项目 · {{facts}} 条事实",
+176 -61
View File
@@ -22,6 +22,7 @@ var dashboardState = {
dismissedAlertKey: null, // 当前会话中被用户「×」掉的告警内容指纹(同样的 reasons 不再弹)
lastResources: null, // 上一轮关键资源快照,用于判断是否首次有数据 / 智能 CTA
recentFeedTab: 'vulns', // 最近漏洞 / 近期事实 Tab
accessTab: 'c2', // 接入概览 Tabc2 | webshell
lastProjectSummary: null, // 最近一次项目仪表盘摘要(供 Tab 切换时重绘)
};
@@ -60,9 +61,13 @@ async function refreshDashboard() {
hideEl('dashboard-alert-banner');
setRecentVulnsLoading();
setRecentFactsLoading();
['tools', 'skills', 'knowledge', 'roles', 'agents', 'webshell'].forEach(function (k) {
['tools', 'skills', 'knowledge', 'roles', 'agents'].forEach(function (k) {
setEl('dashboard-resource-' + k, '…');
});
setEl('dashboard-webshell-connections', '…');
setEl('dashboard-c2-listeners-running', '…');
setEl('dashboard-c2-sessions-online', '…');
setEl('dashboard-c2-tasks-pending', '…');
var chartPlaceholder = document.getElementById('dashboard-tools-pie-placeholder');
if (chartPlaceholder) { chartPlaceholder.style.removeProperty('display'); chartPlaceholder.textContent = (typeof window.t === 'function' ? window.t('common.loading') : '加载中…'); }
var barChartEl = document.getElementById('dashboard-tools-bar-chart');
@@ -378,18 +383,6 @@ async function refreshDashboard() {
} else {
setEl('dashboard-resource-agents', '-');
}
// WebShell 已建立的连接:/api/webshell/connections 直接返回数组(不带包裹),
// 兼容一下 { connections: [...] } 形式以防后续接口变更
var webshellList = null;
if (Array.isArray(webshellRes)) webshellList = webshellRes;
else if (webshellRes && Array.isArray(webshellRes.connections)) webshellList = webshellRes.connections;
var webshellCount = webshellList ? webshellList.length : null;
if (webshellCount !== null) {
setEl('dashboard-resource-webshell', formatNumber(webshellCount));
} else {
setEl('dashboard-resource-webshell', '-');
}
// 最近漏洞列表
renderRecentVulns(recentVulnsRes);
dashboardState.lastProjectSummary = projectSummaryRes;
@@ -404,8 +397,8 @@ async function refreshDashboard() {
// 「最近事件」内联展示(来自通知摘要,过滤掉已经被仪表盘其他位置覆盖的类型)
renderRecentEvents(notificationsRes);
// C2 概览条(监听器 / 在线会话 / 待处理任务
renderDashboardC2Overview(c2ListenersRes, c2SessionsRes, c2TasksRes);
// 接入概览(C2 + WebShell
renderDashboardAccessOverview(c2ListenersRes, c2SessionsRes, c2TasksRes, webshellRes);
// 关键提醒条:把所有可能的告警源(漏洞/HITL/失败率/MCP健康)合并展示
renderDashboardAlertBanner({
@@ -455,11 +448,11 @@ async function refreshDashboard() {
setKpiSubText('dashboard-kpi-vuln-sub-text', '-');
setKpiSubText('dashboard-kpi-tools-sub-text', '-');
setKpiSubText('dashboard-kpi-rate-sub-text', '-');
['tools', 'skills', 'knowledge', 'roles', 'agents', 'webshell'].forEach(function (k) {
['tools', 'skills', 'knowledge', 'roles', 'agents'].forEach(function (k) {
setEl('dashboard-resource-' + k, '-');
});
var c2secErr = document.getElementById('dashboard-section-c2');
if (c2secErr) c2secErr.hidden = true;
var accessSecErr = document.getElementById('dashboard-section-access');
if (accessSecErr) accessSecErr.hidden = true;
setRecentVulnsError();
setRecentFactsError();
renderDashboardToolsBar(null);
@@ -475,53 +468,181 @@ async function refreshDashboard() {
}
}
/** C2 概览条:依赖 /api/c2/listeners、sessions、tasks;任一路由失败则整块隐藏 */
function renderDashboardC2Overview(listenersRes, sessionsRes, tasksRes) {
var section = document.getElementById('dashboard-section-c2');
/** 接入概览:C2 / WebShell Tab 切换;C2 禁用时仅保留 WebShell Tab */
function renderDashboardAccessOverview(listenersRes, sessionsRes, tasksRes, webshellRes) {
var section = document.getElementById('dashboard-section-access');
if (!section) return;
if (listenersRes === null && sessionsRes === null && tasksRes === null) {
var c2ConfigOn = window.__c2Enabled !== false;
var webshellList = null;
if (Array.isArray(webshellRes)) webshellList = webshellRes;
else if (webshellRes && Array.isArray(webshellRes.connections)) webshellList = webshellRes.connections;
var wsApiOk = webshellRes !== null;
var c2ApiOk = listenersRes !== null || sessionsRes !== null || tasksRes !== null;
var showC2 = c2ConfigOn && c2ApiOk;
var showWs = wsApiOk;
section.dataset.c2Available = showC2 ? '1' : '0';
section.dataset.webshellAvailable = showWs ? '1' : '0';
if (!showC2 && !showWs) {
section.hidden = true;
return;
}
var running = '-';
if (listenersRes && Array.isArray(listenersRes.listeners)) {
running = String(listenersRes.listeners.filter(function (l) {
return (l && (l.status || '').toLowerCase() === 'running');
}).length);
} else if (listenersRes === null) {
running = '-';
} else {
running = '0';
if (showC2) {
var running = '-';
if (listenersRes && Array.isArray(listenersRes.listeners)) {
running = String(listenersRes.listeners.filter(function (l) {
return (l && (l.status || '').toLowerCase() === 'running');
}).length);
} else if (listenersRes === null) {
running = '-';
} else {
running = '0';
}
var online = '-';
if (sessionsRes && Array.isArray(sessionsRes.sessions)) {
online = String(sessionsRes.sessions.filter(function (s) {
if (!s) return false;
var st = (s.status || '').toLowerCase();
return st === 'active' || st === 'sleeping';
}).length);
} else if (sessionsRes === null) {
online = '-';
} else {
online = '0';
}
var pending = '-';
if (tasksRes && typeof tasksRes.pending_queued_count === 'number') {
pending = String(tasksRes.pending_queued_count);
} else if (tasksRes === null) {
pending = '-';
} else {
pending = '0';
}
setEl('dashboard-c2-listeners-running', running);
setEl('dashboard-c2-sessions-online', online);
setEl('dashboard-c2-tasks-pending', pending);
}
var online = '-';
if (sessionsRes && Array.isArray(sessionsRes.sessions)) {
online = String(sessionsRes.sessions.filter(function (s) {
if (!s) return false;
var st = (s.status || '').toLowerCase();
return st === 'active' || st === 'sleeping';
}).length);
} else if (sessionsRes === null) {
online = '-';
} else {
online = '0';
if (showWs) {
var wsCount = webshellList ? webshellList.length : 0;
setEl('dashboard-webshell-connections', formatNumber(wsCount));
renderDashboardWebshellRecent(webshellList || []);
}
var pending = '-';
if (tasksRes && typeof tasksRes.pending_queued_count === 'number') {
pending = String(tasksRes.pending_queued_count);
} else if (tasksRes === null) {
pending = '-';
} else {
pending = '0';
}
setEl('dashboard-c2-listeners-running', running);
setEl('dashboard-c2-sessions-online', online);
setEl('dashboard-c2-tasks-pending', pending);
section.hidden = false;
syncDashboardAccessTabs();
if (typeof applyTranslations === 'function') {
try { applyTranslations(section); } catch (_e) { /* ignore */ }
}
}
/** C2 / WebShell Tab 切换(样式与「最近漏洞 / 近期事实」一致) */
function switchDashboardAccessTab(tab) {
tab = tab === 'webshell' ? 'webshell' : 'c2';
dashboardState.accessTab = tab;
applyDashboardAccessTabUI(tab);
}
function applyDashboardAccessTabUI(tab) {
var tabC2 = document.getElementById('dashboard-access-tab-c2');
var tabWs = document.getElementById('dashboard-access-tab-webshell');
var panelC2 = document.getElementById('dashboard-access-panel-c2');
var panelWs = document.getElementById('dashboard-access-panel-webshell');
if (tabC2) {
tabC2.classList.toggle('is-active', tab === 'c2');
tabC2.setAttribute('aria-selected', tab === 'c2' ? 'true' : 'false');
}
if (tabWs) {
tabWs.classList.toggle('is-active', tab === 'webshell');
tabWs.setAttribute('aria-selected', tab === 'webshell' ? 'true' : 'false');
}
if (panelC2) panelC2.hidden = tab !== 'c2';
if (panelWs) panelWs.hidden = tab !== 'webshell';
updateDashboardAccessViewAll(tab);
}
function updateDashboardAccessViewAll(tab) {
var link = document.getElementById('dashboard-access-view-all');
if (!link) return;
if (tab === 'webshell') {
link.onclick = function () { try { switchPage('webshell'); } catch (_) {} };
link.setAttribute('data-i18n', 'dashboard.webshellGoManage');
link.textContent = dt('dashboard.webshellGoManage', null, '进入 WebShell →');
} else {
link.onclick = function () { try { switchPage('c2-listeners'); } catch (_) {} };
link.setAttribute('data-i18n', 'dashboard.c2GoManage');
link.textContent = dt('dashboard.c2GoManage', null, '进入 C2 →');
}
}
/** 根据可用模块同步 Tab 可见性与默认选中项 */
function syncDashboardAccessTabs() {
var section = document.getElementById('dashboard-section-access');
if (!section || section.hidden) return;
var showC2 = section.dataset.c2Available === '1';
var showWs = section.dataset.webshellAvailable === '1';
var tabNav = document.getElementById('dashboard-access-tabs');
var tabC2 = document.getElementById('dashboard-access-tab-c2');
var tabWs = document.getElementById('dashboard-access-tab-webshell');
if (tabC2) tabC2.hidden = !showC2;
if (tabWs) tabWs.hidden = !showWs;
if (tabNav) tabNav.hidden = false;
var tab = dashboardState.accessTab;
if (tab === 'c2' && !showC2) tab = 'webshell';
if (tab === 'webshell' && !showWs) tab = 'c2';
if (!showC2 && showWs) tab = 'webshell';
if (showC2 && !showWs) tab = 'c2';
dashboardState.accessTab = tab;
applyDashboardAccessTabUI(tab);
}
/** WebShell 接入概览:最近 3 条连接摘要 */
function renderDashboardWebshellRecent(list) {
var container = document.getElementById('dashboard-webshell-recent');
if (!container) return;
container.innerHTML = '';
if (!list || list.length === 0) {
container.hidden = true;
return;
}
var sorted = list.slice().sort(function (a, b) {
var ta = (a && a.createdAt) ? Date.parse(a.createdAt) : 0;
var tb = (b && b.createdAt) ? Date.parse(b.createdAt) : 0;
return tb - ta;
});
var recent = sorted.slice(0, 3);
recent.forEach(function (conn) {
if (!conn) return;
var item = document.createElement('div');
item.className = 'dashboard-webshell-recent-item';
item.setAttribute('role', 'button');
item.setAttribute('tabindex', '0');
var label = (conn.remark || '').trim() || (conn.url || '').trim() || (conn.id || '');
var typeTag = (conn.type || 'shell').toUpperCase();
item.innerHTML =
'<span class="dashboard-webshell-recent-type">' + esc(typeTag) + '</span>' +
'<span class="dashboard-webshell-recent-label" title="' + esc(label) + '">' + esc(label) + '</span>';
var openWs = function () {
try { switchPage('webshell'); } catch (_) {}
};
item.addEventListener('click', openWs);
item.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openWs();
}
});
container.appendChild(item);
});
container.hidden = false;
}
function setEl(id, text) {
const el = document.getElementById(id);
if (el) el.textContent = text;
@@ -1096,12 +1217,9 @@ function renderRecentVulns(res) {
if (list.length === 0) {
if (empty) {
empty.hidden = false;
// 升级版空状态:图标 + 标题 + 描述 + 行动按钮,比纯文本更易引导用户下一步
// 升级版空状态:标题 + 描述 + 行动按钮,比纯文本更易引导用户下一步
empty.classList.add('is-rich');
empty.innerHTML = (
'<span class="dashboard-empty-icon" aria-hidden="true">' +
'<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg>' +
'</span>' +
'<div class="dashboard-empty-title">' + esc(dt('dashboard.noVulnYet', null, '暂无最近漏洞')) + '</div>' +
'<div class="dashboard-empty-desc">' + esc(dt('dashboard.noVulnDesc', null, '此处展示近期漏洞记录;在对话中完成检测后,新结果会出现在这里')) + '</div>' +
'<button type="button" class="dashboard-empty-action" data-action="scan">' +
@@ -1261,16 +1379,13 @@ function renderRecentFacts(res) {
empty.hidden = false;
empty.classList.add('is-rich');
var desc = activeProjects > 0
? dt('dashboard.noFactsDesc', null, '在绑定项目的对话中,Agent 会自动记录目标、漏洞、攻击链等事实;新事实会出现在这里')
? dt('dashboard.noFactsDesc', null, '在绑定项目的对话中,Agent 会自动记录目标、漏洞、攻击链等事实')
: dt('projects.selectOrCreateHint', null, '项目用于跨对话共享「事实黑板」:目标、环境、认证等信息会在绑定项目的对话中自动注入。');
var ctaLabel = activeProjects > 0
? dt('dashboard.goToChat', null, '前往对话')
: dt('dashboard.createFirstProjectBtn', null, '创建第一个项目');
var ctaAction = activeProjects > 0 ? 'chat' : 'project';
empty.innerHTML = (
'<span class="dashboard-empty-icon" aria-hidden="true">' +
'<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><line x1="12" y1="6" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>' +
'</span>' +
'<div class="dashboard-empty-title">' + esc(dt('dashboard.noFactsYet', null, '暂无近期事实')) + '</div>' +
'<div class="dashboard-empty-desc">' + esc(desc) + '</div>' +
'<button type="button" class="dashboard-empty-action" data-action="' + esc(ctaAction) + '">' +
+9 -5
View File
@@ -61,21 +61,25 @@ window.syncC2NavOnceFromServer = async function syncC2NavOnceFromServer() {
}
};
// 根据 C2 是否启用显示主导航 C2 入口与仪表盘 C2 块(与 /api/config 的 c2.enabled 一致)
// 根据 C2 是否启用显示主导航 C2 入口与仪表盘接入概览中的 C2 块(与 /api/config 的 c2.enabled 一致)
function syncC2NavFromConfig(cfg) {
const on = cfg && cfg.c2 && cfg.c2.enabled !== false;
const nav = document.getElementById('nav-c2');
if (nav) {
nav.style.display = on ? '' : 'none';
}
const dash = document.getElementById('dashboard-section-c2');
if (dash) {
const c2Tab = document.getElementById('dashboard-access-tab-c2');
if (c2Tab) {
if (!on) {
dash.hidden = true;
c2Tab.hidden = true;
} else {
dash.removeAttribute('hidden');
c2Tab.removeAttribute('hidden');
}
}
window.__c2Enabled = on;
if (typeof syncDashboardAccessTabs === 'function') {
syncDashboardAccessTabs();
}
}
// 切换设置分类