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:
176
frontend/src/components/analysis/CountryAnalysis.tsx
Normal file
176
frontend/src/components/analysis/CountryAnalysis.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface CountryData {
|
||||
code: string;
|
||||
name: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
interface CountryAnalysisProps {
|
||||
ip?: string; // Si fourni, affiche stats relatives à cette IP
|
||||
asn?: string; // Si fourni, affiche stats relatives à cet ASN
|
||||
}
|
||||
|
||||
interface CountryAnalysisData {
|
||||
ip_country?: { code: string; name: string };
|
||||
asn_countries: CountryData[];
|
||||
}
|
||||
|
||||
export function CountryAnalysis({ ip, asn }: CountryAnalysisProps) {
|
||||
const [data, setData] = useState<CountryAnalysisData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCountryAnalysis = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (ip) {
|
||||
// Mode Investigation IP: Récupérer le pays de l'IP + répartition ASN
|
||||
const response = await fetch(`/api/analysis/${encodeURIComponent(ip)}/country`);
|
||||
if (!response.ok) throw new Error('Erreur chargement pays');
|
||||
const result = await response.json();
|
||||
setData(result);
|
||||
} else if (asn) {
|
||||
// Mode Investigation ASN
|
||||
const response = await fetch(`/api/analysis/asn/${encodeURIComponent(asn)}/country`);
|
||||
if (!response.ok) throw new Error('Erreur chargement pays');
|
||||
const result = await response.json();
|
||||
setData(result);
|
||||
} else {
|
||||
// Mode Global (stats générales)
|
||||
const response = await fetch('/api/analysis/country?days=1');
|
||||
if (!response.ok) throw new Error('Erreur chargement pays');
|
||||
const result = await response.json();
|
||||
setData({
|
||||
ip_country: undefined,
|
||||
asn_countries: result.top_countries || []
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Erreur inconnue');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCountryAnalysis();
|
||||
}, [ip, asn]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="bg-background-secondary rounded-lg p-6">
|
||||
<div className="text-center text-text-secondary">Chargement...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="bg-background-secondary rounded-lg p-6">
|
||||
<div className="text-center text-threat-high">Erreur: {error || 'Données non disponibles'}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getFlag = (code: string) => {
|
||||
return code.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397));
|
||||
};
|
||||
|
||||
// Mode Investigation IP avec pays unique
|
||||
if (ip && data.ip_country) {
|
||||
return (
|
||||
<div className="bg-background-secondary rounded-lg p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium text-text-primary">2. PAYS DE L'IP</h3>
|
||||
</div>
|
||||
|
||||
{/* Pays de l'IP */}
|
||||
<div className="bg-background-card rounded-lg p-4 mb-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="text-4xl">{getFlag(data.ip_country.code)}</span>
|
||||
<div>
|
||||
<div className="text-text-primary font-bold text-lg">
|
||||
{data.ip_country.name} ({data.ip_country.code})
|
||||
</div>
|
||||
<div className="text-text-secondary text-sm">Pays de l'IP</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Répartition ASN par pays */}
|
||||
{data.asn_countries.length > 0 && (
|
||||
<div>
|
||||
<div className="text-sm text-text-secondary mb-3">
|
||||
Autres pays du même ASN (24h)
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{data.asn_countries.slice(0, 5).map((country, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">{getFlag(country.code)}</span>
|
||||
<span className="text-text-primary text-sm">{country.name}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-text-primary font-bold text-sm">{country.count}</div>
|
||||
<div className="text-text-secondary text-xs">{country.percentage.toFixed(1)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Mode Global ou ASN
|
||||
const getThreatColor = (percentage: number, baseline: number) => {
|
||||
if (baseline > 0 && percentage > baseline * 2) return 'bg-threat-high';
|
||||
if (percentage > 30) return 'bg-threat-medium';
|
||||
return 'bg-accent-primary';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-background-secondary rounded-lg p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium text-text-primary">
|
||||
{asn ? '2. TOP Pays (ASN)' : '2. TOP Pays (Global)'}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{data.asn_countries.map((country, idx) => {
|
||||
const baselinePct = 0; // Pas de baseline en mode ASN
|
||||
|
||||
return (
|
||||
<div key={idx} className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">{getFlag(country.code)}</span>
|
||||
<div>
|
||||
<div className="text-text-primary font-medium text-sm">
|
||||
{country.name} ({country.code})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-text-primary font-bold">{country.count}</div>
|
||||
<div className="text-text-secondary text-xs">{country.percentage.toFixed(1)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-background-card rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all ${getThreatColor(country.percentage, baselinePct)}`}
|
||||
style={{ width: `${Math.min(country.percentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user