diff --git a/.research/frontend-refactor/detection.tsx b/.research/frontend-refactor/detection.tsx new file mode 100644 index 0000000..1ca2a76 --- /dev/null +++ b/.research/frontend-refactor/detection.tsx @@ -0,0 +1,762 @@ +import React, { useContext, useEffect, useState, useCallback, useMemo, useRef } from 'react' +import Head from 'next/head' +import { motion, AnimatePresence } from 'framer-motion' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { + faShieldAlt, + faExclamationTriangle, + faFilter, + faCheck, + faGlobe, + faTimes, + faArrowRight, + faEye, + faList, + faPause, + faPlay, + faBrain, + faScroll, + faLayerGroup, +} from '@fortawesome/free-solid-svg-icons' +import { WebsocketContext } from '../providers/WebSocketProvider' +import { putData } from '../utils/connectionUtils' +import { urls } from '../config' +import Layout from '../components/Layout' +import LoadingSpinner from '../components/LoadingSpinner' + +// --- Types (mirrors backend UnifiedAlert) --- + +type AlertSource = 'Ml' | 'Rule' | 'Fusion' +type AlertSeverity = 'High' | 'Critical' + +interface UnifiedAlert { + timestamp: number + flow_key: string + src_ip: string + dst_ip: string + src_port: number + dst_port: number + protocol: number + source: AlertSource + severity: AlertSeverity + is_attack: boolean + attack_type: string | null + confidence: number + ae_score: number + rule_sid: number | null + rule_msg: string | null +} + +// --- Display config --- + +const SOURCE_CONFIG = { + Ml: { + label: 'AI', + color: '#7c3aed', + bgColor: 'rgba(124,58,237,0.10)', + icon: faBrain, + }, + Rule: { + label: '規則', + color: '#2563eb', + bgColor: 'rgba(37,99,235,0.10)', + icon: faScroll, + }, + Fusion: { + label: '融合', + color: '#dc2626', + bgColor: 'rgba(220,38,38,0.10)', + icon: faLayerGroup, + }, +} as const + +const SEVERITY_CONFIG = { + Critical: { + label: '嚴重', + color: '#dc2626', + bgColor: 'rgba(220,38,38,0.10)', + icon: faExclamationTriangle, + }, + High: { + label: '高', + color: '#d97706', + bgColor: 'rgba(217,119,6,0.10)', + icon: faExclamationTriangle, + }, +} as const + +const PROTOCOL_MAP: Record = { + 1: 'ICMP', + 6: 'TCP', + 17: 'UDP', + 58: 'ICMPv6', + 132: 'SCTP', +} + +const formatProtocol = (proto: number): string => + PROTOCOL_MAP[proto] ?? `Proto(${proto})` + +const formatTimestamp = (unix: number): string => + new Date(unix * 1000).toLocaleString('zh-TW', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }) + +// --- Notification --- + +interface NotificationProps { + message: string + type: 'success' | 'error' | 'warning' | 'info' + onClose: () => void +} + +const Notification: React.FC = ({ message, type, onClose }) => { + useEffect(() => { + const t = setTimeout(onClose, 4000) + return () => clearTimeout(t) + }, [onClose]) + + const styles = { + success: 'bg-green-100 border-green-400 text-green-700', + error: 'bg-red-100 border-red-400 text-red-700', + warning: 'bg-yellow-100 border-yellow-400 text-yellow-700', + info: 'bg-blue-100 border-blue-400 text-blue-700', + } + + return ( + +
+ {message} + +
+
+ ) +} + +// --- FilterButton --- + +interface FilterButtonProps { + isActive: boolean + onClick: () => void + children: React.ReactNode + activeColor?: string +} + +const FilterButton: React.FC = ({ + isActive, + onClick, + children, + activeColor = '#374151', +}) => ( + +) + +// --- AlertItem --- + +interface AlertItemProps { + alert: UnifiedAlert + index: number + onClick: (index: number) => void +} + +const AlertItem: React.FC = ({ alert, index, onClick }) => { + const srcCfg = SOURCE_CONFIG[alert.source] + const sevCfg = SEVERITY_CONFIG[alert.severity] + const displayMessage = alert.attack_type ?? alert.rule_msg ?? `SID ${alert.rule_sid ?? '—'}` + + const handleClick = useCallback(() => onClick(index), [index, onClick]) + + return ( + +
+
+ +
+ +
+
+ {formatTimestamp(alert.timestamp)} +
+ + + {srcCfg.label} + + + {sevCfg.label} + +
+
+ +
+ {displayMessage} +
+ +
+ {alert.src_ip}:{alert.src_port} + + {alert.dst_ip}:{alert.dst_port} + + {formatProtocol(alert.protocol)} + +
+
+
+
+ ) +} + +// --- AlertDetails --- + +interface AlertDetailsProps { + alert: UnifiedAlert + onClose: () => void + showNotification: (message: string, type?: 'success' | 'error' | 'warning' | 'info') => void +} + +const AlertDetails: React.FC = ({ alert, onClose, showNotification }) => { + const srcCfg = SOURCE_CONFIG[alert.source] + const sevCfg = SEVERITY_CONFIG[alert.severity] + const isIPv6Src = alert.src_ip.includes(':') && !alert.src_ip.includes('.') + const isIPv6Dst = alert.dst_ip.includes(':') && !alert.dst_ip.includes('.') + + const handleBlockIP = useCallback( + (ip: string, port: number, isSource: boolean, blockAll = false) => { + const ipv6 = isSource ? isIPv6Src : isIPv6Dst + const formatted = ipv6 ? `[${ip}]` : ip + const url = ipv6 + ? urls.access_control.ipv6.black_list + : urls.access_control.ipv4.black_list + const portStr = blockAll ? '0' : String(port) + putData( + url, + `${formatted}:${portStr}`, + () => showNotification(`已成功添加到黑名單: ${ip}:${blockAll ? '*' : port}`, 'success'), + (err: any) => showNotification(`添加到黑名單失敗: ${err.message}`, 'error') + ) + }, + [isIPv6Src, isIPv6Dst, showNotification] + ) + + const handleAllowIP = useCallback( + (ip: string, port: number, isSource: boolean, allowAll = false) => { + const ipv6 = isSource ? isIPv6Src : isIPv6Dst + const formatted = ipv6 ? `[${ip}]` : ip + const url = ipv6 + ? urls.access_control.ipv6.white_list + : urls.access_control.ipv4.white_list + const portStr = allowAll ? '0' : String(port) + putData( + url, + `${formatted}:${portStr}`, + () => showNotification(`已成功添加到白名單: ${ip}:${allowAll ? '*' : port}`, 'success'), + (err: any) => showNotification(`添加到白名單失敗: ${err.message}`, 'error') + ) + }, + [isIPv6Src, isIPv6Dst, showNotification] + ) + + const IpActions: React.FC<{ ip: string; port: number; isSource: boolean; label: string; bg: string }> = ({ + ip, port, isSource, label, bg, + }) => ( +
+
{label}: {ip}
+
+ + + + +
+
+ ) + + return ( + + +
+

+ + 警報詳情 +

+ +
+ + {/* Basic metadata */} +
+
+ 時間: + {formatTimestamp(alert.timestamp)} +
+
+ 來源: + + + {srcCfg.label} + +
+
+ 嚴重程度: + + + {sevCfg.label} + +
+
+ 協議: + {formatProtocol(alert.protocol)} +
+
+ + {/* Attack / rule message */} + {(alert.attack_type || alert.rule_msg) && ( +
+ + {alert.source === 'Ml' ? '攻擊類型' : '規則訊息'}: + +
+

{alert.attack_type ?? alert.rule_msg}

+
+
+ )} + + {/* ML inference metrics */} + {(alert.source === 'Ml' || alert.source === 'Fusion') && ( +
+

AI 推論指標

+
+ 信心度: +
+
+
0.8 ? '#dc2626' : '#d97706', + }} + /> +
+ + {(alert.confidence * 100).toFixed(1)}% + +
+
+
+ AE Score: + {alert.ae_score.toFixed(6)} +
+
+ )} + + {/* Rule SID */} + {(alert.source === 'Rule' || alert.source === 'Fusion') && alert.rule_sid !== null && ( +
+

規則資訊

+
+ SID: + {alert.rule_sid} +
+
+ )} + + {/* Connection */} +
+

