import { useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useDetections } from '../hooks/useDetections'; type SortField = 'detected_at' | 'threat_level' | 'anomaly_score' | 'src_ip' | 'country_code' | 'asn_number' | 'host' | 'hits' | 'hit_velocity'; type SortOrder = 'asc' | 'desc'; interface ColumnConfig { key: string; label: string; visible: boolean; sortable: boolean; } export function DetectionsList() { const [searchParams, setSearchParams] = useSearchParams(); const page = parseInt(searchParams.get('page') || '1'); const modelName = searchParams.get('model_name') || undefined; const search = searchParams.get('search') || undefined; const sortField = (searchParams.get('sort_by') || searchParams.get('sort') || 'anomaly_score') as SortField; const sortOrder = (searchParams.get('sort_order') || searchParams.get('order') || 'asc') as SortOrder; const { data, loading, error } = useDetections({ page, page_size: 25, model_name: modelName, search, sort_by: sortField, sort_order: sortOrder, }); const [searchInput, setSearchInput] = useState(search || ''); const [showColumnSelector, setShowColumnSelector] = useState(false); const [groupByIP, setGroupByIP] = useState(true); // Grouper par IP par défaut // Configuration des colonnes const [columns, setColumns] = useState([ { key: 'ip_ja4', label: 'IP / JA4', visible: true, sortable: true }, { key: 'host', label: 'Host', visible: true, sortable: true }, { key: 'client_headers', label: 'Client Headers', visible: false, sortable: false }, { key: 'model_name', label: 'Modèle', visible: true, sortable: true }, { key: 'anomaly_score', label: 'Score', visible: true, sortable: true }, { key: 'hits', label: 'Hits', visible: true, sortable: true }, { key: 'hit_velocity', label: 'Velocity', visible: true, sortable: true }, { key: 'asn', label: 'ASN', visible: true, sortable: true }, { key: 'country', label: 'Pays', visible: true, sortable: true }, { key: 'detected_at', label: 'Date', visible: true, sortable: true }, ]); const handleSearch = (e: React.FormEvent) => { e.preventDefault(); const newParams = new URLSearchParams(searchParams); if (searchInput.trim()) { newParams.set('search', searchInput.trim()); } else { newParams.delete('search'); } newParams.set('page', '1'); setSearchParams(newParams); }; const handleFilterChange = (key: string, value: string) => { const newParams = new URLSearchParams(searchParams); if (value) { newParams.set(key, value); } else { newParams.delete(key); } newParams.set('page', '1'); setSearchParams(newParams); }; const handleSort = (field: SortField) => { const newParams = new URLSearchParams(searchParams); const currentSortField = newParams.get('sort_by') || 'detected_at'; const currentOrder = newParams.get('sort_order') || 'desc'; if (currentSortField === field) { // Inverser l'ordre ou supprimer le tri if (currentOrder === 'desc') { newParams.set('sort_order', 'asc'); } else { newParams.delete('sort_by'); newParams.delete('sort_order'); } } else { newParams.set('sort_by', field); newParams.set('sort_order', 'desc'); } setSearchParams(newParams); }; const toggleColumn = (key: string) => { setColumns(cols => cols.map(col => col.key === key ? { ...col, visible: !col.visible } : col )); }; const handlePageChange = (newPage: number) => { const newParams = new URLSearchParams(searchParams); newParams.set('page', newPage.toString()); setSearchParams(newParams); }; const getSortIcon = (field: SortField) => { if (sortField !== field) return '⇅'; return sortOrder === 'asc' ? '↑' : '↓'; }; // Par défaut, trier par score croissant (scores négatifs en premier) const getDefaultSortIcon = (field: SortField) => { if (!searchParams.has('sort_by') && !searchParams.has('sort')) { if (field === 'anomaly_score') return '↑'; return '⇅'; } return getSortIcon(field); }; if (loading) { return (
Chargement...
); } if (error) { return (

Erreur: {error.message}

); } if (!data) return null; // Traiter les données pour le regroupement par IP const processedData = (() => { if (!groupByIP) { return data; } // Grouper par IP const ipGroups = new Map(); const ipStats = new Map; hosts: Set; clientHeaders: Set; }>(); data.items.forEach(item => { if (!ipGroups.has(item.src_ip)) { ipGroups.set(item.src_ip, item); ipStats.set(item.src_ip, { first: new Date(item.detected_at), last: new Date(item.detected_at), count: 1, ja4s: new Set([item.ja4 || '']), hosts: new Set([item.host || '']), clientHeaders: new Set([item.client_headers || '']) }); } else { const stats = ipStats.get(item.src_ip)!; const itemDate = new Date(item.detected_at); if (itemDate < stats.first) stats.first = itemDate; if (itemDate > stats.last) stats.last = itemDate; stats.count++; if (item.ja4) stats.ja4s.add(item.ja4); if (item.host) stats.hosts.add(item.host); if (item.client_headers) stats.clientHeaders.add(item.client_headers); } }); return { ...data, items: Array.from(ipGroups.values()).map(item => ({ ...item, hits: ipStats.get(item.src_ip)!.count, first_seen: ipStats.get(item.src_ip)!.first.toISOString(), last_seen: ipStats.get(item.src_ip)!.last.toISOString(), unique_ja4s: Array.from(ipStats.get(item.src_ip)!.ja4s), unique_hosts: Array.from(ipStats.get(item.src_ip)!.hosts), unique_client_headers: Array.from(ipStats.get(item.src_ip)!.clientHeaders) })) }; })(); return (
{/* En-tête */}

