feat: tooltips InvestigationView/BruteForce/HeaderFingerprint + fix RouteTracker

- InvestigationView: InfoTip sur Risk Score, JA4 Rotation, TCP Spoof,
  Persistance, UA/CH mismatch, Browser score, JA4 distincts, JA4 rares,
  JA4 Légitimes (baseline)
- BruteForceView: tooltip Params combos, Top JA4, Credential Stuffing, Énumération
- HeaderFingerprintView: tooltip colonnes Hash cluster, Browser Score,
  UA/CH Mismatch %, Sec-Fetch modes
- App.tsx: RouteTracker ignore /investigation/ip/ (route alias)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
SOC Analyst
2026-03-19 12:20:35 +01:00
parent fe7e11615e
commit b0999ee83a
4 changed files with 42 additions and 17 deletions

View File

@ -330,6 +330,8 @@ function RouteTracker() {
const p = location.pathname;
if (p.startsWith('/investigation/ja4/')) {
saveRecent({ type: 'ja4', value: decodeURIComponent(p.split('/investigation/ja4/')[1] || '') });
} else if (p.startsWith('/investigation/ip/')) {
// Redirigé — ne pas sauvegarder l'alias (la route finale /investigation/:ip sera sauvegardée)
} else if (p.startsWith('/investigation/')) {
saveRecent({ type: 'ip', value: decodeURIComponent(p.split('/investigation/')[1] || '') });
} else if (p.startsWith('/entities/subnet/')) {

View File

@ -1,6 +1,8 @@
import { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import DataTable, { Column } from './ui/DataTable';
import { InfoTip } from './ui/Tooltip';
import { TIPS } from './ui/tooltips';
// ─── Types ────────────────────────────────────────────────────────────────────
@ -87,12 +89,14 @@ function AttackersTable({
{
key: 'total_params',
label: 'Params',
tooltip: TIPS.params_combos,
align: 'right',
render: (v: number) => formatNumber(v),
},
{
key: 'ja4',
label: 'JA4',
tooltip: TIPS.ja4,
render: (v: string) => (
<span className="font-mono text-xs text-text-secondary">
{v ? `${v.slice(0, 16)}` : '—'}
@ -163,9 +167,9 @@ function TargetRow({ t, navigate }: { t: BruteForceTarget; navigate: (path: stri
<td className="px-4 py-3 text-text-primary">{formatNumber(t.total_params)}</td>
<td className="px-4 py-3">
{t.attack_type === 'credential_stuffing' ? (
<span className="bg-threat-critical/20 text-threat-critical text-xs px-2 py-1 rounded-full">💳 Credential Stuffing</span>
<span className="bg-threat-critical/20 text-threat-critical text-xs px-2 py-1 rounded-full" title={TIPS.credential_stuffing}>💳 Credential Stuffing</span>
) : (
<span className="bg-threat-high/20 text-threat-high text-xs px-2 py-1 rounded-full">🔍 Énumération</span>
<span className="bg-threat-high/20 text-threat-high text-xs px-2 py-1 rounded-full" title={TIPS.enumeration}>🔍 Énumération</span>
)}
</td>
<td className="px-4 py-3">
@ -380,9 +384,15 @@ export function BruteForceView() {
<th className="px-4 py-3">Host (cliquer pour détails)</th>
<th className="px-4 py-3">IPs distinctes</th>
<th className="px-4 py-3">Total hits</th>
<th className="px-4 py-3">Params combos</th>
<th className="px-4 py-3 whitespace-nowrap">
Params combos
<InfoTip content={TIPS.params_combos} />
</th>
<th className="px-4 py-3">Type d'attaque</th>
<th className="px-4 py-3">Top JA4</th>
<th className="px-4 py-3 whitespace-nowrap">
Top JA4
<InfoTip content={TIPS.ja4} />
</th>
</tr>
</thead>
<tbody>

View File

@ -1,6 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import DataTable, { Column } from './ui/DataTable';
import { TIPS } from './ui/tooltips';
// ─── Types ────────────────────────────────────────────────────────────────────
@ -140,6 +141,7 @@ export function HeaderFingerprintView() {
{
key: 'hash',
label: 'Hash cluster',
tooltip: TIPS.hash_cluster,
sortable: true,
render: (_, row) => (
<span>
@ -158,6 +160,7 @@ export function HeaderFingerprintView() {
{
key: 'avg_browser_score',
label: 'Browser Score',
tooltip: TIPS.browser_score,
sortable: true,
render: (v) => (
<div className="flex items-center gap-2">
@ -171,6 +174,7 @@ export function HeaderFingerprintView() {
{
key: 'ua_ch_mismatch_pct',
label: 'UA/CH Mismatch %',
tooltip: TIPS.ua_mismatch,
sortable: true,
align: 'right',
render: (v) => (
@ -191,6 +195,7 @@ export function HeaderFingerprintView() {
{
key: 'top_sec_fetch_modes',
label: 'Sec-Fetch modes',
tooltip: TIPS.sec_fetch,
sortable: false,
render: (v) => (
<div className="flex flex-wrap gap-1">

View File

@ -7,6 +7,8 @@ import { UserAgentAnalysis } from './analysis/UserAgentAnalysis';
import { CorrelationSummary } from './analysis/CorrelationSummary';
import { CorrelationGraph } from './CorrelationGraph';
import { ReputationPanel } from './ReputationPanel';
import { InfoTip } from './ui/Tooltip';
import { TIPS } from './ui/tooltips';
// ─── Multi-source Activity Summary Widget ─────────────────────────────────────
@ -33,7 +35,7 @@ function RiskGauge({ score }: { score: number }) {
transform="rotate(-90 40 40)" />
<text x="40" y="44" textAnchor="middle" fontSize="18" fontWeight="bold" fill={color}>{score}</text>
</svg>
<span className="text-xs text-text-secondary">Risk Score</span>
<span className="text-xs text-text-secondary flex items-center">Risk Score<InfoTip content={TIPS.risk_score_inv} /></span>
</div>
);
}
@ -113,9 +115,9 @@ function IPActivitySummary({ ip }: { ip: string }) {
<div className="flex flex-wrap gap-2">
<ActivityBadge active={data.ml.total_detections > 0} label={`ML: ${data.ml.total_detections} détections`} color="threat-critical" />
<ActivityBadge active={data.bruteforce.active} label={`Brute Force: ${data.bruteforce.hosts_attacked} hosts`} color="threat-high" />
<ActivityBadge active={data.tcp_spoofing.detected} label={`TCP Spoof: TTL ${data.tcp_spoofing.tcp_ttl ?? ''}`} color="threat-medium" />
<ActivityBadge active={data.ja4_rotation.rotating} label={`JA4 Rotation: ${data.ja4_rotation.distinct_ja4_count} signatures`} color="threat-medium" />
<ActivityBadge active={data.persistence.persistent} label={`Persistance: ${data.persistence.recurrence}x récurrences`} color="threat-high" />
<span title={TIPS.spoof_verdict}><ActivityBadge active={data.tcp_spoofing.detected} label={`TCP Spoof: TTL ${data.tcp_spoofing.tcp_ttl ?? ''}`} color="threat-medium" /></span>
<span title={TIPS.ja4_rotation}><ActivityBadge active={data.ja4_rotation.rotating} label={`JA4 Rotation: ${data.ja4_rotation.distinct_ja4_count} signatures`} color="threat-medium" /></span>
<span title={TIPS.persistence}><ActivityBadge active={data.persistence.persistent} label={`Persistance: ${data.persistence.recurrence}x récurrences`} color="threat-high" /></span>
</div>
{/* Detail grid */}
<div className="grid grid-cols-3 gap-3 text-xs">
@ -137,7 +139,7 @@ function IPActivitySummary({ ip }: { ip: string }) {
)}
{data.tcp_spoofing.detected && (
<div className="bg-background-card rounded p-2">
<div className="text-text-disabled mb-1">TCP Spoofing</div>
<div className="text-text-disabled mb-1" title={TIPS.spoof_verdict}>TCP Spoofing</div>
<div className="text-threat-medium font-medium">TTL {data.tcp_spoofing.tcp_ttl} → {data.tcp_spoofing.suspected_os}</div>
<div className="text-text-secondary">UA déclare: {data.tcp_spoofing.declared_os}</div>
</div>
@ -220,8 +222,8 @@ function FingerprintCoherenceWidget({ ip }: { ip: string }) {
<span className="text-2xl">{vs.icon}</span>
<div className="flex-1">
<div className="font-semibold text-sm">{vs.label}</div>
<div className="text-xs opacity-75 mt-0.5">
Score de spoofing: <strong>{data.spoofing_score}/100</strong>
<div className="text-xs opacity-75 mt-0.5 flex items-center gap-1">
Score de spoofing: <strong>{data.spoofing_score}/100</strong><InfoTip content={TIPS.spoofing_score} />
</div>
</div>
<div className="w-24">
@ -250,14 +252,17 @@ function FingerprintCoherenceWidget({ ip }: { ip: string }) {
{/* Key indicators */}
<div className="grid grid-cols-2 gap-2">
{[
{ label: 'UA/CH mismatch', value: `${data.indicators.ua_ch_mismatch_rate}%`, warn: data.indicators.ua_ch_mismatch_rate > 20 },
{ label: 'Browser score', value: `${data.indicators.avg_browser_score}/100`, warn: data.indicators.avg_browser_score > 60 },
{ label: 'JA4 distincts', value: data.indicators.distinct_ja4_count, warn: data.indicators.distinct_ja4_count > 2 },
{ label: 'JA4 rares', value: `${data.indicators.rare_ja4_rate}%`, warn: data.indicators.rare_ja4_rate > 50 },
{ label: 'UA/CH mismatch', tip: TIPS.ua_mismatch, value: `${data.indicators.ua_ch_mismatch_rate}%`, warn: data.indicators.ua_ch_mismatch_rate > 20 },
{ label: 'Browser score', tip: TIPS.browser_score, value: `${data.indicators.avg_browser_score}/100`, warn: data.indicators.avg_browser_score > 60 },
{ label: 'JA4 distincts', tip: TIPS.ja4_distinct, value: data.indicators.distinct_ja4_count, warn: data.indicators.distinct_ja4_count > 2 },
{ label: 'JA4 rares %', tip: TIPS.ja4_rare_pct, value: `${data.indicators.rare_ja4_rate}%`, warn: data.indicators.rare_ja4_rate > 50 },
].map((ind) => (
<div key={ind.label} className={`rounded p-2 text-center ${ind.warn ? 'bg-threat-high/10' : 'bg-background-card'}`}>
<div className={`text-sm font-semibold ${ind.warn ? 'text-threat-high' : 'text-text-primary'}`}>{ind.value}</div>
<div className="text-xs text-text-disabled">{ind.label}</div>
<div className="text-xs text-text-disabled flex items-center justify-center gap-0.5">
{ind.label}
<InfoTip content={ind.tip} />
</div>
</div>
))}
</div>
@ -376,7 +381,10 @@ export function InvestigationView() {
<div className="grid grid-cols-3 gap-6 items-start">
<FingerprintCoherenceWidget ip={ip} />
<div className="col-span-2 bg-background-secondary rounded-lg p-5">
<h3 className="text-base font-semibold text-text-primary mb-3">🔏 JA4 Légitimes (baseline)</h3>
<h3 className="text-base font-semibold text-text-primary mb-3 flex items-center gap-1">
🔏 JA4 Légitimes (baseline)
<InfoTip content={TIPS.baseline_ja4} />
</h3>
<p className="text-xs text-text-secondary mb-3">
Comparez les fingerprints de cette IP avec la baseline des JA4 légitimes pour évaluer le risque de spoofing.
</p>