import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { InfoTip } from './ui/Tooltip'; import { TIPS } from './ui/tooltips'; interface IncidentCluster { id: string; score: number; severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; total_detections: number; unique_ips: number; subnet?: string; sample_ip?: string; ja4?: string; primary_ua?: string; primary_target?: string; countries: { code: string; percentage: number }[]; asn?: string; first_seen: string; last_seen: string; trend: 'up' | 'down' | 'stable'; trend_percentage: number; hits_per_second?: number; } interface MetricsSummary { total_detections: number; critical_count: number; high_count: number; medium_count: number; low_count: number; unique_ips: number; } interface BaselineMetric { today: number; yesterday: number; pct_change: number; } interface BaselineData { total_detections: BaselineMetric; unique_ips: BaselineMetric; critical_alerts: BaselineMetric; } export function IncidentsView() { const navigate = useNavigate(); const [clusters, setClusters] = useState([]); const [metrics, setMetrics] = useState(null); const [baseline, setBaseline] = useState(null); const [loading, setLoading] = useState(true); const [selectedClusters, setSelectedClusters] = useState>(new Set()); useEffect(() => { const fetchIncidents = async () => { setLoading(true); try { const metricsResponse = await fetch('/api/metrics'); if (metricsResponse.ok) { const metricsData = await metricsResponse.json(); setMetrics(metricsData.summary); } const baselineResponse = await fetch('/api/metrics/baseline'); if (baselineResponse.ok) { setBaseline(await baselineResponse.json()); } const clustersResponse = await fetch('/api/incidents/clusters'); if (clustersResponse.ok) { const clustersData = await clustersResponse.json(); setClusters(clustersData.items || []); } } catch (error) { console.error('Error fetching incidents:', error); } finally { setLoading(false); } }; fetchIncidents(); const interval = setInterval(fetchIncidents, 60000); return () => clearInterval(interval); }, []); const toggleCluster = (id: string) => { const newSelected = new Set(selectedClusters); if (newSelected.has(id)) { newSelected.delete(id); } else { newSelected.add(id); } setSelectedClusters(newSelected); }; const selectAll = () => { if (selectedClusters.size === clusters.length) { setSelectedClusters(new Set()); } else { setSelectedClusters(new Set(clusters.map(c => c.id))); } }; const getSeverityColor = (severity: string) => { switch (severity) { case 'CRITICAL': return 'border-red-500 bg-red-500/10'; case 'HIGH': return 'border-orange-500 bg-orange-500/10'; case 'MEDIUM': return 'border-yellow-500 bg-yellow-500/10'; case 'LOW': return 'border-green-500 bg-green-500/10'; default: return 'border-gray-500 bg-gray-500/10'; } }; const getSeverityBadgeColor = (severity: string) => { switch (severity) { case 'CRITICAL': return 'bg-red-500 text-white'; case 'HIGH': return 'bg-orange-500 text-white'; case 'MEDIUM': return 'bg-yellow-500 text-white'; case 'LOW': return 'bg-green-500 text-white'; default: return 'bg-gray-500 text-white'; } }; const getCountryFlag = (code: string) => { return code.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)); }; if (loading) { return (
Chargement...
); } return (
{/* Header with Quick Search */}

SOC Dashboard

Surveillance en temps réel - 24 dernières heures

{/* Baseline comparison */} {baseline && (
{([ { key: 'total_detections', label: 'Détections 24h', icon: '📊', tip: TIPS.total_detections_stat }, { key: 'unique_ips', label: 'IPs uniques', icon: '🖥️', tip: TIPS.unique_ips_stat }, { key: 'critical_alerts', label: 'Alertes CRITICAL', icon: '🔴', tip: TIPS.risk_critical }, ] as { key: keyof BaselineData; label: string; icon: string; tip: string }[]).map(({ key, label, icon, tip }) => { const m = baseline[key]; const up = m.pct_change > 0; const neutral = m.pct_change === 0; return (
{icon}
{label}
{m.today.toLocaleString(navigator.language || undefined)}
hier: {m.yesterday.toLocaleString(navigator.language || undefined)}
{neutral ? '=' : up ? `▲ +${m.pct_change}%` : `▼ ${m.pct_change}%`}
); })}
)} {/* Critical Metrics */} {metrics && (
0 ? 'Requiert action immédiate' : 'Aucune'} color="bg-red-500/20" trend={metrics.critical_count > 10 ? 'up' : 'stable'} />
)} {/* Bulk Actions */} {selectedClusters.size > 0 && (
{selectedClusters.size} incidents sélectionnés
)} {/* Main content: incidents list (2/3) + top threats table (1/3) */}
{/* Incidents list — 2/3 */}

