1208 lines
41 KiB
JavaScript
1208 lines
41 KiB
JavaScript
// threats.js - 威胁告警页面功能实现
|
|
|
|
// ==================== 工具函数 ====================
|
|
|
|
// 带超时的 fetch
|
|
async function fetchWithTimeout(url, options = {}, timeout = 10000) {
|
|
const controller = new AbortController();
|
|
const id = setTimeout(() => controller.abort(), timeout);
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
signal: controller.signal
|
|
});
|
|
clearTimeout(id);
|
|
return response;
|
|
} catch (error) {
|
|
clearTimeout(id);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 带重试的 fetch
|
|
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
|
|
let lastError;
|
|
|
|
for (let i = 0; i < maxRetries; i++) {
|
|
try {
|
|
const response = await fetchWithTimeout(url, options, 10000);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
return await response.json();
|
|
} catch (error) {
|
|
lastError = error;
|
|
console.warn(`请求失败 (${i + 1}/${maxRetries}):`, url, error.message);
|
|
|
|
// 等待后重试(指数退避)
|
|
if (i < maxRetries - 1) {
|
|
const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError;
|
|
}
|
|
|
|
// 并发控制执行
|
|
async function runWithConcurrency(tasks, concurrencyLimit = 5) {
|
|
const results = [];
|
|
const executing = [];
|
|
|
|
for (const task of tasks) {
|
|
const p = task().then(result => {
|
|
executing.splice(executing.indexOf(p), 1);
|
|
return result;
|
|
});
|
|
results.push(p);
|
|
executing.push(p);
|
|
|
|
if (executing.length >= concurrencyLimit) {
|
|
await Promise.race(executing);
|
|
}
|
|
}
|
|
|
|
return Promise.all(results);
|
|
}
|
|
|
|
// ==================== 缓存管理 ====================
|
|
|
|
// 威胁查询缓存(批量查询结果缓存)
|
|
const threatCache = {
|
|
cache: new Map(),
|
|
ttl: 10 * 60 * 1000, // 10 分钟
|
|
lastQueryTime: 0,
|
|
queryInterval: 30 * 1000, // 30 秒内不重复查询
|
|
|
|
// 检查是否可以查询(避免频繁查询)
|
|
canQuery() {
|
|
return Date.now() - this.lastQueryTime >= this.queryInterval;
|
|
},
|
|
|
|
// 获取缓存的查询结果
|
|
getCachedResult() {
|
|
if (this.cache.size === 0) return null;
|
|
|
|
// 检查是否过期
|
|
if (Date.now() - this.lastQueryTime > this.ttl) {
|
|
console.log('缓存已过期,清除缓存');
|
|
this.cache.clear();
|
|
return null;
|
|
}
|
|
|
|
// 转换为数组返回
|
|
const results = [];
|
|
this.cache.forEach((value, domain) => {
|
|
results.push({
|
|
domain,
|
|
isThreat: value.isThreat,
|
|
data: value.data
|
|
});
|
|
});
|
|
|
|
console.log('使用缓存结果,记录数:', results.length);
|
|
return results;
|
|
},
|
|
|
|
// 缓存批量查询结果
|
|
cacheResults(results) {
|
|
this.cache.clear();
|
|
results.forEach(result => {
|
|
if (result.isThreat && result.data) {
|
|
this.cache.set(result.domain, {
|
|
isThreat: true,
|
|
data: result.data,
|
|
timestamp: Date.now()
|
|
});
|
|
} else {
|
|
this.cache.set(result.domain, {
|
|
isThreat: false,
|
|
timestamp: Date.now()
|
|
});
|
|
}
|
|
});
|
|
this.lastQueryTime = Date.now();
|
|
console.log('缓存批量查询结果,记录数:', this.cache.size);
|
|
|
|
// 持久化到 LocalStorage
|
|
this.saveToLocalStorage();
|
|
},
|
|
|
|
// 保存到 LocalStorage
|
|
saveToLocalStorage() {
|
|
try {
|
|
const serializable = {
|
|
cache: Array.from(this.cache.entries()),
|
|
lastQueryTime: this.lastQueryTime
|
|
};
|
|
localStorage.setItem('threatCache', JSON.stringify(serializable));
|
|
} catch (e) {
|
|
console.warn('LocalStorage 保存失败:', e);
|
|
}
|
|
},
|
|
|
|
// 从 LocalStorage 加载
|
|
loadFromLocalStorage() {
|
|
try {
|
|
const data = localStorage.getItem('threatCache');
|
|
if (data) {
|
|
const parsed = JSON.parse(data);
|
|
// 检查是否过期(最多 1 小时)
|
|
if (Date.now() - parsed.lastQueryTime < 60 * 60 * 1000) {
|
|
parsed.cache.forEach(([key, value]) => {
|
|
this.cache.set(key, value);
|
|
});
|
|
this.lastQueryTime = parsed.lastQueryTime;
|
|
console.log('从 LocalStorage 加载缓存,记录数:', this.cache.size);
|
|
return true;
|
|
} else {
|
|
console.log('LocalStorage 缓存已过期');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('LocalStorage 加载失败:', e);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// 清除缓存
|
|
clear() {
|
|
this.cache.clear();
|
|
this.lastQueryTime = 0;
|
|
localStorage.removeItem('threatCache');
|
|
}
|
|
};
|
|
|
|
// 页面加载时从 LocalStorage 恢复缓存
|
|
threatCache.loadFromLocalStorage();
|
|
|
|
// ==================== 全局变量 ====================
|
|
let threatsDatabase = [];
|
|
let matchedThreats = [];
|
|
let isDatabaseLoaded = false;
|
|
|
|
// 分页相关变量
|
|
const paginationState = {
|
|
currentPage: 1,
|
|
pageSize: 30,
|
|
totalItems: 0,
|
|
totalPages: 0
|
|
};
|
|
|
|
// 加载威胁数据库
|
|
async function loadThreatsDatabase() {
|
|
if (isDatabaseLoaded && threatsDatabase.length > 0) {
|
|
console.log('使用缓存的威胁数据库:', threatsDatabase.length, '条记录');
|
|
return threatsDatabase;
|
|
}
|
|
|
|
try {
|
|
console.log('尝试加载威胁数据库...');
|
|
|
|
// 使用新的API端点获取威胁数据库
|
|
const response = await fetch('/api/threat/domain');
|
|
console.log('威胁数据库API请求状态:', response.status);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`无法加载威胁数据库,状态: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('威胁数据库API返回数据:', data);
|
|
|
|
if (!Array.isArray(data)) {
|
|
throw new Error('威胁数据库API返回的数据格式错误,不是数组');
|
|
}
|
|
|
|
// 解析API返回的数据
|
|
threatsDatabase = data.map(domain => ({
|
|
type: '未知',
|
|
name: '未知',
|
|
riskLevel: '2',
|
|
domain: domain
|
|
})).filter(item => item.domain);
|
|
|
|
isDatabaseLoaded = true;
|
|
console.log('威胁数据库加载成功:', threatsDatabase.length, '条记录');
|
|
console.log('威胁数据库前5条记录:', threatsDatabase.slice(0, 5));
|
|
return threatsDatabase;
|
|
} catch (error) {
|
|
console.error('加载威胁数据库失败:', error);
|
|
console.error('错误堆栈:', error.stack);
|
|
// 即使加载失败,也返回一个包含示例数据的数组,确保页面能显示内容
|
|
console.log('加载失败,使用示例威胁数据...');
|
|
return [
|
|
{ type: '钓鱼网站', name: 'Silver fox 团伙', riskLevel: '2', domain: 'kefubahaohonsheng.oss-cn-hongkong.aliyuncs.com' },
|
|
{ type: '钓鱼网站', name: 'Silver fox 团伙', riskLevel: '2', domain: 'baiduwenshen.oss-cn-hongkong.aliyuncs.com' },
|
|
{ type: '木马', name: 'Zegost 后门软件', riskLevel: '1', domain: 'sgkong2.top' },
|
|
{ type: '木马', name: 'Generic 常规木马', riskLevel: '1', domain: 'todesk-1316713808.cos.ap-nanjing.myqcloud.com' }
|
|
];
|
|
}
|
|
}
|
|
|
|
// 加载DNS查询日志
|
|
async function loadDNSLogs() {
|
|
try {
|
|
// 使用 /api/logs/query 接口获取数据,返回DNS查询日志列表
|
|
const response = await fetch('/api/logs/query');
|
|
|
|
// 处理响应
|
|
if (!response.ok) {
|
|
// 如果是未授权错误,记录错误并返回模拟数据
|
|
if (response.status === 401) {
|
|
console.warn('API返回未授权错误,使用模拟数据');
|
|
return getMockDNSLogs();
|
|
}
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const logs = await response.json();
|
|
console.log('DNS查询日志加载成功:', logs.length, '条记录');
|
|
console.log('API返回的前3条数据:', logs.slice(0, 3));
|
|
|
|
// 确保返回的数据格式正确
|
|
if (!Array.isArray(logs)) {
|
|
console.error('API返回的数据格式错误,不是数组:', logs);
|
|
// 返回模拟数据作为备用
|
|
return getMockDNSLogs();
|
|
}
|
|
|
|
// 过滤并处理日志数据,确保每个条目都有 domain 字段
|
|
const processedLogs = logs.filter(log => {
|
|
if (log && log.domain) {
|
|
return true;
|
|
}
|
|
console.warn('过滤掉没有 domain 字段的日志条目:', log);
|
|
return false;
|
|
});
|
|
|
|
console.log('处理后有 domain 字段的日志数量:', processedLogs.length);
|
|
|
|
// 如果处理后没有日志,返回模拟数据
|
|
if (processedLogs.length === 0) {
|
|
console.warn('没有找到有 domain 字段的日志,返回模拟数据');
|
|
return getMockDNSLogs();
|
|
}
|
|
|
|
return processedLogs;
|
|
} catch (error) {
|
|
console.error('加载DNS查询日志失败:', error);
|
|
// 返回模拟数据作为备用,包含威胁数据库中的域名
|
|
return getMockDNSLogs();
|
|
}
|
|
}
|
|
|
|
// 获取模拟DNS查询日志
|
|
function getMockDNSLogs() {
|
|
return [
|
|
{
|
|
timestamp: '2023-10-01 14:32:15',
|
|
domain: 'kefubahaohonsheng.oss-cn-hongkong.aliyuncs.com',
|
|
clientIP: '192.168.1.100',
|
|
result: 'allowed'
|
|
},
|
|
{
|
|
timestamp: '2023-10-01 14:28:45',
|
|
domain: 'baiduwenshen.oss-cn-hongkong.aliyuncs.com',
|
|
clientIP: '192.168.1.101',
|
|
result: 'blocked'
|
|
},
|
|
{
|
|
timestamp: '2023-10-01 14:25:30',
|
|
domain: 'sgkong2.top',
|
|
clientIP: '192.168.1.102',
|
|
result: 'blocked'
|
|
},
|
|
{
|
|
timestamp: '2023-10-01 14:20:15',
|
|
domain: 'todesk-1316713808.cos.ap-nanjing.myqcloud.com',
|
|
clientIP: '192.168.1.103',
|
|
result: 'blocked'
|
|
},
|
|
{
|
|
timestamp: '2023-10-01 14:15:00',
|
|
domain: 'shimo-oss1.oss-cn-hangzhou.aliyuncs.com',
|
|
clientIP: '192.168.1.104',
|
|
result: 'allowed'
|
|
},
|
|
{
|
|
timestamp: '2023-10-01 14:10:45',
|
|
domain: 'example.com',
|
|
clientIP: '192.168.1.105',
|
|
result: 'allowed'
|
|
},
|
|
{
|
|
timestamp: '2023-10-01 14:05:30',
|
|
domain: '11bucketyun.oss-cn-hongkong.aliyuncs.com',
|
|
clientIP: '192.168.1.106',
|
|
result: 'allowed'
|
|
},
|
|
{
|
|
timestamp: '2023-10-01 14:00:15',
|
|
domain: 'supervt.oss-cn-hongkong.aliyuncs.com',
|
|
clientIP: '192.168.1.107',
|
|
result: 'blocked'
|
|
}
|
|
];
|
|
}
|
|
|
|
// 从威胁告警 API 获取威胁告警数据
|
|
async function matchThreatsWithLogs() {
|
|
try {
|
|
console.log('开始从威胁告警 API 获取数据...');
|
|
|
|
// 使用新的威胁告警 API 端点
|
|
const response = await fetchWithRetry('/api/alert');
|
|
|
|
console.log('威胁告警 API 返回数据:', response);
|
|
|
|
if (!response || !response.alerts) {
|
|
console.error('威胁告警 API 返回数据格式错误');
|
|
return [];
|
|
}
|
|
|
|
const alerts = response.alerts;
|
|
console.log('威胁告警 API 返回告警数:', alerts.length);
|
|
|
|
// 转换告警数据格式
|
|
const matchedThreats = alerts.map((alert, index) => {
|
|
// 转换风险等级格式
|
|
let risk;
|
|
switch (alert.level) {
|
|
case 'high':
|
|
risk = 'high';
|
|
break;
|
|
case 'medium':
|
|
risk = 'medium';
|
|
break;
|
|
case 'low':
|
|
risk = 'low';
|
|
break;
|
|
default:
|
|
risk = 'medium';
|
|
}
|
|
|
|
// 转换威胁类型格式
|
|
let typeStr;
|
|
switch (alert.type) {
|
|
case 'phishing':
|
|
typeStr = 'phishing';
|
|
break;
|
|
case 'malware':
|
|
typeStr = 'malware';
|
|
break;
|
|
case 'botnet':
|
|
typeStr = 'botnet';
|
|
break;
|
|
case 'dga':
|
|
typeStr = 'dga';
|
|
break;
|
|
default:
|
|
typeStr = 'suspicious';
|
|
}
|
|
|
|
return {
|
|
id: alert.id || index + 1,
|
|
timestamp: alert.timestamp,
|
|
type: typeStr,
|
|
domain: alert.domain,
|
|
sourceIp: alert.sourceIP,
|
|
risk: risk,
|
|
status: alert.resolved ? (alert.action === 'blocked' ? 'blocked' : 'allowed') : 'monitored',
|
|
threatName: alert.description,
|
|
threatType: alert.type,
|
|
riskLevel: alert.level
|
|
};
|
|
});
|
|
|
|
console.log('威胁告警数据转换完成,记录数:', matchedThreats.length);
|
|
return matchedThreats;
|
|
} catch (error) {
|
|
console.error('获取威胁告警失败:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 生成威胁统计数据
|
|
function generateThreatStats(threats) {
|
|
let total = threats.length;
|
|
let high = threats.filter(t => t.risk === 'high').length;
|
|
let medium = threats.filter(t => t.risk === 'medium').length;
|
|
let low = threats.filter(t => t.risk === 'low').length;
|
|
|
|
return {
|
|
total: total,
|
|
high: high,
|
|
medium: medium,
|
|
low: low,
|
|
percentChange: 12.5 // 模拟数据
|
|
};
|
|
}
|
|
|
|
// 生成威胁趋势数据
|
|
function generateThreatTrends(threats) {
|
|
// 生成过去12小时的时间标签
|
|
const labels = [];
|
|
const data = [];
|
|
|
|
for (let i = 11; i >= 0; i--) {
|
|
const hour = new Date();
|
|
hour.setHours(hour.getHours() - i);
|
|
labels.push(`${i}小时前`);
|
|
|
|
// 统计该小时的威胁数量
|
|
const hourStart = new Date(hour);
|
|
hourStart.setMinutes(0, 0, 0);
|
|
const hourEnd = new Date(hourStart);
|
|
hourEnd.setHours(hourEnd.getHours() + 1);
|
|
|
|
const hourThreats = threats.filter(t => {
|
|
const threatTime = new Date(t.timestamp);
|
|
return threatTime >= hourStart && threatTime < hourEnd;
|
|
});
|
|
|
|
data.push(hourThreats.length);
|
|
}
|
|
|
|
return {
|
|
labels: labels,
|
|
data: data
|
|
};
|
|
}
|
|
|
|
// 威胁数据结构
|
|
const threatData = {
|
|
summary: {
|
|
total: 0,
|
|
high: 0,
|
|
medium: 0,
|
|
low: 0,
|
|
percentChange: 0
|
|
},
|
|
trends: {
|
|
labels: [],
|
|
data: []
|
|
},
|
|
threats: []
|
|
};
|
|
|
|
// 显示局部加载状态
|
|
function showLoadingStates() {
|
|
// 统计卡片显示加载状态
|
|
const statElements = [
|
|
'total-threats',
|
|
'high-risk-threats',
|
|
'medium-risk-threats',
|
|
'low-risk-threats'
|
|
];
|
|
|
|
statElements.forEach(id => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
|
|
}
|
|
});
|
|
|
|
// 图表区域显示加载状态
|
|
const trendChart = document.getElementById('threat-trend-chart');
|
|
if (trendChart) {
|
|
trendChart.style.display = 'none';
|
|
const loadingDiv = document.createElement('div');
|
|
loadingDiv.id = 'trend-chart-loading';
|
|
loadingDiv.className = 'flex items-center justify-center h-48 sm:h-64';
|
|
loadingDiv.innerHTML = '<i class="fa fa-spinner fa-spin text-4xl text-primary"></i>';
|
|
trendChart.parentNode.appendChild(loadingDiv);
|
|
}
|
|
|
|
const riskChart = document.getElementById('risk-distribution-chart');
|
|
if (riskChart) {
|
|
riskChart.style.display = 'none';
|
|
const loadingDiv = document.createElement('div');
|
|
loadingDiv.id = 'risk-chart-loading';
|
|
loadingDiv.className = 'flex items-center justify-center h-48 sm:h-64';
|
|
loadingDiv.innerHTML = '<i class="fa fa-spinner fa-spin text-4xl text-primary"></i>';
|
|
riskChart.parentNode.appendChild(loadingDiv);
|
|
}
|
|
|
|
// 列表区域显示加载状态和进度提示
|
|
const threatList = document.getElementById('threat-list');
|
|
if (threatList) {
|
|
threatList.innerHTML = '<tr><td colspan="8" class="py-8 text-center text-gray-500"><i class="fa fa-spinner fa-spin mr-2"></i>正在查询威胁数据,请稍候...</td></tr>';
|
|
}
|
|
}
|
|
|
|
// 显示加载错误
|
|
function showLoadingError(error) {
|
|
console.error('加载错误:', error);
|
|
|
|
// 统计卡片显示错误
|
|
const statElements = [
|
|
'total-threats',
|
|
'high-risk-threats',
|
|
'medium-risk-threats',
|
|
'low-risk-threats'
|
|
];
|
|
|
|
statElements.forEach(id => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.textContent = '0';
|
|
}
|
|
});
|
|
|
|
// 图表区域显示错误
|
|
const trendChartContainer = document.getElementById('threat-trend-chart')?.parentNode;
|
|
if (trendChartContainer) {
|
|
trendChartContainer.innerHTML = '<div class="flex items-center justify-center h-48 sm:h-64 text-danger"><i class="fa fa-exclamation-triangle mr-2"></i>加载失败</div>';
|
|
}
|
|
|
|
const riskChartContainer = document.getElementById('risk-distribution-chart')?.parentNode;
|
|
if (riskChartContainer) {
|
|
riskChartContainer.innerHTML = '<div class="flex items-center justify-center h-48 sm:h-64 text-danger"><i class="fa fa-exclamation-triangle mr-2"></i>加载失败</div>';
|
|
}
|
|
|
|
// 列表区域显示错误
|
|
const threatList = document.getElementById('threat-list');
|
|
if (threatList) {
|
|
threatList.innerHTML = '<tr><td colspan="7" class="py-8 text-center text-danger"><i class="fa fa-exclamation-triangle mr-2"></i>加载失败,请刷新重试</td></tr>';
|
|
}
|
|
}
|
|
|
|
// 初始化威胁告警页面
|
|
async function initThreatsPage() {
|
|
console.log('初始化威胁告警页面');
|
|
|
|
// 显示局部加载状态
|
|
showLoadingStates();
|
|
|
|
try {
|
|
console.log('开始加载威胁数据库...');
|
|
const database = await loadThreatsDatabase();
|
|
console.log('威胁数据库加载完成,记录数:', database.length);
|
|
|
|
console.log('开始获取威胁告警数据...');
|
|
const threats = await matchThreatsWithLogs();
|
|
console.log('威胁告警数据获取完成,记录数:', threats.length);
|
|
|
|
// 更新威胁数据
|
|
threatData.threats = threats;
|
|
threatData.summary = generateThreatStats(threats);
|
|
threatData.trends = generateThreatTrends(threats);
|
|
|
|
console.log('威胁数据更新完成:', {
|
|
threats: threats.length,
|
|
summary: threatData.summary,
|
|
trends: threatData.trends
|
|
});
|
|
|
|
// 初始化页面组件
|
|
console.log('开始初始化页面组件...');
|
|
populateThreatStats();
|
|
|
|
// 数据加载完成后再渲染图表
|
|
console.log('渲染图表...');
|
|
renderThreatTrendChart();
|
|
renderRiskDistributionChart();
|
|
|
|
populateThreatList();
|
|
bindFilterEvents();
|
|
bindAlertActionEvents();
|
|
console.log('页面组件初始化完成');
|
|
} catch (error) {
|
|
console.error('初始化威胁告警页面失败:', error);
|
|
showLoadingError(error);
|
|
}
|
|
}
|
|
|
|
// 填充威胁统计卡片
|
|
function populateThreatStats() {
|
|
const summary = threatData.summary;
|
|
|
|
// 填充总威胁数
|
|
document.getElementById('total-threats').textContent = summary.total;
|
|
document.getElementById('threats-percent').textContent = `${summary.percentChange}%`;
|
|
|
|
// 填充高风险威胁数
|
|
document.getElementById('high-risk-threats').textContent = summary.high;
|
|
document.getElementById('high-risk-percent').textContent = `${Math.round((summary.high / summary.total) * 100)}%`;
|
|
|
|
// 填充中风险威胁数
|
|
document.getElementById('medium-risk-threats').textContent = summary.medium;
|
|
document.getElementById('medium-risk-percent').textContent = `${Math.round((summary.medium / summary.total) * 100)}%`;
|
|
|
|
// 填充低风险威胁数
|
|
document.getElementById('low-risk-threats').textContent = summary.low;
|
|
document.getElementById('low-risk-percent').textContent = `${Math.round((summary.low / summary.total) * 100)}%`;
|
|
}
|
|
|
|
// 渲染威胁趋势图表
|
|
function renderThreatTrendChart() {
|
|
try {
|
|
const canvas = document.getElementById('threat-trend-chart');
|
|
if (!canvas) {
|
|
console.warn('威胁趋势图表 canvas 元素不存在');
|
|
return;
|
|
}
|
|
|
|
// 移除加载动画
|
|
const loadingDiv = document.getElementById('trend-chart-loading');
|
|
if (loadingDiv) {
|
|
loadingDiv.remove();
|
|
}
|
|
|
|
// 显示 canvas 元素
|
|
canvas.style.display = 'block';
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// 销毁已存在的图表
|
|
if (window.threatTrendChart) {
|
|
window.threatTrendChart.destroy();
|
|
}
|
|
|
|
// 如果没有数据,显示空图表
|
|
const labels = threatData.trends.labels || [];
|
|
const data = threatData.trends.data || [];
|
|
|
|
// 创建新图表
|
|
window.threatTrendChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels.length > 0 ? labels : ['无数据'],
|
|
datasets: [{
|
|
label: '威胁数量',
|
|
data: data.length > 0 ? data : [0],
|
|
borderColor: '#ef4444',
|
|
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
tension: 0.4,
|
|
fill: true
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
color: 'rgba(0, 0, 0, 0.05)'
|
|
}
|
|
},
|
|
x: {
|
|
grid: {
|
|
display: false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('渲染威胁趋势图表失败:', error);
|
|
}
|
|
}
|
|
|
|
// 渲染风险等级分布图表
|
|
function renderRiskDistributionChart() {
|
|
try {
|
|
const canvas = document.getElementById('risk-distribution-chart');
|
|
if (!canvas) {
|
|
console.warn('风险等级分布图表 canvas 元素不存在');
|
|
return;
|
|
}
|
|
|
|
// 移除加载动画
|
|
const loadingDiv = document.getElementById('risk-chart-loading');
|
|
if (loadingDiv) {
|
|
loadingDiv.remove();
|
|
}
|
|
|
|
// 显示 canvas 元素
|
|
canvas.style.display = 'block';
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
const summary = threatData.summary;
|
|
|
|
// 销毁已存在的图表
|
|
if (window.riskDistributionChart) {
|
|
window.riskDistributionChart.destroy();
|
|
}
|
|
|
|
// 检查是否有数据
|
|
const hasData = summary && (summary.high > 0 || summary.medium > 0 || summary.low > 0);
|
|
|
|
if (!hasData) {
|
|
// 没有数据时显示提示
|
|
ctx.font = '16px Arial';
|
|
ctx.fillStyle = '#999';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
ctx.fillText('暂无威胁数据', centerX, centerY);
|
|
return;
|
|
}
|
|
|
|
// 创建新图表
|
|
window.riskDistributionChart = new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['高风险', '中风险', '低风险'],
|
|
datasets: [{
|
|
data: [summary.high, summary.medium, summary.low],
|
|
backgroundColor: ['#ef4444', '#f59e0b', '#3b82f6'],
|
|
borderWidth: 0
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
},
|
|
cutout: '70%'
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('渲染风险等级分布图表失败:', error);
|
|
}
|
|
}
|
|
|
|
// 填充威胁告警列表
|
|
function populateThreatList(filteredThreats = null) {
|
|
const threatList = document.getElementById('threat-list');
|
|
const threats = filteredThreats || threatData.threats;
|
|
|
|
// 清空列表
|
|
threatList.innerHTML = '';
|
|
|
|
// 获取当前页的数据
|
|
const currentPageData = getCurrentPageData(threats);
|
|
|
|
// 如果没有数据
|
|
if (currentPageData.length === 0) {
|
|
threatList.innerHTML = '<tr><td colspan="8" class="py-8 text-center text-gray-500">暂无威胁告警数据</td></tr>';
|
|
updatePaginationState(threats.length);
|
|
renderPagination();
|
|
return;
|
|
}
|
|
|
|
// 填充列表
|
|
currentPageData.forEach(threat => {
|
|
const row = document.createElement('tr');
|
|
|
|
// 获取威胁类型显示文本(优先使用 threatType,如果不存在则使用 type 映射)
|
|
const typeText = threat.threatType || {
|
|
'malware': '恶意软件',
|
|
'phishing': '钓鱼网站',
|
|
'botnet': '僵尸网络',
|
|
'dga': 'DGA 域名',
|
|
'suspicious': '可疑活动'
|
|
}[threat.type] || '未知';
|
|
|
|
// 获取风险等级显示文本和样式
|
|
const riskInfo = {
|
|
'high': { text: '高', class: 'bg-red-100 text-red-800' },
|
|
'medium': { text: '中', class: 'bg-yellow-100 text-yellow-800' },
|
|
'low': { text: '低', class: 'bg-blue-100 text-blue-800' }
|
|
}[threat.risk];
|
|
|
|
// 获取状态显示文本和样式
|
|
const statusInfo = {
|
|
'blocked': { text: '已屏蔽', class: 'bg-green-100 text-green-800' },
|
|
'monitored': { text: '监控中', class: 'bg-yellow-100 text-yellow-800' },
|
|
'allowed': { text: '已放行', class: 'bg-blue-100 text-blue-800' }
|
|
}[threat.status];
|
|
|
|
// 设置行内容
|
|
row.innerHTML = `
|
|
<td class="py-2 sm:py-3 px-2 sm:px-4 text-sm">${threat.timestamp}</td>
|
|
<td class="py-2 sm:py-3 px-2 sm:px-4 text-sm">${typeText}</td>
|
|
<td class="py-2 sm:py-3 px-2 sm:px-4 text-sm">${threat.threatName || '未知'}</td>
|
|
<td class="py-2 sm:py-3 px-2 sm:px-4 text-sm font-medium">${threat.domain}</td>
|
|
<td class="py-2 sm:py-3 px-2 sm:px-4 text-sm">${threat.sourceIp}</td>
|
|
<td class="py-2 sm:py-3 px-2 sm:px-4 text-sm">
|
|
<span class="px-2 py-1 rounded-full text-xs ${riskInfo.class}">${riskInfo.text}</span>
|
|
</td>
|
|
<td class="py-2 sm:py-3 px-2 sm:px-4 text-sm">
|
|
<span class="px-2 py-1 rounded-full text-xs ${statusInfo.class}">${statusInfo.text}</span>
|
|
</td>
|
|
<td class="py-2 sm:py-3 px-2 sm:px-4 text-sm">
|
|
${threat.status === 'blocked' ? `
|
|
<button class="alert-action-btn block px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600" data-alert-id="${threat.id}" data-action="allowed" data-domain="${threat.domain}">放行</button>
|
|
` : threat.status === 'allowed' ? `
|
|
<button class="alert-action-btn block px-2 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600" data-alert-id="${threat.id}" data-action="blocked" data-domain="${threat.domain}">屏蔽</button>
|
|
` : `
|
|
<div class="flex space-x-2">
|
|
<button class="alert-action-btn block px-2 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600" data-alert-id="${threat.id}" data-action="blocked" data-domain="${threat.domain}">屏蔽</button>
|
|
<button class="alert-action-btn block px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600" data-alert-id="${threat.id}" data-action="allowed" data-domain="${threat.domain}">放行</button>
|
|
</div>
|
|
`}
|
|
</td>
|
|
`;
|
|
|
|
// 添加行到列表
|
|
threatList.appendChild(row);
|
|
});
|
|
|
|
// 更新分页状态
|
|
updatePaginationState(threats.length);
|
|
|
|
// 渲染分页控件
|
|
renderPagination();
|
|
|
|
// 绑定告警操作事件
|
|
bindAlertActionEvents();
|
|
}
|
|
|
|
// 更新分页状态
|
|
function updatePaginationState(totalItems) {
|
|
paginationState.totalItems = totalItems;
|
|
paginationState.totalPages = Math.ceil(totalItems / paginationState.pageSize);
|
|
|
|
// 确保当前页码在有效范围内
|
|
if (paginationState.currentPage > paginationState.totalPages) {
|
|
paginationState.currentPage = Math.max(1, paginationState.totalPages);
|
|
}
|
|
}
|
|
|
|
// 渲染分页控件
|
|
function renderPagination() {
|
|
const pageSizeSelect = document.getElementById('page-size');
|
|
const currentPageInput = document.getElementById('current-page-input');
|
|
const prevPageBtn = document.getElementById('prev-page-btn');
|
|
const nextPageBtn = document.getElementById('next-page-btn');
|
|
const goToPageBtn = document.getElementById('go-to-page-btn');
|
|
|
|
if (!pageSizeSelect || !currentPageInput) return;
|
|
|
|
// 更新每页显示数量
|
|
pageSizeSelect.value = paginationState.pageSize;
|
|
|
|
// 更新当前页码输入
|
|
currentPageInput.value = paginationState.currentPage;
|
|
currentPageInput.max = paginationState.totalPages || 1;
|
|
|
|
// 更新按钮状态
|
|
if (prevPageBtn) {
|
|
prevPageBtn.disabled = paginationState.currentPage === 1;
|
|
prevPageBtn.classList.toggle('opacity-50', paginationState.currentPage === 1);
|
|
prevPageBtn.classList.toggle('cursor-not-allowed', paginationState.currentPage === 1);
|
|
}
|
|
|
|
if (nextPageBtn) {
|
|
nextPageBtn.disabled = paginationState.currentPage >= paginationState.totalPages;
|
|
nextPageBtn.classList.toggle('opacity-50', paginationState.currentPage >= paginationState.totalPages);
|
|
nextPageBtn.classList.toggle('cursor-not-allowed', paginationState.currentPage >= paginationState.totalPages);
|
|
}
|
|
|
|
// 绑定事件(只绑定一次)
|
|
if (!window.paginationEventsBound) {
|
|
bindPaginationEvents();
|
|
window.paginationEventsBound = true;
|
|
}
|
|
}
|
|
|
|
// 绑定分页事件
|
|
function bindPaginationEvents() {
|
|
const pageSizeSelect = document.getElementById('page-size');
|
|
const currentPageInput = document.getElementById('current-page-input');
|
|
const prevPageBtn = document.getElementById('prev-page-btn');
|
|
const nextPageBtn = document.getElementById('next-page-btn');
|
|
const goToPageBtn = document.getElementById('go-to-page-btn');
|
|
|
|
// 每页显示数量变化
|
|
if (pageSizeSelect) {
|
|
pageSizeSelect.addEventListener('change', (e) => {
|
|
paginationState.pageSize = parseInt(e.target.value);
|
|
paginationState.currentPage = 1; // 重置到第一页
|
|
applyFilters(); // 重新应用过滤器
|
|
});
|
|
}
|
|
|
|
// 上一页
|
|
if (prevPageBtn) {
|
|
prevPageBtn.addEventListener('click', () => {
|
|
if (paginationState.currentPage > 1) {
|
|
paginationState.currentPage--;
|
|
applyFilters();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 下一页
|
|
if (nextPageBtn) {
|
|
nextPageBtn.addEventListener('click', () => {
|
|
if (paginationState.currentPage < paginationState.totalPages) {
|
|
paginationState.currentPage++;
|
|
applyFilters();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 前往指定页
|
|
if (goToPageBtn && currentPageInput) {
|
|
goToPageBtn.addEventListener('click', () => {
|
|
const page = parseInt(currentPageInput.value);
|
|
if (page >= 1 && page <= paginationState.totalPages) {
|
|
paginationState.currentPage = page;
|
|
applyFilters();
|
|
} else {
|
|
alert(`请输入 1 到 ${paginationState.totalPages} 之间的页码`);
|
|
}
|
|
});
|
|
|
|
// 支持回车键
|
|
currentPageInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
goToPageBtn.click();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// 获取当前页的数据
|
|
function getCurrentPageData(filteredThreats) {
|
|
const start = (paginationState.currentPage - 1) * paginationState.pageSize;
|
|
const end = start + paginationState.pageSize;
|
|
return filteredThreats.slice(start, end);
|
|
}
|
|
|
|
// 绑定过滤器事件
|
|
function bindFilterEvents() {
|
|
const riskFilter = document.getElementById('threat-filter-risk');
|
|
const typeFilter = document.getElementById('threat-filter-type');
|
|
const domainFilter = document.getElementById('threat-filter-domain');
|
|
const domainFilterBtn = document.getElementById('threat-filter-domain-btn');
|
|
const refreshBtn = document.getElementById('refresh-threats-btn');
|
|
|
|
// 域名筛选器事件(回车键)
|
|
if (domainFilter) {
|
|
domainFilter.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
applyFilters();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 域名筛选按钮事件
|
|
if (domainFilterBtn) {
|
|
domainFilterBtn.addEventListener('click', applyFilters);
|
|
}
|
|
|
|
// 风险等级过滤器事件
|
|
if (riskFilter) {
|
|
riskFilter.addEventListener('change', applyFilters);
|
|
}
|
|
|
|
// 威胁类型过滤器事件
|
|
if (typeFilter) {
|
|
typeFilter.addEventListener('change', applyFilters);
|
|
}
|
|
|
|
// 刷新按钮事件
|
|
if (refreshBtn) {
|
|
refreshBtn.addEventListener('click', async function() {
|
|
console.log('点击刷新威胁告警按钮');
|
|
|
|
// 显示加载状态
|
|
const originalIcon = refreshBtn.innerHTML;
|
|
refreshBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
|
|
refreshBtn.disabled = true;
|
|
refreshBtn.classList.add('opacity-70', 'cursor-not-allowed');
|
|
|
|
try {
|
|
// 清除缓存
|
|
threatCache.clear();
|
|
|
|
// 重新加载数据
|
|
const threats = await matchThreatsWithLogs();
|
|
|
|
// 更新威胁数据
|
|
threatData.threats = threats;
|
|
threatData.summary = generateThreatStats(threats);
|
|
threatData.trends = generateThreatTrends(threats);
|
|
|
|
// 重置分页
|
|
paginationState.currentPage = 1;
|
|
|
|
// 重新渲染页面
|
|
populateThreatStats();
|
|
renderThreatTrendChart();
|
|
renderRiskDistributionChart();
|
|
applyFilters();
|
|
|
|
console.log('威胁数据刷新完成');
|
|
} catch (error) {
|
|
console.error('刷新威胁数据失败:', error);
|
|
alert('刷新失败,请重试');
|
|
} finally {
|
|
refreshBtn.innerHTML = originalIcon;
|
|
refreshBtn.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// 绑定告警操作事件
|
|
function bindAlertActionEvents() {
|
|
const actionButtons = document.querySelectorAll('.alert-action-btn');
|
|
|
|
actionButtons.forEach(button => {
|
|
button.addEventListener('click', async function() {
|
|
const alertId = this.getAttribute('data-alert-id');
|
|
const action = this.getAttribute('data-action');
|
|
const domain = this.getAttribute('data-domain');
|
|
|
|
console.log(`处理告警 ${alertId},动作: ${action},域名: ${domain}`);
|
|
|
|
// 显示加载状态
|
|
const originalText = this.textContent;
|
|
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
|
|
this.disabled = true;
|
|
|
|
try {
|
|
// 发送请求到告警解决 API
|
|
const response = await fetchWithRetry('/api/alert/resolve', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
alertId: alertId,
|
|
action: action
|
|
})
|
|
});
|
|
|
|
console.log('告警解决 API 返回:', response);
|
|
|
|
if (response.status === 'success') {
|
|
// 将规则加入到屏蔽管理的自定义规则列表
|
|
if (action === 'blocked') {
|
|
// 添加到屏蔽规则
|
|
await addToCustomRules(domain, 'block');
|
|
} else if (action === 'allowed') {
|
|
// 添加到允许规则
|
|
await addToCustomRules(domain, 'allow');
|
|
}
|
|
|
|
// 刷新威胁告警数据
|
|
await refreshThreatData();
|
|
console.log('告警处理成功');
|
|
} else {
|
|
throw new Error('告警处理失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('处理告警失败:', error);
|
|
alert('处理告警失败,请重试');
|
|
} finally {
|
|
this.textContent = originalText;
|
|
this.disabled = false;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// 添加到自定义规则列表
|
|
async function addToCustomRules(domain, action) {
|
|
try {
|
|
console.log(`添加 ${action} 规则: ${domain}`);
|
|
|
|
// 根据操作类型选择 HTTP 方法
|
|
const method = action === 'block' ? 'POST' : 'DELETE';
|
|
|
|
// 发送请求到屏蔽规则 API
|
|
const response = await fetch('/api/shield', {
|
|
method: method,
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
rule: domain
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
console.log('添加规则 API 返回:', result);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(result.error || '添加规则失败');
|
|
}
|
|
|
|
console.log('规则添加成功');
|
|
} catch (error) {
|
|
console.error('添加规则失败:', error);
|
|
// 即使添加规则失败,也继续处理告警
|
|
}
|
|
}
|
|
|
|
// 刷新威胁告警数据
|
|
async function refreshThreatData() {
|
|
try {
|
|
console.log('刷新威胁告警数据...');
|
|
|
|
// 清除缓存
|
|
threatCache.clear();
|
|
|
|
// 重新加载数据
|
|
const threats = await matchThreatsWithLogs();
|
|
|
|
// 更新威胁数据
|
|
threatData.threats = threats;
|
|
threatData.summary = generateThreatStats(threats);
|
|
threatData.trends = generateThreatTrends(threats);
|
|
|
|
// 重置分页
|
|
paginationState.currentPage = 1;
|
|
|
|
// 重新渲染页面
|
|
populateThreatStats();
|
|
renderThreatTrendChart();
|
|
renderRiskDistributionChart();
|
|
applyFilters();
|
|
|
|
console.log('威胁数据刷新完成');
|
|
} catch (error) {
|
|
console.error('刷新威胁数据失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 应用过滤器
|
|
function applyFilters() {
|
|
const riskFilter = document.getElementById('threat-filter-risk')?.value || 'all';
|
|
const typeFilter = document.getElementById('threat-filter-type')?.value || 'all';
|
|
const domainFilter = document.getElementById('threat-filter-domain')?.value?.toLowerCase() || '';
|
|
|
|
// 过滤威胁数据
|
|
const filteredThreats = threatData.threats.filter(threat => {
|
|
// 风险等级匹配
|
|
const riskMatch = riskFilter === 'all' || threat.risk === riskFilter;
|
|
|
|
// 威胁类型匹配(使用 threatType,因为它是中文的)
|
|
const typeMatch = typeFilter === 'all' || threat.threatType === typeFilter;
|
|
|
|
// 域名匹配(支持模糊匹配)
|
|
const domainMatch = domainFilter === '' ||
|
|
threat.domain.toLowerCase().includes(domainFilter) ||
|
|
(threat.threatName && threat.threatName.toLowerCase().includes(domainFilter));
|
|
|
|
return riskMatch && typeMatch && domainMatch;
|
|
});
|
|
|
|
// 重置分页到第一页
|
|
paginationState.currentPage = 1;
|
|
|
|
// 填充过滤后的威胁列表
|
|
populateThreatList(filteredThreats);
|
|
} |