feat(audit): F1+F2 告警静默规则标记完成 + 审计日志CSV导出实现

F1 告警静默规则 [done]:
- 后端 alert_silence.go: CRUD + Toggle (onetime/daily/weekly 三模式)
- 告警引擎 isSilenced 集成 (alert_engine.go)
- 前端 SilenceRuleTab + SilenceDialog
- 路由 /alerts/silences 已注册

F2 审计日志导出 [done]:
- 重写 ExportAuditLogs: 真正的 CSV 导出 (encoding/csv + UTF-8 BOM)
- 支持 module/action/username/startTime/endTime 筛选
- 导出限制 10000 条防溢出,自动记录审计日志
- JSON 格式向后兼容 (format=json)
- 前端 exportLogs 传递查询参数

同时包含累积的前期工作:
- 目录重构 internal/ -> controller/model/router (v0.55-v0.57)
- 后端分层架构 dto/repository/service/utils
- 审计模块全功能 (终端审计/操作审计/登录日志)
- asciicast v2 录制格式
- SFTP 文件浏览器
- StatStrip 紧凑统计条
- 在线会话自动刷新
- LiveMonitor WebSocket 诊断修复

验证: go build ✅ | npm run build ✅ | Docker 部署 ✅ | 10 回归端点 ✅ | CSV 导出 ✅ | 静默 CRUD ✅
This commit is contained in:
attacker committed 2026-06-25 20:16:30 +08:00
1 parent e57718998a
commit 77c234b060
314 files changed
+10050 -4065

No files matched your search

@@ -0,0 +1,220 @@
// 终端审计实时监控 Dialog — xterm.js 实时查看在线会话 // Logan.Li
import { useEffect, useRef, useState } from 'react';
import { Terminal as XTerm } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { Loader2, Monitor, Server, User as UserIcon, Clock } from 'lucide-react';
import { useUserStore } from '@/store';
interface SessionInfo {
session_id: number;
username: string;
server_name: string;
server_ip: string;
start_time: string;
status: string;
}
interface LiveMonitorProps {
open: boolean;
onOpenChange: (open: boolean) => void;
sessionId: number | null;
sessionInfo?: SessionInfo | null;
}
export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }: LiveMonitorProps) {
const containerRef = useRef<HTMLDivElement>(null);
const termRef = useRef<XTerm | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const [connected, setConnected] = useState(false);
const [connectError, setConnectError] = useState('');
const [meta, setMeta] = useState<SessionInfo | null>(sessionInfo || null);
useEffect(() => {
if (!open || !sessionId) return;
const container = containerRef.current;
if (!container) return;
// Init xterm
const term = new XTerm({
cursorBlink: false,
fontSize: 13,
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
disableStdin: true,
theme: {
background: '#0a0e14',
foreground: '#d6d6d6',
cursor: '#d6d6d6',
selectionBackground: '#264f78',
black: '#1a1a2e',
red: '#ff6b6b',
green: '#69db7c',
yellow: '#ffd43b',
blue: '#4dabf7',
magenta: '#da77f2',
cyan: '#63e6be',
white: '#d6d6d6',
brightBlack: '#4a4a4a',
brightRed: '#ff8787',
brightGreen: '#8ce99a',
brightYellow: '#ffe066',
brightBlue: '#74c0fc',
brightMagenta: '#e599f7',
brightCyan: '#7ae5d5',
brightWhite: '#f8f9fa',
},
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(container);
fitAddon.fit();
termRef.current = term;
// Connect WebSocket — try zustand store first, fallback to localStorage
let token = useUserStore.getState().token;
if (!token) {
try {
const stored = JSON.parse(localStorage.getItem('user-storage') || '');
token = stored?.state?.token || '';
} catch { /* ignore */ }
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/audit/${sessionId}?token=${encodeURIComponent(token || '')}`;
console.log('[LiveMonitor] Connecting to:', wsUrl.replace(/token=[^&]+/, 'token=***'));
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
let didOpen = false;
ws.onopen = () => {
console.log('[LiveMonitor] WS connected for session:', sessionId);
didOpen = true;
setConnected(true);
setConnectError('');
term.writeln('\x1b[36m[审计监控] 已连接到会话...\x1b[0m');
};
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
switch (msg.type) {
case 'session_info':
setMeta(msg.payload);
term.writeln(`\x1b[36m用户: ${msg.payload.username} 服务器: ${msg.payload.server_name}\x1b[0m`);
break;
case 'history':
if (msg.data) {
term.write(msg.data);
}
break;
case 'output':
if (msg.data) {
term.write(msg.data);
}
break;
}
} catch {
// ignore parse errors
}
};
ws.onerror = (e) => {
console.log('[LiveMonitor] WS error:', e);
setConnectError('WebSocket 连接失败');
};
ws.onclose = (e) => {
console.log('[LiveMonitor] WS closed code:', e.code, 'reason:', e.reason);
if (!didOpen) {
setConnectError(`连接失败 (code=${e.code}): ${e.reason || '认证失败或网络问题'}`);
}
setConnected(false);
term.writeln('\r\n\x1b[33m[审计监控] 连接已断开\x1b[0m');
};
// 5s 超时:如果还没连上,显示错误
const timeout = setTimeout(() => {
if (!didOpen) {
setConnectError('连接超时 — 请确认已登录且有活跃会话');
ws.close();
}
}, 5000);
return () => {
clearTimeout(timeout);
ws.close();
term.dispose();
termRef.current = null;
wsRef.current = null;
setConnected(false);
};
}, [open, sessionId]);
const formatTime = (t: string) => {
if (!t) return '--';
return new Date(t).toLocaleString();
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent showOverlay={false} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-[95vw] max-h-[90vh] w-[1200px]">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<Monitor className="h-5 w-5 text-emerald-400" />
终端实时监控
{connected && (
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10 ml-2 text-xs">
● 在线
</Badge>
)}
{!connected && !connectError && (
<Badge variant="outline" className="border-yellow-500/30 text-yellow-400 bg-yellow-500/10 ml-2 text-xs">
<Loader2 className="h-3 w-3 animate-spin mr-1" />
连接中
</Badge>
)}
{connectError && (
<Badge variant="outline" className="border-red-500/30 text-red-400 bg-red-500/10 ml-2 text-xs">
连接失败
</Badge>
)}
</DialogTitle>
<DialogDescription className="text-[var(--dg-text-secondary)]">
{meta && (
<div className="flex items-center gap-4 text-xs mt-1 flex-wrap">
<span className="flex items-center gap-1">
<UserIcon className="h-3 w-3" />
{meta.username}
</span>
<span className="flex items-center gap-1">
<Server className="h-3 w-3" />
{meta.server_name} ({meta.server_ip})
</span>
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{formatTime(meta.start_time)}
</span>
</div>
)}
</DialogDescription>
</DialogHeader>
<div
ref={containerRef}
className="w-full rounded-md overflow-hidden border border-[var(--dg-border)]"
style={{ height: '55vh' }}
/>
</DialogContent>
</Dialog>
);
}