From 9e0525abc18fc0f326f29db5ab01b1a0f0e96c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Tue, 9 Jun 2026 20:44:41 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 112 +++++++++++++++--- web/static/i18n/en-US.json | 7 +- web/static/i18n/zh-CN.json | 7 +- web/static/js/dashboard.js | 237 +++++++++++++++++++++++++++---------- web/static/js/settings.js | 14 ++- web/templates/index.html | 54 +++++---- 6 files changed, 322 insertions(+), 109 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 0ffa3138..f7efe3d5 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -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; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 74368758..a20d2210 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -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", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index bc7387d7..bb9b1079 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -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}} 条事实", diff --git a/web/static/js/dashboard.js b/web/static/js/dashboard.js index 8eb3c32d..fdd69ce1 100644 --- a/web/static/js/dashboard.js +++ b/web/static/js/dashboard.js @@ -22,6 +22,7 @@ var dashboardState = { dismissedAlertKey: null, // 当前会话中被用户「×」掉的告警内容指纹(同样的 reasons 不再弹) lastResources: null, // 上一轮关键资源快照,用于判断是否首次有数据 / 智能 CTA recentFeedTab: 'vulns', // 最近漏洞 / 近期事实 Tab + accessTab: 'c2', // 接入概览 Tab:c2 | 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 = + '' + esc(typeTag) + '' + + '' + esc(label) + ''; + 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 = ( - '' + '