From bd423148e63ca4073d1ce66fca3bd78799f642e1 Mon Sep 17 00:00:00 2001 From: SOC Analyst Date: Fri, 20 Mar 2026 10:13:00 +0100 Subject: [PATCH] Refactor frontend components and cleanup Co-authored-by: Qwen-Coder --- frontend/src/components/BotnetMapView.tsx | 314 ---------- frontend/src/components/BruteForceView.tsx | 11 +- .../src/components/BulkClassification.tsx | 21 +- frontend/src/components/CampaignsView.tsx | 10 +- frontend/src/components/CorrelationGraph.tsx | 7 +- .../components/EntityInvestigationView.tsx | 11 +- frontend/src/components/FingerprintsView.tsx | 13 +- .../src/components/HeaderFingerprintView.tsx | 3 +- frontend/src/components/HeatmapView.tsx | 318 ---------- frontend/src/components/IncidentsView.tsx | 5 +- .../src/components/InvestigationPanel.tsx | 351 ----------- frontend/src/components/MLFeaturesView.tsx | 11 +- frontend/src/components/PivotView.tsx | 7 +- frontend/src/components/RotationView.tsx | 590 ------------------ frontend/src/components/SearchModal.tsx | 182 ------ .../src/components/SubnetInvestigation.tsx | 12 +- frontend/src/components/TcpSpoofingView.tsx | 11 +- .../analysis/CorrelationSummary.tsx | 21 +- .../analysis/JA4CorrelationSummary.tsx | 26 +- frontend/src/utils/classifications.ts | 36 ++ 20 files changed, 61 insertions(+), 1899 deletions(-) delete mode 100644 frontend/src/components/BotnetMapView.tsx delete mode 100644 frontend/src/components/HeatmapView.tsx delete mode 100644 frontend/src/components/InvestigationPanel.tsx delete mode 100644 frontend/src/components/RotationView.tsx delete mode 100644 frontend/src/components/SearchModal.tsx create mode 100644 frontend/src/utils/classifications.ts diff --git a/frontend/src/components/BotnetMapView.tsx b/frontend/src/components/BotnetMapView.tsx deleted file mode 100644 index ecd9340..0000000 --- a/frontend/src/components/BotnetMapView.tsx +++ /dev/null @@ -1,314 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { formatNumber } from '../utils/dateUtils'; - -// ─── Types ──────────────────────────────────────────────────────────────────── - -interface BotnetItem { - ja4: string; - unique_ips: number; - unique_countries: number; - targeted_hosts: number; - distribution_score: number; - botnet_class: string; -} - -interface BotnetSummary { - total_global_botnets: number; - total_ips_in_botnets: number; - most_spread_ja4: string; - most_ips_ja4: string; -} - -interface CountryEntry { - country_code: string; - unique_ips: number; - hits: number; -} - -type SortField = 'unique_ips' | 'unique_countries' | 'targeted_hosts'; - -// ─── Helpers ────────────────────────────────────────────────────────────────── - - -function getCountryFlag(code: string): string { - if (!code || code.length !== 2) return '🌐'; - return code - .toUpperCase() - .replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397)); -} - -function botnetClassBadge(cls: string): { bg: string; text: string; label: string } { - switch (cls) { - case 'global': - return { bg: 'bg-threat-critical/20', text: 'text-threat-critical', label: '🌐 Global' }; - case 'regional': - return { bg: 'bg-threat-high/20', text: 'text-threat-high', label: '🗺️ Régional' }; - case 'concentrated': - return { bg: 'bg-threat-medium/20', text: 'text-threat-medium', label: '📍 Concentré' }; - default: - return { bg: 'bg-background-card', text: 'text-text-secondary', label: cls }; - } -} - -// ─── Sub-components ─────────────────────────────────────────────────────────── - -function StatCard({ label, value, accent, mono }: { label: string; value: string | number; accent?: string; mono?: boolean }) { - return ( -
- {label} - - {value} - -
- ); -} - -function LoadingSpinner() { - return ( -
-
-
- ); -} - -function ErrorMessage({ message }: { message: string }) { - return ( -
- ⚠️ {message} -
- ); -} - -// ─── Botnet row with expandable countries ───────────────────────────────────── - -function BotnetRow({ - item, - onInvestigate, -}: { - item: BotnetItem; - onInvestigate: (ja4: string) => void; -}) { - const [expanded, setExpanded] = useState(false); - const [countries, setCountries] = useState([]); - const [countriesLoading, setCountriesLoading] = useState(false); - const [countriesError, setCountriesError] = useState(null); - const [countriesLoaded, setCountriesLoaded] = useState(false); - - const toggle = async () => { - setExpanded((prev) => !prev); - if (!countriesLoaded && !expanded) { - setCountriesLoading(true); - try { - const res = await fetch(`/api/botnets/ja4/${encodeURIComponent(item.ja4)}/countries?limit=30`); - if (!res.ok) throw new Error('Erreur chargement des pays'); - const data: { items: CountryEntry[] } = await res.json(); - setCountries(data.items ?? []); - setCountriesLoaded(true); - } catch (err) { - setCountriesError(err instanceof Error ? err.message : 'Erreur inconnue'); - } finally { - setCountriesLoading(false); - } - } - }; - - const badge = botnetClassBadge(item.botnet_class); - - return ( - <> - - - {expanded ? '▾' : '▸'} - {item.ja4 ? item.ja4.slice(0, 20) : '—'}… - - {formatNumber(item.unique_ips)} - - 🌍 {formatNumber(item.unique_countries)} - - {formatNumber(item.targeted_hosts)} - -
-
-
-
- {Math.round(item.distribution_score)} -
- - - {badge.label} - - - - - - {expanded && ( - - - {countriesLoading ? ( -
-
- Chargement des pays… -
- ) : countriesError ? ( - ⚠️ {countriesError} - ) : ( -
- {countries.map((c) => ( - - {getCountryFlag(c.country_code)} {c.country_code} - · - {formatNumber(c.unique_ips)} IPs - - ))} -
- )} - - - )} - - ); -} - -// ─── Main Component ─────────────────────────────────────────────────────────── - -export function BotnetMapView() { - const navigate = useNavigate(); - - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const [summary, setSummary] = useState(null); - const [summaryLoading, setSummaryLoading] = useState(true); - const [summaryError, setSummaryError] = useState(null); - - const [sortField, setSortField] = useState('unique_ips'); - - useEffect(() => { - const fetchItems = async () => { - setLoading(true); - try { - const res = await fetch('/api/botnets/ja4-spread'); - if (!res.ok) throw new Error('Erreur chargement des botnets'); - const data: { items: BotnetItem[]; total: number } = await res.json(); - setItems(data.items ?? []); - } catch (err) { - setError(err instanceof Error ? err.message : 'Erreur inconnue'); - } finally { - setLoading(false); - } - }; - const fetchSummary = async () => { - setSummaryLoading(true); - try { - const res = await fetch('/api/botnets/summary'); - if (!res.ok) throw new Error('Erreur chargement du résumé'); - const data: BotnetSummary = await res.json(); - setSummary(data); - } catch (err) { - setSummaryError(err instanceof Error ? err.message : 'Erreur inconnue'); - } finally { - setSummaryLoading(false); - } - }; - fetchItems(); - fetchSummary(); - }, []); - - const sortedItems = [...items].sort((a, b) => b[sortField] - a[sortField]); - - const sortButton = (field: SortField, label: string) => ( - - ); - - return ( -
- {/* Header */} -
-

