diff --git a/.research/frontend-refactor/detection.tsx b/.research/frontend-refactor/detection.tsx index f8ed249..0ad5d72 100644 --- a/.research/frontend-refactor/detection.tsx +++ b/.research/frontend-refactor/detection.tsx @@ -3,28 +3,30 @@ import Head from 'next/head' import { motion, AnimatePresence } from 'framer-motion' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { + faRobot, faShieldAlt, faExclamationTriangle, faFilter, faCheck, faGlobe, faTimes, + faClock, faArrowRight, faEye, faList, faPause, faPlay, - faBrain, - faScroll, - faLayerGroup, } from '@fortawesome/free-solid-svg-icons' import { WebsocketContext } from '../providers/WebSocketProvider' +import { useTheme } from '../providers/ThemeProvider' import { putData } from '../utils/connectionUtils' import { urls } from '../config' import Layout from '../components/Layout' import LoadingSpinner from '../components/LoadingSpinner' -// --- Types (mirrors backend UnifiedAlert) --- +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- type AlertSource = 'ml' | 'rule' | 'fusion' type AlertSeverity = 'high' | 'critical' @@ -47,84 +49,129 @@ interface UnifiedAlert { rule_msg: string | null } -// --- Display config --- - -const SOURCE_CONFIG = { - ml: { - label: 'ML', - color: '#7c3aed', - bgColor: 'rgba(124,58,237,0.10)', - icon: faBrain, - }, - rule: { - label: 'Rule', - color: '#2563eb', - bgColor: 'rgba(37,99,235,0.10)', - icon: faScroll, - }, - fusion: { - label: 'Fusion', - color: '#dc2626', - bgColor: 'rgba(220,38,38,0.10)', - icon: faLayerGroup, - }, -} as const - -const SEVERITY_CONFIG = { - critical: { - label: 'Critical', - color: '#dc2626', - bgColor: 'rgba(220,38,38,0.10)', - icon: faExclamationTriangle, - }, - high: { - label: 'High', - 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', +interface PriorityInfo { + color: string + bgColor: string + label: string } -const formatProtocol = (proto: number): string => - PROTOCOL_MAP[proto] ?? `Proto(${proto})` - -const formatTimestamp = (unix: number): string => - new Date(unix * 1000).toLocaleString('en-US', { - 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 } +interface AlertDetailsProps { + log: UnifiedAlert + onClose: () => void + showNotification: (message: string, type: 'success' | 'error' | 'warning' | 'info') => void +} + +interface AlertItemProps { + log: UnifiedAlert + index: number + onClick: (index: number) => void +} + +interface FilterButtonProps { + isActive: boolean + onClick: () => void + children: React.ReactNode + className?: string + activeClassName?: string + activeHoverClassName?: string +} + +// --------------------------------------------------------------------------- +// Constants & helpers +// --------------------------------------------------------------------------- + +const UPDATE_INTERVALS = [ + { label: 'immediate', value: 0 }, + { label: '1s', value: 1000 }, + { label: '3s', value: 3000 }, + { label: '5s', value: 5000 }, + { label: '10s', value: 10000 }, +] + +const SOURCE_CONFIG: Record = { + ml: { label: 'ML', color: '#7c3aed', bgColor: 'rgba(124,58,237,0.1)' }, + rule: { label: 'Rule', color: '#0284c7', bgColor: 'rgba(2,132,199,0.1)' }, + fusion: { label: 'Fusion', color: '#0f766e', bgColor: 'rgba(15,118,110,0.1)' }, +} + +const SEVERITY_CONFIG: Record = { + critical: { color: '#e53e3e', bgColor: 'rgba(239,68,68,0.1)', label: 'Critical' }, + high: { color: '#dd6b20', bgColor: 'rgba(251,146,60,0.1)', label: 'High' }, +} + +const getProtocolName = (protocol: number): string => { + switch (protocol) { + case 1: return 'ICMP' + case 6: return 'TCP' + case 17: return 'UDP' + case 47: return 'GRE' + case 50: return 'ESP' + case 51: return 'AH' + case 58: return 'ICMPv6' + default: return `Proto ${protocol}` + } +} + +const formatTimestamp = (timestamp: number): string => { + if (!timestamp) return 'N/A' + return new Date(timestamp * 1000).toLocaleString('en-GB', { + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit', + }) +} + +const parseAlertData = (rawData: any): UnifiedAlert | null => { + try { + const data = typeof rawData === 'string' ? JSON.parse(rawData) : rawData + if (!data || typeof data !== 'object') return null + + const source: AlertSource = data.source === 'ml' || data.source === 'rule' || data.source === 'fusion' + ? data.source : 'ml' + const severity: AlertSeverity = data.severity === 'critical' ? 'critical' : 'high' + + return { + timestamp: data.timestamp ?? 0, + flow_key: data.flow_key ?? '', + src_ip: data.src_ip ?? 'Unknown', + dst_ip: data.dst_ip ?? 'Unknown', + src_port: data.src_port ?? 0, + dst_port: data.dst_port ?? 0, + protocol: data.protocol ?? 0, + source, + severity, + is_attack: data.is_attack ?? false, + attack_type: data.attack_type ?? null, + confidence: data.confidence ?? 0, + ae_score: data.ae_score ?? 0, + rule_sid: data.rule_sid ?? null, + rule_msg: data.rule_msg ?? null, + } + } catch { + return null + } +} + +// --------------------------------------------------------------------------- +// Notification +// --------------------------------------------------------------------------- + const Notification: React.FC = ({ message, type, onClose }) => { useEffect(() => { - const t = setTimeout(onClose, 4000) - return () => clearTimeout(t) + const timer = setTimeout(onClose, 4000) + return () => clearTimeout(timer) }, [onClose]) - const styles = { + const typeStyles = { success: 'bg-green-100 border-green-400 text-green-700', - error: 'bg-red-100 border-red-400 text-red-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', + info: 'bg-blue-100 border-blue-400 text-blue-700', } return ( @@ -132,202 +179,58 @@ const Notification: React.FC = ({ message, type, onClose }) = initial={{ opacity: 0, y: -50, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: -50, scale: 0.95 }} - className={`fixed top-4 right-4 z-50 px-4 py-3 border rounded-lg shadow-lg max-w-md ${styles[type]}`} + className={`fixed top-4 right-4 z-50 px-4 py-3 border rounded-lg shadow-lg max-w-md ${typeStyles[type]}`} >
{message} - +
) } -// --- FilterButton --- +// --------------------------------------------------------------------------- +// AlertDetails +// --------------------------------------------------------------------------- -interface FilterButtonProps { - isActive: boolean - onClick: () => void - children: React.ReactNode - activeColor?: string -} +const AlertDetails: React.FC = ({ log, onClose, showNotification }) => { + const { actualTheme } = useTheme() + const isDark = actualTheme === 'dark' + const priorityInfo = SEVERITY_CONFIG[log.severity] + const sourceInfo = SOURCE_CONFIG[log.source] -const FilterButton: React.FC = ({ - isActive, - onClick, - children, - activeColor = '#374151', -}) => ( - -) + const isIPv6Source = log.src_ip.includes(':') && !log.src_ip.includes('.') + const isIPv6Dest = log.dst_ip.includes(':') && !log.dst_ip.includes('.') -// --- AlertItem --- + const handleBlockIP = useCallback((ip: string, port: number, isSource: boolean, blockAllPorts = false) => { + const isIPv6 = isSource ? isIPv6Source : isIPv6Dest + const formattedIp = isIPv6 ? `[${ip}]` : ip + const url = isIPv6 ? urls.access_control.ipv6.black_list : urls.access_control.ipv4.black_list + const portToSend = blockAllPorts ? '0' : String(port) + const displayPort = blockAllPorts ? '*' : String(port) -interface AlertItemProps { - alert: UnifiedAlert - index: number - onClick: (index: number) => void -} + putData( + url, + `${formattedIp}:${portToSend}`, + () => showNotification(`Successfully added to blacklist: ${ip}:${displayPort}`, 'success'), + (error) => showNotification(`Failed to add to blacklist: ${error.message}`, 'error'), + ) + }, [isIPv6Source, isIPv6Dest, showNotification]) -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 handleAllowIP = useCallback((ip: string, port: number, isSource: boolean, allowAllPorts = false) => { + const isIPv6 = isSource ? isIPv6Source : isIPv6Dest + const formattedIp = isIPv6 ? `[${ip}]` : ip + const url = isIPv6 ? urls.access_control.ipv6.white_list : urls.access_control.ipv4.white_list + const portToSend = allowAllPorts ? '0' : String(port) + const displayPort = allowAllPorts ? '*' : String(port) - 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(`Added to blocklist: ${ip}:${blockAll ? '*' : port}`, 'success'), - (err: any) => showNotification(`Failed to add to blocklist: ${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(`Added to allowlist: ${ip}:${allowAll ? '*' : port}`, 'success'), - (err: any) => showNotification(`Failed to add to allowlist: ${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}
-
- - - - -
-
- ) + putData( + url, + `${formattedIp}:${portToSend}`, + () => showNotification(`Successfully added to whitelist: ${ip}:${displayPort}`, 'success'), + (error) => showNotification(`Failed to add to whitelist: ${error.message}`, 'error'), + ) + }, [isIPv6Source, isIPv6Dest, showNotification]) return ( @@ -335,159 +238,339 @@ const AlertDetails: React.FC = ({ alert, onClose, showNotific initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 20 }} - className="bg-white rounded-lg shadow-md p-6 max-h-[calc(100vh-2rem)] overflow-y-auto" + className={`rounded-lg shadow-md p-6 max-h-[600px] overflow-y-auto ${isDark ? 'bg-gray-600' : 'bg-white'}`} > + {/* Header */}
-

+

Alert Details

-
- {/* Basic metadata */} -
-
- Time: - {formatTimestamp(alert.timestamp)} + {/* Basic info */} +
+
+ Timestamp: + {formatTimestamp(log.timestamp)}
- Source: + Severity: - - {srcCfg.label} + + {priorityInfo.label}
- Severity: + Source: - - {sevCfg.label} + {sourceInfo.label}
-
- Protocol: - {formatProtocol(alert.protocol)} +
+ Attack Type: + {log.attack_type ?? '—'} +
+
+ Protocol: + {getProtocolName(log.protocol)} +
+
+ Is Attack: + + {log.is_attack ? 'Yes' : 'No'} +
- {/* Attack / rule message */} - {(alert.attack_type || alert.rule_msg) && ( + {/* ML scores — only when source is ml or fusion */} + {(log.source === 'ml' || log.source === 'fusion') && (
- - {alert.source === 'ml' ? 'Attack Type' : 'Rule Message'}: - -
-

{alert.attack_type ?? alert.rule_msg}

-
-
- )} - - {/* ML inference metrics */} - {(alert.source === 'ml' || alert.source === 'fusion') && ( -
-

ML Inference Metrics

-
- Confidence: -
-
-
0.8 ? '#dc2626' : '#d97706', - }} - /> + ML Score Details: +
+ {[ + { label: 'Confidence', value: log.confidence, color: 'bg-blue-500' }, + { label: 'AE Score', value: log.ae_score, color: 'bg-purple-500' }, + ].map(({ label, value, color }) => ( +
+ {label}: +
+
+
+
+ + {(value * 100).toFixed(1)}% + +
- - {(alert.confidence * 100).toFixed(1)}% - -
-
-
- AE Score: - {alert.ae_score.toFixed(6)} + ))}
)} - {/* Rule SID */} - {(alert.source === 'rule' || alert.source === 'fusion') && alert.rule_sid !== null && ( + {/* Rule details — only when source is rule or fusion */} + {(log.source === 'rule' || log.source === 'fusion') && (log.rule_sid !== null || log.rule_msg !== null) && (
-

Rule Info

-
- SID: - {alert.rule_sid} + Rule Match: +
+ {log.rule_sid !== null && ( +
+ SID: + {log.rule_sid} +
+ )} + {log.rule_msg !== null && ( +
+ Message: + {log.rule_msg} +
+ )}
)} - {/* Connection */} + {/* Connection details */}
-

Connection Details

-
+

Connection Details

+
-
Source
-
{alert.src_ip}
-
:{alert.src_port}
+
Source
+
{log.src_ip}
+
Port: {log.src_port}
- +
-
Destination
-
{alert.dst_ip}
-
:{alert.dst_port}
+
Destination
+
{log.dst_ip}
+
Port: {log.dst_port}
{/* IP management */} -
-

IP Management

- - +
+

IP Management

+ + {[ + { label: 'Source IP', ip: log.src_ip, port: log.src_port, isSource: true, bgClass: isDark ? 'bg-blue-900/30' : 'bg-blue-50' }, + { label: 'Destination IP', ip: log.dst_ip, port: log.dst_port, isSource: false, bgClass: isDark ? 'bg-red-900/30' : 'bg-red-50' }, + ].map(({ label, ip, port, isSource, bgClass }) => ( +
+
{label}: {ip}
+
+ + + + +
+
+ ))}
) } -// --- Main page --- +// --------------------------------------------------------------------------- +// AlertItem +// --------------------------------------------------------------------------- -const Detection: React.FC = () => { - // NOTE: Replace getAiAlertStream with the WebsocketContext key that connects - // to /detection/websocket/alert (unified alert stream). +const AlertItem: React.FC = ({ log, index, onClick }) => { + const { actualTheme } = useTheme() + const isDark = actualTheme === 'dark' + const priorityInfo = SEVERITY_CONFIG[log.severity] + const sourceInfo = SOURCE_CONFIG[log.source] + + const handleClick = useCallback(() => onClick(index), [index, onClick]) + + return ( + +
+
+ +
+
+
+ + {formatTimestamp(log.timestamp)} + +
+ + {sourceInfo.label} + + {log.attack_type && ( + + {log.attack_type} + + )} +
+
+
+ {log.src_ip}:{log.src_port} + + {log.dst_ip}:{log.dst_port} + ({getProtocolName(log.protocol)}) +
+
+ + {priorityInfo.label} + + {(log.source === 'ml' || log.source === 'fusion') && ( + + {(log.confidence * 100).toFixed(0)}% confidence + + )} + {(log.source === 'rule' || log.source === 'fusion') && log.rule_sid !== null && ( + + SID {log.rule_sid} + + )} +
+
+
+
+ ) +} + +// --------------------------------------------------------------------------- +// FilterButton +// --------------------------------------------------------------------------- + +const FilterButton: React.FC = ({ + isActive, onClick, children, + className = '', activeClassName = '', activeHoverClassName = '', +}) => { + const { actualTheme } = useTheme() + const isDark = actualTheme === 'dark' + + return ( + + ) +} + +// --------------------------------------------------------------------------- +// Main page +// --------------------------------------------------------------------------- + +const AIDetection: React.FC = () => { const { getDetectionAlertStream } = useContext(WebsocketContext) + const { actualTheme } = useTheme() + const isDark = actualTheme === 'dark' - 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 [logs, setLogs] = useState([]) + const [selectedLogIndex, setSelectedLogIndex] = useState(null) + const [severityFilter, setSeverityFilter] = useState('all') + const [sourceFilter, setSourceFilter] = 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) + const [updateInterval, setUpdateInterval] = useState(0) + const [lastUpdateTime, setLastUpdateTime] = useState(null) - // 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 bufferRef = useRef([]) + const updateIntervalRef = useRef(updateInterval) + const isPausedRef = useRef(isPaused) - const showNotification = useCallback( - (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'success') => { - setNotification({ message, type }) - }, - [] - ) + useEffect(() => { updateIntervalRef.current = updateInterval }, [updateInterval]) + useEffect(() => { isPausedRef.current = isPaused }, [isPaused]) + + const showNotification = useCallback((message: string, type: 'success' | 'error' | 'warning' | 'info' = 'success') => { + setNotification({ message, type }) + }, []) + + const closeNotification = useCallback(() => setNotification(null), []) + + const flushBuffer = useCallback(() => { + if (bufferRef.current.length === 0) return + const newEntries = [...bufferRef.current] + bufferRef.current = [] + setLogs(prev => [...newEntries, ...prev].slice(0, 200)) + setLastUpdateTime(new Date()) + }, []) + + useEffect(() => { + if (updateInterval === 0 || isPaused) return + const timer = setInterval(flushBuffer, updateInterval) + return () => clearInterval(timer) + }, [updateInterval, isPaused, flushBuffer]) + + // Reads isPaused via ref so this callback never changes, avoiding WS re-subscription on pause toggle + const processNewLogData = useCallback((rawData: any) => { + if (isPausedRef.current) return + const alert = parseAlertData(rawData) + if (!alert) return + + if (updateIntervalRef.current === 0) { + setLogs(prev => [alert, ...prev.slice(0, 199)]) + setLastUpdateTime(new Date()) + } else { + bufferRef.current.push(alert) + } + }, []) useEffect(() => { if (!getDetectionAlertStream) { @@ -495,59 +578,52 @@ const Detection: React.FC = () => { return } - const timeout = setTimeout(() => setIsLoading(false), 3000) + const loadingTimeout = setTimeout(() => setIsLoading(false), 3000) - const sub = getDetectionAlertStream().subscribe({ - next: (data: UnifiedAlert) => { - clearTimeout(timeout) + const subscription = getDetectionAlertStream().subscribe({ + next: (data: any) => { + clearTimeout(loadingTimeout) + processNewLogData(data) setIsLoading(false) - if (!isPausedRef.current) { - setAlerts(prev => [data, ...prev.slice(0, 199)]) - } }, error: () => { - clearTimeout(timeout) + clearTimeout(loadingTimeout) setIsLoading(false) }, }) return () => { - clearTimeout(timeout) - sub.unsubscribe() + clearTimeout(loadingTimeout) + subscription.unsubscribe() } - }, [getDetectionAlertStream]) + }, [getDetectionAlertStream, processNewLogData]) - 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 filteredLogs = useMemo(() => { + return logs.filter(log => { + const severityOk = severityFilter === 'all' || log.severity === severityFilter + const sourceOk = sourceFilter === 'all' || log.source === sourceFilter + return severityOk && sourceOk + }) + }, [logs, severityFilter, sourceFilter]) - 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 stats = useMemo(() => ({ + total: logs.length, + critical: logs.filter(l => l.severity === 'critical').length, + high: logs.filter(l => l.severity === 'high').length, + ml: logs.filter(l => l.source === 'ml').length, + rule: logs.filter(l => l.source === 'rule').length, + fusion: logs.filter(l => l.source === 'fusion').length, + uniqueIPs: new Set([...logs.map(l => l.src_ip), ...logs.map(l => l.dst_ip)]).size, + }), [logs]) - const handleAlertClick = useCallback((idx: number) => setSelectedIdx(idx), []) - const handleCloseDetails = useCallback(() => setSelectedIdx(null), []) - const togglePause = useCallback(() => setIsPaused(p => !p), []) + const handleAlertClick = useCallback((index: number) => setSelectedLogIndex(index), []) + const handleCloseDetails = useCallback(() => setSelectedLogIndex(null), []) + const togglePause = useCallback(() => setIsPaused(p => !p), []) if (isLoading) { return ( <> - Threat Detection - NetGuardia + Detection - NetGuardia
@@ -560,177 +636,195 @@ const Detection: React.FC = () => { return ( <> - Threat Detection - NetGuardia + Detection - NetGuardia {notification && ( - setNotification(null)} - /> + )} - {/* Page title */} - -

- + {/* Page header */} + +

+ Threat Detection

-

Real-time threat monitoring powered by ML and rule engine

+

+ Real-time alerts from ML inference, rule matching, and fusion engine +

{/* Stats cards */} -
- {( - [ - { label: 'Total', value: stats.total, color: '#2563eb', icon: faList }, - { label: 'Critical', value: stats.critical, color: '#dc2626', icon: faExclamationTriangle }, - { label: 'High', value: stats.high, color: '#d97706', icon: faExclamationTriangle }, - { label: 'ML', value: stats.ml, color: '#7c3aed', icon: faBrain }, - { label: 'Rule', value: stats.rule, color: '#2563eb', icon: faScroll }, - { label: 'Fusion', value: stats.fusion, color: '#dc2626', icon: faLayerGroup }, - { label: 'Unique IPs', value: stats.uniqueIPs, color: '#059669', icon: faGlobe }, - ] as const - ).map(({ label, value, color, icon }, i) => ( +
+ {[ + { label: 'Total', value: stats.total, icon: faList, bg: 'bg-blue-500' }, + { label: 'Critical', value: stats.critical, icon: faExclamationTriangle, bg: 'bg-red-500' }, + { label: 'High', value: stats.high, icon: faExclamationTriangle, bg: 'bg-orange-500' }, + { label: 'ML', value: stats.ml, icon: faRobot, bg: 'bg-purple-500' }, + { label: 'Rule', value: stats.rule, icon: faShieldAlt, bg: 'bg-sky-500' }, + { label: 'Fusion', value: stats.fusion, icon: faShieldAlt, bg: 'bg-teal-500' }, + { label: 'Unique IPs', value: stats.uniqueIPs, icon: faGlobe, bg: 'bg-indigo-500' }, + ].map(({ label, value, icon, bg }, i) => ( -
- -
-
-

{label}

-

{value}

+
+
+

{label}

+

{value}

+
+
+ +
))}
- {/* Controls */} + {/* Controls & filters */} -
- {/* Pause toggle */} + {/* System controls */} +
+
+ + Update Interval: + +
+ - {/* Live indicator */} -
-
- - {isPaused ? 'Paused' : 'Live'} - -
+ {lastUpdateTime && ( +
+ Last Update: {lastUpdateTime.toLocaleTimeString()} +
+ )} +
-
+ {/* Severity filter */} +
+ Severity: + { setSeverityFilter('all'); setSelectedLogIndex(null) }}> + All + + { setSeverityFilter('critical'); setSelectedLogIndex(null) }} + activeClassName="bg-red-600 text-white shadow-lg" activeHoverClassName="hover:bg-red-700" + > + Critical ({stats.critical}) + + { setSeverityFilter('high'); setSelectedLogIndex(null) }} + activeClassName="bg-orange-600 text-white shadow-lg" activeHoverClassName="hover:bg-orange-700" + > + High ({stats.high}) + +
- {/* Source filter */} -
- Source: - {(['all', 'ml', 'rule', 'fusion'] as const).map(s => ( - setSourceFilter(s)} - activeColor={ - s === 'all' ? '#374151' - : s === 'ml' ? '#7c3aed' - : s === 'rule' ? '#2563eb' - : '#dc2626' - } - > - {s === 'all' ? 'All' : SOURCE_CONFIG[s].label} - - ))} -
- -
- - {/* Severity filter */} -
- Severity: - {(['all', 'critical', 'high'] as const).map(s => ( - setSeverityFilter(s)} - activeColor={ - s === 'all' ? '#374151' : s === 'critical' ? '#dc2626' : '#d97706' - } - > - {s === 'all' ? 'All' : SEVERITY_CONFIG[s].label} - - ))} -
- -
- {filteredAlerts.length} / {alerts.length} alerts -
+ {/* Source filter */} +
+ Source: + { setSourceFilter('all'); setSelectedLogIndex(null) }}> + All + + { setSourceFilter('ml'); setSelectedLogIndex(null) }} + activeClassName="bg-purple-600 text-white shadow-lg" activeHoverClassName="hover:bg-purple-700" + > + ML ({stats.ml}) + + { setSourceFilter('rule'); setSelectedLogIndex(null) }} + activeClassName="bg-sky-600 text-white shadow-lg" activeHoverClassName="hover:bg-sky-700" + > + Rule ({stats.rule}) + + { setSourceFilter('fusion'); setSelectedLogIndex(null) }} + activeClassName="bg-teal-600 text-white shadow-lg" activeHoverClassName="hover:bg-teal-700" + > + Fusion ({stats.fusion}) +
{/* Main content */} -
+
-
-
-

- - Security Alerts -

- {filteredAlerts.length} alerts +
+
+
+

+ + Security Alerts +

+ + {filteredLogs.length} Alerts + +
-
- {filteredAlerts.length === 0 ? ( +
+ {filteredLogs.length === 0 ? (
- -

- {alerts.length === 0 ? 'No alerts' : 'No matching alerts'} + +

+ {logs.length === 0 ? 'No Alerts Detected' : 'No Matching Alerts'}

-

- {alerts.length === 0 +

+ {logs.length === 0 ? 'Alerts will appear here when threats are detected' - : 'Try adjusting the filters'} + : 'Try adjusting filter criteria'}

) : ( -
- {filteredAlerts.map((alert, i) => ( +
+ {filteredLogs.map((log, index) => ( ))} @@ -740,14 +834,14 @@ const Detection: React.FC = () => {
- {selectedIdx !== null && filteredAlerts[selectedIdx] && ( + {selectedLogIndex !== null && filteredLogs[selectedLogIndex] && ( @@ -759,4 +853,4 @@ const Detection: React.FC = () => { ) } -export default Detection +export default AIDetection