Files
dashboard/frontend/src/components/EntityInvestigationView.tsx
2026-03-20 10:13:00 +01:00

398 lines
16 KiB
TypeScript

import { useParams, useNavigate } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { InfoTip } from './ui/Tooltip';
import { TIPS } from './ui/tooltips';
import { formatDateOnly } from '../utils/dateUtils';
import { getCountryFlag } from '../utils/countryUtils';
interface EntityStats {
entity_type: string;
entity_value: string;
total_requests: number;
unique_ips: number;
first_seen: string;
last_seen: string;
}
interface EntityRelatedAttributes {
ips: string[];
ja4s: string[];
hosts: string[];
asns: string[];
countries: string[];
}
interface AttributeValue {
value: string;
count: number;
percentage: number;
}
interface EntityInvestigationData {
stats: EntityStats;
related: EntityRelatedAttributes;
user_agents: AttributeValue[];
client_headers: AttributeValue[];
paths: AttributeValue[];
query_params: AttributeValue[];
}
export function EntityInvestigationView() {
const { type, value } = useParams<{ type: string; value: string }>();
const navigate = useNavigate();
const [data, setData] = useState<EntityInvestigationData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showAllUA, setShowAllUA] = useState(false);
useEffect(() => {
if (!type || !value) {
setError("Type ou valeur d'entité manquant");
setLoading(false);
return;
}
const fetchInvestigation = async () => {
setLoading(true);
try {
const response = await fetch(`/api/entities/${type}/${encodeURIComponent(value)}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Erreur chargement données');
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur inconnue');
} finally {
setLoading(false);
}
};
fetchInvestigation();
}, [type, value]);
const getEntityLabel = (entityType: string) => {
const labels: Record<string, string> = {
ip: 'Adresse IP',
ja4: 'Fingerprint JA4',
user_agent: 'User-Agent',
client_header: 'Client Header',
host: 'Host',
path: 'Path',
query_param: 'Query Params'
};
return labels[entityType] || entityType;
};
;
if (loading) {
return (
<div className="min-h-screen bg-background-primary">
<div className="container mx-auto px-4 py-8">
<div className="text-center text-text-secondary">Chargement...</div>
</div>
</div>
);
}
if (error || !data) {
return (
<div className="min-h-screen bg-background-primary">
<div className="container mx-auto px-4 py-8">
<div className="bg-threat-high/10 border border-threat-high rounded-lg p-6 text-center">
<div className="text-threat-high font-medium mb-2">Erreur</div>
<div className="text-text-secondary">{error || 'Données non disponibles'}</div>
<button
onClick={() => navigate(-1)}
className="mt-4 bg-accent-primary text-white px-6 py-2 rounded-lg hover:bg-accent-primary/80"
>
Retour
</button>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-background-primary">
<div className="container mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8">
<button
onClick={() => navigate(-1)}
className="text-text-secondary hover:text-text-primary transition-colors mb-4"
>
Retour
</button>
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-text-primary mb-2">
Investigation: {getEntityLabel(data.stats.entity_type)}
</h1>
<div className="text-text-secondary font-mono text-sm break-all max-w-4xl">
{data.stats.entity_value}
</div>
</div>
<div className="text-right text-sm text-text-secondary">
<div>Requêtes: <span className="text-text-primary font-bold">{data.stats.total_requests.toLocaleString()}</span></div>
<div>IPs Uniques: <span className="text-text-primary font-bold">{data.stats.unique_ips.toLocaleString()}</span></div>
</div>
</div>
</div>
{/* Stats Summary */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<StatCard
label="Total Requêtes"
value={data.stats.total_requests.toLocaleString()}
/>
<StatCard
label="IPs Uniques"
value={data.stats.unique_ips.toLocaleString()}
/>
<StatCard
label="Première Détection"
value={formatDateOnly(data.stats.first_seen)}
/>
<StatCard
label="Dernière Détection"
value={formatDateOnly(data.stats.last_seen)}
/>
</div>
{/* Panel 1: IPs Associées */}
<div className="bg-background-secondary rounded-lg p-6 mb-6">
<h3 className="text-lg font-medium text-text-primary mb-4">1. IPs Associées</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
{data.related.ips.slice(0, 20).map((ip, idx) => (
<button
key={idx}
onClick={() => navigate(`/investigation/${ip}`)}
className="text-left px-3 py-2 bg-background-card rounded-lg text-sm text-text-primary hover:bg-background-card/80 transition-colors font-mono"
>
{ip}
</button>
))}
</div>
{data.related.ips.length === 0 && (
<div className="text-center text-text-secondary py-8">Aucune IP associée</div>
)}
{data.related.ips.length > 20 && (
<div className="text-center text-text-secondary mt-4 text-sm">
+{data.related.ips.length - 20} autres IPs
</div>
)}
</div>
{/* Panel 2: JA4 Fingerprints */}
<div className="bg-background-secondary rounded-lg p-6 mb-6">
<h3 className="text-lg font-medium text-text-primary mb-4"><span className="flex items-center gap-1">2. JA4 Fingerprints<InfoTip content={TIPS.ja4} /></span></h3>
<div className="space-y-2">
{data.related.ja4s.slice(0, 10).map((ja4, idx) => (
<div key={idx} className="flex items-center justify-between bg-background-card rounded-lg p-3">
<div className="font-mono text-sm text-text-primary break-all flex-1">
{ja4}
</div>
<button
onClick={() => navigate(`/investigation/ja4/${encodeURIComponent(ja4)}`)}
className="ml-4 text-xs bg-accent-primary text-white px-3 py-1 rounded hover:bg-accent-primary/80 whitespace-nowrap"
>
Investigation
</button>
</div>
))}
</div>
{data.related.ja4s.length === 0 && (
<div className="text-center text-text-secondary py-8">Aucun JA4 associé</div>
)}
{data.related.ja4s.length > 10 && (
<div className="text-center text-text-secondary mt-4 text-sm">
+{data.related.ja4s.length - 10} autres JA4
</div>
)}
</div>
{/* Panel 3: User-Agents */}
<div className="bg-background-secondary rounded-lg p-6 mb-6">
<h3 className="text-lg font-medium text-text-primary mb-4">3. User-Agents</h3>
<div className="space-y-3">
{(showAllUA ? data.user_agents : data.user_agents.slice(0, 10)).map((ua, idx) => (
<div key={idx} className="bg-background-card rounded-lg p-3 space-y-2">
<div className="text-xs text-text-primary font-mono break-all leading-relaxed">
{ua.value}
</div>
<div className="flex items-center gap-2">
<div className="text-text-secondary text-xs">{ua.count} requêtes</div>
<div className="text-text-secondary text-xs">{ua.percentage.toFixed(1)}%</div>
</div>
</div>
))}
</div>
{data.user_agents.length === 0 && (
<div className="text-center text-text-secondary py-8">Aucun User-Agent</div>
)}
{data.user_agents.length > 10 && (
<button
onClick={() => setShowAllUA(v => !v)}
className="mt-4 w-full text-xs text-accent-primary hover:text-accent-primary/80 transition-colors"
>
{showAllUA ? '↑ Réduire' : `↓ Voir les ${data.user_agents.length - 10} autres`}
</button>
)}
</div>
{/* Panel 4: Client Headers */}
<div className="bg-background-secondary rounded-lg p-6 mb-6">
<h3 className="text-lg font-medium text-text-primary mb-4"><span className="flex items-center gap-1">4. Client Headers<InfoTip content={TIPS.accept_encoding} /></span></h3>
<div className="space-y-3">
{data.client_headers.slice(0, 10).map((header, idx) => (
<div key={idx} className="bg-background-card rounded-lg p-3 space-y-2">
<div className="text-xs text-text-primary font-mono break-all">
{header.value}
</div>
<div className="flex items-center gap-2">
<div className="text-text-secondary text-xs">{header.count} requêtes</div>
<div className="text-text-secondary text-xs">{header.percentage.toFixed(1)}%</div>
</div>
</div>
))}
</div>
{data.client_headers.length === 0 && (
<div className="text-center text-text-secondary py-8">Aucun Client Header</div>
)}
{data.client_headers.length > 10 && (
<div className="text-center text-text-secondary mt-4 text-sm">
+{data.client_headers.length - 10} autres Client Headers
</div>
)}
</div>
{/* Panel 5: Hosts */}
<div className="bg-background-secondary rounded-lg p-6 mb-6">
<h3 className="text-lg font-medium text-text-primary mb-4">5. Hosts Ciblés</h3>
<div className="space-y-2">
{data.related.hosts.slice(0, 15).map((host, idx) => (
<div key={idx} className="bg-background-card rounded-lg p-3">
<div className="text-sm text-text-primary break-all">{host}</div>
</div>
))}
</div>
{data.related.hosts.length === 0 && (
<div className="text-center text-text-secondary py-8">Aucun Host associé</div>
)}
{data.related.hosts.length > 15 && (
<div className="text-center text-text-secondary mt-4 text-sm">
+{data.related.hosts.length - 15} autres Hosts
</div>
)}
</div>
{/* Panel 6: Paths */}
<div className="bg-background-secondary rounded-lg p-6 mb-6">
<h3 className="text-lg font-medium text-text-primary mb-4">6. Paths</h3>
<div className="space-y-2">
{data.paths.slice(0, 15).map((path, idx) => (
<div key={idx} className="bg-background-card rounded-lg p-3">
<div className="text-sm text-text-primary font-mono break-all">{path.value}</div>
<div className="flex items-center gap-2 mt-1">
<div className="text-text-secondary text-xs">{path.count} requêtes</div>
<div className="text-text-secondary text-xs">{path.percentage.toFixed(1)}%</div>
</div>
</div>
))}
</div>
{data.paths.length === 0 && (
<div className="text-center text-text-secondary py-8">Aucun Path</div>
)}
{data.paths.length > 15 && (
<div className="text-center text-text-secondary mt-4 text-sm">
+{data.paths.length - 15} autres Paths
</div>
)}
</div>
{/* Panel 7: Query Params */}
<div className="bg-background-secondary rounded-lg p-6 mb-6">
<h3 className="text-lg font-medium text-text-primary mb-4">7. Query Params</h3>
<div className="space-y-2">
{data.query_params.slice(0, 15).map((qp, idx) => (
<div key={idx} className="bg-background-card rounded-lg p-3">
<div className="text-sm text-text-primary font-mono break-all">{qp.value}</div>
<div className="flex items-center gap-2 mt-1">
<div className="text-text-secondary text-xs">{qp.count} requêtes</div>
<div className="text-text-secondary text-xs">{qp.percentage.toFixed(1)}%</div>
</div>
</div>
))}
</div>
{data.query_params.length === 0 && (
<div className="text-center text-text-secondary py-8">Aucun Query Param</div>
)}
{data.query_params.length > 15 && (
<div className="text-center text-text-secondary mt-4 text-sm">
+{data.query_params.length - 15} autres Query Params
</div>
)}
</div>
{/* Panel 8: ASNs & Pays */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{/* ASNs */}
<div className="bg-background-secondary rounded-lg p-6">
<h3 className="text-lg font-medium text-text-primary mb-4"><span className="flex items-center gap-1">ASNs<InfoTip content={TIPS.asn} /></span></h3>
<div className="space-y-2">
{data.related.asns.slice(0, 10).map((asn, idx) => (
<div key={idx} className="bg-background-card rounded-lg p-3">
<div className="text-sm text-text-primary">{asn}</div>
</div>
))}
</div>
{data.related.asns.length === 0 && (
<div className="text-center text-text-secondary py-8">Aucun ASN</div>
)}
{data.related.asns.length > 10 && (
<div className="text-center text-text-secondary mt-4 text-sm">
+{data.related.asns.length - 10} autres ASNs
</div>
)}
</div>
{/* Pays */}
<div className="bg-background-secondary rounded-lg p-6">
<h3 className="text-lg font-medium text-text-primary mb-4">Pays</h3>
<div className="space-y-2">
{data.related.countries.slice(0, 10).map((country, idx) => (
<div key={idx} className="bg-background-card rounded-lg p-3 flex items-center gap-2">
<span className="text-xl">{getCountryFlag(country)}</span>
<span className="text-sm text-text-primary">{country}</span>
</div>
))}
</div>
{data.related.countries.length === 0 && (
<div className="text-center text-text-secondary py-8">Aucun pays</div>
)}
{data.related.countries.length > 10 && (
<div className="text-center text-text-secondary mt-4 text-sm">
+{data.related.countries.length - 10} autres pays
</div>
)}
</div>
</div>
</div>
</div>
);
}
function StatCard({ label, value }: { label: string; value: string }) {
return (
<div className="bg-background-secondary rounded-lg p-4">
<div className="text-xs text-text-secondary mb-1">{label}</div>
<div className="text-2xl font-bold text-text-primary">{value}</div>
</div>
);
}