From 485b95b62e82f8f320085155fb35f542a1e548e0 Mon Sep 17 00:00:00 2001 From: SOC Analyst Date: Thu, 19 Mar 2026 12:02:15 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20syst=C3=A8me=20de=20tooltips=20universe?= =?UTF-8?q?l=20sur=20tous=20les=20termes=20techniques?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nouveau composant ui/Tooltip.tsx (createPortal → pas de clipping overflow) - Tooltip : bulle au survol avec ajustement viewport automatique - InfoTip : icône ⓘ avec bulle intégrée - Nouveau ui/tooltips.ts : 50+ définitions en français (clustering, ML features, TCP spoofing, general) - ui/DataTable.tsx : prop tooltip sur Column → InfoTip dans les en-têtes - ClusteringView : ⓘ sur Sensibilité, k, arêtes, toutes les stats, légende CRITICAL/HIGH/MEDIUM/LOW, sidebar (score risque, radar, TTL/MSS/Score ML/Vélocité/Headless/UA-CH) - MLFeaturesView : SVG sur axes radar et scatter, tooltip sur colonnes Fuzzing/Type/Signaux - TcpSpoofingView : tooltip sur colonnes TTL/MSS/Scale/OS/Confiance/Verdict - App.tsx : tooltip sur Alertes 24h et niveaux CRITICAL/HIGH/MEDIUM Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- frontend/src/App.tsx | 20 +- frontend/src/components/ClusteringView.tsx | 78 ++++-- frontend/src/components/MLFeaturesView.tsx | 29 +- frontend/src/components/TcpSpoofingView.tsx | 8 + frontend/src/components/ui/DataTable.tsx | 3 + frontend/src/components/ui/Tooltip.tsx | 145 ++++++++++ frontend/src/components/ui/tooltips.ts | 285 ++++++++++++++++++++ 7 files changed, 529 insertions(+), 39 deletions(-) create mode 100644 frontend/src/components/ui/Tooltip.tsx create mode 100644 frontend/src/components/ui/tooltips.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4155bb7..379c752 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,6 +21,8 @@ import { HeaderFingerprintView } from './components/HeaderFingerprintView'; import { MLFeaturesView } from './components/MLFeaturesView'; import ClusteringView from './components/ClusteringView'; import { useTheme } from './ThemeContext'; +import { Tooltip } from './components/ui/Tooltip'; +import { TIPS } from './components/ui/tooltips'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -144,19 +146,29 @@ function Sidebar({ counts }: { counts: AlertCounts | null }) { {/* Alert stats */} {counts && ( <div className="mx-3 mt-5 bg-background-card rounded-lg p-3 space-y-2"> - <div className="text-xs font-semibold text-text-disabled uppercase tracking-wider mb-2">Alertes 24h</div> + <Tooltip content={TIPS.alertes_24h}> + <div className="text-xs font-semibold text-text-disabled uppercase tracking-wider mb-2 cursor-help"> + Alertes 24h ⓘ + </div> + </Tooltip> {counts.critical > 0 && ( <div className="flex justify-between items-center"> - <span className="text-xs text-red-400 flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-red-500 inline-block animate-pulse" /> CRITICAL</span> + <Tooltip content={TIPS.risk_critical}> + <span className="text-xs text-red-400 flex items-center gap-1 cursor-help"><span className="w-1.5 h-1.5 rounded-full bg-red-500 inline-block animate-pulse" /> CRITICAL</span> + </Tooltip> <span className="text-xs font-bold text-red-400 bg-red-500/20 px-1.5 py-0.5 rounded">{counts.critical}</span> </div> )} <div className="flex justify-between items-center"> - <span className="text-xs text-orange-400 flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-orange-500 inline-block" /> HIGH</span> + <Tooltip content={TIPS.risk_high}> + <span className="text-xs text-orange-400 flex items-center gap-1 cursor-help"><span className="w-1.5 h-1.5 rounded-full bg-orange-500 inline-block" /> HIGH</span> + </Tooltip> <span className="text-xs font-bold text-orange-400 bg-orange-500/20 px-1.5 py-0.5 rounded">{counts.high}</span> </div> <div className="flex justify-between items-center"> - <span className="text-xs text-yellow-400 flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-yellow-500 inline-block" /> MEDIUM</span> + <Tooltip content={TIPS.risk_medium}> + <span className="text-xs text-yellow-400 flex items-center gap-1 cursor-help"><span className="w-1.5 h-1.5 rounded-full bg-yellow-500 inline-block" /> MEDIUM</span> + </Tooltip> <span className="text-xs font-bold text-yellow-400 bg-yellow-500/20 px-1.5 py-0.5 rounded">{counts.medium}</span> </div> <div className="border-t border-background-secondary pt-1.5 flex justify-between items-center mt-1"> diff --git a/frontend/src/components/ClusteringView.tsx b/frontend/src/components/ClusteringView.tsx index fdf2e93..e6010d2 100644 --- a/frontend/src/components/ClusteringView.tsx +++ b/frontend/src/components/ClusteringView.tsx @@ -14,6 +14,8 @@ import DeckGL from '@deck.gl/react'; import { OrthographicView } from '@deck.gl/core'; import { ScatterplotLayer, PolygonLayer, TextLayer, LineLayer } from '@deck.gl/layers'; import { RadarChart, PolarGrid, PolarAngleAxis, Radar, ResponsiveContainer, Tooltip } from 'recharts'; +import { InfoTip } from './ui/Tooltip'; +import { TIPS } from './ui/tooltips'; import axios from 'axios'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -359,10 +361,13 @@ export default function ClusteringView() { {/* Sensibilité */} <div className="space-y-1"> <div className="flex justify-between text-xs text-text-secondary"> - <span>Sensibilité</span> + <span className="flex items-center"> + Sensibilité + <InfoTip content={TIPS.sensitivity} /> + </span> <span className="font-mono text-white"> {sensitivity <= 0.5 ? 'Grossière' : sensitivity <= 1.0 ? 'Normale' : sensitivity <= 2.0 ? 'Fine' : sensitivity <= 3.5 ? 'Très fine' : sensitivity <= 4.5 ? 'Maximale' : 'Extrême'} - {' '}({Math.round(k * sensitivity)} clusters effectifs) + {' '}(<span title={TIPS.k_actual}>{Math.round(k * sensitivity)} clusters effectifs</span>) </span> </div> <input type="range" min={0.5} max={5.0} step={0.5} value={sensitivity} @@ -378,7 +383,10 @@ export default function ClusteringView() { <summary className="cursor-pointer hover:text-white">Paramètres avancés</summary> <div className="mt-2 space-y-2"> <label className="block"> - Clusters de base (k) + <span className="flex items-center gap-1"> + Clusters de base (k) + <InfoTip content={TIPS.k_base} /> + </span> <input type="range" min={4} max={100} value={k} onChange={e => setK(+e.target.value)} className="w-full mt-1 accent-accent-primary" /> @@ -401,7 +409,10 @@ export default function ClusteringView() { <label className="flex items-center gap-2 text-xs text-text-secondary cursor-pointer"> <input type="checkbox" checked={showEdges} onChange={e => setShowEdges(e.target.checked)} className="accent-accent-primary" /> - Afficher les arêtes + <span className="flex items-center"> + Afficher les arêtes + <InfoTip content={TIPS.show_edges} /> + </span> </label> <button onClick={() => fetchClusters(true)} disabled={loading} @@ -414,12 +425,12 @@ export default function ClusteringView() { {data?.stats && ( <div className="bg-background-card rounded-lg p-3 space-y-1 text-xs"> <div className="font-semibold text-sm mb-2">Résultats</div> - <Stat label="Clusters" value={data.stats.total_clusters} /> - <Stat label="IPs totales" value={data.stats.total_ips.toLocaleString()} /> - <Stat label="IPs bots 🤖" value={data.stats.bot_ips.toLocaleString()} color="text-red-400" /> - <Stat label="Risque élevé" value={data.stats.high_risk_ips.toLocaleString()} color="text-orange-400" /> - <Stat label="Hits totaux" value={data.stats.total_hits.toLocaleString()} /> - <Stat label="Calcul" value={`${data.stats.elapsed_s}s`} /> + <Stat label="Clusters" value={data.stats.total_clusters} tooltip={TIPS.k_actual} /> + <Stat label="IPs totales" value={data.stats.total_ips.toLocaleString()} tooltip={TIPS.pca_2d} /> + <Stat label="IPs bots 🤖" value={data.stats.bot_ips.toLocaleString()} color="text-red-400" tooltip={TIPS.ips_bots} /> + <Stat label="Risque élevé" value={data.stats.high_risk_ips.toLocaleString()} color="text-orange-400" tooltip={TIPS.high_risk} /> + <Stat label="Hits totaux" value={data.stats.total_hits.toLocaleString()} tooltip={TIPS.total_hits} /> + <Stat label="Calcul" value={`${data.stats.elapsed_s}s`} tooltip={TIPS.calc_time} /> </div> )} @@ -514,14 +525,22 @@ export default function ClusteringView() { controller={true} > {/* Légende overlay */} - <div style={{ position: 'absolute', bottom: 16, left: 16, pointerEvents: 'none' }}> + <div style={{ position: 'absolute', bottom: 16, left: 16, pointerEvents: 'all' }}> <div className="bg-black/70 rounded-lg p-2 text-xs flex flex-col gap-1"> - {[['#dc2626', 'CRITICAL'], ['#f97316', 'HIGH'], ['#eab308', 'MEDIUM'], ['#22c55e', 'LOW']].map(([c, l]) => ( - <div key={l} className="flex items-center gap-2"> - <span className="w-3 h-3 rounded-full" style={{ background: c }} /> - <span className="text-white/80">{l}</span> + {([ + ['#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]) => ( + <div key={l} className="flex items-center gap-2" title={tip}> + <span className="w-3 h-3 rounded-full flex-shrink-0" style={{ background: c }} /> + <span className="text-white/80 cursor-help">{l}</span> </div> ))} + <div className="mt-1 pt-1 border-t border-white/10 text-white/40 text-[10px] cursor-help" title={TIPS.features_31}> + 31 features · PCA 2D ⓘ + </div> </div> </div> {/* 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 ( <div className="flex justify-between items-center"> - <span className="text-text-secondary">{label}</span> + <span className="text-text-secondary flex items-center"> + {label} + {tooltip && <InfoTip content={tooltip} />} + </span> <span className={`font-mono font-semibold ${color ?? 'text-white'}`}>{value}</span> </div> ); @@ -604,7 +626,10 @@ function ClusterSidebar({ node, ipDetails, ipTotal, ipPage, clusterPoints, onClo <div className="flex-1 overflow-y-auto px-4 py-3 space-y-4"> {/* Score risque */} <div className="bg-background-card rounded-lg p-3"> - <div className="text-xs text-text-secondary mb-2">Score de risque</div> + <div className="text-xs text-text-secondary mb-2 flex items-center"> + Score de risque + <InfoTip content={TIPS.risk_score} /> + </div> <div className="flex items-center gap-3"> <div className="flex-1 h-3 bg-gray-700 rounded-full overflow-hidden"> <div className="h-full rounded-full" style={{ width: `${node.risk_score * 100}%`, background: node.color }} /> @@ -618,7 +643,10 @@ function ClusterSidebar({ node, ipDetails, ipTotal, ipPage, clusterPoints, onClo {/* Radar chart */} {node.radar?.length > 0 && ( <div className="bg-background-card rounded-lg p-3"> - <div className="text-xs text-text-secondary mb-2">Profil 21 features</div> + <div className="text-xs text-text-secondary mb-2 flex items-center"> + Profil {node.radar?.length ?? 21} features + <InfoTip content={TIPS.radar_profile} /> + </div> <ResponsiveContainer width="100%" height={200}> <RadarChart data={node.radar}> <PolarGrid stroke="#374151" /> @@ -636,12 +664,12 @@ function ClusterSidebar({ node, ipDetails, ipTotal, ipPage, clusterPoints, onClo {/* TCP stack */} <div className="bg-background-card rounded-lg p-3 text-xs space-y-1"> <div className="font-semibold mb-2">Stack TCP</div> - <Stat label="TTL moyen" value={node.mean_ttl} /> - <Stat label="MSS moyen" value={node.mean_mss} /> - <Stat label="Score ML" value={`${(node.mean_score * 100).toFixed(1)}%`} /> - <Stat label="Vélocité" value={node.mean_velocity?.toFixed ? `${node.mean_velocity.toFixed(2)} rps` : '-'} /> - <Stat label="Headless" value={node.mean_headless ? `${(node.mean_headless * 100).toFixed(0)}%` : '-'} /> - <Stat label="UA-CH Mismatch" value={node.mean_ua_ch ? `${(node.mean_ua_ch * 100).toFixed(0)}%` : '-'} /> + <Stat label="TTL moyen" value={node.mean_ttl} tooltip={TIPS.mean_ttl} /> + <Stat label="MSS moyen" value={node.mean_mss} tooltip={TIPS.mean_mss} /> + <Stat label="Score ML" value={`${(node.mean_score * 100).toFixed(1)}%`} tooltip={TIPS.mean_score} /> + <Stat label="Vélocité" value={node.mean_velocity?.toFixed ? `${node.mean_velocity.toFixed(2)} rps` : '-'} tooltip={TIPS.mean_velocity} /> + <Stat label="Headless" value={node.mean_headless ? `${(node.mean_headless * 100).toFixed(0)}%` : '-'} tooltip={TIPS.mean_headless} /> + <Stat label="UA-CH Mismatch" value={node.mean_ua_ch ? `${(node.mean_ua_ch * 100).toFixed(0)}%` : '-'} tooltip={TIPS.mean_ua_ch} /> </div> {/* 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 }) { <circle key={i} cx={x} cy={y} r="3" fill="rgba(239,68,68,0.9)" /> ))} - {/* 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' }} > + <title>{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 %', +};