import { useParams, useNavigate, Link } from 'react-router-dom'; import { useState, useEffect } from 'react'; import { useVariability } from '../hooks/useVariability'; import { VariabilityPanel } from './VariabilityPanel'; import { formatDateShort } from '../utils/dateUtils'; import { SubnetAnalysis } from './analysis/SubnetAnalysis'; import { CountryAnalysis } from './analysis/CountryAnalysis'; import { JA4Analysis } from './analysis/JA4Analysis'; 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 ───────────────────────────────────── interface IPSummary { ip: string; risk_score: number; ml: { max_score: number; threat_level: string; attack_type: string; total_detections: number; distinct_hosts: number; distinct_ja4: number }; bruteforce: { active: boolean; hosts_attacked: number; total_hits: number; total_params: number; top_hosts: string[] }; tcp_spoofing: { detected: boolean; tcp_ttl: number | null; suspected_os: string | null; declared_os: string | null }; ja4_rotation: { rotating: boolean; distinct_ja4_count: number; total_hits?: number }; persistence: { persistent: boolean; recurrence: number; worst_score?: number; worst_threat_level?: string; first_seen?: string; last_seen?: string }; timeline_24h: { hour: number; hits: number; ja4s: string[] }[]; } function RiskGauge({ score }: { score: number }) { const color = score >= 75 ? '#ef4444' : score >= 50 ? '#f97316' : score >= 25 ? '#eab308' : '#22c55e'; return (
{score} Risk Score
); } function ActivityBadge({ active, label, color }: { active: boolean; label: string; color: string }) { return (
{active ? '●' : '○'} {label}
); } function MiniTimeline({ data }: { data: { hour: number; hits: number }[] }) { if (!data.length) return Pas d'activité 24h; const max = Math.max(...data.map(d => d.hits), 1); return (
{Array.from({ length: 24 }, (_, h) => { const d = data.find(x => x.hour === h); const pct = d ? (d.hits / max) * 100 : 0; return (
0 ? 'bg-accent-primary' : 'bg-background-card'}`} style={{ height: `${Math.max(pct, 2)}%` }} />
); })}
); } function IPActivitySummary({ ip }: { ip: string }) { const [open, setOpen] = useState(false); // fermée par défaut const [loading, setLoading] = useState(true); const [data, setData] = useState(null); useEffect(() => { setLoading(true); fetch(`/api/investigation/${encodeURIComponent(ip)}/summary`) .then(r => r.ok ? r.json() : null) .then(d => setData(d)) .catch(() => null) .finally(() => setLoading(false)); }, [ip]); return (
{open && (
{loading &&
Chargement des données multi-sources…
} {!loading && !data &&
Données insuffisantes pour cette IP.
} {data && (
{/* Risk + badges row */}
0} label={`ML: ${data.ml.total_detections} détections`} color="threat-critical" />
{/* Detail grid */}
{data.ml.total_detections > 0 && (
ML Detection
{data.ml.threat_level || '—'} · {data.ml.attack_type || '—'}
Score: {data.ml.max_score} · {data.ml.distinct_ja4} JA4(s)
)} {data.bruteforce.active && (
Brute Force
{data.bruteforce.total_hits.toLocaleString(navigator.language || undefined)} hits
{data.bruteforce.top_hosts[0] ?? '—'}
)} {data.tcp_spoofing.detected && (
TCP Spoofing
TTL {data.tcp_spoofing.tcp_ttl} → {data.tcp_spoofing.suspected_os}
UA déclare: {data.tcp_spoofing.declared_os}
)} {data.persistence.persistent && (
Persistance
{data.persistence.recurrence}× sessions
{data.persistence.first_seen?.substring(0, 10)} → {data.persistence.last_seen?.substring(0, 10)}
)}
{/* Mini timeline */}
Activité dernières 24h
0h12h23h
)}
)}
); } interface CoherenceData { verdict: string; spoofing_score: number; explanation: string[]; indicators: { ua_ch_mismatch_rate: number; sni_mismatch_rate: number; avg_browser_score: number; distinct_ja4_count: number; is_ua_rotating: boolean; rare_ja4_rate: number; }; fingerprints: { ja4_list: string[]; latest_ja4: string }; user_agents: { ua: string; count: number; type: string }[]; } const VERDICT_STYLE: Record = { high_confidence_spoofing: { cls: 'bg-threat-critical/10 border-threat-critical/40 text-threat-critical', icon: '🎭', label: 'Spoofing haute confiance' }, suspicious_spoofing: { cls: 'bg-threat-high/10 border-threat-high/40 text-threat-high', icon: '⚠️', label: 'Spoofing suspect' }, known_bot_no_spoofing: { cls: 'bg-threat-medium/10 border-threat-medium/40 text-threat-medium', icon: '🤖', label: 'Bot connu (pas de spoofing)' }, legitimate_browser: { cls: 'bg-threat-low/10 border-threat-low/40 text-threat-low', icon: '✅', label: 'Navigateur légitime' }, inconclusive: { cls: 'bg-background-card border-background-card text-text-secondary', icon: '❓', label: 'Non concluant' }, }; function FingerprintCoherenceWidget({ ip }: { ip: string }) { const navigate = useNavigate(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); useEffect(() => { setLoading(true); setError(false); fetch(`/api/fingerprints/ip/${encodeURIComponent(ip)}/coherence`) .then((r) => (r.ok ? r.json() : null)) .then((d) => { if (d) setData(d); else setError(true); }) .catch(() => setError(true)) .finally(() => setLoading(false)); }, [ip]); const vs = data ? (VERDICT_STYLE[data.verdict] ?? VERDICT_STYLE.inconclusive) : null; return (

🎭 Cohérence JA4 / User-Agent

{loading &&
Analyse en cours…
} {error &&
Données insuffisantes pour cette IP
} {data && vs && (
{/* Verdict badge + score */}
{vs.icon}
{vs.label}
Score de spoofing: {data.spoofing_score}/100
= 70 ? 'bg-threat-critical' : data.spoofing_score >= 40 ? 'bg-threat-high' : data.spoofing_score >= 20 ? 'bg-threat-medium' : 'bg-threat-low' }`} style={{ width: `${data.spoofing_score}%` }} />
{/* Explanation */}
    {data.explanation.map((e, i) => (
  • {e}
  • ))}
{/* Key indicators */}
{[ { 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) => (
{ind.value}
{ind.label}
))}
{/* Top UAs */} {data.user_agents.length > 0 && (
User-Agents observés
{data.user_agents.slice(0, 4).map((u, i) => (
{u.type} {u.ua.length > 45 ? u.ua.slice(0, 45) + '…' : u.ua}
))}
)} {/* JA4 links */} {data.fingerprints.ja4_list.length > 0 && (
JA4 utilisés
{data.fingerprints.ja4_list.map((j4) => ( ))}
)}
)}
); } // ─── Section "Attributs détectés" (données de variabilité, ex-DetailsView) ─── function Metric({ label, value, accent }: { label: string; value: string; accent?: boolean }) { return (

