Services: - ja4sentinel: TLS/JA4 fingerprint capture daemon (Go, libpcap) - logcorrelator: JA4 log correlation engine (Go, ClickHouse) - mod_reqin_log: Apache module (C, JSON request logging) - bot_detector: ML bot detection pipeline (Python) - dashboard: FastAPI/Streamlit analytics UI (Python) Shared libraries: - shared/go/ja4common: logger, config, shutdown, ipfilter (Go module) - shared/python/ja4_common: ClickHouseClient, ClickHouseSettings (Python package) - shared/clickhouse/: canonical SQL migrations (10 files) Build & packaging: - Unified 3-stage Dockerfile.package for Go RPMs (el8/el9/el10) - go.work workspace linking sentinel, correlator, ja4common - Makefile with test-all, build-all, rpm-* targets Fixes applied: - go.work: 1.21 → 1.24.6 (required by sentinel) - correlator Dockerfiles: golang:1.21 → golang:1.24 - replace directives in go.mod for ja4common local path - pyproject.toml: setuptools.backends → setuptools.build_meta - Removed static libpcap linking (unavailable on Rocky 9) - Fixed data races in output/writers_test.go (sync.Mutex + atomic.Int32) - Rewrote corrupted test files (logger_test.go × 2) Test coverage: - correlator: 67.1% total (unixsocket 80.5%, config 91.7%, app 83.3%, multi 87.7%, stdout 100%) - sentinel: all 10 packages pass (api, capture, config, fingerprint, ipfilter, logging, output, tlsparse) Documentation: - README.md + docs/ (architecture, development, 5 services, shared libs, DB schema & migrations) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
525 lines
25 KiB
TypeScript
525 lines
25 KiB
TypeScript
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 (
|
||
<div className="flex flex-col items-center gap-1">
|
||
<svg width="80" height="80" viewBox="0 0 80 80">
|
||
<circle cx="40" cy="40" r="34" fill="none" stroke="rgba(100,116,139,0.2)" strokeWidth="8" />
|
||
<circle cx="40" cy="40" r="34" fill="none" stroke={color} strokeWidth="8"
|
||
strokeDasharray={`${(score / 100) * 213.6} 213.6`}
|
||
strokeLinecap="round"
|
||
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 flex items-center">Risk Score<InfoTip content={TIPS.risk_score_inv} /></span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ActivityBadge({ active, label, color }: { active: boolean; label: string; color: string }) {
|
||
return (
|
||
<div className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border text-xs font-medium ${
|
||
active ? `border-${color}/40 bg-${color}/10 text-${color}` : 'border-border bg-background-card text-text-disabled'
|
||
}`}>
|
||
<span>{active ? '●' : '○'}</span>
|
||
{label}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function MiniTimeline({ data }: { data: { hour: number; hits: number }[] }) {
|
||
if (!data.length) return <span className="text-text-disabled text-xs">Pas d'activité 24h</span>;
|
||
const max = Math.max(...data.map(d => d.hits), 1);
|
||
return (
|
||
<div className="flex items-end gap-0.5 h-8">
|
||
{Array.from({ length: 24 }, (_, h) => {
|
||
const d = data.find(x => x.hour === h);
|
||
const pct = d ? (d.hits / max) * 100 : 0;
|
||
return (
|
||
<div key={h} className="flex-1 flex flex-col justify-end" title={d ? `${h}h: ${d.hits} hits` : `${h}h: 0`}>
|
||
<div className={`w-full rounded-sm ${pct > 0 ? 'bg-accent-primary' : 'bg-background-card'}`}
|
||
style={{ height: `${Math.max(pct, 2)}%` }} />
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function IPActivitySummary({ ip }: { ip: string }) {
|
||
const [open, setOpen] = useState(false); // fermée par défaut
|
||
const [loading, setLoading] = useState(true);
|
||
const [data, setData] = useState<IPSummary | null>(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 (
|
||
<div className="bg-background-secondary rounded-lg border border-border">
|
||
<button
|
||
onClick={() => setOpen(o => !o)}
|
||
className="w-full flex items-center justify-between px-5 py-4 hover:bg-background-card/50 transition-colors"
|
||
>
|
||
<span className="font-semibold text-text-primary flex items-center gap-2">
|
||
🔎 Synthèse multi-sources
|
||
{data && <span className={`text-xs px-2 py-0.5 rounded-full font-bold ${
|
||
data.risk_score >= 75 ? 'bg-threat-critical/20 text-threat-critical' :
|
||
data.risk_score >= 50 ? 'bg-threat-high/20 text-threat-high' :
|
||
data.risk_score >= 25 ? 'bg-threat-medium/20 text-threat-medium' :
|
||
'bg-threat-low/20 text-threat-low'
|
||
}`}>Score: {data.risk_score}</span>}
|
||
</span>
|
||
<span className="text-text-secondary">{open ? '▲' : '▼'}</span>
|
||
</button>
|
||
|
||
{open && (
|
||
<div className="px-5 pb-5">
|
||
{loading && <div className="text-text-disabled text-sm py-4">Chargement des données multi-sources…</div>}
|
||
{!loading && !data && <div className="text-text-disabled text-sm py-4">Données insuffisantes pour cette IP.</div>}
|
||
{data && (
|
||
<div className="space-y-4">
|
||
{/* Risk + badges row */}
|
||
<div className="flex items-start gap-6">
|
||
<RiskGauge score={data.risk_score} />
|
||
<div className="flex-1 space-y-3">
|
||
<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" />
|
||
<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">
|
||
{data.ml.total_detections > 0 && (
|
||
<div className="bg-background-card rounded p-2">
|
||
<div className="text-text-disabled mb-1">ML Detection</div>
|
||
<div className="text-text-primary font-medium">{data.ml.threat_level || '—'} · {data.ml.attack_type || '—'}</div>
|
||
<div className="text-text-secondary">Score: {data.ml.max_score} · {data.ml.distinct_ja4} JA4(s)</div>
|
||
</div>
|
||
)}
|
||
{data.bruteforce.active && (
|
||
<div className="bg-background-card rounded p-2">
|
||
<div className="text-text-disabled mb-1">Brute Force</div>
|
||
<div className="text-threat-high font-medium">{data.bruteforce.total_hits.toLocaleString(navigator.language || undefined)} hits</div>
|
||
<div className="text-text-secondary truncate" title={data.bruteforce.top_hosts.join(', ')}>
|
||
{data.bruteforce.top_hosts[0] ?? '—'}
|
||
</div>
|
||
</div>
|
||
)}
|
||
{data.tcp_spoofing.detected && (
|
||
<div className="bg-background-card rounded p-2">
|
||
<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>
|
||
)}
|
||
{data.persistence.persistent && (
|
||
<div className="bg-background-card rounded p-2">
|
||
<div className="text-text-disabled mb-1">Persistance</div>
|
||
<div className="text-threat-high font-medium">{data.persistence.recurrence}× sessions</div>
|
||
<div className="text-text-secondary">{data.persistence.first_seen?.substring(0, 10)} → {data.persistence.last_seen?.substring(0, 10)}</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/* Mini timeline */}
|
||
<div>
|
||
<div className="text-xs text-text-disabled mb-1 font-medium uppercase tracking-wide">Activité dernières 24h</div>
|
||
<MiniTimeline data={data.timeline_24h} />
|
||
<div className="flex justify-between text-xs text-text-disabled mt-0.5"><span>0h</span><span>12h</span><span>23h</span></div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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<string, { cls: string; icon: string; label: string }> = {
|
||
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<CoherenceData | null>(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 (
|
||
<div className="bg-background-secondary rounded-lg p-5">
|
||
<h3 className="text-base font-semibold text-text-primary mb-4">🎭 Cohérence JA4 / User-Agent</h3>
|
||
{loading && <div className="text-text-disabled text-sm">Analyse en cours…</div>}
|
||
{error && <div className="text-text-disabled text-sm">Données insuffisantes pour cette IP</div>}
|
||
{data && vs && (
|
||
<div className="space-y-4">
|
||
{/* Verdict badge + score */}
|
||
<div className={`flex items-center gap-3 p-3 rounded-lg border ${vs.cls}`}>
|
||
<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 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">
|
||
<div className="h-2 bg-white/20 rounded-full overflow-hidden">
|
||
<div
|
||
className={`h-2 rounded-full ${
|
||
data.spoofing_score >= 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}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Explanation */}
|
||
<ul className="space-y-1">
|
||
{data.explanation.map((e, i) => (
|
||
<li key={i} className="text-xs text-text-secondary flex items-start gap-1.5">
|
||
<span className="text-text-disabled mt-0.5">•</span> {e}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
|
||
{/* Key indicators */}
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{[
|
||
{ 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 flex items-center justify-center gap-0.5">
|
||
{ind.label}
|
||
<InfoTip content={ind.tip} />
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Top UAs */}
|
||
{data.user_agents.length > 0 && (
|
||
<div>
|
||
<div className="text-xs text-text-disabled mb-1.5 font-medium uppercase tracking-wide">User-Agents observés</div>
|
||
<div className="space-y-1">
|
||
{data.user_agents.slice(0, 4).map((u, i) => (
|
||
<div key={i} className="flex items-center gap-2 text-xs">
|
||
<span className={`shrink-0 px-1.5 py-0.5 rounded text-xs ${
|
||
u.type === 'bot' ? 'bg-threat-critical/20 text-threat-critical' :
|
||
u.type === 'browser' ? 'bg-accent-primary/20 text-accent-primary' :
|
||
'bg-background-card text-text-secondary'
|
||
}`}>{u.type}</span>
|
||
<span className="truncate text-text-secondary font-mono" title={u.ua}>
|
||
{u.ua.length > 45 ? u.ua.slice(0, 45) + '…' : u.ua}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* JA4 links */}
|
||
{data.fingerprints.ja4_list.length > 0 && (
|
||
<div>
|
||
<div className="text-xs text-text-disabled mb-1 font-medium uppercase tracking-wide">JA4 utilisés</div>
|
||
<div className="flex flex-wrap gap-1">
|
||
{data.fingerprints.ja4_list.map((j4) => (
|
||
<button
|
||
key={j4}
|
||
onClick={() => navigate(`/investigation/ja4/${encodeURIComponent(j4)}`)}
|
||
className="text-xs font-mono text-accent-primary hover:underline truncate max-w-[140px]"
|
||
title={j4}
|
||
>
|
||
{j4.length > 18 ? `${j4.slice(0, 9)}…${j4.slice(-8)}` : j4}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── Section "Attributs détectés" (données de variabilité, ex-DetailsView) ───
|
||
|
||
function Metric({ label, value, accent }: { label: string; value: string; accent?: boolean }) {
|
||
return (
|
||
<div className="bg-background-card rounded-xl p-3">
|
||
<p className="text-[10px] font-semibold text-text-secondary uppercase tracking-wider mb-1">{label}</p>
|
||
<p className={`text-xl font-bold ${accent ? 'text-accent-primary' : 'text-text-primary'}`}>{value}</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<div className="bg-background-secondary rounded-lg border border-border">
|
||
<button
|
||
onClick={() => setOpen(o => !o)}
|
||
className="w-full flex items-center justify-between px-5 py-4 hover:bg-background-card/50 transition-colors"
|
||
>
|
||
<span className="font-semibold text-text-primary flex items-center gap-2">
|
||
📋 Attributs détectés
|
||
{data && (
|
||
<span className="text-xs px-2 py-0.5 rounded-full bg-accent-primary/20 text-accent-primary font-normal">
|
||
{data.total_detections} détections · {data.attributes.user_agents?.length ?? 0} UA · {data.attributes.ja4?.length ?? 0} JA4
|
||
</span>
|
||
)}
|
||
</span>
|
||
<span className="text-text-secondary">{open ? '▲' : '▼'}</span>
|
||
</button>
|
||
|
||
{open && (
|
||
<div className="px-5 pb-5 space-y-4">
|
||
{loading && <div className="text-text-disabled text-sm py-4">Chargement…</div>}
|
||
{data && (
|
||
<>
|
||
{/* Métriques */}
|
||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||
<Metric label="Détections (24h)" value={data.total_detections.toLocaleString()} accent />
|
||
<Metric label="User-Agents" value={(data.attributes.user_agents?.length ?? 0).toString()} />
|
||
{first && last && (
|
||
sameDate ? (
|
||
<Metric label="Détecté le" value={fmt(last!)} />
|
||
) : (
|
||
<div className="bg-background-card rounded-xl p-3 col-span-2">
|
||
<p className="text-[10px] font-semibold text-text-secondary uppercase tracking-wider mb-1">Période</p>
|
||
<p className="text-xs text-text-primary font-medium">{fmt(first)}</p>
|
||
<p className="text-[10px] text-text-secondary">→ {fmt(last!)}</p>
|
||
</div>
|
||
)
|
||
)}
|
||
</div>
|
||
|
||
{/* Insights */}
|
||
{data.insights.length > 0 && (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||
{data.insights.map((ins, i) => {
|
||
const s: Record<string, string> = {
|
||
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 (
|
||
<div key={i} className={`${s[ins.type] ?? s.info} border rounded-xl p-3 text-sm`}>
|
||
{ins.message}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{/* Attributs (JA4, hosts, ASN, pays, UA…) */}
|
||
<VariabilityPanel attributes={data.attributes} hideAssociatedIPs />
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function InvestigationView() {
|
||
const { ip } = useParams<{ ip: string }>();
|
||
const navigate = useNavigate();
|
||
|
||
if (!ip) {
|
||
return (
|
||
<div className="text-center text-text-secondary py-12">
|
||
IP non spécifiée
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<div className="space-y-6 animate-fade-in">
|
||
{/* Breadcrumb */}
|
||
<nav className="flex items-center gap-2 text-xs text-text-secondary">
|
||
<Link to="/" className="hover:text-text-primary">Dashboard</Link>
|
||
<span>/</span>
|
||
<Link to="/detections" className="hover:text-text-primary">Détections</Link>
|
||
<span>/</span>
|
||
<span className="text-text-primary font-mono">{ip}</span>
|
||
</nav>
|
||
|
||
{/* En-tête */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<div className="flex items-center gap-4 mb-2">
|
||
<button
|
||
onClick={() => navigate(-1)}
|
||
className="text-text-secondary hover:text-text-primary transition-colors"
|
||
>
|
||
← Retour
|
||
</button>
|
||
<h1 className="text-2xl font-bold text-text-primary">Investigation: {ip}</h1>
|
||
</div>
|
||
<div className="text-text-secondary text-sm">
|
||
Analyse de corrélations pour classification SOC
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Navigation ancres inter-sections */}
|
||
<div className="flex items-center gap-2 overflow-x-auto pb-1 text-xs font-medium sticky top-0 z-10 bg-background py-2">
|
||
<span className="text-text-disabled shrink-0">Aller à :</span>
|
||
{[
|
||
{ 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 }) => (
|
||
<a key={id} href={`#${id}`} className="shrink-0 px-3 py-1 rounded-full bg-background-card text-text-secondary hover:text-text-primary hover:bg-background-secondary transition-colors">
|
||
{label}
|
||
</a>
|
||
))}
|
||
</div>
|
||
|
||
{/* Attributs détectés (ex-DetailsView) */}
|
||
<div id="section-attributs">
|
||
<DetectionAttributesSection ip={ip} />
|
||
</div>
|
||
|
||
{/* Synthèse multi-sources */}
|
||
<div id="section-synthese">
|
||
<IPActivitySummary ip={ip} />
|
||
</div>
|
||
|
||
{/* Réputation (1/3) + Graph de corrélations (2/3) */}
|
||
<div id="section-reputation" className="grid grid-cols-3 gap-6 items-start">
|
||
<div className="bg-background-secondary rounded-lg p-6 h-full">
|
||
<h3 className="text-lg font-medium text-text-primary mb-4">🌍 Réputation IP</h3>
|
||
<ReputationPanel ip={ip} />
|
||
</div>
|
||
<div id="section-correlations" className="col-span-2 bg-background-secondary rounded-lg p-6">
|
||
<h3 className="text-lg font-medium text-text-primary mb-4">🕸️ Graph de Corrélations</h3>
|
||
<CorrelationGraph ip={ip} height="600px" />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Subnet / Country / JA4 */}
|
||
<div id="section-geo" className="grid grid-cols-3 gap-6 items-start">
|
||
<SubnetAnalysis ip={ip} />
|
||
<CountryAnalysis ip={ip} />
|
||
<JA4Analysis ip={ip} />
|
||
</div>
|
||
|
||
{/* User-Agents (1/2) + Classification (1/2) */}
|
||
<div id="section-classification" className="grid grid-cols-2 gap-6 items-start">
|
||
<UserAgentAnalysis ip={ip} />
|
||
<CorrelationSummary ip={ip} onClassify={handleClassify} />
|
||
</div>
|
||
|
||
{/* Cohérence JA4/UA (spoofing) */}
|
||
<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 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>
|
||
<button
|
||
onClick={() => navigate('/fingerprints?tab=spoofing')}
|
||
className="text-sm px-4 py-2 rounded-lg bg-accent-primary/20 text-accent-primary hover:bg-accent-primary/30 transition-colors"
|
||
>
|
||
🎭 Voir l'analyse de spoofing globale →
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|