Détections

{groupByIP ? processedData.items.length : data.items.length} {data.total} détections
{/* Toggle Grouper par IP */} {/* Sélecteur de colonnes */}
{showColumnSelector && (

Afficher les colonnes

{columns.map(col => ( ))}
)}
{/* Recherche */}
setSearchInput(e.target.value)} placeholder="Rechercher IP, JA4, Host..." className="bg-background-card border border-background-card rounded-lg px-4 py-2 text-text-primary placeholder-text-disabled focus:outline-none focus:border-accent-primary w-64" />
{/* Filtres */}
{(modelName || search || sortField) && ( )}
{/* Tableau */}
{columns.filter(col => col.visible).map(col => ( ))} {processedData.items.map((detection) => ( { window.location.href = `/detections/ip/${encodeURIComponent(detection.src_ip)}`; }} > {columns.filter(col => col.visible).map(col => { if (col.key === 'ip_ja4') { const detectionAny = detection as any; return ( ); } if (col.key === 'host') { const detectionAny = detection as any; return ( ); } if (col.key === 'client_headers') { const detectionAny = detection as any; return ( ); } if (col.key === 'model_name') { return ( ); } if (col.key === 'anomaly_score') { return ( ); } if (col.key === 'hits') { return ( ); } if (col.key === 'hit_velocity') { return ( ); } if (col.key === 'asn') { return ( ); } if (col.key === 'country') { return ( ); } if (col.key === 'detected_at') { const detectionAny = detection as any; return ( ); } return null; })} ))}
col.sortable && handleSort(col.key as SortField)} >
{col.label} {col.sortable && ( {getDefaultSortIcon(col.key as SortField)} )}
{detection.src_ip}
{groupByIP && detectionAny.unique_ja4s?.length > 0 ? (
{detectionAny.unique_ja4s.length} JA4{detectionAny.unique_ja4s.length > 1 ? 's' : ''} unique{detectionAny.unique_ja4s.length > 1 ? 's' : ''}
{detectionAny.unique_ja4s.slice(0, 3).map((ja4: string, idx: number) => (
{ja4}
))} {detectionAny.unique_ja4s.length > 3 && (
+{detectionAny.unique_ja4s.length - 3} autre{detectionAny.unique_ja4s.length - 3 > 1 ? 's' : ''}
)}
) : (
{detection.ja4 || '-'}
)}
{groupByIP && detectionAny.unique_hosts?.length > 0 ? (
{detectionAny.unique_hosts.length} Host{detectionAny.unique_hosts.length > 1 ? 's' : ''} unique{detectionAny.unique_hosts.length > 1 ? 's' : ''}
{detectionAny.unique_hosts.slice(0, 3).map((host: string, idx: number) => (
{host}
))} {detectionAny.unique_hosts.length > 3 && (
+{detectionAny.unique_hosts.length - 3} autre{detectionAny.unique_hosts.length - 3 > 1 ? 's' : ''}
)}
) : (
{detection.host || '-'}
)}
{groupByIP && detectionAny.unique_client_headers?.length > 0 ? (
{detectionAny.unique_client_headers.length} Header{detectionAny.unique_client_headers.length > 1 ? 's' : ''} unique{detectionAny.unique_client_headers.length > 1 ? 's' : ''}
{detectionAny.unique_client_headers.slice(0, 3).map((header: string, idx: number) => (
{header}
))} {detectionAny.unique_client_headers.length > 3 && (
+{detectionAny.unique_client_headers.length - 3} autre{detectionAny.unique_client_headers.length - 3 > 1 ? 's' : ''}
)}
) : (
{detection.client_headers || '-'}
)}
{detection.hits || 0}
10 ? 'text-threat-high' : detection.hit_velocity && detection.hit_velocity > 1 ? 'text-threat-medium' : 'text-text-primary' }`}> {detection.hit_velocity ? detection.hit_velocity.toFixed(2) : '0.00'} req/s
{detection.asn_org || detection.asn_number || '-'}
{detection.asn_number && (
AS{detection.asn_number}
)}
{detection.country_code ? ( {getFlag(detection.country_code)} ) : ( '-' )} {groupByIP && detectionAny.first_seen ? (
Premier:{' '} {new Date(detectionAny.first_seen).toLocaleDateString('fr-FR')}{' '} {new Date(detectionAny.first_seen).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
Dernier:{' '} {new Date(detectionAny.last_seen).toLocaleDateString('fr-FR')}{' '} {new Date(detectionAny.last_seen).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
) : ( <>
{new Date(detection.detected_at).toLocaleDateString('fr-FR')}
{new Date(detection.detected_at).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
)}
{data.items.length === 0 && (
Aucune détection trouvée
)}
{/* Pagination */} {data.total_pages > 1 && (

Page {data.page} sur {data.total_pages} ({data.total} détections)

)}
); } // Composant ModelBadge function ModelBadge({ model }: { model: string }) { const styles: Record = { Complet: 'bg-accent-primary/20 text-accent-primary', Applicatif: 'bg-purple-500/20 text-purple-400', }; return ( {model} ); } // Composant ScoreBadge function ScoreBadge({ score }: { score: number }) { let color = 'text-threat-low'; if (score < -0.3) color = 'text-threat-critical'; else if (score < -0.15) color = 'text-threat-high'; else if (score < -0.05) color = 'text-threat-medium'; return ( {score.toFixed(3)} ); } // Helper pour les drapeaux function getFlag(countryCode: string): string { const code = countryCode.toUpperCase(); return code.replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)); }