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>
This commit is contained in:
SOC Analyst
2026-03-15 23:10:35 +01:00
parent 8d35b91642
commit 1455e04303
50 changed files with 5442 additions and 7325 deletions

View File

@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { QuickSearch } from './QuickSearch';
interface IncidentCluster {
id: string;
@ -124,10 +123,7 @@ export function IncidentsView() {
<p className="text-text-secondary text-sm mt-1">
Surveillance en temps réel - 24 dernières heures
</p>
</div>
<div className="w-full md:w-auto">
<QuickSearch />
</div>
</div>
</div>
{/* Critical Metrics */}
@ -212,8 +208,10 @@ export function IncidentsView() {
</div>
)}
{/* Priority Incidents */}
<div>
{/* Main content: incidents list (2/3) + top threats table (1/3) */}
<div className="grid grid-cols-3 gap-6 items-start">
{/* Incidents list — 2/3 */}
<div className="col-span-2">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-text-primary">
Incidents Prioritaires
@ -367,41 +365,35 @@ export function IncidentsView() {
</div>
)}
</div>
</div>
</div>{/* end col-span-2 */}
{/* Top Active Threats */}
<div className="bg-background-secondary rounded-lg p-6">
<h2 className="text-xl font-semibold text-text-primary mb-4">
Top Menaces Actives
</h2>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-background-card">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">#</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Entité</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Type</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Score</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Pays</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">ASN</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Hits/s</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase">Tendance</th>
</tr>
</thead>
<tbody className="divide-y divide-background-card">
{clusters.slice(0, 10).map((cluster, index) => (
<tr
key={cluster.id}
className="hover:bg-background-card/50 transition-colors cursor-pointer"
onClick={() => navigate(`/investigation/${cluster.subnet?.split('/')[0] || ''}`)}
{/* Top threats sidebar — 1/3 */}
<div className="sticky top-4">
<div className="bg-background-secondary rounded-lg overflow-hidden">
<div className="p-4 border-b border-background-card">
<h3 className="text-base font-semibold text-text-primary">🔥 Top Menaces</h3>
</div>
<div className="divide-y divide-background-card">
{clusters.slice(0, 12).map((cluster, index) => (
<div
key={cluster.id}
className="px-4 py-3 flex items-center gap-3 hover:bg-background-card/50 transition-colors cursor-pointer"
onClick={() => navigate(`/investigation/${cluster.sample_ip || cluster.subnet?.split('/')[0] || ''}`)}
>
<td className="px-4 py-3 text-text-secondary">{index + 1}</td>
<td className="px-4 py-3 font-mono text-sm text-text-primary">
{cluster.subnet?.split('/')[0] || 'Unknown'}
</td>
<td className="px-4 py-3 text-sm text-text-secondary">IP</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded text-xs font-bold ${
<span className="text-text-disabled text-xs w-4">{index + 1}</span>
<div className="flex-1 min-w-0">
<div className="font-mono text-xs text-text-primary truncate">
{cluster.sample_ip || cluster.subnet?.split('/')[0] || 'Unknown'}
</div>
<div className="text-xs text-text-secondary flex gap-2 mt-0.5">
{cluster.countries[0] && (
<span>{getCountryFlag(cluster.countries[0].code)} {cluster.countries[0].code}</span>
)}
<span>AS{cluster.asn || '?'}</span>
</div>
</div>
<div className="flex flex-col items-end gap-1">
<span className={`px-1.5 py-0.5 rounded text-xs font-bold ${
cluster.score > 80 ? 'bg-red-500 text-white' :
cluster.score > 60 ? 'bg-orange-500 text-white' :
cluster.score > 40 ? 'bg-yellow-500 text-white' :
@ -409,33 +401,25 @@ export function IncidentsView() {
}`}>
{cluster.score}
</span>
</td>
<td className="px-4 py-3 text-text-primary">
{cluster.countries[0] && (
<>
{getCountryFlag(cluster.countries[0].code)} {cluster.countries[0].code}
</>
)}
</td>
<td className="px-4 py-3 text-sm text-text-primary">
AS{cluster.asn || '?'}
</td>
<td className="px-4 py-3 text-text-primary font-bold">
{Math.round(cluster.total_detections / 24) || 0}
</td>
<td className={`px-4 py-3 font-bold ${
cluster.trend === 'up' ? 'text-red-500' :
cluster.trend === 'down' ? 'text-green-500' :
'text-gray-400'
}`}>
{cluster.trend === 'up' ? '↑' : cluster.trend === 'down' ? '↓' : '→'} {cluster.trend_percentage}%
</td>
</tr>
<span className={`text-xs font-bold ${
cluster.trend === 'up' ? 'text-red-500' :
cluster.trend === 'down' ? 'text-green-500' :
'text-gray-400'
}`}>
{cluster.trend === 'up' ? '↑' : cluster.trend === 'down' ? '↓' : '→'}
</span>
</div>
</div>
))}
</tbody>
</table>
{clusters.length === 0 && (
<div className="px-4 py-8 text-center text-text-secondary text-sm">
Aucune menace active
</div>
)}
</div>
</div>
</div>
</div>
</div>{/* end grid */}
</div>
);
}