Incidents Prioritaires

{clusters.map((cluster) => (
{/* Checkbox */} toggleCluster(cluster.id)} className="mt-1 w-4 h-4 rounded bg-background-card border-background-card text-accent-primary focus:ring-accent-primary" /> {/* Content */}
{cluster.severity} {cluster.id} | {cluster.subnet || ''}
{cluster.score}/100
Score de risque
IPs
{cluster.unique_ips}
Détections
{cluster.total_detections}
Pays
{cluster.countries[0] && ( <> {getCountryFlag(cluster.countries[0].code)} {cluster.countries[0].code} )}
ASN
AS{cluster.asn || '?'}
Tendance
{cluster.trend === 'up' ? '↑' : cluster.trend === 'down' ? '↓' : '→'} {cluster.trend_percentage}%
{cluster.ja4 && (
JA4 Principal
{cluster.ja4}
)}
))} {clusters.length === 0 && (

Aucun incident actif

Le système ne détecte aucun incident prioritaire en ce moment.

)}
{/* end col-span-2 */} {/* Top threats sidebar — 1/3 */}

🔥 Top Menaces

{clusters.slice(0, 12).map((cluster, index) => (
navigate(`/investigation/${cluster.sample_ip || cluster.subnet?.split('/')[0] || ''}`)} > {index + 1}
{cluster.sample_ip || cluster.subnet?.split('/')[0] || 'Unknown'}
{cluster.countries[0] && ( {getCountryFlag(cluster.countries[0].code)} {cluster.countries[0].code} )} AS{cluster.asn || '?'}
80 ? 'bg-red-500 text-white' : cluster.score > 60 ? 'bg-orange-500 text-white' : cluster.score > 40 ? 'bg-yellow-500 text-white' : 'bg-green-500 text-white' }`}> {cluster.score} {cluster.trend === 'up' ? '↑' : cluster.trend === 'down' ? '↓' : '→'}
))} {clusters.length === 0 && (
Aucune menace active
)}
{/* end grid */}
); } // Metric Card Component function MetricCard({ title, value, subtitle, color, trend }: { title: string; value: string | number; subtitle: string; color: string; trend: 'up' | 'down' | 'stable'; }) { return (

{title}

{trend === 'up' ? '↑' : trend === 'down' ? '↓' : '→'}

{value}

{subtitle}

); } // ─── Mini Heatmap ───────────────────────────────────────────────────────────── interface HeatmapHour { hour: number; hits: number; unique_ips: number; } function MiniHeatmap() { const [data, setData] = useState([]); useEffect(() => { fetch('/api/heatmap/hourly') .then(r => r.ok ? r.json() : null) .then(d => { if (d) setData(d.hours ?? d.items ?? []); }) .catch(() => {}); }, []); if (data.length === 0) return null; const maxHits = Math.max(...data.map(d => d.hits), 1); const barColor = (hits: number) => { const pct = (hits / maxHits) * 100; if (pct >= 75) return 'bg-red-500/70'; if (pct >= 50) return 'bg-purple-500/60'; if (pct >= 25) return 'bg-blue-500/50'; if (pct >= 5) return 'bg-blue-400/30'; return 'bg-slate-700/30'; }; return (
⏱️ Activité par heure (72h)
{data.map((d, i) => (
{d.hits.toLocaleString()} hits — {d.unique_ips} IPs
{[0, 6, 12, 18].includes(d.hour) ? `${d.hour}h` : '\u00a0'}
))}
); }