- {[['#dc2626', 'CRITICAL'], ['#f97316', 'HIGH'], ['#eab308', 'MEDIUM'], ['#22c55e', 'LOW']].map(([c, l]) => (
-
-
-
{l}
+ {([
+ ['#dc2626', 'CRITICAL', TIPS.risk_critical],
+ ['#f97316', 'HIGH', TIPS.risk_high],
+ ['#eab308', 'MEDIUM', TIPS.risk_medium],
+ ['#22c55e', 'LOW', TIPS.risk_low],
+ ] as const).map(([c, l, tip]) => (
+
+
+ {l}
))}
+
+ 31 features · PCA 2D ⓘ
+
{/* Tooltip zoom hint */}
@@ -550,10 +569,13 @@ export default function ClusteringView() {
// ─── Stat helper ─────────────────────────────────────────────────────────────
-function Stat({ label, value, color }: { label: string; value: string | number; color?: string }) {
+function Stat({ label, value, color, tooltip }: { label: string; value: string | number; color?: string; tooltip?: string }) {
return (
- {label}
+
+ {label}
+ {tooltip && }
+
{value}
);
@@ -604,7 +626,10 @@ function ClusterSidebar({ node, ipDetails, ipTotal, ipPage, clusterPoints, onClo
{/* Score risque */}
-
Score de risque
+
+ Score de risque
+
+
@@ -618,7 +643,10 @@ function ClusterSidebar({ node, ipDetails, ipTotal, ipPage, clusterPoints, onClo
{/* Radar chart */}
{node.radar?.length > 0 && (
-
Profil 21 features
+
+ Profil {node.radar?.length ?? 21} features
+
+
@@ -636,12 +664,12 @@ function ClusterSidebar({ node, ipDetails, ipTotal, ipPage, clusterPoints, onClo
{/* TCP stack */}
Stack TCP
-
-
-
-
-
-
+
+
+
+
+
+
{/* Contexte */}
diff --git a/frontend/src/components/MLFeaturesView.tsx b/frontend/src/components/MLFeaturesView.tsx
index a8ac38b..da38495 100644
--- a/frontend/src/components/MLFeaturesView.tsx
+++ b/frontend/src/components/MLFeaturesView.tsx
@@ -1,6 +1,7 @@
import { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import DataTable, { Column } from './ui/DataTable';
+import { TIPS } from './ui/tooltips';
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -100,14 +101,14 @@ function ErrorMessage({ message }: { message: string }) {
// ─── Radar Chart (SVG octagonal) ─────────────────────────────────────────────
const RADAR_AXES = [
- { key: 'fuzzing_score', label: 'Fuzzing' },
- { key: 'velocity_score', label: 'Vélocité' },
- { key: 'fake_nav_score', label: 'Fausse nav' },
- { key: 'ua_mismatch_score', label: 'UA/CH mismatch' },
- { key: 'sni_mismatch_score', label: 'SNI mismatch' },
- { key: 'orphan_score', label: 'Orphan ratio' },
- { key: 'path_repetition_score', label: 'Répétition URL' },
- { key: 'payload_anomaly_score', label: 'Payload anormal' },
+ { key: 'fuzzing_score', label: 'Fuzzing', tip: TIPS.fuzzing },
+ { key: 'velocity_score', label: 'Vélocité', tip: TIPS.velocity },
+ { key: 'fake_nav_score', label: 'Fausse nav', tip: TIPS.fake_nav },
+ { key: 'ua_mismatch_score', label: 'UA/CH mismatch', tip: TIPS.ua_mismatch },
+ { key: 'sni_mismatch_score', label: 'SNI mismatch', tip: TIPS.sni_mismatch },
+ { key: 'orphan_score', label: 'Orphan ratio', tip: TIPS.orphan_ratio },
+ { key: 'path_repetition_score', label: 'Répétition URL', tip: TIPS.path_repetition },
+ { key: 'payload_anomaly_score', label: 'Payload anormal', tip: TIPS.payload_anomaly },
] as const;
type RadarKey = typeof RADAR_AXES[number]['key'];
@@ -182,7 +183,7 @@ function RadarChart({ data }: { data: RadarData }) {
))}
- {/* Axis labels */}
+ {/* Axis labels — survolez pour la définition */}
{RADAR_AXES.map((axis, i) => {
const [x, y] = pointFor(i, maxR + 18);
const anchor = x < cx - 5 ? 'end' : x > cx + 5 ? 'start' : 'middle';
@@ -195,7 +196,9 @@ function RadarChart({ data }: { data: RadarData }) {
fontSize="10"
fill="rgba(148,163,184,0.9)"
dominantBaseline="middle"
+ style={{ cursor: 'help' }}
>
+ {axis.tip}
{axis.label}
);
@@ -257,7 +260,10 @@ function ScatterPlot({ points }: { points: ScatterPoint[] }) {
{xTicks.map((v) => (
{v}
))}
- Fuzzing Index →
+
+ {TIPS.fuzzing_index}
+ Fuzzing Index →
+
{/* Y axis */}
@@ -361,6 +367,7 @@ function AnomaliesTable({
{
key: 'fuzzing_index',
label: 'Fuzzing',
+ tooltip: TIPS.fuzzing_index,
align: 'right',
render: (v: number) => (
@@ -371,6 +378,7 @@ function AnomaliesTable({
{
key: 'attack_type',
label: 'Type',
+ tooltip: 'Type d\'attaque détecté : Brute Force 🔑, Flood 🌊, Scraper 🕷️, Spoofing 🎭, Scanner 🔍',
render: (v: string) => (
{attackTypeEmoji(v)}
),
@@ -378,6 +386,7 @@ function AnomaliesTable({
{
key: '_signals',
label: 'Signaux',
+ tooltip: '⚠️ UA/CH mismatch · 🎭 Fausse navigation · 🔄 UA rotatif · 🌐 SNI mismatch',
sortable: false,
render: (_: unknown, row: MLAnomaly) => (
diff --git a/frontend/src/components/TcpSpoofingView.tsx b/frontend/src/components/TcpSpoofingView.tsx
index 2e60076..968350b 100644
--- a/frontend/src/components/TcpSpoofingView.tsx
+++ b/frontend/src/components/TcpSpoofingView.tsx
@@ -1,6 +1,7 @@
import { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import DataTable, { Column } from './ui/DataTable';
+import { TIPS } from './ui/tooltips';
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -144,6 +145,7 @@ function TcpDetectionsTable({
{
key: 'tcp_ttl',
label: 'TTL obs. / init.',
+ tooltip: TIPS.ttl,
align: 'right',
render: (_: number, row: TcpSpoofingItem) => (
@@ -159,6 +161,7 @@ function TcpDetectionsTable({
{
key: 'tcp_mss',
label: 'MSS',
+ tooltip: TIPS.mss,
align: 'right',
render: (v: number) => (
@@ -169,6 +172,7 @@ function TcpDetectionsTable({
{
key: 'tcp_win_scale',
label: 'Scale',
+ tooltip: TIPS.win_scale,
align: 'right',
render: (v: number) => (
{v}
@@ -177,6 +181,7 @@ function TcpDetectionsTable({
{
key: 'suspected_os',
label: 'OS suspecté (TCP)',
+ tooltip: TIPS.os_tcp,
render: (v: string, row: TcpSpoofingItem) => (
{osIcon(v)}
@@ -187,6 +192,7 @@ function TcpDetectionsTable({
{
key: 'confidence',
label: 'Confiance',
+ tooltip: TIPS.confidence,
render: (v: number) => confidenceBar(v),
},
{
@@ -197,11 +203,13 @@ function TcpDetectionsTable({
{
key: 'declared_os',
label: 'OS déclaré (UA)',
+ tooltip: TIPS.os_ua,
render: (v: string) => {v || '—'},
},
{
key: 'spoof_flag',
label: 'Verdict',
+ tooltip: TIPS.spoof_verdict,
sortable: false,
render: (v: boolean, row: TcpSpoofingItem) => {
if (row.is_bot_tool) {
diff --git a/frontend/src/components/ui/DataTable.tsx b/frontend/src/components/ui/DataTable.tsx
index 4389ea5..4798579 100644
--- a/frontend/src/components/ui/DataTable.tsx
+++ b/frontend/src/components/ui/DataTable.tsx
@@ -1,9 +1,11 @@
import React from 'react';
import { useSort, SortDir } from '../../hooks/useSort';
+import { InfoTip } from './Tooltip';
export interface Column {
key: string;
label: string;
+ tooltip?: string;
sortable?: boolean;
align?: 'left' | 'right' | 'center';
width?: string;
@@ -96,6 +98,7 @@ export default function DataTable>({
>
{col.label}
+ {col.tooltip && }
{isSortable &&
(isActive ? (
diff --git a/frontend/src/components/ui/Tooltip.tsx b/frontend/src/components/ui/Tooltip.tsx
new file mode 100644
index 0000000..03a61e3
--- /dev/null
+++ b/frontend/src/components/ui/Tooltip.tsx
@@ -0,0 +1,145 @@
+/**
+ * Tooltip — composant universel de survol
+ *
+ * Rendu via createPortal dans document.body pour éviter tout clipping
+ * par les conteneurs overflow:hidden / overflow-y:auto.
+ *
+ * Usage :
+ * label
+ * ← ajoute un ⓘ cliquable
+ */
+
+import { useState, useRef, useCallback, useEffect } from 'react';
+import { createPortal } from 'react-dom';
+
+interface TooltipProps {
+ content: string | React.ReactNode;
+ children: React.ReactNode;
+ className?: string;
+ delay?: number;
+ maxWidth?: number;
+}
+
+interface TooltipPos {
+ x: number;
+ y: number;
+}
+
+export function Tooltip({ content, children, className = '', delay = 250, maxWidth = 300 }: TooltipProps) {
+ const [pos, setPos] = useState(null);
+ const timer = useRef | null>(null);
+ const spanRef = useRef(null);
+
+ const show = useCallback(
+ (e: React.MouseEvent) => {
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
+ // Position au-dessus du centre de l'élément, ajustée si trop haut
+ const x = Math.round(rect.left + rect.width / 2);
+ const y = Math.round(rect.top);
+ if (timer.current) clearTimeout(timer.current);
+ timer.current = setTimeout(() => setPos({ x, y }), delay);
+ },
+ [delay],
+ );
+
+ const hide = useCallback(() => {
+ if (timer.current) clearTimeout(timer.current);
+ setPos(null);
+ }, []);
+
+ // Nettoyage si le composant est démonté pendant le délai
+ useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
+
+ if (!content) return <>{children}>;
+
+ return (
+ <>
+
+ {children}
+
+
+ {pos &&
+ createPortal(
+
+ {content}
+ ,
+ document.body,
+ )}
+ >
+ );
+}
+
+/** Bulle de tooltip positionnée en fixed par rapport au viewport */
+function TooltipBubble({
+ x,
+ y,
+ maxWidth,
+ children,
+}: {
+ x: number;
+ y: number;
+ maxWidth: number;
+ children: React.ReactNode;
+}) {
+ const bubbleRef = useRef(null);
+ const [adjust, setAdjust] = useState({ dx: 0, dy: 0 });
+
+ // Ajustement viewport pour éviter les débordements
+ useEffect(() => {
+ if (!bubbleRef.current) return;
+ const el = bubbleRef.current;
+ const rect = el.getBoundingClientRect();
+ let dx = 0;
+ let dy = 0;
+ if (rect.left < 8) dx = 8 - rect.left;
+ if (rect.right > window.innerWidth - 8) dx = window.innerWidth - 8 - rect.right;
+ if (rect.top < 8) dy = 16; // bascule en dessous si trop haut
+ setAdjust({ dx, dy });
+ }, [x, y]);
+
+ return (
+
+
+ {children}
+
+ {/* Flèche vers le bas */}
+
+
+ );
+}
+
+/**
+ * InfoTip — icône ⓘ avec tooltip intégré.
+ * S'insère après un label pour donner une explication au survol.
+ */
+export function InfoTip({
+ content,
+ className = '',
+}: {
+ content: string | React.ReactNode;
+ className?: string;
+}) {
+ return (
+
+
+ ⓘ
+
+
+ );
+}
diff --git a/frontend/src/components/ui/tooltips.ts b/frontend/src/components/ui/tooltips.ts
new file mode 100644
index 0000000..2af8216
--- /dev/null
+++ b/frontend/src/components/ui/tooltips.ts
@@ -0,0 +1,285 @@
+/**
+ * tooltips.ts — Textes d'aide pour tous les termes techniques du dashboard.
+ *
+ * Toutes les chaînes sont en français, multi-lignes via \n.
+ * Utilisé avec ou .
+ */
+
+export const TIPS = {
+
+ // ── Clustering ──────────────────────────────────────────────────────────────
+
+ sensitivity:
+ 'Contrôle la granularité du clustering.\n' +
+ '· Grossière → grands groupes comportementaux\n' +
+ '· Fine → distinction fine entre sous-comportements\n' +
+ '· Extrême → jusqu\'à k × 5 clusters, calcul long',
+
+ k_base:
+ 'Nombre de clusters de base (k).\n' +
+ 'Clusters effectifs = k × sensibilité (limité à 300).\n' +
+ 'Augmenter k permet plus de nuance dans la classification.',
+
+ k_actual:
+ 'Nombre réel de clusters calculés = k × sensibilité.\n' +
+ 'Exemple : k=20 × sensibilité=3 = 60 clusters effectifs.\n' +
+ 'Limité à 300 pour rester calculable.',
+
+ show_edges:
+ 'Affiche les relations de similarité entre clusters proches.\n' +
+ 'Seules les paires au-dessus d\'un seuil de similarité sont reliées.\n' +
+ 'Utile pour identifier des groupes comportementaux liés.',
+
+ ips_bots:
+ 'IPs avec score de risque ML > 70 %.\n' +
+ 'Présentent les signaux les plus forts de comportement automatisé.\n' +
+ 'Action immédiate recommandée.',
+
+ high_risk:
+ 'IPs avec score de risque entre 45 % et 70 %.\n' +
+ 'Activité suspecte nécessitant une analyse approfondie.',
+
+ total_hits:
+ 'Nombre total de requêtes HTTP de toutes les IPs\n' +
+ 'dans la fenêtre d\'analyse sélectionnée.',
+
+ calc_time:
+ 'Durée du calcul K-means++ sur l\'ensemble des IPs.\n' +
+ 'Augmente avec k × sensibilité et le nombre d\'IPs.',
+
+ pca_2d:
+ 'Projection PCA (Analyse en Composantes Principales).\n' +
+ '31 features → 2 dimensions pour la visualisation.\n' +
+ 'Les clusters proches ont des comportements similaires.',
+
+ features_31:
+ '31 métriques utilisées pour le clustering :\n' +
+ '· TCP : TTL, MSS, fenêtre de congestion\n' +
+ '· ML : score de détection bot\n' +
+ '· TLS/JA4 : fingerprint client\n' +
+ '· User-Agent, OS, headless, UA-CH\n' +
+ '· Pays, ASN (cloud/datacenter)\n' +
+ '· Headers HTTP : Accept-Language, Encoding, Sec-Fetch\n' +
+ '· Fingerprint headers : popularité, rotation, cookie, referer',
+
+ // ── Légende risque ──────────────────────────────────────────────────────────
+
+ risk_critical:
+ 'CRITICAL — Score > 70 %\nBot très probable. Action immédiate recommandée.',
+
+ risk_high:
+ 'HIGH — Score 45–70 %\nActivité suspecte. Investigation recommandée.',
+
+ risk_medium:
+ 'MEDIUM — Score 25–45 %\nComportement anormal. Surveillance renforcée.',
+
+ risk_low:
+ 'LOW — Score < 25 %\nTrafic probablement légitime.',
+
+ // ── Sidebar cluster ─────────────────────────────────────────────────────────
+
+ risk_score:
+ 'Score composite [0–100 %] calculé à partir de 14 sous-scores pondérés :\n' +
+ '· ML score (25 %) · Fuzzing (9 %)\n' +
+ '· UA-CH mismatch (7 %) · Headless (6 %)\n' +
+ '· Pays risqué (9 %) · ASN cloud (6 %)\n' +
+ '· Headers HTTP (12 %) · Fingerprint (12 %)\n' +
+ '· Vélocité (5 %) · IP ID zéro (5 %)',
+
+ radar_profile:
+ 'Radar comportemental sur les features principales.\n' +
+ 'Chaque axe = un sous-score normalisé entre 0 et 1.\n' +
+ 'Survolez un point pour voir la valeur exacte.',
+
+ mean_ttl:
+ 'Time-To-Live TCP moyen du cluster.\n' +
+ '· Linux ≈ 64 · Windows ≈ 128 · Cisco ≈ 255\n' +
+ 'Différence TTL_initial − TTL_observé = nombre de sauts réseau.',
+
+ mean_mss:
+ 'Maximum Segment Size TCP moyen.\n' +
+ '· Ethernet = 1460 B · PPPoE ≈ 1452 B\n' +
+ '· VPN ≈ 1380–1420 B · Bas débit < 1380 B\n' +
+ 'MSS anormalement bas → probable tunnel ou VPN.',
+
+ mean_score:
+ 'Score moyen du modèle ML de détection de bots.\n' +
+ '0 % = trafic légitime · 100 % = bot confirmé',
+
+ mean_velocity:
+ 'Nombre moyen de requêtes par seconde (rps).\n' +
+ 'Taux élevé → outil automatisé ou attaque volumétrique.',
+
+ mean_headless:
+ 'Proportion d\'IPs utilisant un navigateur headless.\n' +
+ '(Puppeteer, Playwright, PhantomJS, Chromium sans UI…)\n' +
+ 'Les bots utilisent fréquemment des navigateurs sans interface.',
+
+ mean_ua_ch:
+ 'User-Agent / Client Hints mismatch.\n' +
+ 'Le UA déclaré (ex: Chrome/Windows) contredit les hints\n' +
+ '(ex: Linux, version différente).\n' +
+ 'Signal fort de spoofing d\'identité navigateur.',
+
+ // ── ML Features ─────────────────────────────────────────────────────────────
+
+ fuzzing:
+ 'Score de fuzzing : variété anormale de paramètres ou payloads.\n' +
+ 'Caractéristique des scanners de vulnérabilités\n' +
+ '(SQLi, XSS, path traversal, injection d\'en-têtes…).',
+
+ velocity:
+ 'Score de vélocité : taux de requêtes / unité de temps normalisé.\n' +
+ 'Au-dessus du seuil → outil automatisé confirmé.',
+
+ fake_nav:
+ 'Fausse navigation : séquences de pages non conformes au comportement humain.\n' +
+ 'Ex : accès direct à des API sans passer par les pages d\'entrée.',
+
+ ua_mismatch:
+ 'User-Agent / Client Hints mismatch.\n' +
+ 'Contradiction entre l\'OS/navigateur déclaré dans le UA\n' +
+ 'et les Client Hints envoyés par le navigateur réel.',
+
+ sni_mismatch:
+ 'SNI mismatch : le nom dans le SNI TLS ≠ le Host HTTP.\n' +
+ 'Signe de proxying, de bot, ou de spoofing TLS.',
+
+ orphan_ratio:
+ 'Orphan ratio : proportion de requêtes sans referer ni session.\n' +
+ 'Les bots accèdent souvent directement aux URLs\n' +
+ 'sans parcours préalable sur le site.',
+
+ path_repetition:
+ 'Répétition URL : taux de requêtes sur les mêmes endpoints.\n' +
+ 'Les bots ciblés répètent des patterns d\'URLs précis\n' +
+ '(ex : /login, /api/search, /admin…).',
+
+ payload_anomaly:
+ 'Payload anormal : ratio de requêtes avec contenu inhabituel.\n' +
+ '(taille hors norme, encoding bizarre, corps non standard)\n' +
+ 'Peut indiquer une injection ou une tentative de bypass.',
+
+ fuzzing_index:
+ 'Indice brut de fuzzing mesuré sur les paramètres des requêtes.\n' +
+ 'Valeur haute → tentative d\'injection ou fuzzing actif.',
+
+ hit_velocity_scatter:
+ 'Taux de requêtes par seconde de cette IP.\n' +
+ 'Valeur haute → outil automatisé ou attaque volumétrique.',
+
+ temporal_entropy:
+ 'Entropie temporelle : irrégularité des intervalles entre requêtes.\n' +
+ '· Faible = bot régulier (machine, intervalles constants)\n' +
+ '· Élevée = humain ou bot à timing aléatoire',
+
+ anomalous_payload_ratio:
+ 'Ratio de requêtes avec payload anormal / total de l\'IP.\n' +
+ 'Ex : headers malformés, corps non HTTP standard.',
+
+ attack_brute_force:
+ 'Brute Force : tentatives répétées d\'authentification\n' +
+ 'ou d\'énumération de ressources (login, tokens, IDs…).',
+
+ attack_flood:
+ 'Flood : envoi massif de requêtes pour saturer le service.\n' +
+ '(DoS/DDoS, rate limit bypass…)',
+
+ attack_scraper:
+ 'Scraper : extraction systématique de contenu.\n' +
+ '(web scraping, crawling non autorisé, récolte de données)',
+
+ attack_spoofing:
+ 'Spoofing : usurpation d\'identité UA/TLS\n' +
+ 'pour contourner la détection de bots.',
+
+ attack_scanner:
+ 'Scanner : exploration automatique des endpoints\n' +
+ 'pour découvrir des vulnérabilités (CVE, misconfigs…).',
+
+ // ── TCP Spoofing ─────────────────────────────────────────────────────────────
+
+ ttl:
+ 'TTL (Time-To-Live) : valeur observée vs initiale estimée.\n' +
+ '· Initiale typique : Linux=64, Windows=128, Cisco=255\n' +
+ '· Hops réseau = TTL_init − TTL_observé',
+
+ mss:
+ 'Maximum Segment Size TCP.\n' +
+ '· Ethernet = 1460 B · PPPoE ≈ 1452 B\n' +
+ '· VPN ≈ 1380–1420 B · Bas débit < 1380 B\n' +
+ 'Révèle le type de réseau sous-jacent.',
+
+ win_scale:
+ 'Window Scale TCP (RFC 1323).\n' +
+ 'Facteur d\'échelle de la fenêtre de congestion.\n' +
+ '· Linux ≈ 7 · Windows ≈ 8 · Absent = vieux OS ou bot',
+
+ os_tcp:
+ 'OS suspecté via fingerprinting TCP passif.\n' +
+ 'Analyse combinée : TTL + MSS + Window Scale + options TCP.\n' +
+ 'Indépendant du User-Agent déclaré.',
+
+ os_ua:
+ 'OS déclaré dans le User-Agent HTTP.\n' +
+ 'Comparé à l\'OS TCP pour détecter les usurpations d\'identité.',
+
+ confidence:
+ 'Niveau de confiance du fingerprinting TCP [0–100 %].\n' +
+ 'Basé sur le nombre de signaux concordants\n' +
+ '(TTL, MSS, Window Scale, options TCP).',
+
+ spoof_verdict:
+ 'Verdict de spoofing : OS TCP (réel) vs OS User-Agent (déclaré).\n' +
+ 'Un écart indique une probable usurpation d\'identité.\n' +
+ 'Ex : TCP→Linux mais UA→Windows/Chrome.',
+
+ // ── Général ──────────────────────────────────────────────────────────────────
+
+ ja4:
+ 'JA4 : fingerprint TLS client.\n' +
+ 'Basé sur : version TLS, suites chiffrées, extensions,\n' +
+ 'algorithmes de signature et SNI.\n' +
+ 'Identifie un client TLS de façon quasi-unique.',
+
+ asn:
+ 'ASN (Autonomous System Number).\n' +
+ 'Identifiant du réseau auquel appartient l\'IP.\n' +
+ 'Permet d\'identifier l\'opérateur (AWS, GCP, Azure, hébergeur, FAI…).',
+
+ header_fingerprint:
+ 'Fingerprint des headers HTTP.\n' +
+ '· Popularité : fréquence de ce profil dans le trafic global\n' +
+ ' (rare = suspect, populaire = navigateur standard)\n' +
+ '· Rotation : le client change fréquemment de profil headers\n' +
+ ' (signal fort de bot rotatif)',
+
+ header_count:
+ 'Nombre de headers HTTP envoyés par le client.\n' +
+ 'Navigateur standard ≈ 10–15 headers.\n' +
+ 'Bot HTTP basique ≈ 2–5 headers.',
+
+ accept_language:
+ 'Header Accept-Language.\n' +
+ 'Absent chez les bots HTTP basiques.\n' +
+ 'Présent chez les navigateurs légitimes (fr-FR, en-US…).',
+
+ accept_encoding:
+ 'Header Accept-Encoding.\n' +
+ 'Absent → client HTTP basique ou bot simple.\n' +
+ 'Présent (gzip, br…) → navigateur ou client HTTP moderne.',
+
+ sec_fetch:
+ 'Headers Sec-Fetch-* (Site, Mode, Dest, User).\n' +
+ 'Envoyés uniquement par les navigateurs Chromium/Firefox réels.\n' +
+ 'Absent → bot, curl, ou client HTTP non-navigateur.',
+
+ alertes_24h:
+ 'Alertes générées par le moteur de détection ML\n' +
+ 'dans les dernières 24 heures, classifiées par niveau de menace.',
+
+ threat_level:
+ 'Niveau de menace composite :\n' +
+ '· CRITICAL > 70 % · HIGH 45–70 %\n' +
+ '· MEDIUM 25–45 % · LOW < 25 %',
+};