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:
313
frontend/src/components/VariabilityPanel.tsx
Normal file
313
frontend/src/components/VariabilityPanel.tsx
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user