Files
dashboard/frontend/src/components/InvestigationPanel.tsx
SOC Analyst 1455e04303 fix: correct CampaignsView, analysis.py IPv4 split, entities date filter
- CampaignsView: update ClusterData interface to match real API response
  (severity/unique_ips/score instead of threat_level/total_ips/confidence_range)
  Fix fetch to use data.items, rewrite ClusterCard and BehavioralTab
  Remove unused getClassificationColor and THREAT_ORDER constants
- analysis.py: fix IPv4Address object has no attribute 'split' on line 322
  Add str() conversion before calling .split('.')
- entities.py: fix Date vs DateTime comparison — log_date is a Date column,
  comparing against now()-INTERVAL HOUR caused yesterday's entries to be excluded
  Use toDate(now() - INTERVAL X HOUR) for correct Date-level comparison

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-15 23:10:35 +01:00

352 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
interface InvestigationPanelProps {
entityType: 'ip' | 'ja4' | 'asn' | 'host';
entityValue: string;
onClose: () => void;
}
interface AttributeValue {
value: string;
count: number;
percentage: number;
first_seen?: string;
last_seen?: string;
}
interface EntityData {
type: string;
value: string;
total_detections: number;
unique_ips: number;
threat_level?: string;
anomaly_score?: number;
country_code?: string;
asn_number?: string;
user_agents?: { value: string; count: number }[];
ja4s?: string[];
hosts?: string[];
attributes?: {
user_agents?: AttributeValue[];
ja4?: AttributeValue[];
countries?: AttributeValue[];
asns?: AttributeValue[];
hosts?: AttributeValue[];
};
}
export function InvestigationPanel({ entityType, entityValue, onClose }: InvestigationPanelProps) {
const navigate = useNavigate();
const [data, setData] = useState<EntityData | null>(null);
const [loading, setLoading] = useState(true);
const [classifying, setClassifying] = useState(false);
const [showAllUA, setShowAllUA] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(`/api/variability/${entityType}/${encodeURIComponent(entityValue)}`);
if (response.ok) {
const result = await response.json();
setData(result);
}
} catch (error) {
console.error('Error fetching entity data:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, [entityType, entityValue]);
const getSeverityColor = (score?: number) => {
if (!score) return 'bg-gray-500';
if (score < -0.7) return 'bg-threat-critical';
if (score < -0.3) return 'bg-threat-high';
if (score < 0) return 'bg-threat-medium';
return 'bg-threat-low';
};
const getSeverityLabel = (score?: number) => {
if (!score) return 'Unknown';
if (score < -0.7) return 'CRITICAL';
if (score < -0.3) return 'HIGH';
if (score < 0) return 'MEDIUM';
return 'LOW';
};
const getCountryFlag = (code?: string) => {
if (!code) return '';
return code.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397));
};
const handleQuickClassify = async (label: string) => {
setClassifying(true);
try {
await fetch('/api/analysis/classifications', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
[entityType]: entityValue,
label,
tags: ['quick-classification'],
comment: 'Classification rapide depuis panel latéral',
confidence: 0.7,
analyst: 'soc_user'
})
});
alert(`Classification sauvegardée: ${label}`);
} catch (error) {
alert('Erreur lors de la classification');
} finally {
setClassifying(false);
}
};
return (
<div className="fixed inset-0 z-50 flex justify-end">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Panel */}
<div className="relative w-full max-w-md bg-background-secondary h-full shadow-2xl overflow-y-auto animate-slide-in-right">
{/* Header */}
<div className="sticky top-0 bg-background-secondary border-b border-background-card p-6 z-10">
<div className="flex items-center justify-between mb-4">
<button
onClick={onClose}
className="text-text-secondary hover:text-text-primary transition-colors"
>
Fermer
</button>
<button
onClick={() => navigate(`/investigation/${entityType === 'ip' ? entityValue : ''}`)}
className="text-accent-primary hover:text-accent-primary/80 text-sm transition-colors"
>
Vue complète
</button>
</div>
<div className="flex items-start gap-3">
<div className="text-3xl">
{entityType === 'ip' && '🌐'}
{entityType === 'ja4' && '🔐'}
{entityType === 'asn' && '🏢'}
{entityType === 'host' && '🖥️'}
</div>
<div className="flex-1 min-w-0">
<div className="text-xs text-text-secondary uppercase tracking-wide">
{entityType.toUpperCase()}
</div>
<div className="font-mono text-sm text-text-primary break-all">
{entityValue}
</div>
</div>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-6">
{loading ? (
<div className="text-center text-text-secondary py-12">
Chargement...
</div>
) : data ? (
<>
{/* Quick Stats */}
<div className="grid grid-cols-2 gap-4">
<StatBox
label="Détections"
value={data.total_detections.toLocaleString()}
/>
<StatBox
label="IPs Uniques"
value={data.unique_ips.toLocaleString()}
/>
</div>
{/* Risk Score */}
<div className="bg-background-card rounded-lg p-4">
<div className="text-sm text-text-secondary mb-2">Score de Risque Estimé</div>
<div className="flex items-center gap-3">
<div className={`px-3 py-1 rounded font-bold text-white ${getSeverityColor(data.anomaly_score)}`}>
{getSeverityLabel(data.anomaly_score)}
</div>
<div className="flex-1 bg-background-secondary rounded-full h-3">
<div
className={`h-3 rounded-full ${getSeverityColor(data.anomaly_score)}`}
style={{ width: `${Math.min(100, Math.abs((data.anomaly_score || 0) * 100))}%` }}
/>
</div>
</div>
</div>
{/* User-Agents */}
{data.attributes?.user_agents && data.attributes.user_agents.length > 0 && (
<div>
<div className="text-sm font-semibold text-text-primary mb-2">
🤖 User-Agents ({data.attributes.user_agents.length})
</div>
<div className="space-y-2">
{(showAllUA ? data.attributes.user_agents : data.attributes.user_agents.slice(0, 5)).map((ua: any, idx: number) => (
<div key={idx} className="bg-background-card rounded-lg p-3">
<div className="text-xs text-text-primary font-mono break-all leading-relaxed">
{ua.value}
</div>
<div className="text-xs text-text-secondary mt-1">
{ua.count} détections {ua.percentage.toFixed(1)}%
</div>
</div>
))}
{data.attributes.user_agents.length > 5 && (
<button
onClick={() => setShowAllUA(v => !v)}
className="w-full text-xs text-accent-primary hover:text-accent-primary/80 transition-colors"
>
{showAllUA ? '↑ Réduire' : `↓ Voir les ${data.attributes.user_agents.length - 5} autres`}
</button>
)}
</div>
</div>
)}
{/* JA4 Fingerprints */}
{data.attributes?.ja4 && data.attributes.ja4.length > 0 && (
<div>
<div className="text-sm font-semibold text-text-primary mb-2">
🔐 JA4 Fingerprints ({data.attributes.ja4.length})
</div>
<div className="space-y-2">
{data.attributes.ja4.slice(0, 5).map((ja4: any, idx: number) => (
<div
key={idx}
className="bg-background-card rounded-lg p-3 flex items-center justify-between cursor-pointer hover:bg-background-card/80 transition-colors"
onClick={() => navigate(`/investigation/ja4/${encodeURIComponent(ja4.value)}`)}
>
<div className="font-mono text-xs text-text-primary break-all flex-1">
{ja4.value}
</div>
<div className="text-xs text-text-secondary ml-2 whitespace-nowrap">
{ja4.count}
</div>
</div>
))}
</div>
</div>
)}
{/* Countries */}
{data.attributes?.countries && data.attributes.countries.length > 0 && (
<div>
<div className="text-sm font-semibold text-text-primary mb-2">
🌍 Pays ({data.attributes.countries.length})
</div>
<div className="space-y-2">
{data.attributes.countries.slice(0, 5).map((country: any, idx: number) => (
<div key={idx} className="bg-background-card rounded-lg p-3 flex items-center gap-3">
<span className="text-xl">
{getCountryFlag(country.value)}
</span>
<div className="flex-1">
<div className="text-sm text-text-primary">
{country.value}
</div>
<div className="text-xs text-text-secondary">
{country.percentage.toFixed(1)}%
</div>
</div>
<div className="text-sm text-text-primary font-bold">
{country.count}
</div>
</div>
))}
</div>
</div>
)}
{/* Quick Classification */}
<div className="border-t border-background-card pt-6">
<div className="text-sm font-semibold text-text-primary mb-4">
Classification Rapide
</div>
<div className="grid grid-cols-3 gap-2 mb-4">
<button
onClick={() => handleQuickClassify('legitimate')}
disabled={classifying}
className="py-3 px-2 bg-threat-low/20 text-threat-low rounded-lg text-sm font-medium hover:bg-threat-low/30 transition-colors disabled:opacity-50"
>
Légitime
</button>
<button
onClick={() => handleQuickClassify('suspicious')}
disabled={classifying}
className="py-3 px-2 bg-threat-medium/20 text-threat-medium rounded-lg text-sm font-medium hover:bg-threat-medium/30 transition-colors disabled:opacity-50"
>
Suspect
</button>
<button
onClick={() => handleQuickClassify('malicious')}
disabled={classifying}
className="py-3 px-2 bg-threat-high/20 text-threat-high rounded-lg text-sm font-medium hover:bg-threat-high/30 transition-colors disabled:opacity-50"
>
Malveillant
</button>
</div>
<div className="flex gap-2">
<button
onClick={() => navigate(`/investigation/${entityType === 'ip' ? entityValue : ''}`)}
className="flex-1 py-3 px-4 bg-accent-primary text-white rounded-lg text-sm font-medium hover:bg-accent-primary/80 transition-colors"
>
🔍 Investigation Complète
</button>
<button
onClick={() => {
// Export IOC
const blob = new Blob([JSON.stringify({
type: entityType,
value: entityValue,
timestamp: new Date().toISOString()
}, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `ioc_${entityType}_${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}}
className="py-3 px-4 bg-background-card text-text-primary rounded-lg text-sm font-medium hover:bg-background-card/80 transition-colors"
>
📤 Export IOC
</button>
</div>
</div>
</>
) : (
<div className="text-center text-text-secondary py-12">
Aucune donnée disponible
</div>
)}
</div>
</div>
</div>
);
}
// Stat Box Component
function StatBox({ label, value }: { label: string; value: string }) {
return (
<div className="bg-background-card 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>
);
}