feat: 6 améliorations SOC — synthèse IP, baseline, sophistication, chasse proactive, badge ASN, 2 nouveaux onglets rotation

- investigation_summary.py: nouveau endpoint GET /api/investigation/{ip}/summary
  agrège 6 sources (ML, bruteforce, TCP spoofing, JA4 rotation, persistance, timeline 24h)
  en un score de risque 0-100 avec signaux détaillés
- InvestigationView.tsx: widget IPActivitySummary avec jauge Risk Score SVG,
  badges multi-sources et mini-timeline 24h barres
- metrics.py: endpoint GET /api/metrics/baseline — comparaison 24h vs hier
  (total détections, IPs uniques, alertes CRITICAL) avec % de variation
- IncidentsView.tsx: widget baseline avec ▲▼ sur le dashboard principal
- rotation.py: endpoints /sophistication et /proactive-hunt
  Score sophistication = JOIN 3 tables (rotation×10 + récurrence×20 + log(bf+1)×5)
  Chasse proactive = IPs récurrentes sous le seuil ML (abs(score) < 0.5)
- RotationView.tsx: onglets 🏆 Sophistication et 🕵️ Chasse proactive
  avec tier APT-like/Advanced/Automated/Basic et boutons investigation
- detections.py: LEFT JOIN asn_reputation, badge coloré rouge/orange/vert
  selon label (bot/scanner → score 0.05, human → 0.9)
- models.py: ajout champs asn_score et asn_rep_label dans Detection

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
SOC Analyst
2026-03-16 00:43:27 +01:00
parent 8032ebaab8
commit d4c3512572
11 changed files with 815 additions and 6 deletions

View File

@ -30,10 +30,22 @@ interface MetricsSummary {
unique_ips: number;
}
interface BaselineMetric {
today: number;
yesterday: number;
pct_change: number;
}
interface BaselineData {
total_detections: BaselineMetric;
unique_ips: BaselineMetric;
critical_alerts: BaselineMetric;
}
export function IncidentsView() {
const navigate = useNavigate();
const [clusters, setClusters] = useState<IncidentCluster[]>([]);
const [metrics, setMetrics] = useState<MetricsSummary | null>(null);
const [baseline, setBaseline] = useState<BaselineData | null>(null);
const [loading, setLoading] = useState(true);
const [selectedClusters, setSelectedClusters] = useState<Set<string>>(new Set());
@ -47,6 +59,11 @@ export function IncidentsView() {
setMetrics(metricsData.summary);
}
const baselineResponse = await fetch('/api/metrics/baseline');
if (baselineResponse.ok) {
setBaseline(await baselineResponse.json());
}
const clustersResponse = await fetch('/api/incidents/clusters');
if (clustersResponse.ok) {
const clustersData = await clustersResponse.json();
@ -126,6 +143,38 @@ export function IncidentsView() {
</div>
</div>
{/* Baseline comparison */}
{baseline && (
<div className="grid grid-cols-3 gap-3">
{([
{ key: 'total_detections', label: 'Détections 24h', icon: '📊' },
{ key: 'unique_ips', label: 'IPs uniques', icon: '🖥️' },
{ key: 'critical_alerts', label: 'Alertes CRITICAL', icon: '🔴' },
] as { key: keyof BaselineData; label: string; icon: string }[]).map(({ key, label, icon }) => {
const m = baseline[key];
const up = m.pct_change > 0;
const neutral = m.pct_change === 0;
return (
<div key={key} className="bg-background-card border border-border rounded-lg px-4 py-3 flex items-center gap-3">
<span className="text-xl">{icon}</span>
<div className="flex-1 min-w-0">
<div className="text-xs text-text-disabled uppercase tracking-wide">{label}</div>
<div className="text-xl font-bold text-text-primary">{m.today.toLocaleString('fr-FR')}</div>
<div className="text-xs text-text-secondary">hier: {m.yesterday.toLocaleString('fr-FR')}</div>
</div>
<div className={`text-sm font-bold px-2 py-1 rounded ${
neutral ? 'text-text-disabled' :
up ? 'text-threat-critical bg-threat-critical/10' :
'text-threat-low bg-threat-low/10'
}`}>
{neutral ? '=' : up ? `▲ +${m.pct_change}%` : `${m.pct_change}%`}
</div>
</div>
);
})}
</div>
)}
{/* Critical Metrics */}
{metrics && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">