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,313 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { VariabilityAttributes, AttributeValue } from '../api/client';
interface VariabilityPanelProps {
attributes: VariabilityAttributes;
}
export function VariabilityPanel({ attributes }: VariabilityPanelProps) {
const [showModal, setShowModal] = useState<{
type: string;
title: string;
items: string[];
total: number;
} | null>(null);
const [loading, setLoading] = useState(false);
// Fonction pour charger la liste des IPs associées
const loadAssociatedIPs = async (attrType: string, value: string, total: number) => {
setLoading(true);
try {
const response = await fetch(`/api/variability/${attrType}/${encodeURIComponent(value)}/ips?limit=100`);
const data = await response.json();
setShowModal({
type: 'ips',
title: `${data.total || total} IPs associées à ${value}`,
items: data.ips || [],
total: data.total || total,
});
} catch (error) {
console.error('Erreur chargement IPs:', error);
}
setLoading(false);
};
return (
<div className="space-y-6">
<h2 className="text-xl font-semibold text-text-primary">Variabilité des Attributs</h2>
{/* JA4 Fingerprints */}
{attributes.ja4 && attributes.ja4.length > 0 && (
<AttributeSection
title="JA4 Fingerprints"
items={attributes.ja4}
getValue={(item) => item.value}
getLink={(item) => `/investigation/ja4/${encodeURIComponent(item.value)}`}
onViewAll={(value, count) => loadAssociatedIPs('ja4', value, count)}
showViewAll
viewAllLabel="Voir les IPs"
/>
)}
{/* User-Agents */}
{attributes.user_agents && attributes.user_agents.length > 0 && (
<div className="bg-background-secondary rounded-lg p-6">
<h3 className="text-lg font-medium text-text-primary mb-4">
User-Agents ({attributes.user_agents.length})
</h3>
<div className="space-y-3">
{attributes.user_agents.slice(0, 10).map((item, index) => (
<div key={index} className="space-y-1">
<div className="flex items-center justify-between">
<div className="text-text-primary font-medium truncate max-w-lg text-sm">
{item.value}
</div>
<div className="text-right">
<div className="text-text-primary font-medium">{item.count}</div>
<div className="text-text-secondary text-xs">{item.percentage?.toFixed(1)}%</div>
</div>
</div>
<div className="w-full bg-background-card rounded-full h-2">
<div
className="h-2 rounded-full bg-threat-medium transition-all"
style={{ width: `${item.percentage}%` }}
/>
</div>
</div>
))}
</div>
{attributes.user_agents.length > 10 && (
<p className="text-text-secondary text-sm mt-4 text-center">
... et {attributes.user_agents.length - 10} autres (top 10 affiché)
</p>
)}
</div>
)}
{/* Pays */}
{attributes.countries && attributes.countries.length > 0 && (
<AttributeSection
title="Pays"
items={attributes.countries}
getValue={(item) => item.value}
getLink={(item) => `/detections/country/${encodeURIComponent(item.value)}`}
onViewAll={(value, count) => loadAssociatedIPs('country', value, count)}
showViewAll
viewAllLabel="Voir les IPs"
/>
)}
{/* ASN */}
{attributes.asns && attributes.asns.length > 0 && (
<AttributeSection
title="ASN"
items={attributes.asns}
getValue={(item) => item.value}
getLink={(item) => {
const asnNumber = item.value.match(/AS(\d+)/)?.[1] || item.value;
return `/detections/asn/${encodeURIComponent(asnNumber)}`;
}}
onViewAll={(value, count) => loadAssociatedIPs('asn', value, count)}
showViewAll
viewAllLabel="Voir les IPs"
/>
)}
{/* Hosts */}
{attributes.hosts && attributes.hosts.length > 0 && (
<AttributeSection
title="Hosts"
items={attributes.hosts}
getValue={(item) => item.value}
getLink={(item) => `/detections/host/${encodeURIComponent(item.value)}`}
onViewAll={(value, count) => loadAssociatedIPs('host', value, count)}
showViewAll
viewAllLabel="Voir les IPs"
/>
)}
{/* Threat Levels */}
{attributes.threat_levels && attributes.threat_levels.length > 0 && (
<AttributeSection
title="Niveaux de Menace"
items={attributes.threat_levels}
getValue={(item) => item.value}
getLink={(item) => `/detections?threat_level=${encodeURIComponent(item.value)}`}
onViewAll={(value, count) => loadAssociatedIPs('threat_level', value, count)}
showViewAll
viewAllLabel="Voir les IPs"
/>
)}
{/* Modal pour afficher la liste complète */}
{showModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-background-secondary rounded-lg max-w-4xl w-full max-h-[80vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-background-card">
<h3 className="text-xl font-semibold text-text-primary">{showModal.title}</h3>
<button
onClick={() => setShowModal(null)}
className="text-text-secondary hover:text-text-primary transition-colors text-xl"
>
×
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
{loading ? (
<div className="text-center text-text-secondary py-8">Chargement...</div>
) : showModal.items.length > 0 ? (
<div className="space-y-2">
{showModal.items.map((item, index) => (
<div
key={index}
className="bg-background-card rounded-lg p-3 font-mono text-sm text-text-primary break-all"
>
{item}
</div>
))}
{showModal.total > showModal.items.length && (
<p className="text-center text-text-secondary text-sm mt-4">
Affichage de {showModal.items.length} sur {showModal.total} éléments
</p>
)}
</div>
) : (
<div className="text-center text-text-secondary py-8">
Aucune donnée disponible
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-background-card text-right">
<button
onClick={() => setShowModal(null)}
className="bg-accent-primary hover:bg-accent-primary/80 text-white px-6 py-2 rounded-lg transition-colors"
>
Fermer
</button>
</div>
</div>
</div>
)}
</div>
);
}
// Composant AttributeSection
function AttributeSection({
title,
items,
getValue,
getLink,
onViewAll,
showViewAll = false,
viewAllLabel = 'Voir les IPs',
}: {
title: string;
items: AttributeValue[];
getValue: (item: AttributeValue) => string;
getLink: (item: AttributeValue) => string;
onViewAll?: (value: string, count: number) => void;
showViewAll?: boolean;
viewAllLabel?: string;
}) {
const displayItems = items.slice(0, 10);
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">
{title} ({items.length})
</h3>
{showViewAll && items.length > 0 && (
<select
onChange={(e) => {
if (e.target.value && onViewAll) {
const item = items.find(i => i.value === e.target.value);
if (item) {
onViewAll(item.value, item.count);
}
}
}}
defaultValue=""
className="bg-background-card border border-background-card rounded-lg px-3 py-1 text-sm text-text-primary focus:outline-none focus:border-accent-primary"
>
<option value="">{viewAllLabel}...</option>
{displayItems.map((item, idx) => (
<option key={idx} value={item.value}>
{getValue(item).substring(0, 40)}{getValue(item).length > 40 ? '...' : ''}
</option>
))}
</select>
)}
</div>
<div className="space-y-3">
{displayItems.map((item, index) => (
<AttributeRow
key={index}
value={item}
getValue={getValue}
getLink={getLink}
/>
))}
</div>
{items.length > 10 && (
<p className="text-text-secondary text-sm mt-4 text-center">
... et {items.length - 10} autres (top 10 affiché)
</p>
)}
</div>
);
}
// Composant AttributeRow
function AttributeRow({
value,
getValue,
getLink,
}: {
value: AttributeValue;
getValue: (item: AttributeValue) => string;
getLink: (item: AttributeValue) => string;
}) {
const percentage = value.percentage || 0;
return (
<div className="space-y-1">
<div className="flex items-center justify-between">
<Link
to={getLink(value)}
className="text-text-primary hover:text-accent-primary transition-colors font-medium truncate max-w-md"
>
{getValue(value)}
</Link>
<div className="text-right">
<div className="text-text-primary font-medium">{value.count}</div>
<div className="text-text-secondary text-xs">{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 ${getPercentageColor(percentage)}`}
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
}
// Helper pour la couleur de la barre
function getPercentageColor(percentage: number): string {
if (percentage >= 50) return 'bg-threat-critical';
if (percentage >= 25) return 'bg-threat-high';
if (percentage >= 10) return 'bg-threat-medium';
return 'bg-threat-low';
}