🌍 Botnets Distribués

-

- Analyse de la distribution géographique des fingerprints JA4 pour détecter les botnets globaux. -

-
- - {/* Stat cards */} - {summaryLoading ? ( - - ) : summaryError ? ( - - ) : summary ? ( -
- - - - -
- ) : null} - - {/* Sort controls */} -
- Trier par : - {sortButton('unique_ips', '🖥️ IPs')} - {sortButton('unique_countries', '🌍 Pays')} - {sortButton('targeted_hosts', '🎯 Hosts')} -
- - {/* Table */} -
- {loading ? ( - - ) : error ? ( -
- ) : ( - - - - - - - - - - - - - - {sortedItems.map((item) => ( - navigate(`/investigation/ja4/${ja4}`)} - /> - ))} - -
JA4IPsPaysHosts ciblésScore distributionClasse
- )} -
-
- ); -} diff --git a/frontend/src/components/BruteForceView.tsx b/frontend/src/components/BruteForceView.tsx index e12d3c3..230f467 100644 --- a/frontend/src/components/BruteForceView.tsx +++ b/frontend/src/components/BruteForceView.tsx @@ -4,6 +4,7 @@ import DataTable, { Column } from './ui/DataTable'; import { InfoTip } from './ui/Tooltip'; import { TIPS } from './ui/tooltips'; import { formatNumber } from '../utils/dateUtils'; +import { LoadingSpinner, ErrorMessage } from './ui/Feedback'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -46,15 +47,7 @@ function StatCard({ label, value, accent }: { label: string; value: string | num ); } -function LoadingSpinner() { - return ( -
-
-
- ); -} - -function ErrorMessage({ message }: { message: string }) { +: { message: string }) { return (
⚠️ {message} diff --git a/frontend/src/components/BulkClassification.tsx b/frontend/src/components/BulkClassification.tsx index 60a215e..515efad 100644 --- a/frontend/src/components/BulkClassification.tsx +++ b/frontend/src/components/BulkClassification.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { PREDEFINED_TAGS } from '../utils/classifications'; interface BulkClassificationProps { selectedIPs: string[]; @@ -6,26 +7,6 @@ interface BulkClassificationProps { onSuccess: () => void; } -const PREDEFINED_TAGS = [ - 'scraping', - 'bot-network', - 'scanner', - 'bruteforce', - 'data-exfil', - 'ddos', - 'spam', - 'proxy', - 'tor', - 'vpn', - 'hosting-asn', - 'distributed', - 'ja4-rotation', - 'ua-rotation', - 'country-cn', - 'country-us', - 'country-ru', -]; - export function BulkClassification({ selectedIPs, onClose, onSuccess }: BulkClassificationProps) { const [selectedLabel, setSelectedLabel] = useState('suspicious'); const [selectedTags, setSelectedTags] = useState([]); diff --git a/frontend/src/components/CampaignsView.tsx b/frontend/src/components/CampaignsView.tsx index 36dc10e..49a3a1c 100644 --- a/frontend/src/components/CampaignsView.tsx +++ b/frontend/src/components/CampaignsView.tsx @@ -4,6 +4,8 @@ import DataTable, { Column } from './ui/DataTable'; import ThreatBadge from './ui/ThreatBadge'; import { InfoTip } from './ui/Tooltip'; import { TIPS } from './ui/tooltips'; +import { formatNumber } from '../utils/dateUtils'; +import { getCountryFlag } from '../utils/countryUtils'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -854,14 +856,6 @@ interface BotnetCountryEntry { hits: number; } -function formatNumber(n: number): string { - return n.toLocaleString(navigator.language || undefined); -} - -function getCountryFlag(code: string): string { - if (!code || code.length !== 2) return '🌐'; - return code.toUpperCase().replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397)); -} function botnetClassBadge(cls: string): { bg: string; text: string; label: string } { switch (cls) { diff --git a/frontend/src/components/CorrelationGraph.tsx b/frontend/src/components/CorrelationGraph.tsx index f823dec..66bb44b 100644 --- a/frontend/src/components/CorrelationGraph.tsx +++ b/frontend/src/components/CorrelationGraph.tsx @@ -17,6 +17,7 @@ import 'reactflow/dist/style.css'; import { useEffect, useState, useCallback, memo } from 'react'; import { InfoTip } from './ui/Tooltip'; import { TIPS } from './ui/tooltips'; +import { getCountryFlag } from '../utils/countryUtils'; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -47,12 +48,6 @@ function cleanIP(address: string): string { return address.replace(/^::ffff:/i, ''); } -function getCountryFlag(code: string): string { - return (code || '').toUpperCase().replace(/./g, (char) => - String.fromCodePoint(char.charCodeAt(0) + 127397) - ); -} - function classifyUA(ua: string): 'bot' | 'script' | 'normal' { const u = ua.toLowerCase(); if (u.includes('bot') || u.includes('crawler') || u.includes('spider')) return 'bot'; diff --git a/frontend/src/components/EntityInvestigationView.tsx b/frontend/src/components/EntityInvestigationView.tsx index bef3c00..e85e86a 100644 --- a/frontend/src/components/EntityInvestigationView.tsx +++ b/frontend/src/components/EntityInvestigationView.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import { InfoTip } from './ui/Tooltip'; import { TIPS } from './ui/tooltips'; import { formatDateOnly } from '../utils/dateUtils'; +import { getCountryFlag } from '../utils/countryUtils'; interface EntityStats { entity_type: string; @@ -84,15 +85,7 @@ export function EntityInvestigationView() { return labels[entityType] || entityType; }; - const getCountryFlag = (code: string) => { - const flags: Record = { - CN: '🇨🇳', US: '🇺🇸', FR: '🇫🇷', DE: '🇩🇪', GB: '🇬🇧', - RU: '🇷🇺', CA: '🇨🇦', AU: '🇦🇺', JP: '🇯🇵', IN: '🇮🇳', - BR: '🇧🇷', IT: '🇮🇹', ES: '🇪🇸', NL: '🇳🇱', BE: '🇧🇪', - CH: '🇨🇭', SE: '🇸🇪', NO: '🇳🇴', DK: '🇩🇰', FI: '🇫🇮' - }; - return flags[code] || code; - }; + ; if (loading) { diff --git a/frontend/src/components/FingerprintsView.tsx b/frontend/src/components/FingerprintsView.tsx index 6a4823d..0e28ea5 100644 --- a/frontend/src/components/FingerprintsView.tsx +++ b/frontend/src/components/FingerprintsView.tsx @@ -4,7 +4,8 @@ import DataTable, { Column } from './ui/DataTable'; import ThreatBadge from './ui/ThreatBadge'; import { InfoTip } from './ui/Tooltip'; import { TIPS } from './ui/tooltips'; -import { formatNumber as fmtNum, formatDateShort } from '../utils/dateUtils'; +import { formatNumber, formatDateShort } from '../utils/dateUtils'; +import { getCountryFlag } from '../utils/countryUtils'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -123,16 +124,6 @@ function botnetScore(uniqueIps: number, botUaPct: number): number { return Math.round(ipScore + botBonus); } -function getCountryFlag(code: string): string { - return code - .toUpperCase() - .replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397)); -} - -function formatNumber(n: number): string { - return fmtNum(n); -} - function botUaPercentage(userAgents: AttributeValue[]): number { if (!userAgents.length) return 0; const botOrScript = userAgents.filter((ua) => classifyUA(ua.value) !== 'normal'); diff --git a/frontend/src/components/HeaderFingerprintView.tsx b/frontend/src/components/HeaderFingerprintView.tsx index f6a2e38..1cca724 100644 --- a/frontend/src/components/HeaderFingerprintView.tsx +++ b/frontend/src/components/HeaderFingerprintView.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import DataTable, { Column } from './ui/DataTable'; import { TIPS } from './ui/tooltips'; import { formatNumber } from '../utils/dateUtils'; +import { ErrorMessage } from './ui/Feedback'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -65,7 +66,7 @@ function StatCard({ label, value, accent }: { label: string; value: string | num ); } -function ErrorMessage({ message }: { message: string }) { +: { message: string }) { return (
⚠️ {message} diff --git a/frontend/src/components/HeatmapView.tsx b/frontend/src/components/HeatmapView.tsx deleted file mode 100644 index 86d44c5..0000000 --- a/frontend/src/components/HeatmapView.tsx +++ /dev/null @@ -1,318 +0,0 @@ -import { useState, useEffect } from 'react'; -import { formatNumber } from '../utils/dateUtils'; - -// ─── Types ──────────────────────────────────────────────────────────────────── - -interface HourlyEntry { - hour: number; - hits: number; - unique_ips: number; - max_rps: number; -} - -interface TopHost { - host: string; - total_hits: number; - unique_ips: number; - unique_ja4s: number; - hourly_hits: number[]; -} - -interface HeatmapMatrix { - hosts: string[]; - matrix: number[][]; -} - -// ─── Helpers ────────────────────────────────────────────────────────────────── - - -function heatmapCellStyle(value: number, maxValue: number): React.CSSProperties { - if (maxValue === 0 || value === 0) return { backgroundColor: 'transparent' }; - const ratio = value / maxValue; - if (ratio >= 0.75) return { backgroundColor: 'rgba(239, 68, 68, 0.85)' }; - if (ratio >= 0.5) return { backgroundColor: 'rgba(168, 85, 247, 0.7)' }; - if (ratio >= 0.25) return { backgroundColor: 'rgba(59, 130, 246, 0.6)' }; - if (ratio >= 0.05) return { backgroundColor: 'rgba(96, 165, 250, 0.35)' }; - return { backgroundColor: 'rgba(147, 197, 253, 0.15)' }; -} - -// ─── Sub-components ─────────────────────────────────────────────────────────── - -function StatCard({ label, value, accent }: { label: string; value: string | number; accent?: string }) { - return ( -
- {label} - {value} -
- ); -} - -function LoadingSpinner() { - return ( -
-
-
- ); -} - -function ErrorMessage({ message }: { message: string }) { - return ( -
- ⚠️ {message} -
- ); -} - -// Mini sparkline for a host row -function Sparkline({ data }: { data: number[] }) { - const max = Math.max(...data, 1); - return ( -
- {data.map((v, i) => { - const pct = (v / max) * 100; - return ( -
= 70 ? 'bg-threat-critical' : pct >= 30 ? 'bg-threat-medium' : 'bg-accent-primary/50'}`} - /> - ); - })} -
- ); -} - -// ─── Main Component ─────────────────────────────────────────────────────────── - -export function HeatmapView() { - const [hourly, setHourly] = useState([]); - const [hourlyLoading, setHourlyLoading] = useState(true); - const [hourlyError, setHourlyError] = useState(null); - - const [topHosts, setTopHosts] = useState([]); - const [topHostsLoading, setTopHostsLoading] = useState(true); - const [topHostsError, setTopHostsError] = useState(null); - - const [matrixData, setMatrixData] = useState(null); - const [matrixLoading, setMatrixLoading] = useState(true); - const [matrixError, setMatrixError] = useState(null); - - useEffect(() => { - const fetchHourly = async () => { - try { - const res = await fetch('/api/heatmap/hourly'); - if (!res.ok) throw new Error('Erreur chargement courbe horaire'); - const data: { hours: HourlyEntry[] } = await res.json(); - setHourly(data.hours ?? []); - } catch (err) { - setHourlyError(err instanceof Error ? err.message : 'Erreur inconnue'); - } finally { - setHourlyLoading(false); - } - }; - const fetchTopHosts = async () => { - try { - const res = await fetch('/api/heatmap/top-hosts?limit=20'); - if (!res.ok) throw new Error('Erreur chargement top hosts'); - const data: { items: TopHost[] } = await res.json(); - setTopHosts(data.items ?? []); - } catch (err) { - setTopHostsError(err instanceof Error ? err.message : 'Erreur inconnue'); - } finally { - setTopHostsLoading(false); - } - }; - const fetchMatrix = async () => { - try { - const res = await fetch('/api/heatmap/matrix'); - if (!res.ok) throw new Error('Erreur chargement heatmap matrix'); - const data: HeatmapMatrix = await res.json(); - setMatrixData(data); - } catch (err) { - setMatrixError(err instanceof Error ? err.message : 'Erreur inconnue'); - } finally { - setMatrixLoading(false); - } - }; - fetchHourly(); - fetchTopHosts(); - fetchMatrix(); - }, []); - - const peakHour = hourly.reduce((best, h) => (h.hits > best.hits ? h : best), { hour: 0, hits: 0, unique_ips: 0, max_rps: 0 }); - const totalHits = hourly.reduce((s, h) => s + h.hits, 0); - const maxHits = hourly.length > 0 ? Math.max(...hourly.map((h) => h.hits)) : 1; - - const matrixMax = matrixData - ? Math.max(...matrixData.matrix.flatMap((row) => row), 1) - : 1; - - const displayHosts = matrixData ? matrixData.hosts.slice(0, 15) : []; - - return ( -
- {/* Header */} -
-

⏱️ Heatmap Temporelle d'Attaques

-

- Distribution horaire de l'activité malveillante par host cible. -

-
- - {/* Stat cards */} -
- 0 ? `${peakHour.hour}h (${formatNumber(peakHour.hits)} hits)` : '—'} - accent="text-threat-critical" - /> - - -
- - {/* Section 1: Courbe horaire */} -
-

Activité horaire — 24h

- {hourlyLoading ? ( - - ) : hourlyError ? ( - - ) : ( - <> -
- {Array.from({ length: 24 }, (_, i) => { - const entry = hourly.find((h) => h.hour === i) ?? { hour: i, hits: 0, unique_ips: 0, max_rps: 0 }; - const pct = maxHits > 0 ? (entry.hits / maxHits) * 100 : 0; - return ( -
-
-
= 70 ? 'bg-threat-critical' : pct >= 30 ? 'bg-threat-medium' : 'bg-accent-primary/60' - }`} - /> -
- {i} -
- ); - })} -
-
- Élevé (≥70%) - Moyen (≥30%) - Faible -
- - )} -
- - {/* Section 2: Heatmap matrix */} -
-

Heatmap Host × Heure

- {matrixLoading ? ( - - ) : matrixError ? ( - - ) : !matrixData || displayHosts.length === 0 ? ( -

Aucune donnée disponible.

- ) : ( -
-
- {/* Hour headers */} -
- {Array.from({ length: 24 }, (_, i) => ( -
- {i} -
- ))} -
- {/* Rows */} - {displayHosts.map((host, rowIdx) => { - const rowData = matrixData.matrix[rowIdx] ?? Array(24).fill(0); - return ( -
-
- {host} -
- {Array.from({ length: 24 }, (_, h) => { - const val = rowData[h] ?? 0; - return ( -
- ); - })} -
- ); - })} -
-
- Intensité : - {[ - { label: 'Nul', style: { backgroundColor: 'transparent', border: '1px solid rgba(255,255,255,0.1)' } }, - { label: 'Très faible', style: { backgroundColor: 'rgba(147, 197, 253, 0.15)' } }, - { label: 'Faible', style: { backgroundColor: 'rgba(96, 165, 250, 0.35)' } }, - { label: 'Moyen', style: { backgroundColor: 'rgba(59, 130, 246, 0.6)' } }, - { label: 'Élevé', style: { backgroundColor: 'rgba(168, 85, 247, 0.7)' } }, - { label: 'Critique', style: { backgroundColor: 'rgba(239, 68, 68, 0.85)' } }, - ].map(({ label, style }) => ( - - - {label} - - ))} -
-
- )} -
- - {/* Section 3: Top hosts table */} -
-
-

Top Hosts ciblés

-
- {topHostsLoading ? ( - - ) : topHostsError ? ( -
- ) : ( - - - - - - - - - - - - {topHosts.map((h) => ( - - - - - - - - ))} - -
HostTotal hitsIPs uniquesJA4 uniquesActivité 24h
{h.host}{formatNumber(h.total_hits)}{formatNumber(h.unique_ips)}{formatNumber(h.unique_ja4s)} - -
- )} -
-
- ); -} diff --git a/frontend/src/components/IncidentsView.tsx b/frontend/src/components/IncidentsView.tsx index 2188767..66405b1 100644 --- a/frontend/src/components/IncidentsView.tsx +++ b/frontend/src/components/IncidentsView.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { InfoTip } from './ui/Tooltip'; import { TIPS } from './ui/tooltips'; +import { getCountryFlag } from '../utils/countryUtils'; interface IncidentCluster { id: string; @@ -123,9 +124,7 @@ export function IncidentsView() { } }; - const getCountryFlag = (code: string) => { - return code.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)); - }; + ; if (loading) { return ( diff --git a/frontend/src/components/InvestigationPanel.tsx b/frontend/src/components/InvestigationPanel.tsx deleted file mode 100644 index 2255cf9..0000000 --- a/frontend/src/components/InvestigationPanel.tsx +++ /dev/null @@ -1,351 +0,0 @@ -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(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 ( -
- {/* Backdrop */} -
- - {/* Panel */} -
- {/* Header */} -
-
- - -
- -
-
- {entityType === 'ip' && '🌐'} - {entityType === 'ja4' && '🔐'} - {entityType === 'asn' && '🏢'} - {entityType === 'host' && '🖥️'} -
-
-
- {entityType.toUpperCase()} -
-
- {entityValue} -
-
-
-
- - {/* Content */} -
- {loading ? ( -
- Chargement... -
- ) : data ? ( - <> - {/* Quick Stats */} -
- - -
- - {/* Risk Score */} -
-
Score de Risque Estimé
-
-
- {getSeverityLabel(data.anomaly_score)} -
-
-
-
-
-
- - {/* User-Agents */} - {data.attributes?.user_agents && data.attributes.user_agents.length > 0 && ( -
-
- 🤖 User-Agents ({data.attributes.user_agents.length}) -
-
- {(showAllUA ? data.attributes.user_agents : data.attributes.user_agents.slice(0, 5)).map((ua: any, idx: number) => ( -
-
- {ua.value} -
-
- {ua.count} détections • {ua.percentage.toFixed(1)}% -
-
- ))} - {data.attributes.user_agents.length > 5 && ( - - )} -
-
- )} - - {/* JA4 Fingerprints */} - {data.attributes?.ja4 && data.attributes.ja4.length > 0 && ( -
-
- 🔐 JA4 Fingerprints ({data.attributes.ja4.length}) -
-
- {data.attributes.ja4.slice(0, 5).map((ja4: any, idx: number) => ( -
navigate(`/investigation/ja4/${encodeURIComponent(ja4.value)}`)} - > -
- {ja4.value} -
-
- {ja4.count} -
-
- ))} -
-
- )} - - {/* Countries */} - {data.attributes?.countries && data.attributes.countries.length > 0 && ( -
-
- 🌍 Pays ({data.attributes.countries.length}) -
-
- {data.attributes.countries.slice(0, 5).map((country: any, idx: number) => ( -
- - {getCountryFlag(country.value)} - -
-
- {country.value} -
-
- {country.percentage.toFixed(1)}% -
-
-
- {country.count} -
-
- ))} -
-
- )} - - {/* Quick Classification */} -
-
- ⚡ Classification Rapide -
-
- - - -
- -
- - -
-
- - ) : ( -
- Aucune donnée disponible -
- )} -
-
-
- ); -} - -// Stat Box Component -function StatBox({ label, value }: { label: string; value: string }) { - return ( -
-
{label}
-
{value}
-
- ); -} diff --git a/frontend/src/components/MLFeaturesView.tsx b/frontend/src/components/MLFeaturesView.tsx index 4f6c79c..e457abc 100644 --- a/frontend/src/components/MLFeaturesView.tsx +++ b/frontend/src/components/MLFeaturesView.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import DataTable, { Column } from './ui/DataTable'; import { TIPS } from './ui/tooltips'; import { formatNumber } from '../utils/dateUtils'; +import { LoadingSpinner, ErrorMessage } from './ui/Feedback'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -80,15 +81,7 @@ function fuzzingBadgeClass(value: number): string { // ─── Sub-components ─────────────────────────────────────────────────────────── -function LoadingSpinner() { - return ( -
-
-
- ); -} - -function ErrorMessage({ message }: { message: string }) { +: { message: string }) { return (
⚠️ {message} diff --git a/frontend/src/components/PivotView.tsx b/frontend/src/components/PivotView.tsx index 4ecf728..4e31c04 100644 --- a/frontend/src/components/PivotView.tsx +++ b/frontend/src/components/PivotView.tsx @@ -14,6 +14,7 @@ import { useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { InfoTip } from './ui/Tooltip'; import { TIPS } from './ui/tooltips'; +import { getCountryFlag } from '../utils/countryUtils'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -67,12 +68,6 @@ function detectType(input: string): EntityType { return 'ja4'; } -function getCountryFlag(code: string): string { - return (code || '').toUpperCase().replace(/./g, c => - String.fromCodePoint(c.charCodeAt(0) + 127397) - ); -} - // ─── Component ──────────────────────────────────────────────────────────────── export function PivotView() { diff --git a/frontend/src/components/RotationView.tsx b/frontend/src/components/RotationView.tsx deleted file mode 100644 index ebb2799..0000000 --- a/frontend/src/components/RotationView.tsx +++ /dev/null @@ -1,590 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { formatDateShort } { formatNumber; } from '../utils/dateUtils' - -// ─── Types ──────────────────────────────────────────────────────────────────── - -interface JA4Rotator { - ip: string; - distinct_ja4_count: number; - total_hits: number; - evasion_score: number; -} - -interface PersistentThreat { - ip: string; - recurrence: number; - worst_score: number; - worst_threat_level: string; - first_seen: string; - last_seen: string; - persistence_score: number; -} - -interface JA4HistoryEntry { - ja4: string; - hits: number; - window_start: string; -} - -interface SophisticationItem { - ip: string; - ja4_rotation_count: number; - recurrence: number; - bruteforce_hits: number; - sophistication_score: number; - tier: string; -} - -interface ProactiveHuntItem { - ip: string; - recurrence: number; - worst_score: number; - worst_threat_level: string; - first_seen: string; - last_seen: string; - days_active: number; - risk_assessment: string; -} - -type ActiveTab = 'rotators' | 'persistent' | 'sophistication' | 'hunt'; - -// ─── Helpers ────────────────────────────────────────────────────────────────── - - -function formatDate(iso: string): string { - if (!iso) return '—'; - try { - return formatDateShort(iso); - } catch { - return iso; - } -} - -function threatLevelBadge(level: string): { bg: string; text: string } { - switch (level?.toLowerCase()) { - case 'critical': return { bg: 'bg-threat-critical/20', text: 'text-threat-critical' }; - case 'high': return { bg: 'bg-threat-high/20', text: 'text-threat-high' }; - case 'medium': return { bg: 'bg-threat-medium/20', text: 'text-threat-medium' }; - case 'low': return { bg: 'bg-threat-low/20', text: 'text-threat-low' }; - default: return { bg: 'bg-background-card', text: 'text-text-secondary' }; - } -} - -function tierBadge(tier: string): { bg: string; text: string } { - switch (tier) { - case 'APT-like': return { bg: 'bg-threat-critical/20', text: 'text-threat-critical' }; - case 'Advanced': return { bg: 'bg-threat-high/20', text: 'text-threat-high' }; - case 'Automated': return { bg: 'bg-threat-medium/20', text: 'text-threat-medium' }; - default: return { bg: 'bg-background-card', text: 'text-text-secondary' }; - } -} - -// ─── Sub-components ─────────────────────────────────────────────────────────── - -function StatCard({ label, value, accent }: { label: string; value: string | number; accent?: string }) { - return ( -
- {label} - {value} -
- ); -} - -function LoadingSpinner() { - return ( -
-
-
- ); -} - -function ErrorMessage({ message }: { message: string }) { - return ( -
- ⚠️ {message} -
- ); -} - -// ─── Rotator row with expandable JA4 history ───────────────────────────────── - -function RotatorRow({ item }: { item: JA4Rotator }) { - const [expanded, setExpanded] = useState(false); - const [history, setHistory] = useState([]); - const [historyLoading, setHistoryLoading] = useState(false); - const [historyError, setHistoryError] = useState(null); - const [historyLoaded, setHistoryLoaded] = useState(false); - - const toggle = async () => { - setExpanded((prev) => !prev); - if (!historyLoaded && !expanded) { - setHistoryLoading(true); - try { - const res = await fetch(`/api/rotation/ip/${encodeURIComponent(item.ip)}/ja4-history`); - if (!res.ok) throw new Error('Erreur chargement historique JA4'); - const data: { ja4_history: JA4HistoryEntry[] } = await res.json(); - setHistory(data.ja4_history ?? []); - setHistoryLoaded(true); - } catch (err) { - setHistoryError(err instanceof Error ? err.message : 'Erreur inconnue'); - } finally { - setHistoryLoading(false); - } - } - }; - - const isHighRotation = item.distinct_ja4_count > 5; - - return ( - <> - - - {expanded ? '▾' : '▸'} - {item.ip} - - - - {item.distinct_ja4_count} JA4 - - - {formatNumber(item.total_hits)} - -
-
-
-
- {Math.round(item.evasion_score)} -
- - - {expanded && ( - - - {historyLoading ? ( -
-
- Chargement de l'historique… -
- ) : historyError ? ( - ⚠️ {historyError} - ) : history.length === 0 ? ( - Aucun historique disponible. - ) : ( -
-

Historique des JA4 utilisés :

- {history.map((entry, idx) => ( -
- - {entry.ja4} - - {formatNumber(entry.hits)} hits - {formatDate(entry.window_start)} -
- ))} -
- )} - - - )} - - ); -} - -// ─── Main Component ─────────────────────────────────────────────────────────── - -export function RotationView() { - const navigate = useNavigate(); - - const [activeTab, setActiveTab] = useState('rotators'); - - const [rotators, setRotators] = useState([]); - const [rotatorsLoading, setRotatorsLoading] = useState(true); - const [rotatorsError, setRotatorsError] = useState(null); - - const [persistent, setPersistent] = useState([]); - const [persistentLoading, setPersistentLoading] = useState(false); - const [persistentError, setPersistentError] = useState(null); - const [persistentLoaded, setPersistentLoaded] = useState(false); - - const [sophistication, setSophistication] = useState([]); - const [sophisticationLoading, setSophisticationLoading] = useState(false); - const [sophisticationError, setSophisticationError] = useState(null); - const [sophisticationLoaded, setSophisticationLoaded] = useState(false); - - const [proactive, setProactive] = useState([]); - const [proactiveLoading, setProactiveLoading] = useState(false); - const [proactiveError, setProactiveError] = useState(null); - const [proactiveLoaded, setProactiveLoaded] = useState(false); - - useEffect(() => { - const fetchRotators = async () => { - setRotatorsLoading(true); - try { - const res = await fetch('/api/rotation/ja4-rotators?limit=50'); - if (!res.ok) throw new Error('Erreur chargement des rotateurs'); - const data: { items: JA4Rotator[] } = await res.json(); - setRotators(data.items ?? []); - } catch (err) { - setRotatorsError(err instanceof Error ? err.message : 'Erreur inconnue'); - } finally { - setRotatorsLoading(false); - } - }; - fetchRotators(); - }, []); - - const loadPersistent = async () => { - if (persistentLoaded) return; - setPersistentLoading(true); - try { - const res = await fetch('/api/rotation/persistent-threats?limit=100'); - if (!res.ok) throw new Error('Erreur chargement des menaces persistantes'); - const data: { items: PersistentThreat[] } = await res.json(); - const sorted = [...(data.items ?? [])].sort((a, b) => b.persistence_score - a.persistence_score); - setPersistent(sorted); - setPersistentLoaded(true); - } catch (err) { - setPersistentError(err instanceof Error ? err.message : 'Erreur inconnue'); - } finally { - setPersistentLoading(false); - } - }; - - const loadSophistication = async () => { - if (sophisticationLoaded) return; - setSophisticationLoading(true); - try { - const res = await fetch('/api/rotation/sophistication?limit=50'); - if (!res.ok) throw new Error('Erreur chargement sophistication'); - const data: { items: SophisticationItem[] } = await res.json(); - setSophistication(data.items ?? []); - setSophisticationLoaded(true); - } catch (err) { - setSophisticationError(err instanceof Error ? err.message : 'Erreur inconnue'); - } finally { - setSophisticationLoading(false); - } - }; - - const loadProactive = async () => { - if (proactiveLoaded) return; - setProactiveLoading(true); - try { - const res = await fetch('/api/rotation/proactive-hunt?min_recurrence=1&min_days=0&limit=50'); - if (!res.ok) throw new Error('Erreur chargement chasse proactive'); - const data: { items: ProactiveHuntItem[] } = await res.json(); - setProactive(data.items ?? []); - setProactiveLoaded(true); - } catch (err) { - setProactiveError(err instanceof Error ? err.message : 'Erreur inconnue'); - } finally { - setProactiveLoading(false); - } - }; - - const handleTabChange = (tab: ActiveTab) => { - setActiveTab(tab); - if (tab === 'persistent') loadPersistent(); - if (tab === 'sophistication') loadSophistication(); - if (tab === 'hunt') loadProactive(); - }; - - const maxEvasion = rotators.length > 0 ? Math.max(...rotators.map((r) => r.evasion_score)) : 0; - const maxPersistence = persistent.length > 0 ? Math.max(...persistent.map((p) => p.persistence_score)) : 0; - - const tabs: { id: ActiveTab; label: string }[] = [ - { id: 'rotators', label: '🎭 Rotateurs JA4' }, - { id: 'persistent', label: '🕰️ Menaces Persistantes' }, - { id: 'sophistication', label: '🏆 Sophistication' }, - { id: 'hunt', label: '🕵️ Chasse proactive' }, - ]; - - return ( -
- {/* Header */} -
-

