feat: ja4-platform monorepo — 5 services unified, tests & RPM builds standardized
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>
This commit is contained in:
153
services/dashboard/frontend/src/components/DetailsView.tsx
Normal file
153
services/dashboard/frontend/src/components/DetailsView.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user