1109 lines
37 KiB
JavaScript
1109 lines
37 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/domain-info?threats');
|
|
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(item => ({
|
|
type: item.type || '',
|
|
name: item.name || '',
|
|
riskLevel: item.level || '',
|
|
domain: item.domain || ''
|
|
})).filter(item => item.type && item.name && item.riskLevel && 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'
|
|
}
|
|
];
|
|
}
|
|
|
|
// 匹配威胁数据库与 DNS 查询日志
|
|
async function matchThreatsWithLogs() {
|
|
try {
|
|
// 加载 DNS 查询日志
|
|
const logs = await loadDNSLogs();
|
|
console.log('DNS 查询日志加载完成,记录数:', logs.length);
|
|
|
|
// 提取所有唯一的域名
|
|
const uniqueDomains = [...new Set(logs.filter(log => log && log.domain).map(log => log.domain))];
|
|
console.log('唯一域名数量:', uniqueDomains.length);
|
|
|
|
if (uniqueDomains.length === 0) {
|
|
console.log('没有有效的域名,返回空结果');
|
|
return [];
|
|
}
|
|
|
|
// 检查缓存
|
|
const cachedResults = threatCache.getCachedResult();
|
|
if (cachedResults) {
|
|
console.log('使用缓存的威胁数据');
|
|
// 使用缓存数据匹配
|
|
return matchThreatsFromCache(logs, cachedResults);
|
|
}
|
|
|
|
console.log('开始批量查询威胁数据库...');
|
|
|
|
// 批量查询威胁
|
|
const response = await fetchWithRetry('/api/threat/batch', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
domains: uniqueDomains
|
|
})
|
|
});
|
|
|
|
const { results } = response;
|
|
console.log('批量查询完成,结果数:', results.length);
|
|
|
|
// 缓存结果
|
|
threatCache.cacheResults(results);
|
|
|
|
// 匹配日志和威胁信息
|
|
return matchThreatsFromResults(logs, results);
|
|
} catch (error) {
|
|
console.error('匹配威胁失败:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// 从缓存结果匹配威胁
|
|
function matchThreatsFromCache(logs, cachedResults) {
|
|
// 构建域名到威胁信息的映射
|
|
const threatMap = new Map();
|
|
cachedResults.forEach(result => {
|
|
if (result.isThreat && result.data) {
|
|
threatMap.set(result.domain, result.data);
|
|
}
|
|
});
|
|
|
|
return matchThreatsFromMap(logs, threatMap);
|
|
}
|
|
|
|
// 从批量查询结果匹配威胁
|
|
function matchThreatsFromResults(logs, results) {
|
|
// 构建域名到威胁信息的映射
|
|
const threatMap = new Map();
|
|
results.forEach(result => {
|
|
if (result.isThreat && result.data) {
|
|
threatMap.set(result.domain, result.data);
|
|
console.log('发现威胁域名:', result.domain);
|
|
}
|
|
});
|
|
|
|
return matchThreatsFromMap(logs, threatMap);
|
|
}
|
|
|
|
// 从威胁映射匹配日志
|
|
function matchThreatsFromMap(logs, threatMap) {
|
|
const matchedThreats = [];
|
|
let id = 1;
|
|
|
|
logs.forEach(log => {
|
|
const threatData = threatMap.get(log.domain);
|
|
if (threatData) {
|
|
const parts = threatData.split(',');
|
|
if (parts.length >= 4) {
|
|
const [type, name, riskLevel, domain] = parts;
|
|
|
|
// 转换风险等级格式
|
|
let risk;
|
|
switch (riskLevel) {
|
|
case '1':
|
|
risk = 'high';
|
|
break;
|
|
case '2':
|
|
risk = 'medium';
|
|
break;
|
|
case '3':
|
|
risk = 'low';
|
|
break;
|
|
default:
|
|
risk = 'medium';
|
|
}
|
|
|
|
// 转换威胁类型格式
|
|
let typeStr;
|
|
switch (type) {
|
|
case '钓鱼网站':
|
|
typeStr = 'phishing';
|
|
break;
|
|
case '木马':
|
|
typeStr = 'malware';
|
|
break;
|
|
case '僵尸网络':
|
|
typeStr = 'botnet';
|
|
break;
|
|
case 'DGA 域名':
|
|
typeStr = 'dga';
|
|
break;
|
|
default:
|
|
typeStr = 'suspicious';
|
|
}
|
|
|
|
matchedThreats.push({
|
|
id: id++,
|
|
timestamp: log.timestamp,
|
|
type: typeStr,
|
|
domain: log.domain,
|
|
sourceIp: log.clientIP,
|
|
risk: risk,
|
|
status: log.result === 'blocked' ? 'blocked' : 'monitored',
|
|
threatName: name,
|
|
threatType: type,
|
|
riskLevel: riskLevel
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log('威胁匹配完成,匹配记录数:', matchedThreats.length);
|
|
return matchedThreats;
|
|
}
|
|
|
|
// 生成威胁统计数据
|
|
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.parentNode) {
|
|
trendChart.parentNode.innerHTML = '<div class="flex items-center justify-center h-48 sm:h-64"><i class="fa fa-spinner fa-spin text-4xl text-primary"></i></div>';
|
|
}
|
|
|
|
const riskChart = document.getElementById('risk-distribution-chart');
|
|
if (riskChart && riskChart.parentNode) {
|
|
riskChart.parentNode.innerHTML = '<div class="flex items-center justify-center h-48 sm:h-64"><i class="fa fa-spinner fa-spin text-4xl text-primary"></i></div>';
|
|
}
|
|
|
|
// 列表区域显示加载状态和进度提示
|
|
const threatList = document.getElementById('threat-list');
|
|
if (threatList) {
|
|
threatList.innerHTML = '<tr><td colspan="7" 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('开始加载 DNS 查询日志...');
|
|
const logs = await loadDNSLogs();
|
|
console.log('DNS 查询日志加载完成,记录数:', logs.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();
|
|
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 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 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="7" 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' }
|
|
}[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>
|
|
`;
|
|
|
|
// 添加行到列表
|
|
threatList.appendChild(row);
|
|
});
|
|
|
|
// 更新分页状态
|
|
updatePaginationState(threats.length);
|
|
|
|
// 渲染分页控件
|
|
renderPagination();
|
|
}
|
|
|
|
// 更新分页状态
|
|
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 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);
|
|
} |