🔄 Rotation JA4 & Persistance

-

- Détection des IPs qui changent de fingerprint pour contourner les détections, et des menaces persistantes. -

-
- - {/* Stat cards */} -
- - - - -
- - {/* Tabs */} -
- {tabs.map((tab) => ( - - ))} -
- - {/* Rotateurs tab */} - {activeTab === 'rotators' && ( -
- {rotatorsLoading ? ( - - ) : rotatorsError ? ( -
- ) : ( - - - - - - - - - - - {rotators.map((item) => ( - - ))} - -
IPJA4 distinctsTotal hitsScore d'évasion
- )} -
- )} - - {/* Persistantes tab */} - {activeTab === 'persistent' && ( -
- {persistentLoading ? ( - - ) : persistentError ? ( -
- ) : ( - - - - - - - - - - - - - - - {persistent.map((item) => { - const badge = threatLevelBadge(item.worst_threat_level); - return ( - - - - - - - - - - - ); - })} - -
IPRécurrenceScore menaceNiveauPremière vueDernière vueScore persistance
{item.ip} - - {item.recurrence}j - - {Math.round(item.worst_score)} - - {item.worst_threat_level?.toUpperCase() || '—'} - - {formatDate(item.first_seen)}{formatDate(item.last_seen)} -
-
-
-
- {Math.round(item.persistence_score)} -
-
- -
- )} -
- )} - - {/* Sophistication tab */} - {activeTab === 'sophistication' && ( -
- {sophisticationLoading ? ( - - ) : sophisticationError ? ( -
- ) : ( - <> -
- Score de sophistication = rotation JA4 × 10 + récurrence × 20 + log(bruteforce+1) × 5 -
- - - - - - - - - - - - - - {sophistication.map((item) => { - const tb = tierBadge(item.tier); - return ( - - - - - - - - - - ); - })} - -
IPRotation JA4RécurrenceHits bruteforceScore sophisticationTier
{item.ip} - - {item.ja4_rotation_count} JA4 - - {item.recurrence}{formatNumber(item.bruteforce_hits)} -
-
-
-
- - {item.sophistication_score} - -
-
- - {item.tier} - - - -
- {sophistication.length === 0 && ( -
Aucune donnée de sophistication disponible.
- )} - - )} -
- )} - - {/* Chasse proactive tab */} - {activeTab === 'hunt' && ( -
- {proactiveLoading ? ( - - ) : proactiveError ? ( -
- ) : ( - <> -
- IPs récurrentes volant sous le radar (score < 0.5) — persistantes mais non détectées comme critiques. -
- - - - - - - - - - - - - - {proactive.map((item) => ( - - - - - - - - - - ))} - -
IPRécurrenceScore maxJours actifsTimelineÉvaluation
{item.ip} - - {item.recurrence}× - - - {item.worst_score.toFixed(3)} - {item.days_active}j -
-
Premier: {formatDate(item.first_seen)}
-
Dernier: {formatDate(item.last_seen)}
-
-
- - {item.risk_assessment} - - - -
- {proactive.length === 0 && ( -
Aucune IP sous le radar détectée avec ces critères.
- )} - - )} -
- )} -
- ); -} diff --git a/frontend/src/components/SearchModal.tsx b/frontend/src/components/SearchModal.tsx deleted file mode 100644 index 51289d1..0000000 --- a/frontend/src/components/SearchModal.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { useState, useEffect, useRef, useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; - -interface SearchResult { - type: 'ip' | 'ja4' | 'host' | 'asn'; - value: string; - label: string; - meta: string; - url: string; - investigation_url?: string; -} - -const TYPE_ICON: Record = { - ip: '🌐', - ja4: '🔏', - host: '🖥️', - asn: '🏢', -}; - -const TYPE_LABEL: Record = { - ip: 'IP', - ja4: 'JA4', - host: 'Host', - asn: 'ASN', -}; - -interface SearchModalProps { - open: boolean; - onClose: () => void; -} - -export default function SearchModal({ open, onClose }: SearchModalProps) { - const navigate = useNavigate(); - const [query, setQuery] = useState(''); - const [results, setResults] = useState([]); - const [loading, setLoading] = useState(false); - const [selected, setSelected] = useState(0); - const inputRef = useRef(null); - const debounce = useRef | null>(null); - - // Focus input when modal opens - useEffect(() => { - if (open) { - setQuery(''); - setResults([]); - setSelected(0); - setTimeout(() => inputRef.current?.focus(), 50); - } - }, [open]); - - const search = useCallback(async (q: string) => { - if (q.length < 2) { setResults([]); return; } - setLoading(true); - try { - const res = await fetch(`/api/search/quick?q=${encodeURIComponent(q)}`); - if (!res.ok) return; - const data = await res.json(); - setResults(data.results || []); - setSelected(0); - } catch { - // ignore - } finally { - setLoading(false); - } - }, []); - - const handleChange = (e: React.ChangeEvent) => { - const val = e.target.value; - setQuery(val); - if (debounce.current) clearTimeout(debounce.current); - debounce.current = setTimeout(() => search(val), 200); - }; - - const go = (result: SearchResult, useInvestigation = false) => { - const url = (useInvestigation && result.investigation_url) ? result.investigation_url : result.url; - navigate(url); - onClose(); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { onClose(); return; } - if (e.key === 'ArrowDown') { e.preventDefault(); setSelected(s => Math.min(s + 1, results.length - 1)); } - if (e.key === 'ArrowUp') { e.preventDefault(); setSelected(s => Math.max(s - 1, 0)); } - if (e.key === 'Enter' && results[selected]) { - go(results[selected], e.metaKey || e.ctrlKey); - } - }; - - if (!open) return null; - - return ( -
- {/* Backdrop */} -
- - {/* Modal */} -
e.stopPropagation()} - onKeyDown={handleKeyDown} - > - {/* Input */} -
- 🔍 - - {loading && ( - - )} - - Esc - -
- - {/* Results */} - {results.length > 0 && ( -
    - {results.map((r, i) => ( -
  • - - )} - -
  • - ))} -
- )} - - {/* Empty state */} - {query.length >= 2 && !loading && results.length === 0 && ( -
- Aucun résultat pour "{query}" -
- )} - - {/* Footer hints */} -
- ↑↓ naviguer - ouvrir - ⌘↵ investigation - Recherche sur les 24 dernières heures -
-
-
- ); -} diff --git a/frontend/src/components/SubnetInvestigation.tsx b/frontend/src/components/SubnetInvestigation.tsx index ba79601..30514a6 100644 --- a/frontend/src/components/SubnetInvestigation.tsx +++ b/frontend/src/components/SubnetInvestigation.tsx @@ -3,6 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'; import { InfoTip } from './ui/Tooltip'; import { TIPS } from './ui/tooltips'; import { formatDateShort } from '../utils/dateUtils'; +import { getCountryFlag } from '../utils/countryUtils'; interface SubnetIP { ip: string; @@ -62,9 +63,7 @@ export function SubnetInvestigation() { fetchSubnet(); }, [formattedSubnet]); - const getCountryFlag = (code: string) => { - return code.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)); - }; + ; const getThreatLevelColor = (level: string) => { switch (level) { @@ -76,11 +75,6 @@ export function SubnetInvestigation() { } }; - const formatDate = (dateString: string) => { - if (!dateString) return '-'; - return formatDateShort(dateString); - }; - if (loading) { return (
@@ -164,7 +158,7 @@ export function SubnetInvestigation() {
Période
- {formatDate(stats.first_seen)} – {formatDate(stats.last_seen)} + {formatDateShort(stats.first_seen)} – {formatDateShort(stats.last_seen)}
diff --git a/frontend/src/components/TcpSpoofingView.tsx b/frontend/src/components/TcpSpoofingView.tsx index b324669..8199ed7 100644 --- a/frontend/src/components/TcpSpoofingView.tsx +++ b/frontend/src/components/TcpSpoofingView.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import DataTable, { Column } from './ui/DataTable'; import { TIPS } from './ui/tooltips'; import { formatNumber } from '../utils/dateUtils'; +import { LoadingSpinner, ErrorMessage } from './ui/Feedback'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -109,15 +110,7 @@ function StatCard({ label, value, accent }: { label: string; value: string | num ); } -function LoadingSpinner() { - return ( -
-
-
- ); -} - -function ErrorMessage({ message }: { message: string }) { +: { message: string }) { return (
⚠️ {message} diff --git a/frontend/src/components/analysis/CorrelationSummary.tsx b/frontend/src/components/analysis/CorrelationSummary.tsx index ca1074b..ac0a86c 100644 --- a/frontend/src/components/analysis/CorrelationSummary.tsx +++ b/frontend/src/components/analysis/CorrelationSummary.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { PREDEFINED_TAGS } from '../../utils/classifications'; interface CorrelationIndicators { subnet_ips_count: number; @@ -22,26 +23,6 @@ interface CorrelationSummaryProps { onClassify?: (label: string, tags: string[], comment: string, confidence: number) => void; } -const PREDEFINED_TAGS = [ - 'scraping', - 'bot-network', - 'scanner', - 'bruteforce', - 'data-exfil', - 'ddos', - 'spam', - 'proxy', - 'tor', - 'vpn', - 'hosting-asn', - 'distributed', - 'ja4-rotation', - 'ua-rotation', - 'country-cn', - 'country-us', - 'country-ru', -]; - export function CorrelationSummary({ ip, onClassify }: CorrelationSummaryProps) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); diff --git a/frontend/src/components/analysis/JA4CorrelationSummary.tsx b/frontend/src/components/analysis/JA4CorrelationSummary.tsx index 5b7fdca..349c678 100644 --- a/frontend/src/components/analysis/JA4CorrelationSummary.tsx +++ b/frontend/src/components/analysis/JA4CorrelationSummary.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { PREDEFINED_TAGS_JA4 } from '../../utils/classifications'; interface CorrelationIndicators { subnet_ips_count: number; @@ -22,29 +23,6 @@ interface JA4CorrelationSummaryProps { onClassify?: (label: string, tags: string[], comment: string, confidence: number) => void; } -const PREDEFINED_TAGS = [ - 'scraping', - 'bot-network', - 'scanner', - 'bruteforce', - 'data-exfil', - 'ddos', - 'spam', - 'proxy', - 'tor', - 'vpn', - 'hosting-asn', - 'distributed', - 'ja4-rotation', - 'ua-rotation', - 'country-cn', - 'country-us', - 'country-ru', - 'known-bot', - 'crawler', - 'search-engine', -]; - export function JA4CorrelationSummary({ ja4, onClassify }: JA4CorrelationSummaryProps) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); @@ -309,7 +287,7 @@ export function JA4CorrelationSummary({ ja4, onClassify }: JA4CorrelationSummary
Tags
- {PREDEFINED_TAGS.map(tag => ( + {PREDEFINED_TAGS_JA4.map(tag => (