Initial commit: Bot Detector Dashboard for SOC Incident Response
🛡️ Dashboard complet pour l'analyse et la classification des menaces Fonctionnalités principales: - Visualisation des détections en temps réel (24h) - Investigation multi-entités (IP, JA4, ASN, Host, User-Agent) - Analyse de corrélation pour classification SOC - Clustering automatique par subnet/JA4/UA - Export des classifications pour ML Composants: - Backend: FastAPI (Python) + ClickHouse - Frontend: React + TypeScript + TailwindCSS - 6 routes API: metrics, detections, variability, attributes, analysis, entities - 7 types d'entités investigables Documentation ajoutée: - NAVIGATION_GRAPH.md: Graph complet de navigation - SOC_OPTIMIZATION_PROPOSAL.md: Proposition d'optimisation pour SOC • Réduction de 7 à 2 clics pour classification • Nouvelle vue /incidents clusterisée • Panel latéral d'investigation • Quick Search (Cmd+K) • Timeline interactive • Graph de corrélations Sécurité: - .gitignore configuré (exclut .env, secrets, node_modules) - Credentials dans .env (à ne pas committer) ⚠️ Audit sécurité réalisé - Voir recommandations dans SOC_OPTIMIZATION_PROPOSAL.md Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
401
frontend/src/components/EntityInvestigationView.tsx
Normal file
401
frontend/src/components/EntityInvestigationView.tsx
Normal file
@ -0,0 +1,401 @@
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const getCountryFlag = (code: string) => {
|
||||
const flags: Record<string, string> = {
|
||||
CN: '🇨🇳', US: '🇺🇸', FR: '🇫🇷', DE: '🇩🇪', GB: '🇬🇧',
|
||||
RU: '🇷🇺', CA: '🇨🇦', AU: '🇦🇺', JP: '🇯🇵', IN: '🇮🇳',
|
||||
BR: '🇧🇷', IT: '🇮🇹', ES: '🇪🇸', NL: '🇳🇱', BE: '🇧🇪',
|
||||
CH: '🇨🇭', SE: '🇸🇪', NO: '🇳🇴', DK: '🇩🇰', FI: '🇫🇮'
|
||||
};
|
||||
return flags[code] || code;
|
||||
};
|
||||
|
||||
const truncateUA = (ua: string, maxLength: number = 150) => {
|
||||
if (ua.length <= maxLength) return ua;
|
||||
return ua.substring(0, maxLength) + '...';
|
||||
};
|
||||
|
||||
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={new Date(data.stats.first_seen).toLocaleDateString('fr-FR')}
|
||||
/>
|
||||
<StatCard
|
||||
label="Dernière Détection"
|
||||
value={new Date(data.stats.last_seen).toLocaleDateString('fr-FR')}
|
||||
/>
|
||||
</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">2. JA4 Fingerprints</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">
|
||||
{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">
|
||||
{truncateUA(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 && (
|
||||
<div className="text-center text-text-secondary mt-4 text-sm">
|
||||
+{data.user_agents.length - 10} autres User-Agents
|
||||
</div>
|
||||
)}
|
||||
</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">4. Client Headers</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">ASNs</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user