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>
154 lines
5.7 KiB
TypeScript
154 lines
5.7 KiB
TypeScript
import { useParams, useNavigate, Link } from 'react-router-dom';
|
|
import { useVariability } from '../hooks/useVariability';
|
|
import { VariabilityPanel } from './VariabilityPanel';
|
|
import { formatDateShort } from '../utils/dateUtils';
|
|
|
|
export function DetailsView() {
|
|
const { type, value } = useParams<{ type: string; value: string }>();
|
|
const navigate = useNavigate();
|
|
|
|
const { data, loading, error } = useVariability(type || '', value || '');
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64 text-text-secondary">
|
|
Chargement…
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="bg-threat-critical_bg border border-threat-critical rounded-xl p-6">
|
|
<p className="text-threat-critical font-semibold mb-4">Erreur : {error.message}</p>
|
|
<button
|
|
onClick={() => navigate('/detections')}
|
|
className="bg-accent-primary hover:bg-accent-primary/80 text-white px-4 py-2 rounded-lg text-sm"
|
|
>
|
|
← Retour
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!data) return null;
|
|
|
|
const typeLabels: Record<string, string> = {
|
|
ip: 'IP',
|
|
ja4: 'JA4',
|
|
country: 'Pays',
|
|
asn: 'ASN',
|
|
host: 'Host',
|
|
user_agent: 'User-Agent',
|
|
};
|
|
const typeLabel = typeLabels[type || ''] || type;
|
|
const isIP = type === 'ip';
|
|
const isJA4 = type === 'ja4';
|
|
|
|
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 fmtDate = (d: Date) => formatDateShort(d.toISOString());
|
|
|
|
return (
|
|
<div className="space-y-5 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">{typeLabel}: {value}</span>
|
|
</nav>
|
|
|
|
{/* Header card */}
|
|
<div className="bg-background-secondary rounded-xl p-5">
|
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
{/* Identité */}
|
|
<div>
|
|
<p className="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-1">{typeLabel}</p>
|
|
<p className="text-lg font-mono font-bold text-text-primary break-all">{value}</p>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex flex-wrap gap-2">
|
|
{isIP && (
|
|
<button
|
|
onClick={() => navigate(`/investigation/${encodeURIComponent(value!)}`)}
|
|
className="bg-accent-primary hover:bg-accent-primary/80 text-white px-4 py-2 rounded-lg text-sm font-medium"
|
|
>
|
|
🔍 Investigation complète
|
|
</button>
|
|
)}
|
|
{isJA4 && (
|
|
<button
|
|
onClick={() => navigate(`/investigation/ja4/${encodeURIComponent(value!)}`)}
|
|
className="bg-accent-primary hover:bg-accent-primary/80 text-white px-4 py-2 rounded-lg text-sm font-medium"
|
|
>
|
|
🔍 Investigation JA4
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => navigate('/detections')}
|
|
className="bg-background-card hover:bg-background-card/70 text-text-primary px-4 py-2 rounded-lg text-sm"
|
|
>
|
|
← Retour
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Métriques clés */}
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-5">
|
|
<Metric label="Détections (24h)" value={data.total_detections.toLocaleString()} accent />
|
|
{!isIP && (
|
|
<Metric label="IPs uniques" value={data.unique_ips.toLocaleString()} />
|
|
)}
|
|
<Metric label="User-Agents" value={(data.attributes.user_agents?.length ?? 0).toString()} />
|
|
{first && last && (
|
|
sameDate ? (
|
|
<Metric label="Détecté le" value={fmtDate(last)} />
|
|
) : (
|
|
<div className="bg-background-card rounded-xl p-3">
|
|
<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">{fmtDate(first)}</p>
|
|
<p className="text-[10px] text-text-secondary">→ {fmtDate(last)}</p>
|
|
</div>
|
|
)
|
|
)}
|
|
</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 */}
|
|
<VariabilityPanel attributes={data.attributes} hideAssociatedIPs={isIP} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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>
|
|
);
|
|
}
|