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:
SOC Analyst
2026-03-14 21:33:55 +01:00
commit a61828d1e7
55 changed files with 11189 additions and 0 deletions

View 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>
);
}