feat: système de tooltips universel sur tous les termes techniques
- 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 : <title> 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>
This commit is contained in:
@ -21,6 +21,8 @@ import { HeaderFingerprintView } from './components/HeaderFingerprintView';
|
|||||||
import { MLFeaturesView } from './components/MLFeaturesView';
|
import { MLFeaturesView } from './components/MLFeaturesView';
|
||||||
import ClusteringView from './components/ClusteringView';
|
import ClusteringView from './components/ClusteringView';
|
||||||
import { useTheme } from './ThemeContext';
|
import { useTheme } from './ThemeContext';
|
||||||
|
import { Tooltip } from './components/ui/Tooltip';
|
||||||
|
import { TIPS } from './components/ui/tooltips';
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -144,19 +146,29 @@ function Sidebar({ counts }: { counts: AlertCounts | null }) {
|
|||||||
{/* Alert stats */}
|
{/* Alert stats */}
|
||||||
{counts && (
|
{counts && (
|
||||||
<div className="mx-3 mt-5 bg-background-card rounded-lg p-3 space-y-2">
|
<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 && (
|
{counts.critical > 0 && (
|
||||||
<div className="flex justify-between items-center">
|
<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>
|
<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>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-between items-center">
|
<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>
|
<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>
|
||||||
<div className="flex justify-between items-center">
|
<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>
|
<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>
|
||||||
<div className="border-t border-background-secondary pt-1.5 flex justify-between items-center mt-1">
|
<div className="border-t border-background-secondary pt-1.5 flex justify-between items-center mt-1">
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import DeckGL from '@deck.gl/react';
|
|||||||
import { OrthographicView } from '@deck.gl/core';
|
import { OrthographicView } from '@deck.gl/core';
|
||||||
import { ScatterplotLayer, PolygonLayer, TextLayer, LineLayer } from '@deck.gl/layers';
|
import { ScatterplotLayer, PolygonLayer, TextLayer, LineLayer } from '@deck.gl/layers';
|
||||||
import { RadarChart, PolarGrid, PolarAngleAxis, Radar, ResponsiveContainer, Tooltip } from 'recharts';
|
import { RadarChart, PolarGrid, PolarAngleAxis, Radar, ResponsiveContainer, Tooltip } from 'recharts';
|
||||||
|
import { InfoTip } from './ui/Tooltip';
|
||||||
|
import { TIPS } from './ui/tooltips';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
@ -359,10 +361,13 @@ export default function ClusteringView() {
|
|||||||
{/* Sensibilité */}
|
{/* Sensibilité */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex justify-between text-xs text-text-secondary">
|
<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">
|
<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'}
|
{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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="range" min={0.5} max={5.0} step={0.5} value={sensitivity}
|
<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>
|
<summary className="cursor-pointer hover:text-white">Paramètres avancés</summary>
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
<label className="block">
|
<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}
|
<input type="range" min={4} max={100} value={k}
|
||||||
onChange={e => setK(+e.target.value)}
|
onChange={e => setK(+e.target.value)}
|
||||||
className="w-full mt-1 accent-accent-primary" />
|
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">
|
<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)}
|
<input type="checkbox" checked={showEdges} onChange={e => setShowEdges(e.target.checked)}
|
||||||
className="accent-accent-primary" />
|
className="accent-accent-primary" />
|
||||||
Afficher les arêtes
|
<span className="flex items-center">
|
||||||
|
Afficher les arêtes
|
||||||
|
<InfoTip content={TIPS.show_edges} />
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<button onClick={() => fetchClusters(true)}
|
<button onClick={() => fetchClusters(true)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@ -414,12 +425,12 @@ export default function ClusteringView() {
|
|||||||
{data?.stats && (
|
{data?.stats && (
|
||||||
<div className="bg-background-card rounded-lg p-3 space-y-1 text-xs">
|
<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>
|
<div className="font-semibold text-sm mb-2">Résultats</div>
|
||||||
<Stat label="Clusters" value={data.stats.total_clusters} />
|
<Stat label="Clusters" value={data.stats.total_clusters} tooltip={TIPS.k_actual} />
|
||||||
<Stat label="IPs totales" value={data.stats.total_ips.toLocaleString()} />
|
<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" />
|
<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" />
|
<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()} />
|
<Stat label="Hits totaux" value={data.stats.total_hits.toLocaleString()} tooltip={TIPS.total_hits} />
|
||||||
<Stat label="Calcul" value={`${data.stats.elapsed_s}s`} />
|
<Stat label="Calcul" value={`${data.stats.elapsed_s}s`} tooltip={TIPS.calc_time} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -514,14 +525,22 @@ export default function ClusteringView() {
|
|||||||
controller={true}
|
controller={true}
|
||||||
>
|
>
|
||||||
{/* Légende overlay */}
|
{/* 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">
|
<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">
|
['#dc2626', 'CRITICAL', TIPS.risk_critical],
|
||||||
<span className="w-3 h-3 rounded-full" style={{ background: c }} />
|
['#f97316', 'HIGH', TIPS.risk_high],
|
||||||
<span className="text-white/80">{l}</span>
|
['#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>
|
||||||
))}
|
))}
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{/* Tooltip zoom hint */}
|
{/* Tooltip zoom hint */}
|
||||||
@ -550,10 +569,13 @@ export default function ClusteringView() {
|
|||||||
|
|
||||||
// ─── Stat helper ─────────────────────────────────────────────────────────────
|
// ─── 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 (
|
return (
|
||||||
<div className="flex justify-between items-center">
|
<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>
|
<span className={`font-mono font-semibold ${color ?? 'text-white'}`}>{value}</span>
|
||||||
</div>
|
</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">
|
<div className="flex-1 overflow-y-auto px-4 py-3 space-y-4">
|
||||||
{/* Score risque */}
|
{/* Score risque */}
|
||||||
<div className="bg-background-card rounded-lg p-3">
|
<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 items-center gap-3">
|
||||||
<div className="flex-1 h-3 bg-gray-700 rounded-full overflow-hidden">
|
<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 }} />
|
<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 */}
|
{/* Radar chart */}
|
||||||
{node.radar?.length > 0 && (
|
{node.radar?.length > 0 && (
|
||||||
<div className="bg-background-card rounded-lg p-3">
|
<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}>
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
<RadarChart data={node.radar}>
|
<RadarChart data={node.radar}>
|
||||||
<PolarGrid stroke="#374151" />
|
<PolarGrid stroke="#374151" />
|
||||||
@ -636,12 +664,12 @@ function ClusterSidebar({ node, ipDetails, ipTotal, ipPage, clusterPoints, onClo
|
|||||||
{/* TCP stack */}
|
{/* TCP stack */}
|
||||||
<div className="bg-background-card rounded-lg p-3 text-xs space-y-1">
|
<div className="bg-background-card rounded-lg p-3 text-xs space-y-1">
|
||||||
<div className="font-semibold mb-2">Stack TCP</div>
|
<div className="font-semibold mb-2">Stack TCP</div>
|
||||||
<Stat label="TTL moyen" value={node.mean_ttl} />
|
<Stat label="TTL moyen" value={node.mean_ttl} tooltip={TIPS.mean_ttl} />
|
||||||
<Stat label="MSS moyen" value={node.mean_mss} />
|
<Stat label="MSS moyen" value={node.mean_mss} tooltip={TIPS.mean_mss} />
|
||||||
<Stat label="Score ML" value={`${(node.mean_score * 100).toFixed(1)}%`} />
|
<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` : '-'} />
|
<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)}%` : '-'} />
|
<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)}%` : '-'} />
|
<Stat label="UA-CH Mismatch" value={node.mean_ua_ch ? `${(node.mean_ua_ch * 100).toFixed(0)}%` : '-'} tooltip={TIPS.mean_ua_ch} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contexte */}
|
{/* Contexte */}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import DataTable, { Column } from './ui/DataTable';
|
import DataTable, { Column } from './ui/DataTable';
|
||||||
|
import { TIPS } from './ui/tooltips';
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -100,14 +101,14 @@ function ErrorMessage({ message }: { message: string }) {
|
|||||||
// ─── Radar Chart (SVG octagonal) ─────────────────────────────────────────────
|
// ─── Radar Chart (SVG octagonal) ─────────────────────────────────────────────
|
||||||
|
|
||||||
const RADAR_AXES = [
|
const RADAR_AXES = [
|
||||||
{ key: 'fuzzing_score', label: 'Fuzzing' },
|
{ key: 'fuzzing_score', label: 'Fuzzing', tip: TIPS.fuzzing },
|
||||||
{ key: 'velocity_score', label: 'Vélocité' },
|
{ key: 'velocity_score', label: 'Vélocité', tip: TIPS.velocity },
|
||||||
{ key: 'fake_nav_score', label: 'Fausse nav' },
|
{ key: 'fake_nav_score', label: 'Fausse nav', tip: TIPS.fake_nav },
|
||||||
{ key: 'ua_mismatch_score', label: 'UA/CH mismatch' },
|
{ key: 'ua_mismatch_score', label: 'UA/CH mismatch', tip: TIPS.ua_mismatch },
|
||||||
{ key: 'sni_mismatch_score', label: 'SNI mismatch' },
|
{ key: 'sni_mismatch_score', label: 'SNI mismatch', tip: TIPS.sni_mismatch },
|
||||||
{ key: 'orphan_score', label: 'Orphan ratio' },
|
{ key: 'orphan_score', label: 'Orphan ratio', tip: TIPS.orphan_ratio },
|
||||||
{ key: 'path_repetition_score', label: 'Répétition URL' },
|
{ key: 'path_repetition_score', label: 'Répétition URL', tip: TIPS.path_repetition },
|
||||||
{ key: 'payload_anomaly_score', label: 'Payload anormal' },
|
{ key: 'payload_anomaly_score', label: 'Payload anormal', tip: TIPS.payload_anomaly },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type RadarKey = typeof RADAR_AXES[number]['key'];
|
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)" />
|
<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) => {
|
{RADAR_AXES.map((axis, i) => {
|
||||||
const [x, y] = pointFor(i, maxR + 18);
|
const [x, y] = pointFor(i, maxR + 18);
|
||||||
const anchor = x < cx - 5 ? 'end' : x > cx + 5 ? 'start' : 'middle';
|
const anchor = x < cx - 5 ? 'end' : x > cx + 5 ? 'start' : 'middle';
|
||||||
@ -195,7 +196,9 @@ function RadarChart({ data }: { data: RadarData }) {
|
|||||||
fontSize="10"
|
fontSize="10"
|
||||||
fill="rgba(148,163,184,0.9)"
|
fill="rgba(148,163,184,0.9)"
|
||||||
dominantBaseline="middle"
|
dominantBaseline="middle"
|
||||||
|
style={{ cursor: 'help' }}
|
||||||
>
|
>
|
||||||
|
<title>{axis.tip}</title>
|
||||||
{axis.label}
|
{axis.label}
|
||||||
</text>
|
</text>
|
||||||
);
|
);
|
||||||
@ -257,7 +260,10 @@ function ScatterPlot({ points }: { points: ScatterPoint[] }) {
|
|||||||
{xTicks.map((v) => (
|
{xTicks.map((v) => (
|
||||||
<text key={v} x={toSvgX(v)} y={H - padB + 12} textAnchor="middle" fontSize="9" fill="rgba(148,163,184,0.7)">{v}</text>
|
<text key={v} x={toSvgX(v)} y={H - padB + 12} textAnchor="middle" fontSize="9" fill="rgba(148,163,184,0.7)">{v}</text>
|
||||||
))}
|
))}
|
||||||
<text x={(W - padL - padR) / 2 + padL} y={H - 2} textAnchor="middle" fontSize="10" fill="rgba(148,163,184,0.8)">Fuzzing Index →</text>
|
<text x={(W - padL - padR) / 2 + padL} y={H - 2} textAnchor="middle" fontSize="10" fill="rgba(148,163,184,0.8)" style={{ cursor: 'help' }}>
|
||||||
|
<title>{TIPS.fuzzing_index}</title>
|
||||||
|
Fuzzing Index →
|
||||||
|
</text>
|
||||||
|
|
||||||
{/* Y axis */}
|
{/* Y axis */}
|
||||||
<line x1={padL} y1={padT} x2={padL} y2={H - padB} stroke="rgba(100,116,139,0.4)" strokeWidth="1" />
|
<line x1={padL} y1={padT} x2={padL} y2={H - padB} stroke="rgba(100,116,139,0.4)" strokeWidth="1" />
|
||||||
@ -361,6 +367,7 @@ function AnomaliesTable({
|
|||||||
{
|
{
|
||||||
key: 'fuzzing_index',
|
key: 'fuzzing_index',
|
||||||
label: 'Fuzzing',
|
label: 'Fuzzing',
|
||||||
|
tooltip: TIPS.fuzzing_index,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
render: (v: number) => (
|
render: (v: number) => (
|
||||||
<span className={`px-1.5 py-0.5 rounded text-xs font-semibold ${fuzzingBadgeClass(v)}`}>
|
<span className={`px-1.5 py-0.5 rounded text-xs font-semibold ${fuzzingBadgeClass(v)}`}>
|
||||||
@ -371,6 +378,7 @@ function AnomaliesTable({
|
|||||||
{
|
{
|
||||||
key: 'attack_type',
|
key: 'attack_type',
|
||||||
label: 'Type',
|
label: 'Type',
|
||||||
|
tooltip: 'Type d\'attaque détecté : Brute Force 🔑, Flood 🌊, Scraper 🕷️, Spoofing 🎭, Scanner 🔍',
|
||||||
render: (v: string) => (
|
render: (v: string) => (
|
||||||
<span title={v} className="text-sm">{attackTypeEmoji(v)}</span>
|
<span title={v} className="text-sm">{attackTypeEmoji(v)}</span>
|
||||||
),
|
),
|
||||||
@ -378,6 +386,7 @@ function AnomaliesTable({
|
|||||||
{
|
{
|
||||||
key: '_signals',
|
key: '_signals',
|
||||||
label: 'Signaux',
|
label: 'Signaux',
|
||||||
|
tooltip: '⚠️ UA/CH mismatch · 🎭 Fausse navigation · 🔄 UA rotatif · 🌐 SNI mismatch',
|
||||||
sortable: false,
|
sortable: false,
|
||||||
render: (_: unknown, row: MLAnomaly) => (
|
render: (_: unknown, row: MLAnomaly) => (
|
||||||
<span className="flex gap-0.5">
|
<span className="flex gap-0.5">
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import DataTable, { Column } from './ui/DataTable';
|
import DataTable, { Column } from './ui/DataTable';
|
||||||
|
import { TIPS } from './ui/tooltips';
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -144,6 +145,7 @@ function TcpDetectionsTable({
|
|||||||
{
|
{
|
||||||
key: 'tcp_ttl',
|
key: 'tcp_ttl',
|
||||||
label: 'TTL obs. / init.',
|
label: 'TTL obs. / init.',
|
||||||
|
tooltip: TIPS.ttl,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
render: (_: number, row: TcpSpoofingItem) => (
|
render: (_: number, row: TcpSpoofingItem) => (
|
||||||
<span className="font-mono text-xs">
|
<span className="font-mono text-xs">
|
||||||
@ -159,6 +161,7 @@ function TcpDetectionsTable({
|
|||||||
{
|
{
|
||||||
key: 'tcp_mss',
|
key: 'tcp_mss',
|
||||||
label: 'MSS',
|
label: 'MSS',
|
||||||
|
tooltip: TIPS.mss,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
render: (v: number) => (
|
render: (v: number) => (
|
||||||
<span className={`font-mono text-xs ${mssColor(v)}`} title={mssLabel(v)}>
|
<span className={`font-mono text-xs ${mssColor(v)}`} title={mssLabel(v)}>
|
||||||
@ -169,6 +172,7 @@ function TcpDetectionsTable({
|
|||||||
{
|
{
|
||||||
key: 'tcp_win_scale',
|
key: 'tcp_win_scale',
|
||||||
label: 'Scale',
|
label: 'Scale',
|
||||||
|
tooltip: TIPS.win_scale,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
render: (v: number) => (
|
render: (v: number) => (
|
||||||
<span className="font-mono text-xs text-text-secondary">{v}</span>
|
<span className="font-mono text-xs text-text-secondary">{v}</span>
|
||||||
@ -177,6 +181,7 @@ function TcpDetectionsTable({
|
|||||||
{
|
{
|
||||||
key: 'suspected_os',
|
key: 'suspected_os',
|
||||||
label: 'OS suspecté (TCP)',
|
label: 'OS suspecté (TCP)',
|
||||||
|
tooltip: TIPS.os_tcp,
|
||||||
render: (v: string, row: TcpSpoofingItem) => (
|
render: (v: string, row: TcpSpoofingItem) => (
|
||||||
<span className={`text-xs flex items-center gap-1 ${row.is_bot_tool ? 'text-threat-critical font-semibold' : 'text-text-primary'}`}>
|
<span className={`text-xs flex items-center gap-1 ${row.is_bot_tool ? 'text-threat-critical font-semibold' : 'text-text-primary'}`}>
|
||||||
<span>{osIcon(v)}</span>
|
<span>{osIcon(v)}</span>
|
||||||
@ -187,6 +192,7 @@ function TcpDetectionsTable({
|
|||||||
{
|
{
|
||||||
key: 'confidence',
|
key: 'confidence',
|
||||||
label: 'Confiance',
|
label: 'Confiance',
|
||||||
|
tooltip: TIPS.confidence,
|
||||||
render: (v: number) => confidenceBar(v),
|
render: (v: number) => confidenceBar(v),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -197,11 +203,13 @@ function TcpDetectionsTable({
|
|||||||
{
|
{
|
||||||
key: 'declared_os',
|
key: 'declared_os',
|
||||||
label: 'OS déclaré (UA)',
|
label: 'OS déclaré (UA)',
|
||||||
|
tooltip: TIPS.os_ua,
|
||||||
render: (v: string) => <span className="text-xs text-text-secondary">{v || '—'}</span>,
|
render: (v: string) => <span className="text-xs text-text-secondary">{v || '—'}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'spoof_flag',
|
key: 'spoof_flag',
|
||||||
label: 'Verdict',
|
label: 'Verdict',
|
||||||
|
tooltip: TIPS.spoof_verdict,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
render: (v: boolean, row: TcpSpoofingItem) => {
|
render: (v: boolean, row: TcpSpoofingItem) => {
|
||||||
if (row.is_bot_tool) {
|
if (row.is_bot_tool) {
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSort, SortDir } from '../../hooks/useSort';
|
import { useSort, SortDir } from '../../hooks/useSort';
|
||||||
|
import { InfoTip } from './Tooltip';
|
||||||
|
|
||||||
export interface Column<T> {
|
export interface Column<T> {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
tooltip?: string;
|
||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
align?: 'left' | 'right' | 'center';
|
align?: 'left' | 'right' | 'center';
|
||||||
width?: string;
|
width?: string;
|
||||||
@ -96,6 +98,7 @@ export default function DataTable<T extends Record<string, any>>({
|
|||||||
>
|
>
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
{col.label}
|
{col.label}
|
||||||
|
{col.tooltip && <InfoTip content={col.tooltip} />}
|
||||||
{isSortable &&
|
{isSortable &&
|
||||||
(isActive ? (
|
(isActive ? (
|
||||||
<span className="text-accent-primary">
|
<span className="text-accent-primary">
|
||||||
|
|||||||
145
frontend/src/components/ui/Tooltip.tsx
Normal file
145
frontend/src/components/ui/Tooltip.tsx
Normal file
@ -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 :
|
||||||
|
* <Tooltip content="Explication…"><span>label</span></Tooltip>
|
||||||
|
* <InfoTip content="Explication…" /> ← 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<TooltipPos | null>(null);
|
||||||
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const spanRef = useRef<HTMLSpanElement>(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 (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
ref={spanRef}
|
||||||
|
className={`inline-flex items-center ${className}`}
|
||||||
|
onMouseEnter={show}
|
||||||
|
onMouseLeave={hide}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{pos &&
|
||||||
|
createPortal(
|
||||||
|
<TooltipBubble x={pos.x} y={pos.y} maxWidth={maxWidth}>
|
||||||
|
{content}
|
||||||
|
</TooltipBubble>,
|
||||||
|
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<HTMLDivElement>(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 (
|
||||||
|
<div
|
||||||
|
ref={bubbleRef}
|
||||||
|
className="fixed z-[9999] pointer-events-none"
|
||||||
|
style={{
|
||||||
|
left: x + adjust.dx,
|
||||||
|
top: y + adjust.dy - 8,
|
||||||
|
transform: 'translate(-50%, -100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-xs text-slate-100 shadow-2xl leading-relaxed whitespace-pre-line text-left"
|
||||||
|
style={{ maxWidth }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{/* Flèche vers le bas */}
|
||||||
|
<div className="w-0 h-0 mx-auto border-[5px] border-transparent border-t-slate-600" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<Tooltip content={content} className={className}>
|
||||||
|
<span className="ml-1 text-[10px] text-slate-500 cursor-help select-none hover:text-slate-300 transition-colors leading-none">
|
||||||
|
ⓘ
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
285
frontend/src/components/ui/tooltips.ts
Normal file
285
frontend/src/components/ui/tooltips.ts
Normal file
@ -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 <InfoTip content={TIPS.xxx} /> ou <Tooltip content={TIPS.xxx}>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 %',
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user