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; const p = location.pathname;
if (p.startsWith('/investigation/ja4/')) { if (p.startsWith('/investigation/ja4/')) {
saveRecent({ type: 'ja4', value: decodeURIComponent(p.split('/investigation/ja4/')[1] || '') }); 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/')) { } else if (p.startsWith('/investigation/')) {
saveRecent({ type: 'ip', value: decodeURIComponent(p.split('/investigation/')[1] || '') }); saveRecent({ type: 'ip', value: decodeURIComponent(p.split('/investigation/')[1] || '') });
} else if (p.startsWith('/entities/subnet/')) { } else if (p.startsWith('/entities/subnet/')) {

View File

@ -1,6 +1,8 @@
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 { InfoTip } from './ui/Tooltip';
import { TIPS } from './ui/tooltips';
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
@ -87,12 +89,14 @@ function AttackersTable({
{ {
key: 'total_params', key: 'total_params',
label: 'Params', label: 'Params',
tooltip: TIPS.params_combos,
align: 'right', align: 'right',
render: (v: number) => formatNumber(v), render: (v: number) => formatNumber(v),
}, },
{ {
key: 'ja4', key: 'ja4',
label: 'JA4', label: 'JA4',
tooltip: TIPS.ja4,
render: (v: string) => ( render: (v: string) => (
<span className="font-mono text-xs text-text-secondary"> <span className="font-mono text-xs text-text-secondary">
{v ? `${v.slice(0, 16)}` : '—'} {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 text-text-primary">{formatNumber(t.total_params)}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
{t.attack_type === 'credential_stuffing' ? ( {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>
<td className="px-4 py-3"> <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">Host (cliquer pour détails)</th>
<th className="px-4 py-3">IPs distinctes</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">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">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> </tr>
</thead> </thead>
<tbody> <tbody>

View File

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

View File

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