連線詳情

+
+
+
來源
+
{alert.src_ip}
+
:{alert.src_port}
+
+ +
+
目標
+
{alert.dst_ip}
+
:{alert.dst_port}
+
+
+
+ + {/* IP management */} +
+

IP 管理

+ + +
+ + + ) +} + +// --- Main page --- + +const Detection: React.FC = () => { + // NOTE: Replace getAiAlertStream with the WebsocketContext key that connects + // to /detection/websocket/alert (unified alert stream). + const { getDetectionAlertStream } = useContext(WebsocketContext) + + const [alerts, setAlerts] = useState([]) + const [selectedIdx, setSelectedIdx] = useState(null) + const [sourceFilter, setSourceFilter] = useState('all') + const [severityFilter, setSeverityFilter] = useState('all') + const [notification, setNotification] = useState<{ + message: string + type: 'success' | 'error' | 'warning' | 'info' + } | null>(null) + const [isLoading, setIsLoading] = useState(true) + const [isPaused, setIsPaused] = useState(false) + + // Use a ref so the subscription callback reads the latest isPaused + // without causing a resubscribe on every pause toggle. + const isPausedRef = useRef(false) + isPausedRef.current = isPaused + + const showNotification = useCallback( + (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'success') => { + setNotification({ message, type }) + }, + [] + ) + + useEffect(() => { + if (!getDetectionAlertStream) { + setIsLoading(false) + return + } + + const timeout = setTimeout(() => setIsLoading(false), 3000) + + const sub = getDetectionAlertStream().subscribe({ + next: (data: UnifiedAlert) => { + clearTimeout(timeout) + setIsLoading(false) + if (!isPausedRef.current) { + setAlerts(prev => [data, ...prev.slice(0, 199)]) + } + }, + error: () => { + clearTimeout(timeout) + setIsLoading(false) + }, + }) + + return () => { + clearTimeout(timeout) + sub.unsubscribe() + } + }, [getDetectionAlertStream]) + + const filteredAlerts = useMemo( + () => + alerts.filter(a => { + if (sourceFilter !== 'all' && a.source !== sourceFilter) return false + if (severityFilter !== 'all' && a.severity !== severityFilter) return false + return true + }), + [alerts, sourceFilter, severityFilter] + ) + + const stats = useMemo( + () => ({ + total: alerts.length, + critical: alerts.filter(a => a.severity === 'Critical').length, + high: alerts.filter(a => a.severity === 'High').length, + ml: alerts.filter(a => a.source === 'Ml').length, + rule: alerts.filter(a => a.source === 'Rule').length, + fusion: alerts.filter(a => a.source === 'Fusion').length, + uniqueIPs: new Set([...alerts.map(a => a.src_ip), ...alerts.map(a => a.dst_ip)]).size, + }), + [alerts] + ) + + const handleAlertClick = useCallback((idx: number) => setSelectedIdx(idx), []) + const handleCloseDetails = useCallback(() => setSelectedIdx(null), []) + const togglePause = useCallback(() => setIsPaused(p => !p), []) + + if (isLoading) { + return ( + <> + 威脅檢測 - NetGuardia + +
+ +
+
+ + ) + } + + return ( + <> + + 威脅檢測 - NetGuardia + + + + + + {notification && ( + setNotification(null)} + /> + )} + + + {/* Page title */} + +

