Files
dns-server/static/js/threats.js
T
2026-04-03 10:04:07 +08:00

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);
}