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:
@ -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/')) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user