mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-27 13:23:10 +02:00
Add files via upload
This commit is contained in:
+161
-19
@@ -13,6 +13,8 @@
|
||||
var DASHBOARD_POLL_INTERVAL_MS = 60 * 1000;
|
||||
var DASHBOARD_STALE_THRESHOLD_MS = 5 * 60 * 1000;
|
||||
var DASHBOARD_STALE_CHECK_INTERVAL_MS = 30 * 1000;
|
||||
var DASHBOARD_SEVERITY_STATUS_FILTER_STORAGE_KEY = 'cyberstrike.dashboard.severityStatusFilter';
|
||||
var DASHBOARD_SEVERITY_STATUS_FILTER_VALUES = ['', 'open', 'confirmed', 'fixed', 'ignored', 'false_positive'];
|
||||
|
||||
var dashboardState = {
|
||||
currentController: null, // 当前正在进行的 fetch 的 AbortController
|
||||
@@ -23,6 +25,7 @@ var dashboardState = {
|
||||
lastResources: null, // 上一轮关键资源快照,用于判断是否首次有数据 / 智能 CTA
|
||||
recentFeedTab: 'vulns', // 最近漏洞 / 近期事实 Tab
|
||||
accessTab: 'c2', // 接入概览 Tab:c2 | webshell
|
||||
severityStatusFilter: null, // 严重程度分布当前状态筛选:'' | open | confirmed | fixed | false_positive | ignored
|
||||
lastProjectSummary: null, // 最近一次项目仪表盘摘要(供 Tab 切换时重绘)
|
||||
};
|
||||
|
||||
@@ -99,6 +102,7 @@ async function refreshDashboard() {
|
||||
};
|
||||
|
||||
try {
|
||||
var selectedSeverityStatus = getDashboardSeverityStatusFilter();
|
||||
// /api/vulnerabilities/stats 只给出 by_severity 与 by_status 两个独立维度,
|
||||
// 无法得到「严重 × 待处理」的交叉计数。这里按四档各拉一次(limit=1,仅取 total),
|
||||
// 用真实的「待处理 × 各严重度」数量驱动告警条 / KPI 副标 / 风险概览卡的加权分,
|
||||
@@ -113,7 +117,7 @@ async function refreshDashboard() {
|
||||
hitlPendingRes, notificationsRes, externalMcpStatsRes,
|
||||
webshellRes,
|
||||
c2ListenersRes, c2SessionsRes, c2TasksRes,
|
||||
projectSummaryRes
|
||||
projectSummaryRes, severityFilteredStatsRes
|
||||
] = await Promise.all([
|
||||
fetchJson('/api/agent-loop/tasks'),
|
||||
fetchJson('/api/vulnerabilities/stats'),
|
||||
@@ -144,7 +148,8 @@ async function refreshDashboard() {
|
||||
fetchJson('/api/c2/listeners'),
|
||||
fetchJson('/api/c2/sessions?limit=500'),
|
||||
fetchJson('/api/c2/tasks?page=1&page_size=1'),
|
||||
fetchJson('/api/projects/dashboard-summary?fact_limit=10')
|
||||
fetchJson('/api/projects/dashboard-summary?fact_limit=10'),
|
||||
selectedSeverityStatus ? fetchJson('/api/vulnerabilities/stats?status=' + encodeURIComponent(selectedSeverityStatus)) : Promise.resolve(null)
|
||||
]);
|
||||
|
||||
// 如果在 await 期间 controller 已被 abort,说明又有新刷新启动了,丢弃本次结果
|
||||
@@ -191,6 +196,9 @@ async function refreshDashboard() {
|
||||
if (vulnTotalEl) vulnTotalEl.textContent = String(vulnRes.total);
|
||||
const bySeverity = vulnRes.by_severity || {};
|
||||
const total = vulnRes.total || 0;
|
||||
const severityDisplayRes = selectedSeverityStatus && severityFilteredStatsRes ? severityFilteredStatsRes : vulnRes;
|
||||
const displayBySeverity = severityDisplayRes.by_severity || {};
|
||||
const displayTotal = typeof severityDisplayRes.total === 'number' ? severityDisplayRes.total : total;
|
||||
criticalCount = bySeverity.critical || 0;
|
||||
highCount = bySeverity.high || 0;
|
||||
mediumCount = bySeverity.medium || 0;
|
||||
@@ -200,17 +208,7 @@ async function refreshDashboard() {
|
||||
openHighCount = pickOpenCount(openHighRes, highCount);
|
||||
openMediumCount = pickOpenCount(openMediumRes, mediumCount);
|
||||
openLowCount = pickOpenCount(openLowRes, lowCount);
|
||||
severityIds.forEach(sev => {
|
||||
const count = bySeverity[sev] || 0;
|
||||
const el = document.getElementById('dashboard-severity-' + sev);
|
||||
if (el) el.textContent = String(count);
|
||||
const pctEl = document.getElementById('dashboard-severity-' + sev + '-pct');
|
||||
if (pctEl) {
|
||||
const pct = total > 0 ? Math.round((count / total) * 100) : 0;
|
||||
pctEl.textContent = pct + '%';
|
||||
}
|
||||
});
|
||||
renderSeverityDonut(bySeverity, total);
|
||||
renderDashboardSeveritySummary(displayBySeverity, displayTotal, severityIds);
|
||||
renderVulnStatusPanel(vulnRes.by_status || {}, total);
|
||||
renderSeverityInsights(
|
||||
{ critical: openCriticalCount, high: openHighCount, medium: openMediumCount, low: openLowCount },
|
||||
@@ -1483,7 +1481,7 @@ function esc(s) {
|
||||
}
|
||||
|
||||
// 漏洞处置状态 + 修复进度面板
|
||||
// byStatus: { open, confirmed, fixed, false_positive }(任一字段缺失视作 0)
|
||||
// byStatus: { open, confirmed, fixed, false_positive, ignored }(任一字段缺失视作 0)
|
||||
// total: 漏洞总数(来自 stats.total)
|
||||
function renderVulnStatusPanel(byStatus, total) {
|
||||
var get = function (k) {
|
||||
@@ -1494,11 +1492,13 @@ function renderVulnStatusPanel(byStatus, total) {
|
||||
var confirmed = get('confirmed');
|
||||
var fixed = get('fixed');
|
||||
var fp = get('false_positive');
|
||||
var ignored = get('ignored');
|
||||
|
||||
setEl('dashboard-status-open', formatNumber(open));
|
||||
setEl('dashboard-status-confirmed', formatNumber(confirmed));
|
||||
setEl('dashboard-status-fixed', formatNumber(fixed));
|
||||
setEl('dashboard-status-fp', formatNumber(fp));
|
||||
setEl('dashboard-status-ignored', formatNumber(ignored));
|
||||
|
||||
// 修复率:fixed / total(不计入 false_positive 时也可,按 total 维持一致)
|
||||
var t = Number(total || 0);
|
||||
@@ -1705,6 +1705,149 @@ function navigateToVulnerabilitiesWithFilter(opts) {
|
||||
}
|
||||
window.navigateToVulnerabilitiesWithFilter = navigateToVulnerabilitiesWithFilter;
|
||||
|
||||
function normalizeDashboardSeverityStatusFilter(status) {
|
||||
status = String(status || '');
|
||||
return DASHBOARD_SEVERITY_STATUS_FILTER_VALUES.indexOf(status) >= 0 ? status : '';
|
||||
}
|
||||
|
||||
function readDashboardSeverityStatusFilterFromStorage() {
|
||||
try {
|
||||
return normalizeDashboardSeverityStatusFilter(window.localStorage.getItem(DASHBOARD_SEVERITY_STATUS_FILTER_STORAGE_KEY));
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function writeDashboardSeverityStatusFilterToStorage(status) {
|
||||
try {
|
||||
if (status) {
|
||||
window.localStorage.setItem(DASHBOARD_SEVERITY_STATUS_FILTER_STORAGE_KEY, status);
|
||||
} else {
|
||||
window.localStorage.removeItem(DASHBOARD_SEVERITY_STATUS_FILTER_STORAGE_KEY);
|
||||
}
|
||||
} catch (_) {
|
||||
// localStorage 可能被浏览器隐私设置禁用,筛选本身仍可在当前页面生效。
|
||||
}
|
||||
}
|
||||
|
||||
function dashboardSeverityStatusFilterLabel(status) {
|
||||
status = normalizeDashboardSeverityStatusFilter(status);
|
||||
if (!status) return dt('dashboard.allStatuses', null, '全部状态');
|
||||
return statusShortLabel(status);
|
||||
}
|
||||
|
||||
function getDashboardSeverityStatusFilter() {
|
||||
if (dashboardState.severityStatusFilter === null) {
|
||||
dashboardState.severityStatusFilter = readDashboardSeverityStatusFilterFromStorage();
|
||||
}
|
||||
syncDashboardSeverityStatusFilterUI();
|
||||
return dashboardState.severityStatusFilter || '';
|
||||
}
|
||||
|
||||
function updateDashboardSeverityStatusFilter(status) {
|
||||
dashboardState.severityStatusFilter = normalizeDashboardSeverityStatusFilter(status);
|
||||
writeDashboardSeverityStatusFilterToStorage(dashboardState.severityStatusFilter);
|
||||
syncDashboardSeverityStatusFilterUI();
|
||||
refreshDashboard();
|
||||
}
|
||||
window.updateDashboardSeverityStatusFilter = updateDashboardSeverityStatusFilter;
|
||||
|
||||
function selectDashboardSeverityStatusFilter(status, ev) {
|
||||
if (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
closeDashboardSeverityStatusFilterMenu();
|
||||
updateDashboardSeverityStatusFilter(status);
|
||||
}
|
||||
window.selectDashboardSeverityStatusFilter = selectDashboardSeverityStatusFilter;
|
||||
|
||||
function syncDashboardSeverityStatusFilterUI() {
|
||||
var status = dashboardState.severityStatusFilter;
|
||||
if (status === null) status = readDashboardSeverityStatusFilterFromStorage();
|
||||
status = normalizeDashboardSeverityStatusFilter(status);
|
||||
|
||||
var root = document.getElementById('dashboard-severity-status-filter');
|
||||
var textEl = document.getElementById('dashboard-severity-status-filter-text');
|
||||
if (root) root.setAttribute('data-value', status);
|
||||
if (textEl) textEl.textContent = dashboardSeverityStatusFilterLabel(status);
|
||||
|
||||
document.querySelectorAll('.dashboard-severity-status-filter-option[data-status]').forEach(function (item) {
|
||||
var active = item.getAttribute('data-status') === status;
|
||||
item.classList.toggle('is-active', active);
|
||||
item.setAttribute('aria-selected', active ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function toggleDashboardSeverityStatusFilterMenu(ev) {
|
||||
if (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
syncDashboardSeverityStatusFilterUI();
|
||||
var root = document.getElementById('dashboard-severity-status-filter');
|
||||
var btn = document.getElementById('dashboard-severity-status-filter-btn');
|
||||
var menu = document.getElementById('dashboard-severity-status-filter-menu');
|
||||
if (!root || !btn || !menu) return;
|
||||
var willOpen = menu.hasAttribute('hidden');
|
||||
menu.toggleAttribute('hidden', !willOpen);
|
||||
root.classList.toggle('is-open', willOpen);
|
||||
btn.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
|
||||
ensureDashboardSeverityStatusFilterOutsideListener();
|
||||
}
|
||||
window.toggleDashboardSeverityStatusFilterMenu = toggleDashboardSeverityStatusFilterMenu;
|
||||
|
||||
function closeDashboardSeverityStatusFilterMenu() {
|
||||
var root = document.getElementById('dashboard-severity-status-filter');
|
||||
var btn = document.getElementById('dashboard-severity-status-filter-btn');
|
||||
var menu = document.getElementById('dashboard-severity-status-filter-menu');
|
||||
if (menu) menu.setAttribute('hidden', '');
|
||||
if (root) root.classList.remove('is-open');
|
||||
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
function ensureDashboardSeverityStatusFilterOutsideListener() {
|
||||
if (dashboardState.severityStatusFilterOutsideBound) return;
|
||||
dashboardState.severityStatusFilterOutsideBound = true;
|
||||
document.addEventListener('click', function (ev) {
|
||||
var root = document.getElementById('dashboard-severity-status-filter');
|
||||
if (root && root.contains(ev.target)) return;
|
||||
closeDashboardSeverityStatusFilterMenu();
|
||||
});
|
||||
document.addEventListener('keydown', function (ev) {
|
||||
if (ev.key === 'Escape') closeDashboardSeverityStatusFilterMenu();
|
||||
});
|
||||
}
|
||||
|
||||
function openDashboardSeverityVulnerabilities() {
|
||||
navigateToVulnerabilitiesWithFilter({ status: getDashboardSeverityStatusFilter() });
|
||||
}
|
||||
window.openDashboardSeverityVulnerabilities = openDashboardSeverityVulnerabilities;
|
||||
|
||||
function navigateToSeverityWithDashboardStatus(severity) {
|
||||
navigateToVulnerabilitiesWithFilter({
|
||||
severity: severity,
|
||||
status: getDashboardSeverityStatusFilter()
|
||||
});
|
||||
}
|
||||
|
||||
function renderDashboardSeveritySummary(bySeverity, total, severityIds) {
|
||||
severityIds = Array.isArray(severityIds) && severityIds.length ? severityIds : ['critical', 'high', 'medium', 'low', 'info'];
|
||||
total = Number(total || 0);
|
||||
bySeverity = bySeverity && typeof bySeverity === 'object' ? bySeverity : {};
|
||||
severityIds.forEach(function (sev) {
|
||||
var count = Number(bySeverity[sev] || 0) || 0;
|
||||
var el = document.getElementById('dashboard-severity-' + sev);
|
||||
if (el) el.textContent = String(count);
|
||||
var pctEl = document.getElementById('dashboard-severity-' + sev + '-pct');
|
||||
if (pctEl) {
|
||||
var pct = total > 0 ? Math.round((count / total) * 100) : 0;
|
||||
pctEl.textContent = pct + '%';
|
||||
}
|
||||
});
|
||||
renderSeverityDonut(bySeverity, total);
|
||||
}
|
||||
|
||||
// 漏洞严重程度分布:半环形(donut)渲染
|
||||
// 几何参数固定,便于配合 viewBox 0 0 560 320 的 SVG 容器
|
||||
// 段间分隔由 gapRad 几何间隙完成,不使用描边,避免浅色/暗色下白边或黑边过重
|
||||
@@ -2201,7 +2344,7 @@ function severityDonutClick(ev) {
|
||||
var id = target.getAttribute('data-severity');
|
||||
if (!id) return;
|
||||
ev.preventDefault();
|
||||
navigateToVulnerabilitiesWithFilter({ severity: id });
|
||||
navigateToSeverityWithDashboardStatus(id);
|
||||
}
|
||||
|
||||
function severityDonutKeydown(ev) {
|
||||
@@ -2210,7 +2353,7 @@ function severityDonutKeydown(ev) {
|
||||
if (!target) return;
|
||||
ev.preventDefault();
|
||||
var id = target.getAttribute('data-severity');
|
||||
if (id) navigateToVulnerabilitiesWithFilter({ severity: id });
|
||||
if (id) navigateToSeverityWithDashboardStatus(id);
|
||||
}
|
||||
|
||||
function severityLegendPointerOver(ev) {
|
||||
@@ -2236,7 +2379,7 @@ function severityLegendClick(ev) {
|
||||
var id = item.getAttribute('data-severity');
|
||||
if (!id) return;
|
||||
ev.preventDefault();
|
||||
navigateToVulnerabilitiesWithFilter({ severity: id });
|
||||
navigateToSeverityWithDashboardStatus(id);
|
||||
}
|
||||
|
||||
function severityLegendKeydown(ev) {
|
||||
@@ -2245,7 +2388,7 @@ function severityLegendKeydown(ev) {
|
||||
if (!item) return;
|
||||
ev.preventDefault();
|
||||
var id = item.getAttribute('data-severity');
|
||||
if (id) navigateToVulnerabilitiesWithFilter({ severity: id });
|
||||
if (id) navigateToSeverityWithDashboardStatus(id);
|
||||
}
|
||||
|
||||
// SVG 半环(背景轨迹)路径
|
||||
@@ -2325,4 +2468,3 @@ document.addEventListener('click', function (ev) {
|
||||
var banner = document.getElementById('dashboard-alert-banner');
|
||||
if (banner) banner.hidden = true;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 项目事实图渲染(Cytoscape + ELK),供项目管理页使用。
|
||||
* 节点采用 SVG 卡片背景(图标 + 多行文字),避免 Cytoscape 原生 label 定位问题。
|
||||
* 节点采用 SVG 卡片背景(左上角图标 + 多行文字),避免 Cytoscape 原生 label 定位问题。
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
@@ -25,11 +25,11 @@
|
||||
|
||||
const CARD_PAD = 14;
|
||||
const CARD_TEXT_PAD_RIGHT = 12;
|
||||
const CARD_ICON = 36;
|
||||
const CARD_ICON_GAP = 12;
|
||||
const CARD_ICON = 24;
|
||||
const CARD_ICON_GAP = 8;
|
||||
const CARD_TEXT_X = CARD_PAD + CARD_ICON + CARD_ICON_GAP;
|
||||
const CARD_MIN_W = 300;
|
||||
const CARD_TARGET_W = 360;
|
||||
const CARD_MIN_W = 340;
|
||||
const CARD_TARGET_W = 380;
|
||||
const CARD_MIN_H = 88;
|
||||
const CARD_MAX_H = 176;
|
||||
const CARD_HEADER_FS = 11;
|
||||
@@ -320,7 +320,7 @@
|
||||
const isTentative = conf === 'tentative';
|
||||
const isDeprecated = conf === 'deprecated';
|
||||
const iconX = CARD_PAD;
|
||||
const iconY = (height - CARD_ICON) / 2;
|
||||
const iconY = CARD_PAD + 1;
|
||||
const headerY = CARD_PAD + CARD_HEADER_FS;
|
||||
const keyY = CARD_PAD + headerLines.length * CARD_HEADER_LH + CARD_SECTION_GAP + CARD_KEY_FS;
|
||||
const summaryY =
|
||||
|
||||
@@ -72,6 +72,17 @@
|
||||
else t.term.writeln('');
|
||||
}
|
||||
|
||||
function writeTermData(tab, data) {
|
||||
if (!tab || !tab.term || data === undefined || data === null) return;
|
||||
try {
|
||||
tab.term.write(data, function () {
|
||||
try { tab.term.scrollToBottom(); } catch (e) {}
|
||||
});
|
||||
} catch (e) {
|
||||
tab.term.write(data);
|
||||
}
|
||||
}
|
||||
|
||||
function writeOutput(tab, text, isError) {
|
||||
var t = tab || getCurrent();
|
||||
if (!t || !t.term || !text) return;
|
||||
@@ -136,18 +147,18 @@
|
||||
// 处理二进制消息和文本消息
|
||||
if (ev.data instanceof ArrayBuffer) {
|
||||
var decoder = new TextDecoder('utf-8');
|
||||
tab.term.write(decoder.decode(ev.data));
|
||||
writeTermData(tab, decoder.decode(ev.data));
|
||||
} else if (ev.data instanceof Blob) {
|
||||
// Blob 类型,需要异步读取
|
||||
var reader = new FileReader();
|
||||
reader.onload = function () {
|
||||
var decoder = new TextDecoder('utf-8');
|
||||
tab.term.write(decoder.decode(reader.result));
|
||||
writeTermData(tab, decoder.decode(reader.result));
|
||||
};
|
||||
reader.readAsArrayBuffer(ev.data);
|
||||
} else {
|
||||
// 字符串类型
|
||||
tab.term.write(ev.data);
|
||||
writeTermData(tab, ev.data);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -183,6 +194,9 @@
|
||||
fontSize: 13,
|
||||
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
||||
lineHeight: 1.2,
|
||||
smoothScrollDuration: 0,
|
||||
scrollSensitivity: 1,
|
||||
fastScrollSensitivity: 5,
|
||||
scrollback: 1000,
|
||||
theme: {
|
||||
background: '#0d1117',
|
||||
@@ -258,6 +272,19 @@
|
||||
|
||||
tab.term = term;
|
||||
tab.fitAddon = fitAddon;
|
||||
if (typeof ResizeObserver !== 'undefined' && fitAddon) {
|
||||
var resizeTimer;
|
||||
tab.resizeObserver = new ResizeObserver(function () {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function () {
|
||||
try {
|
||||
fitAddon.fit();
|
||||
term.scrollToBottom();
|
||||
} catch (e) {}
|
||||
}, 50);
|
||||
});
|
||||
tab.resizeObserver.observe(container);
|
||||
}
|
||||
// 立即建立 WebSocket,让后端 PTY/Shell 马上启动并输出提示符;
|
||||
// 若等到首次按键才 connect,用户会感觉必须先按回车才能输入(实为连接尚未建立)。
|
||||
ensureTerminalWS(tab);
|
||||
@@ -278,9 +305,19 @@
|
||||
});
|
||||
var t = getCurrent();
|
||||
if (t && t.term) {
|
||||
try {
|
||||
if (t.fitAddon) t.fitAddon.fit();
|
||||
t.term.scrollToBottom();
|
||||
} catch (e) {}
|
||||
if (prevId !== id) {
|
||||
requestAnimationFrame(function () {
|
||||
if (currentTabId === id && t.term) t.term.focus();
|
||||
if (currentTabId === id && t.term) {
|
||||
try {
|
||||
if (t.fitAddon) t.fitAddon.fit();
|
||||
t.term.scrollToBottom();
|
||||
} catch (e) {}
|
||||
t.term.focus();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
t.term.focus();
|
||||
@@ -356,9 +393,11 @@
|
||||
var switchToIndex = deletingCurrent ? (idx > 0 ? idx - 1 : 0) : -1;
|
||||
|
||||
var tab = terminals[idx];
|
||||
if (tab.resizeObserver && tab.resizeObserver.disconnect) tab.resizeObserver.disconnect();
|
||||
if (tab.term && tab.term.dispose) tab.term.dispose();
|
||||
tab.term = null;
|
||||
tab.fitAddon = null;
|
||||
tab.resizeObserver = null;
|
||||
terminals.splice(idx, 1);
|
||||
|
||||
var tabDiv = document.querySelector('.terminal-tab[data-tab-id="' + id + '"]');
|
||||
|
||||
@@ -39,7 +39,7 @@ function vulnStatusLabel(code) {
|
||||
return m[code] ? vulnT(m[code]) : code;
|
||||
}
|
||||
|
||||
const VULN_STATUS_CODES = ['open', 'confirmed', 'fixed', 'false_positive', 'ignored'];
|
||||
const VULN_STATUS_CODES = ['open', 'confirmed', 'fixed', 'ignored', 'false_positive'];
|
||||
const VULNERABILITY_REMOVE_ANIM_MS = 200;
|
||||
|
||||
function getVulnerabilityScrollContainer() {
|
||||
|
||||
Reference in New Issue
Block a user