feat(dashboard): thème auto, config centralisée, dates UTC→TZ navigateur, tooltip Anubis

- ThemeContext: thème par défaut 'auto' (suit prefers-color-scheme du navigateur)
- config.ts: fichier de configuration centrale (API_BASE_URL, DEFAULT_THEME,
  PAGE_SIZES, seuils, description du mécanisme d'identification Anubis)
- dateUtils.ts: utilitaire partagé formatDate/formatDateShort/formatDateOnly/
  formatTimeOnly/formatNumber — convertit les dates UTC ClickHouse dans le
  fuseau horaire et la locale du navigateur (plus de 'fr-FR' hardcodé)
- tooltips.ts: ajout TIPS.anubis_identification — explique que les bots sont
  identifiés par UA (regex), IP/CIDR, ASN, pays via les règles Anubis
- DetectionsList: colonne Anubis avec icône ⓘ affichant le tooltip explicatif
- DataTable: Column.label étendu à React.ReactNode (pour JSX dans les headers)
- 24 composants mis à jour: fr-FR remplacé par locale navigateur partout

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
SOC Analyst
2026-03-19 18:01:11 +01:00
parent 2f73860cc8
commit 9ee3d01059
24 changed files with 238 additions and 50 deletions

View File

@ -2,6 +2,9 @@ import { useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useDetections } from '../hooks/useDetections';
import DataTable, { Column } from './ui/DataTable';
import { InfoTip } from './ui/Tooltip';
import { TIPS } from './ui/tooltips';
import { formatDate, formatDateOnly, formatTimeOnly } from '../utils/dateUtils';
type SortField = 'detected_at' | 'threat_level' | 'anomaly_score' | 'src_ip' | 'country_code' | 'asn_number' | 'host' | 'hits' | 'hit_velocity';
type SortOrder = 'asc' | 'desc';
@ -33,6 +36,9 @@ interface DetectionRow {
unique_ja4s?: string[];
unique_hosts?: string[];
unique_client_headers?: string[];
anubis_bot_name?: string;
anubis_bot_action?: string;
anubis_bot_category?: string;
}
export function DetectionsList() {
@ -65,6 +71,7 @@ export function DetectionsList() {
{ 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: 'anubis', label: '🤖 Anubis', visible: true, sortable: false },
{ 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 },
@ -236,6 +243,38 @@ export function DetectionsList() {
sortable: true,
render: (_, row) => <ModelBadge model={row.model_name} />,
};
case 'anubis':
return {
key: 'anubis_bot_name',
label: (
<span className="inline-flex items-center gap-1">
🤖 Anubis
<InfoTip content={TIPS.anubis_identification} />
</span>
),
sortable: false,
render: (_, row) => {
const name = row.anubis_bot_name;
const action = row.anubis_bot_action;
const category = row.anubis_bot_category;
if (!name) return <span className="text-text-disabled text-xs"></span>;
const actionColor =
action === 'ALLOW' ? 'bg-green-500/15 text-green-400 border-green-500/30' :
action === 'DENY' ? 'bg-red-500/15 text-red-400 border-red-500/30' :
'bg-yellow-500/15 text-yellow-400 border-yellow-500/30';
return (
<div className="space-y-0.5">
<div className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded border ${actionColor}`}>
<span className="font-medium">{name}</span>
</div>
<div className="flex gap-1 flex-wrap">
{action && <span className="text-[10px] text-text-secondary">{action}</span>}
{category && <span className="text-[10px] text-text-disabled">· {category}</span>}
</div>
</div>
);
},
};
case 'anomaly_score':
return {
key: 'anomaly_score',
@ -313,8 +352,7 @@ export function DetectionsList() {
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' })}`;
const fmt = (d: Date) => formatDate(d.toISOString());
return sameTime ? (
<div className="text-xs text-text-secondary">{fmt(last)}</div>
) : (
@ -330,10 +368,10 @@ export function DetectionsList() {
})() : (
<>
<div className="text-sm text-text-primary">
{new Date(row.detected_at).toLocaleDateString('fr-FR')}
{formatDateOnly(row.detected_at)}
</div>
<div className="text-xs text-text-secondary">
{new Date(row.detected_at).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
{formatTimeOnly(row.detected_at)}
</div>
</>
),