{label}

{value}

); } function DetectionAttributesSection({ ip }: { ip: string }) { const [open, setOpen] = useState(true); // ouvert par défaut const { data, loading } = useVariability('ip', ip); const first = data?.date_range.first_seen ? new Date(data.date_range.first_seen) : null; const last = data?.date_range.last_seen ? new Date(data.date_range.last_seen) : null; const sameDate = first && last && first.getTime() === last.getTime(); const fmt = (d: Date) => formatDateShort(d.toISOString()); return (
{open && (
{loading &&
Chargement…
} {data && ( <> {/* Métriques */}
{first && last && ( sameDate ? ( ) : (

Période

{fmt(first)}

→ {fmt(last!)}

) )}
{/* Insights */} {data.insights.length > 0 && (
{data.insights.map((ins, i) => { const s: Record = { warning: 'bg-yellow-500/10 border-yellow-500/40 text-yellow-400', info: 'bg-blue-500/10 border-blue-500/40 text-blue-400', success: 'bg-green-500/10 border-green-500/40 text-green-400', }; return (
{ins.message}
); })}
)} {/* Attributs (JA4, hosts, ASN, pays, UA…) */} )}
)}
); } export function InvestigationView() { const { ip } = useParams<{ ip: string }>(); const navigate = useNavigate(); if (!ip) { return (
IP non spécifiée
); } const handleClassify = (label: string, tags: string[], comment: string, confidence: number) => { // Callback optionnel après classification console.log('IP classifiée:', { ip, label, tags, comment, confidence }); }; return (
{/* Breadcrumb */} {/* En-tête */}

Investigation: {ip}

Analyse de corrélations pour classification SOC
{/* Navigation ancres inter-sections */}
Aller à : {[ { id: 'section-attributs', label: '📡 Attributs' }, { id: 'section-synthese', label: '🔎 Synthèse' }, { id: 'section-reputation', label: '🌍 Réputation' }, { id: 'section-correlations', label: '🕸️ Corrélations' }, { id: 'section-geo', label: '🌐 Géo / JA4' }, { id: 'section-classification', label: '🏷️ Classification' }, ].map(({ id, label }) => ( {label} ))}
{/* Attributs détectés (ex-DetailsView) */}
{/* Synthèse multi-sources */}
{/* Réputation (1/3) + Graph de corrélations (2/3) */}

🌍 Réputation IP

🕸️ Graph de Corrélations

{/* Subnet / Country / JA4 */}
{/* User-Agents (1/2) + Classification (1/2) */}
{/* Cohérence JA4/UA (spoofing) */}

🔏 JA4 Légitimes (baseline)

Comparez les fingerprints de cette IP avec la baseline des JA4 légitimes pour évaluer le risque de spoofing.

); }