maj cumulative

This commit is contained in:
SOC Analyst
2026-03-18 13:56:39 +01:00
parent 32a96966dd
commit c887846af5
18 changed files with 986 additions and 686 deletions

View File

@ -42,8 +42,10 @@ export function DetectionsList() {
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 sortField = (searchParams.get('sort_by') || searchParams.get('sort') || 'detected_at') as SortField;
const sortOrder = (searchParams.get('sort_order') || searchParams.get('order') || 'desc') as SortOrder;
const [groupByIP, setGroupByIP] = useState(true);
const { data, loading, error } = useDetections({
page,
@ -52,13 +54,11 @@ export function DetectionsList() {
search,
sort_by: sortField,
sort_order: sortOrder,
group_by_ip: groupByIP,
});
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<ColumnConfig[]>([
{ key: 'ip_ja4', label: 'IP / JA4', visible: true, sortable: true },
{ key: 'host', label: 'Host', visible: true, sortable: true },
@ -107,6 +107,14 @@ export function DetectionsList() {
setSearchParams(newParams);
};
const handleSort = (key: string, dir: 'asc' | 'desc') => {
const newParams = new URLSearchParams(searchParams);
newParams.set('sort_by', key);
newParams.set('sort_order', dir);
newParams.set('page', '1');
setSearchParams(newParams);
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
@ -125,59 +133,8 @@ export function DetectionsList() {
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<string, typeof data.items[0]>();
const ipStats = new Map<string, {
first: Date;
last: Date;
count: number;
ja4s: Set<string>;
hosts: Set<string>;
clientHeaders: Set<string>;
}>();
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)
}))
};
})();
// Backend handles grouping — data is already grouped when groupByIP=true
const processedData = data;
// Build DataTable columns from visible column configs
const tableColumns: Column<DetectionRow>[] = columns
@ -352,20 +309,25 @@ export function DetectionsList() {
label: col.label,
sortable: true,
render: (_, row) =>
groupByIP && row.first_seen ? (
<div className="space-y-1">
<div className="text-xs text-text-secondary">
<span className="font-medium">Premier:</span>{' '}
{new Date(row.first_seen).toLocaleDateString('fr-FR')}{' '}
{new Date(row.first_seen).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
groupByIP && row.first_seen ? (() => {
const first = new Date(row.first_seen!);
const last = new Date(row.last_seen!);
const sameTime = first.getTime() === last.getTime();
const fmt = (d: Date) =>
`${d.toLocaleDateString('fr-FR')} ${d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`;
return sameTime ? (
<div className="text-xs text-text-secondary">{fmt(last)}</div>
) : (
<div className="space-y-1">
<div className="text-xs text-text-secondary">
<span className="font-medium">Premier:</span> {fmt(first)}
</div>
<div className="text-xs text-text-secondary">
<span className="font-medium">Dernier:</span> {fmt(last)}
</div>
</div>
<div className="text-xs text-text-secondary">
<span className="font-medium">Dernier:</span>{' '}
{new Date(row.last_seen!).toLocaleDateString('fr-FR')}{' '}
{new Date(row.last_seen!).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
) : (
);
})() : (
<>
<div className="text-sm text-text-primary">
{new Date(row.detected_at).toLocaleDateString('fr-FR')}
@ -388,7 +350,7 @@ export function DetectionsList() {
<div className="flex items-center gap-4">
<h1 className="text-2xl font-bold text-text-primary">Détections</h1>
<div className="flex items-center gap-2 text-sm text-text-secondary">
<span>{groupByIP ? processedData.items.length : data.items.length}</span>
<span>{data.items.length}</span>
<span></span>
<span>{data.total} détections</span>
</div>
@ -403,8 +365,9 @@ export function DetectionsList() {
? 'bg-accent-primary text-white border-accent-primary'
: 'bg-background-card text-text-secondary border-background-card hover:text-text-primary'
}`}
title={groupByIP ? 'Passer en vue détections individuelles' : 'Passer en vue groupée par IP'}
>
{groupByIP ? '⊟ Détections individuelles' : '⊞ Grouper par IP'}
{groupByIP ? '⊞ Vue : Groupé par IP' : '⊟ Vue : Individuelle'}
</button>
{/* Sélecteur de colonnes */}
@ -486,7 +449,9 @@ export function DetectionsList() {
data={processedData.items as DetectionRow[]}
columns={tableColumns}
rowKey={(row) => `${row.src_ip}-${row.detected_at}-${groupByIP ? 'g' : 'i'}`}
defaultSortKey="anomaly_score"
defaultSortKey={sortField}
defaultSortDir={sortOrder}
onSort={handleSort}
onRowClick={(row) => navigate(`/detections/ip/${encodeURIComponent(row.src_ip)}`)}
emptyMessage="Aucune détection trouvée"
compact