+ + 威脅檢測 +

+

AI 與規則引擎聯合偵測,實時監控網路威脅

+
+ + {/* Stats cards */} +
+ {( + [ + { label: '總警報', value: stats.total, color: '#2563eb', icon: faList }, + { label: '嚴重', value: stats.critical, color: '#dc2626', icon: faExclamationTriangle }, + { label: '高', value: stats.high, color: '#d97706', icon: faExclamationTriangle }, + { label: 'AI', value: stats.ml, color: '#7c3aed', icon: faBrain }, + { label: '規則', value: stats.rule, color: '#2563eb', icon: faScroll }, + { label: '融合', value: stats.fusion, color: '#dc2626', icon: faLayerGroup }, + { label: '涉及 IP', value: stats.uniqueIPs, color: '#059669', icon: faGlobe }, + ] as const + ).map(({ label, value, color, icon }, i) => ( + +
+ +
+
+

{label}

+

{value}

+
+
+ ))} +
+ + {/* Controls */} + +
+ {/* Pause toggle */} + + + {/* Live indicator */} +
+
+ + {isPaused ? '已暫停' : '監控中'} + +
+ +
+ + {/* Source filter */} +
+ 來源: + {(['all', 'Ml', 'Rule', 'Fusion'] as const).map(s => ( + setSourceFilter(s)} + activeColor={ + s === 'all' ? '#374151' + : s === 'Ml' ? '#7c3aed' + : s === 'Rule' ? '#2563eb' + : '#dc2626' + } + > + {s === 'all' ? '全部' : SOURCE_CONFIG[s].label} + + ))} +
+ +
+ + {/* Severity filter */} +
+ 嚴重度: + {(['all', 'Critical', 'High'] as const).map(s => ( + setSeverityFilter(s)} + activeColor={ + s === 'all' ? '#374151' : s === 'Critical' ? '#dc2626' : '#d97706' + } + > + {s === 'all' ? '全部' : SEVERITY_CONFIG[s].label} + + ))} +
+ +
+ {filteredAlerts.length} / {alerts.length} 筆 +
+
+ + + {/* Main content */} +
+ +
+
+

+ + 安全警報 +

+ {filteredAlerts.length} 條 +
+ +
+ {filteredAlerts.length === 0 ? ( +
+ +

+ {alerts.length === 0 ? '尚無警報' : '無符合條件的警報'} +

+

+ {alerts.length === 0 + ? '系統偵測到威脅時將在此顯示' + : '請調整過濾條件'} +

+
+ ) : ( +
+ {filteredAlerts.map((alert, i) => ( + + ))} +
+ )} +
+
+
+ + {selectedIdx !== null && filteredAlerts[selectedIdx] && ( + + + + )} +
+ + + ) +} + +export default Detection