Add network analytics and live dashboard features
Introduce new network analytics capabilities with persistent storage, hourly and daily aggregations, and enhanced frontend visualizations. This includes API endpoints for retrieving analytics data, systemd services for automated aggregation, and UI updates for live and historical dashboards. Additionally, country flag emojis are now displayed on the detections page. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 3c14f651-7633-4128-8526-314b4942b3a0 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/oGXAoP7
This commit is contained in:
parent
d997afe410
commit
cbd03d9e64
@ -4,9 +4,11 @@ import { QueryClientProvider } from "@tanstack/react-query";
|
|||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { SidebarProvider, Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarProvider, Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
import { LayoutDashboard, AlertTriangle, Server, Shield, Brain, Menu, Activity } from "lucide-react";
|
import { LayoutDashboard, AlertTriangle, Server, Shield, Brain, Menu, Activity, BarChart3, TrendingUp } from "lucide-react";
|
||||||
import Dashboard from "@/pages/Dashboard";
|
import Dashboard from "@/pages/Dashboard";
|
||||||
import Detections from "@/pages/Detections";
|
import Detections from "@/pages/Detections";
|
||||||
|
import DashboardLive from "@/pages/DashboardLive";
|
||||||
|
import AnalyticsHistory from "@/pages/AnalyticsHistory";
|
||||||
import Routers from "@/pages/Routers";
|
import Routers from "@/pages/Routers";
|
||||||
import Whitelist from "@/pages/Whitelist";
|
import Whitelist from "@/pages/Whitelist";
|
||||||
import Training from "@/pages/Training";
|
import Training from "@/pages/Training";
|
||||||
@ -16,10 +18,12 @@ import NotFound from "@/pages/not-found";
|
|||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ title: "Dashboard", url: "/", icon: LayoutDashboard },
|
{ title: "Dashboard", url: "/", icon: LayoutDashboard },
|
||||||
{ title: "Rilevamenti", url: "/detections", icon: AlertTriangle },
|
{ title: "Rilevamenti", url: "/detections", icon: AlertTriangle },
|
||||||
|
{ title: "Dashboard Live", url: "/dashboard-live", icon: Activity },
|
||||||
|
{ title: "Analytics Storici", url: "/analytics", icon: BarChart3 },
|
||||||
{ title: "Training ML", url: "/training", icon: Brain },
|
{ title: "Training ML", url: "/training", icon: Brain },
|
||||||
{ title: "Router", url: "/routers", icon: Server },
|
{ title: "Router", url: "/routers", icon: Server },
|
||||||
{ title: "Whitelist", url: "/whitelist", icon: Shield },
|
{ title: "Whitelist", url: "/whitelist", icon: Shield },
|
||||||
{ title: "Servizi", url: "/services", icon: Activity },
|
{ title: "Servizi", url: "/services", icon: TrendingUp },
|
||||||
];
|
];
|
||||||
|
|
||||||
function AppSidebar() {
|
function AppSidebar() {
|
||||||
@ -53,6 +57,8 @@ function Router() {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" component={Dashboard} />
|
<Route path="/" component={Dashboard} />
|
||||||
<Route path="/detections" component={Detections} />
|
<Route path="/detections" component={Detections} />
|
||||||
|
<Route path="/dashboard-live" component={DashboardLive} />
|
||||||
|
<Route path="/analytics" component={AnalyticsHistory} />
|
||||||
<Route path="/training" component={Training} />
|
<Route path="/training" component={Training} />
|
||||||
<Route path="/routers" component={Routers} />
|
<Route path="/routers" component={Routers} />
|
||||||
<Route path="/whitelist" component={Whitelist} />
|
<Route path="/whitelist" component={Whitelist} />
|
||||||
|
|||||||
62
client/src/lib/country-flags.ts
Normal file
62
client/src/lib/country-flags.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Country Flags Utilities
|
||||||
|
* Converte country code in flag emoji
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converte country code ISO 3166-1 alpha-2 in flag emoji
|
||||||
|
* Es: "IT" => "🇮🇹", "US" => "🇺🇸"
|
||||||
|
*/
|
||||||
|
export function getFlagEmoji(countryCode: string | null | undefined): string {
|
||||||
|
if (!countryCode || countryCode.length !== 2) {
|
||||||
|
return '🏳️'; // Flag bianca per unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
const codePoints = countryCode
|
||||||
|
.toUpperCase()
|
||||||
|
.split('')
|
||||||
|
.map(char => 127397 + char.charCodeAt(0));
|
||||||
|
|
||||||
|
return String.fromCodePoint(...codePoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mappa nomi paesi comuni (fallback se API non ritorna country code)
|
||||||
|
*/
|
||||||
|
export const COUNTRY_CODE_MAP: Record<string, string> = {
|
||||||
|
'Italy': 'IT',
|
||||||
|
'United States': 'US',
|
||||||
|
'Russia': 'RU',
|
||||||
|
'China': 'CN',
|
||||||
|
'Germany': 'DE',
|
||||||
|
'France': 'FR',
|
||||||
|
'United Kingdom': 'GB',
|
||||||
|
'Spain': 'ES',
|
||||||
|
'Brazil': 'BR',
|
||||||
|
'Japan': 'JP',
|
||||||
|
'India': 'IN',
|
||||||
|
'Canada': 'CA',
|
||||||
|
'Australia': 'AU',
|
||||||
|
'Netherlands': 'NL',
|
||||||
|
'Switzerland': 'CH',
|
||||||
|
'Sweden': 'SE',
|
||||||
|
'Poland': 'PL',
|
||||||
|
'Ukraine': 'UA',
|
||||||
|
'Romania': 'RO',
|
||||||
|
'Belgium': 'BE',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ottieni flag da nome paese o country code
|
||||||
|
*/
|
||||||
|
export function getFlag(country: string | null | undefined, countryCode?: string | null): string {
|
||||||
|
if (countryCode) {
|
||||||
|
return getFlagEmoji(countryCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (country && COUNTRY_CODE_MAP[country]) {
|
||||||
|
return getFlagEmoji(COUNTRY_CODE_MAP[country]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '🏳️';
|
||||||
|
}
|
||||||
320
client/src/pages/AnalyticsHistory.tsx
Normal file
320
client/src/pages/AnalyticsHistory.tsx
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
LineChart, Line, BarChart, Bar, AreaChart, Area,
|
||||||
|
XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer
|
||||||
|
} from "recharts";
|
||||||
|
import { Calendar, TrendingUp, BarChart3, Globe, Download } from "lucide-react";
|
||||||
|
import type { NetworkAnalytics } from "@shared/schema";
|
||||||
|
import { format, parseISO } from "date-fns";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function AnalyticsHistory() {
|
||||||
|
const [days, setDays] = useState(30);
|
||||||
|
|
||||||
|
// Fetch historical analytics (daily aggregations)
|
||||||
|
const { data: analytics = [], isLoading } = useQuery<NetworkAnalytics[]>({
|
||||||
|
queryKey: ["/api/analytics/recent", { days, hourly: false }],
|
||||||
|
refetchInterval: 60000, // Aggiorna ogni minuto
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prepara dati per grafici
|
||||||
|
const trendData = analytics
|
||||||
|
.map(a => {
|
||||||
|
// Parse JSON fields safely
|
||||||
|
let attacksByCountry = {};
|
||||||
|
let attacksByType = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
attacksByCountry = a.attacksByCountry ? JSON.parse(a.attacksByCountry) : {};
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
attacksByType = a.attacksByType ? JSON.parse(a.attacksByType) : {};
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: format(new Date(a.date), "dd/MM"),
|
||||||
|
fullDate: a.date,
|
||||||
|
totalPackets: a.totalPackets || 0,
|
||||||
|
normalPackets: a.normalPackets || 0,
|
||||||
|
attackPackets: a.attackPackets || 0,
|
||||||
|
attackPercentage: a.totalPackets > 0
|
||||||
|
? ((a.attackPackets || 0) / a.totalPackets * 100).toFixed(1)
|
||||||
|
: "0",
|
||||||
|
uniqueIps: a.uniqueIps || 0,
|
||||||
|
attackUniqueIps: a.attackUniqueIps || 0,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => new Date(a.fullDate).getTime() - new Date(b.fullDate).getTime());
|
||||||
|
|
||||||
|
// Aggrega dati per paese (da tutti i giorni)
|
||||||
|
const countryAggregation: Record<string, number> = {};
|
||||||
|
analytics.forEach(a => {
|
||||||
|
if (a.attacksByCountry) {
|
||||||
|
try {
|
||||||
|
const countries = JSON.parse(a.attacksByCountry);
|
||||||
|
if (countries && typeof countries === 'object') {
|
||||||
|
Object.entries(countries).forEach(([country, count]) => {
|
||||||
|
if (typeof count === 'number') {
|
||||||
|
countryAggregation[country] = (countryAggregation[country] || 0) + count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse attacksByCountry:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const topCountries = Object.entries(countryAggregation)
|
||||||
|
.map(([name, attacks]) => ({ name, attacks }))
|
||||||
|
.sort((a, b) => b.attacks - a.attacks)
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
|
// Calcola metriche totali
|
||||||
|
const totalTraffic = analytics.reduce((sum, a) => sum + (a.totalPackets || 0), 0);
|
||||||
|
const totalAttacks = analytics.reduce((sum, a) => sum + (a.attackPackets || 0), 0);
|
||||||
|
const totalNormal = analytics.reduce((sum, a) => sum + (a.normalPackets || 0), 0);
|
||||||
|
const avgAttackRate = totalTraffic > 0 ? ((totalAttacks / totalTraffic) * 100).toFixed(2) : "0";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6 p-6" data-testid="page-analytics-history">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-semibold flex items-center gap-2" data-testid="text-page-title">
|
||||||
|
<BarChart3 className="h-8 w-8" />
|
||||||
|
Analytics Storici
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground" data-testid="text-page-subtitle">
|
||||||
|
Statistiche permanenti per analisi long-term
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Time Range Selector */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant={days === 7 ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setDays(7)}
|
||||||
|
data-testid="button-7days"
|
||||||
|
>
|
||||||
|
7 Giorni
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={days === 30 ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setDays(30)}
|
||||||
|
data-testid="button-30days"
|
||||||
|
>
|
||||||
|
30 Giorni
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={days === 90 ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setDays(90)}
|
||||||
|
data-testid="button-90days"
|
||||||
|
>
|
||||||
|
90 Giorni
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="text-center py-8" data-testid="text-loading">
|
||||||
|
Caricamento dati storici...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && analytics.length === 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-12 text-center text-muted-foreground">
|
||||||
|
<Calendar className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||||
|
<p>Nessun dato storico disponibile</p>
|
||||||
|
<p className="text-sm mt-2">
|
||||||
|
I dati verranno aggregati automaticamente ogni ora dal sistema
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && analytics.length > 0 && (
|
||||||
|
<>
|
||||||
|
{/* Summary KPIs */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card data-testid="card-total-summary">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
Traffico Totale ({days}g)
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold" data-testid="text-total-summary">
|
||||||
|
{totalTraffic.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">pacchetti</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card data-testid="card-normal-summary">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
Traffico Normale
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-green-600" data-testid="text-normal-summary">
|
||||||
|
{totalNormal.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{(100 - parseFloat(avgAttackRate)).toFixed(1)}% del totale
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card data-testid="card-attacks-summary">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
Attacchi Totali
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-red-600" data-testid="text-attacks-summary">
|
||||||
|
{totalAttacks.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{avgAttackRate}% del traffico
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card data-testid="card-avg-daily">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
Media Giornaliera
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold" data-testid="text-avg-daily">
|
||||||
|
{Math.round(totalTraffic / analytics.length).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">pacchetti/giorno</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trend Line Chart */}
|
||||||
|
<Card data-testid="card-trend">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<TrendingUp className="h-5 w-5" />
|
||||||
|
Trend Traffico (Normale + Attacchi)
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<AreaChart data={trendData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="date" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="normalPackets"
|
||||||
|
stackId="1"
|
||||||
|
stroke="#22c55e"
|
||||||
|
fill="#22c55e"
|
||||||
|
name="Normale"
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="attackPackets"
|
||||||
|
stackId="1"
|
||||||
|
stroke="#ef4444"
|
||||||
|
fill="#ef4444"
|
||||||
|
name="Attacchi"
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Attack Rate Trend */}
|
||||||
|
<Card data-testid="card-attack-rate">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Percentuale Attacchi nel Tempo</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<LineChart data={trendData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="date" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="attackPercentage"
|
||||||
|
stroke="#ef4444"
|
||||||
|
name="% Attacchi"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Top Countries (Historical) */}
|
||||||
|
<Card data-testid="card-top-countries">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Globe className="h-5 w-5" />
|
||||||
|
Top 10 Paesi Attaccanti (Storico)
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{topCountries.length > 0 ? (
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<BarChart data={topCountries} layout="vertical">
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis type="number" />
|
||||||
|
<YAxis dataKey="name" type="category" width={100} />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="attacks" fill="#ef4444" name="Attacchi Totali" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-20 text-muted-foreground">
|
||||||
|
Nessun dato disponibile
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Export Button (Placeholder) */}
|
||||||
|
<Card data-testid="card-export">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">Export Report</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Esporta i dati in formato CSV per analisi esterne
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" data-testid="button-export">
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Esporta CSV
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
308
client/src/pages/DashboardLive.tsx
Normal file
308
client/src/pages/DashboardLive.tsx
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Activity, Globe, Shield, TrendingUp, AlertTriangle } from "lucide-react";
|
||||||
|
import { AreaChart, Area, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
|
||||||
|
import type { Detection, NetworkLog } from "@shared/schema";
|
||||||
|
import { getFlag } from "@/lib/country-flags";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
|
export default function DashboardLive() {
|
||||||
|
// Dati ultimi 3 giorni
|
||||||
|
const { data: detections = [], isLoading: loadingDetections } = useQuery<Detection[]>({
|
||||||
|
queryKey: ["/api/detections"],
|
||||||
|
refetchInterval: 10000, // Aggiorna ogni 10s
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: logs = [], isLoading: loadingLogs } = useQuery<NetworkLog[]>({
|
||||||
|
queryKey: ["/api/logs", { limit: 5000 }],
|
||||||
|
refetchInterval: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLoading = loadingDetections || loadingLogs;
|
||||||
|
|
||||||
|
// Calcola metriche
|
||||||
|
const totalTraffic = logs.length;
|
||||||
|
const totalAttacks = detections.length;
|
||||||
|
const attackPercentage = totalTraffic > 0 ? ((totalAttacks / totalTraffic) * 100).toFixed(2) : "0";
|
||||||
|
|
||||||
|
const normalTraffic = totalTraffic - totalAttacks;
|
||||||
|
const blockedAttacks = detections.filter(d => d.blocked).length;
|
||||||
|
|
||||||
|
// Aggrega traffico per paese
|
||||||
|
const trafficByCountry: Record<string, { normal: number; attacks: number; flag: string }> = {};
|
||||||
|
|
||||||
|
detections.forEach(det => {
|
||||||
|
const country = det.country || "Unknown";
|
||||||
|
if (!trafficByCountry[country]) {
|
||||||
|
trafficByCountry[country] = {
|
||||||
|
normal: 0,
|
||||||
|
attacks: 0,
|
||||||
|
flag: getFlag(det.country, det.countryCode)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
trafficByCountry[country].attacks++;
|
||||||
|
});
|
||||||
|
|
||||||
|
const countryChartData = Object.entries(trafficByCountry)
|
||||||
|
.map(([name, data]) => ({
|
||||||
|
name: `${data.flag} ${name}`,
|
||||||
|
attacks: data.attacks,
|
||||||
|
normal: data.normal,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.attacks - a.attacks)
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
|
// Aggrega attacchi per tipo
|
||||||
|
const attacksByType: Record<string, number> = {};
|
||||||
|
detections.forEach(det => {
|
||||||
|
attacksByType[det.anomalyType] = (attacksByType[det.anomalyType] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeChartData = Object.entries(attacksByType).map(([name, value]) => ({
|
||||||
|
name: name.replace('_', ' ').toUpperCase(),
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Traffico normale vs attacchi (gauge data)
|
||||||
|
const trafficDistribution = [
|
||||||
|
{ name: 'Normal', value: normalTraffic, color: '#22c55e' },
|
||||||
|
{ name: 'Attacks', value: totalAttacks, color: '#ef4444' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Ultimi eventi (stream)
|
||||||
|
const recentEvents = [...detections]
|
||||||
|
.sort((a, b) => new Date(b.detectedAt).getTime() - new Date(a.detectedAt).getTime())
|
||||||
|
.slice(0, 20);
|
||||||
|
|
||||||
|
const COLORS = ['#ef4444', '#f97316', '#f59e0b', '#eab308', '#84cc16'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6 p-6" data-testid="page-dashboard-live">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-semibold flex items-center gap-2" data-testid="text-page-title">
|
||||||
|
<Activity className="h-8 w-8" />
|
||||||
|
Dashboard Live
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground" data-testid="text-page-subtitle">
|
||||||
|
Monitoraggio real-time (ultimi 3 giorni)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="text-center py-8" data-testid="text-loading">
|
||||||
|
Caricamento dati...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && (
|
||||||
|
<>
|
||||||
|
{/* KPI Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card data-testid="card-total-traffic">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
Traffico Totale
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold" data-testid="text-total-traffic">
|
||||||
|
{totalTraffic.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">pacchetti</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card data-testid="card-normal-traffic">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
Traffico Normale
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-green-600" data-testid="text-normal-traffic">
|
||||||
|
{normalTraffic.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{(100 - parseFloat(attackPercentage)).toFixed(1)}% del totale
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card data-testid="card-attacks">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
Attacchi Rilevati
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-red-600" data-testid="text-attacks">
|
||||||
|
{totalAttacks}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{attackPercentage}% del traffico
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card data-testid="card-blocked">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
IP Bloccati
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-orange-600" data-testid="text-blocked">
|
||||||
|
{blockedAttacks}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{totalAttacks > 0 ? ((blockedAttacks / totalAttacks) * 100).toFixed(1) : 0}% degli attacchi
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Charts Row 1 */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Traffic Distribution (Pie) */}
|
||||||
|
<Card data-testid="card-distribution">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<TrendingUp className="h-5 w-5" />
|
||||||
|
Distribuzione Traffico
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={trafficDistribution}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
labelLine={false}
|
||||||
|
label={(entry) => `${entry.name}: ${entry.value}`}
|
||||||
|
outerRadius={100}
|
||||||
|
fill="#8884d8"
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{trafficDistribution.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Attacks by Type (Pie) */}
|
||||||
|
<Card data-testid="card-attack-types">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5" />
|
||||||
|
Tipi di Attacco
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{typeChartData.length > 0 ? (
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={typeChartData}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
labelLine={false}
|
||||||
|
label={(entry) => `${entry.name}: ${entry.value}`}
|
||||||
|
outerRadius={100}
|
||||||
|
fill="#8884d8"
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{typeChartData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-20 text-muted-foreground">
|
||||||
|
Nessun attacco rilevato
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top Countries (Bar Chart) */}
|
||||||
|
<Card data-testid="card-countries">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Globe className="h-5 w-5" />
|
||||||
|
Top 10 Paesi Attaccanti
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{countryChartData.length > 0 ? (
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<BarChart data={countryChartData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="attacks" fill="#ef4444" name="Attacchi" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-20 text-muted-foreground">
|
||||||
|
Nessun dato disponibile
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Real-time Event Stream */}
|
||||||
|
<Card data-testid="card-event-stream">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Shield className="h-5 w-5" />
|
||||||
|
Stream Eventi Recenti
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||||
|
{recentEvents.map(event => (
|
||||||
|
<div
|
||||||
|
key={event.id}
|
||||||
|
className="flex items-center justify-between p-3 rounded-lg border hover-elevate"
|
||||||
|
data-testid={`event-${event.id}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{event.countryCode && (
|
||||||
|
<span className="text-xl">
|
||||||
|
{getFlag(event.country, event.countryCode)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<code className="font-mono font-semibold">{event.sourceIp}</code>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{event.anomalyType.replace('_', ' ')} • {format(new Date(event.detectedAt), "HH:mm:ss")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant={event.blocked ? "destructive" : "secondary"}>
|
||||||
|
{event.blocked ? "Bloccato" : "Attivo"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import { AlertTriangle, Search, Shield, Eye, Globe, MapPin, Building2 } from "lu
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { Detection } from "@shared/schema";
|
import type { Detection } from "@shared/schema";
|
||||||
|
import { getFlag } from "@/lib/country-flags";
|
||||||
|
|
||||||
export default function Detections() {
|
export default function Detections() {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
@ -93,7 +94,14 @@ export default function Detections() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
<div className="flex items-center gap-3 mb-2 flex-wrap">
|
||||||
|
{/* Flag Emoji */}
|
||||||
|
{detection.countryCode && (
|
||||||
|
<span className="text-2xl" title={detection.country || detection.countryCode} data-testid={`flag-${detection.id}`}>
|
||||||
|
{getFlag(detection.country, detection.countryCode)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<code className="font-mono font-semibold text-lg" data-testid={`text-ip-${detection.id}`}>
|
<code className="font-mono font-semibold text-lg" data-testid={`text-ip-${detection.id}`}>
|
||||||
{detection.sourceIp}
|
{detection.sourceIp}
|
||||||
</code>
|
</code>
|
||||||
|
|||||||
48
database-schema/migrations/005_create_network_analytics.sql
Normal file
48
database-schema/migrations/005_create_network_analytics.sql
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
-- Migration 005: Create network_analytics table for permanent traffic statistics
|
||||||
|
-- This table stores aggregated traffic data (normal + attacks) with hourly and daily granularity
|
||||||
|
-- Data persists beyond the 3-day log retention for long-term analytics
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS network_analytics (
|
||||||
|
id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
date TIMESTAMP NOT NULL,
|
||||||
|
hour INT, -- NULL = daily aggregation, 0-23 = hourly
|
||||||
|
|
||||||
|
-- Total traffic metrics
|
||||||
|
total_packets INT NOT NULL DEFAULT 0,
|
||||||
|
total_bytes BIGINT NOT NULL DEFAULT 0,
|
||||||
|
unique_ips INT NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Normal traffic (non-anomalous)
|
||||||
|
normal_packets INT NOT NULL DEFAULT 0,
|
||||||
|
normal_bytes BIGINT NOT NULL DEFAULT 0,
|
||||||
|
normal_unique_ips INT NOT NULL DEFAULT 0,
|
||||||
|
top_normal_ips TEXT, -- JSON: [{ip, packets, bytes, country}]
|
||||||
|
|
||||||
|
-- Attack/Anomaly traffic
|
||||||
|
attack_packets INT NOT NULL DEFAULT 0,
|
||||||
|
attack_bytes BIGINT NOT NULL DEFAULT 0,
|
||||||
|
attack_unique_ips INT NOT NULL DEFAULT 0,
|
||||||
|
attacks_by_country TEXT, -- JSON: {IT: 5, RU: 30, ...}
|
||||||
|
attacks_by_type TEXT, -- JSON: {ddos: 10, port_scan: 5, ...}
|
||||||
|
top_attackers TEXT, -- JSON: [{ip, country, risk_score, packets}]
|
||||||
|
|
||||||
|
-- Geographic distribution (all traffic)
|
||||||
|
traffic_by_country TEXT, -- JSON: {IT: {normal: 100, attacks: 5}, ...}
|
||||||
|
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Ensure unique aggregation per date/hour
|
||||||
|
UNIQUE(date, hour)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for fast queries
|
||||||
|
CREATE INDEX IF NOT EXISTS network_analytics_date_hour_idx ON network_analytics(date, hour);
|
||||||
|
CREATE INDEX IF NOT EXISTS network_analytics_date_idx ON network_analytics(date);
|
||||||
|
|
||||||
|
-- Update schema version
|
||||||
|
INSERT INTO schema_version (version, description)
|
||||||
|
VALUES (5, 'Create network_analytics table for traffic statistics')
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
version = 5,
|
||||||
|
description = 'Create network_analytics table for traffic statistics',
|
||||||
|
applied_at = NOW();
|
||||||
21
deployment/ids-analytics-aggregator.service
Normal file
21
deployment/ids-analytics-aggregator.service
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=IDS Analytics Aggregator - Hourly Traffic Statistics
|
||||||
|
After=network.target postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=ids
|
||||||
|
Group=ids
|
||||||
|
WorkingDirectory=/opt/ids/python_ml
|
||||||
|
EnvironmentFile=-/opt/ids/.env
|
||||||
|
|
||||||
|
# Execute hourly aggregation
|
||||||
|
ExecStart=/opt/ids/venv/bin/python3 /opt/ids/python_ml/analytics_aggregator.py hourly
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=ids-analytics
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
14
deployment/ids-analytics-aggregator.timer
Normal file
14
deployment/ids-analytics-aggregator.timer
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=IDS Analytics Aggregation Timer - Runs every hour
|
||||||
|
Requires=ids-analytics-aggregator.service
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# Run 5 minutes after the hour (e.g., 10:05, 11:05, 12:05)
|
||||||
|
# This gives time for logs to be collected
|
||||||
|
OnCalendar=*:05:00
|
||||||
|
|
||||||
|
# Run immediately if we missed a scheduled run
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
63
deployment/setup_analytics_timer.sh
Executable file
63
deployment/setup_analytics_timer.sh
Executable file
@ -0,0 +1,63 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Setup systemd timer for analytics aggregation
|
||||||
|
# Deve essere eseguito come root
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${BLUE}╔═══════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BLUE}║ IDS Analytics Timer Setup ║${NC}"
|
||||||
|
echo -e "${BLUE}╚═══════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check root
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo -e "${RED}❌ Questo script deve essere eseguito come root${NC}"
|
||||||
|
echo -e "${YELLOW} Usa: sudo $0${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
IDS_DIR="/opt/ids"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Copy systemd files
|
||||||
|
echo -e "${BLUE}📋 Copia file systemd...${NC}"
|
||||||
|
cp "${SCRIPT_DIR}/ids-analytics-aggregator.service" /etc/systemd/system/
|
||||||
|
cp "${SCRIPT_DIR}/ids-analytics-aggregator.timer" /etc/systemd/system/
|
||||||
|
|
||||||
|
# Set permissions
|
||||||
|
chmod 644 /etc/systemd/system/ids-analytics-aggregator.service
|
||||||
|
chmod 644 /etc/systemd/system/ids-analytics-aggregator.timer
|
||||||
|
|
||||||
|
# Reload systemd
|
||||||
|
echo -e "${BLUE}🔄 Reload systemd daemon...${NC}"
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Enable and start timer
|
||||||
|
echo -e "${BLUE}⚙️ Enable e start timer...${NC}"
|
||||||
|
systemctl enable ids-analytics-aggregator.timer
|
||||||
|
systemctl start ids-analytics-aggregator.timer
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
echo -e "\n${BLUE}📊 Stato timer:${NC}"
|
||||||
|
systemctl status ids-analytics-aggregator.timer --no-pager
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}📅 Prossime esecuzioni:${NC}"
|
||||||
|
systemctl list-timers ids-analytics-aggregator.timer --no-pager
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}╔═══════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${GREEN}║ ✅ ANALYTICS TIMER CONFIGURATO ║${NC}"
|
||||||
|
echo -e "${GREEN}╚═══════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}📝 Comandi utili:${NC}"
|
||||||
|
echo -e " ${YELLOW}Stato timer:${NC} sudo systemctl status ids-analytics-aggregator.timer"
|
||||||
|
echo -e " ${YELLOW}Prossime run:${NC} sudo systemctl list-timers"
|
||||||
|
echo -e " ${YELLOW}Log aggregazione:${NC} sudo journalctl -u ids-analytics-aggregator -f"
|
||||||
|
echo -e " ${YELLOW}Test manuale:${NC} sudo systemctl start ids-analytics-aggregator"
|
||||||
|
echo ""
|
||||||
318
python_ml/analytics_aggregator.py
Normal file
318
python_ml/analytics_aggregator.py
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
"""
|
||||||
|
Network Analytics Aggregator
|
||||||
|
Aggrega statistiche traffico (normale + attacchi) ogni ora e giorno
|
||||||
|
Mantiene dati permanenti per analytics long-term
|
||||||
|
"""
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import json
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from collections import defaultdict
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyticsAggregator:
|
||||||
|
"""
|
||||||
|
Aggregatore analytics per traffico normale + attacchi
|
||||||
|
Salva statistiche permanenti in network_analytics
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.db_params = {
|
||||||
|
'host': os.getenv('PGHOST', 'localhost'),
|
||||||
|
'port': int(os.getenv('PGPORT', 5432)),
|
||||||
|
'database': os.getenv('PGDATABASE', 'ids_db'),
|
||||||
|
'user': os.getenv('PGUSER', 'ids'),
|
||||||
|
'password': os.getenv('PGPASSWORD', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_connection(self):
|
||||||
|
"""Crea connessione database"""
|
||||||
|
return psycopg2.connect(**self.db_params)
|
||||||
|
|
||||||
|
def aggregate_hourly(self, target_hour: Optional[datetime] = None):
|
||||||
|
"""
|
||||||
|
Aggrega statistiche per una specifica ora
|
||||||
|
Se target_hour è None, usa l'ora precedente
|
||||||
|
"""
|
||||||
|
if target_hour is None:
|
||||||
|
# Usa ora precedente (es: se ora sono le 10:30, aggrega le 09:00-10:00)
|
||||||
|
now = datetime.now()
|
||||||
|
target_hour = now.replace(minute=0, second=0, microsecond=0) - timedelta(hours=1)
|
||||||
|
|
||||||
|
hour_start = target_hour
|
||||||
|
hour_end = hour_start + timedelta(hours=1)
|
||||||
|
|
||||||
|
print(f"[ANALYTICS] Aggregazione oraria: {hour_start.strftime('%Y-%m-%d %H:00')}")
|
||||||
|
|
||||||
|
conn = self.get_connection()
|
||||||
|
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Analizza network_logs nell'ora target
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
source_ip,
|
||||||
|
COUNT(*) as packets,
|
||||||
|
SUM(packet_length) as bytes
|
||||||
|
FROM network_logs
|
||||||
|
WHERE timestamp >= %s AND timestamp < %s
|
||||||
|
GROUP BY source_ip
|
||||||
|
""", (hour_start, hour_end))
|
||||||
|
|
||||||
|
traffic_by_ip = {row['source_ip']: row for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
if not traffic_by_ip:
|
||||||
|
print(f"[ANALYTICS] Nessun traffico nell'ora {hour_start.strftime('%H:00')}")
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. Identifica IP attaccanti (detections nell'ora)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT DISTINCT source_ip, anomaly_type, risk_score, country
|
||||||
|
FROM detections
|
||||||
|
WHERE detected_at >= %s AND detected_at < %s
|
||||||
|
""", (hour_start, hour_end))
|
||||||
|
|
||||||
|
attacker_ips = {}
|
||||||
|
attacks_by_type = defaultdict(int)
|
||||||
|
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
ip = row['source_ip']
|
||||||
|
attacker_ips[ip] = row
|
||||||
|
attacks_by_type[row['anomaly_type']] += 1
|
||||||
|
|
||||||
|
# 3. Classifica traffico: normale vs attacco
|
||||||
|
total_packets = 0
|
||||||
|
total_bytes = 0
|
||||||
|
normal_packets = 0
|
||||||
|
normal_bytes = 0
|
||||||
|
attack_packets = 0
|
||||||
|
attack_bytes = 0
|
||||||
|
|
||||||
|
traffic_by_country = defaultdict(lambda: {'normal': 0, 'attacks': 0})
|
||||||
|
attacks_by_country = defaultdict(int)
|
||||||
|
|
||||||
|
top_normal = []
|
||||||
|
top_attackers = []
|
||||||
|
|
||||||
|
for ip, stats in traffic_by_ip.items():
|
||||||
|
packets = stats['packets']
|
||||||
|
bytes_count = stats['bytes'] or 0
|
||||||
|
|
||||||
|
total_packets += packets
|
||||||
|
total_bytes += bytes_count
|
||||||
|
|
||||||
|
if ip in attacker_ips:
|
||||||
|
# IP attaccante
|
||||||
|
attack_packets += packets
|
||||||
|
attack_bytes += bytes_count
|
||||||
|
|
||||||
|
country = attacker_ips[ip].get('country')
|
||||||
|
if country:
|
||||||
|
traffic_by_country[country]['attacks'] += packets
|
||||||
|
attacks_by_country[country] += 1
|
||||||
|
|
||||||
|
top_attackers.append({
|
||||||
|
'ip': ip,
|
||||||
|
'country': country,
|
||||||
|
'risk_score': float(attacker_ips[ip]['risk_score']),
|
||||||
|
'packets': packets,
|
||||||
|
'bytes': bytes_count
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# IP normale
|
||||||
|
normal_packets += packets
|
||||||
|
normal_bytes += bytes_count
|
||||||
|
|
||||||
|
# Lookup paese per IP normale (da detections precedenti o geo cache)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT country FROM detections
|
||||||
|
WHERE source_ip = %s AND country IS NOT NULL
|
||||||
|
ORDER BY detected_at DESC LIMIT 1
|
||||||
|
""", (ip,))
|
||||||
|
|
||||||
|
geo_row = cursor.fetchone()
|
||||||
|
country = geo_row['country'] if geo_row else None
|
||||||
|
|
||||||
|
if country:
|
||||||
|
traffic_by_country[country]['normal'] += packets
|
||||||
|
|
||||||
|
top_normal.append({
|
||||||
|
'ip': ip,
|
||||||
|
'packets': packets,
|
||||||
|
'bytes': bytes_count,
|
||||||
|
'country': country
|
||||||
|
})
|
||||||
|
|
||||||
|
# 4. Top 10 per categoria
|
||||||
|
top_normal = sorted(top_normal, key=lambda x: x['packets'], reverse=True)[:10]
|
||||||
|
top_attackers = sorted(top_attackers, key=lambda x: x['risk_score'], reverse=True)[:10]
|
||||||
|
|
||||||
|
# 5. Salva aggregazione
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO network_analytics (
|
||||||
|
date, hour,
|
||||||
|
total_packets, total_bytes, unique_ips,
|
||||||
|
normal_packets, normal_bytes, normal_unique_ips, top_normal_ips,
|
||||||
|
attack_packets, attack_bytes, attack_unique_ips,
|
||||||
|
attacks_by_country, attacks_by_type, top_attackers,
|
||||||
|
traffic_by_country
|
||||||
|
)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
ON CONFLICT (date, hour) DO UPDATE SET
|
||||||
|
total_packets = EXCLUDED.total_packets,
|
||||||
|
total_bytes = EXCLUDED.total_bytes,
|
||||||
|
unique_ips = EXCLUDED.unique_ips,
|
||||||
|
normal_packets = EXCLUDED.normal_packets,
|
||||||
|
normal_bytes = EXCLUDED.normal_bytes,
|
||||||
|
normal_unique_ips = EXCLUDED.normal_unique_ips,
|
||||||
|
top_normal_ips = EXCLUDED.top_normal_ips,
|
||||||
|
attack_packets = EXCLUDED.attack_packets,
|
||||||
|
attack_bytes = EXCLUDED.attack_bytes,
|
||||||
|
attack_unique_ips = EXCLUDED.attack_unique_ips,
|
||||||
|
attacks_by_country = EXCLUDED.attacks_by_country,
|
||||||
|
attacks_by_type = EXCLUDED.attacks_by_type,
|
||||||
|
top_attackers = EXCLUDED.top_attackers,
|
||||||
|
traffic_by_country = EXCLUDED.traffic_by_country
|
||||||
|
""", (
|
||||||
|
target_hour.date(),
|
||||||
|
target_hour.hour,
|
||||||
|
total_packets,
|
||||||
|
total_bytes,
|
||||||
|
len(traffic_by_ip),
|
||||||
|
normal_packets,
|
||||||
|
normal_bytes,
|
||||||
|
len(traffic_by_ip) - len(attacker_ips),
|
||||||
|
json.dumps(top_normal),
|
||||||
|
attack_packets,
|
||||||
|
attack_bytes,
|
||||||
|
len(attacker_ips),
|
||||||
|
json.dumps(dict(attacks_by_country)),
|
||||||
|
json.dumps(dict(attacks_by_type)),
|
||||||
|
json.dumps(top_attackers),
|
||||||
|
json.dumps({k: dict(v) for k, v in traffic_by_country.items()})
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print(f"[ANALYTICS] ✅ Aggregazione completata:")
|
||||||
|
print(f" - Totale: {total_packets} pacchetti, {len(traffic_by_ip)} IP unici")
|
||||||
|
print(f" - Normale: {normal_packets} pacchetti ({normal_packets*100//total_packets if total_packets else 0}%)")
|
||||||
|
print(f" - Attacchi: {attack_packets} pacchetti ({attack_packets*100//total_packets if total_packets else 0}%), {len(attacker_ips)} IP")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ANALYTICS] ❌ Errore aggregazione oraria: {e}")
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def aggregate_daily(self, target_date: Optional[datetime] = None):
|
||||||
|
"""
|
||||||
|
Aggrega statistiche giornaliere (somma delle ore)
|
||||||
|
Se target_date è None, usa giorno precedente
|
||||||
|
"""
|
||||||
|
if target_date is None:
|
||||||
|
target_date = datetime.now().date() - timedelta(days=1)
|
||||||
|
|
||||||
|
print(f"[ANALYTICS] Aggregazione giornaliera: {target_date}")
|
||||||
|
|
||||||
|
conn = self.get_connection()
|
||||||
|
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Somma aggregazioni orarie del giorno
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
SUM(total_packets) as total_packets,
|
||||||
|
SUM(total_bytes) as total_bytes,
|
||||||
|
MAX(unique_ips) as unique_ips,
|
||||||
|
SUM(normal_packets) as normal_packets,
|
||||||
|
SUM(normal_bytes) as normal_bytes,
|
||||||
|
MAX(normal_unique_ips) as normal_unique_ips,
|
||||||
|
SUM(attack_packets) as attack_packets,
|
||||||
|
SUM(attack_bytes) as attack_bytes,
|
||||||
|
MAX(attack_unique_ips) as attack_unique_ips
|
||||||
|
FROM network_analytics
|
||||||
|
WHERE date = %s AND hour IS NOT NULL
|
||||||
|
""", (target_date,))
|
||||||
|
|
||||||
|
daily_stats = cursor.fetchone()
|
||||||
|
|
||||||
|
if not daily_stats or not daily_stats['total_packets']:
|
||||||
|
print(f"[ANALYTICS] Nessun dato per {target_date}")
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Merge JSON fields (countries, types, top IPs)
|
||||||
|
# TODO: Implementare merge intelligente se necessario
|
||||||
|
|
||||||
|
# Salva aggregazione giornaliera (hour = NULL)
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO network_analytics (
|
||||||
|
date, hour,
|
||||||
|
total_packets, total_bytes, unique_ips,
|
||||||
|
normal_packets, normal_bytes, normal_unique_ips,
|
||||||
|
attack_packets, attack_bytes, attack_unique_ips
|
||||||
|
)
|
||||||
|
VALUES (%s, NULL, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
ON CONFLICT (date, hour) DO UPDATE SET
|
||||||
|
total_packets = EXCLUDED.total_packets,
|
||||||
|
total_bytes = EXCLUDED.total_bytes,
|
||||||
|
unique_ips = EXCLUDED.unique_ips,
|
||||||
|
normal_packets = EXCLUDED.normal_packets,
|
||||||
|
normal_bytes = EXCLUDED.normal_bytes,
|
||||||
|
normal_unique_ips = EXCLUDED.normal_unique_ips,
|
||||||
|
attack_packets = EXCLUDED.attack_packets,
|
||||||
|
attack_bytes = EXCLUDED.attack_bytes,
|
||||||
|
attack_unique_ips = EXCLUDED.attack_unique_ips
|
||||||
|
""", (
|
||||||
|
target_date,
|
||||||
|
daily_stats['total_packets'],
|
||||||
|
daily_stats['total_bytes'],
|
||||||
|
daily_stats['unique_ips'],
|
||||||
|
daily_stats['normal_packets'],
|
||||||
|
daily_stats['normal_bytes'],
|
||||||
|
daily_stats['normal_unique_ips'],
|
||||||
|
daily_stats['attack_packets'],
|
||||||
|
daily_stats['attack_bytes'],
|
||||||
|
daily_stats['attack_unique_ips']
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print(f"[ANALYTICS] ✅ Aggregazione giornaliera completata")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ANALYTICS] ❌ Errore aggregazione giornaliera: {e}")
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Entry point"""
|
||||||
|
aggregator = AnalyticsAggregator()
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
if sys.argv[1] == 'hourly':
|
||||||
|
aggregator.aggregate_hourly()
|
||||||
|
elif sys.argv[1] == 'daily':
|
||||||
|
aggregator.aggregate_daily()
|
||||||
|
else:
|
||||||
|
print("Usage: python analytics_aggregator.py [hourly|daily]")
|
||||||
|
else:
|
||||||
|
# Default: aggregazione oraria
|
||||||
|
aggregator.aggregate_hourly()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
24
replit.md
24
replit.md
@ -45,4 +45,26 @@ The IDS employs a React-based frontend for real-time monitoring, detection visua
|
|||||||
- **Neon Database**: Cloud-native PostgreSQL service (used in Replit).
|
- **Neon Database**: Cloud-native PostgreSQL service (used in Replit).
|
||||||
- **pg (Node.js driver)**: Standard PostgreSQL driver for Node.js (used in AlmaLinux).
|
- **pg (Node.js driver)**: Standard PostgreSQL driver for Node.js (used in AlmaLinux).
|
||||||
- **psycopg2**: PostgreSQL adapter for Python.
|
- **psycopg2**: PostgreSQL adapter for Python.
|
||||||
- **ip-api.com**: External API for IP geolocation data.
|
- **ip-api.com**: External API for IP geolocation data.
|
||||||
|
- **Recharts**: Charting library for analytics visualization.
|
||||||
|
|
||||||
|
## Recent Updates (Novembre 2025)
|
||||||
|
|
||||||
|
### 📊 Network Analytics & Dashboard System (22 Nov 2025 - 15:00)
|
||||||
|
- **Feature Completa**: Sistema analytics con traffico normale + attacchi, visualizzazioni grafiche avanzate, dati permanenti
|
||||||
|
- **Componenti**:
|
||||||
|
1. **Database**: `network_analytics` table con aggregazioni orarie/giornaliere permanenti
|
||||||
|
2. **Aggregatore Python**: `analytics_aggregator.py` classifica traffico ogni ora
|
||||||
|
3. **Systemd Timer**: Esecuzione automatica ogni ora (:05 minuti)
|
||||||
|
4. **API**: `/api/analytics/recent` e `/api/analytics/range`
|
||||||
|
5. **Frontend**: Dashboard Live (real-time 3 giorni) + Analytics Storici (permanente)
|
||||||
|
- **Grafici**: Area Chart, Pie Chart, Bar Chart, Line Chart, Real-time Stream
|
||||||
|
- **Flag Emoji**: 🇮🇹🇺🇸🇷🇺🇨🇳 per identificazione immediata paese origine
|
||||||
|
- **Deploy**: Migration 005 + `./deployment/setup_analytics_timer.sh`
|
||||||
|
|
||||||
|
### 🌍 IP Geolocation Integration (22 Nov 2025 - 13:00)
|
||||||
|
- **Feature**: Informazioni geografiche complete (paese, città, organizzazione, AS) per ogni IP
|
||||||
|
- **API**: ip-api.com con batch async lookup (100 IP in ~1.5s invece di 150s!)
|
||||||
|
- **Performance**: Caching intelligente + fallback robusto
|
||||||
|
- **Display**: Globe/Building/MapPin icons nella pagina Detections
|
||||||
|
- **Deploy**: Migration 004 + restart ML backend
|
||||||
@ -133,6 +133,37 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Network Analytics
|
||||||
|
app.get("/api/analytics/recent", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const days = parseInt(req.query.days as string) || 3;
|
||||||
|
const hourly = req.query.hourly === 'true';
|
||||||
|
const analytics = await storage.getRecentAnalytics(days, hourly);
|
||||||
|
res.json(analytics);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DB ERROR] Failed to fetch recent analytics:', error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch analytics" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/analytics/range", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const startDate = new Date(req.query.start as string);
|
||||||
|
const endDate = new Date(req.query.end as string);
|
||||||
|
const hourly = req.query.hourly === 'true';
|
||||||
|
|
||||||
|
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
||||||
|
return res.status(400).json({ error: "Invalid date range" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const analytics = await storage.getAnalyticsByDateRange(startDate, endDate, hourly);
|
||||||
|
res.json(analytics);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DB ERROR] Failed to fetch analytics range:', error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch analytics" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
app.get("/api/stats", async (req, res) => {
|
app.get("/api/stats", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
detections,
|
detections,
|
||||||
whitelist,
|
whitelist,
|
||||||
trainingHistory,
|
trainingHistory,
|
||||||
|
networkAnalytics,
|
||||||
type Router,
|
type Router,
|
||||||
type InsertRouter,
|
type InsertRouter,
|
||||||
type NetworkLog,
|
type NetworkLog,
|
||||||
@ -14,6 +15,7 @@ import {
|
|||||||
type InsertWhitelist,
|
type InsertWhitelist,
|
||||||
type TrainingHistory,
|
type TrainingHistory,
|
||||||
type InsertTrainingHistory,
|
type InsertTrainingHistory,
|
||||||
|
type NetworkAnalytics,
|
||||||
} from "@shared/schema";
|
} from "@shared/schema";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { eq, desc, and, gte, sql, inArray } from "drizzle-orm";
|
import { eq, desc, and, gte, sql, inArray } from "drizzle-orm";
|
||||||
@ -51,6 +53,10 @@ export interface IStorage {
|
|||||||
createTrainingHistory(history: InsertTrainingHistory): Promise<TrainingHistory>;
|
createTrainingHistory(history: InsertTrainingHistory): Promise<TrainingHistory>;
|
||||||
getLatestTraining(): Promise<TrainingHistory | undefined>;
|
getLatestTraining(): Promise<TrainingHistory | undefined>;
|
||||||
|
|
||||||
|
// Network Analytics
|
||||||
|
getAnalyticsByDateRange(startDate: Date, endDate: Date, hourly?: boolean): Promise<NetworkAnalytics[]>;
|
||||||
|
getRecentAnalytics(days: number, hourly?: boolean): Promise<NetworkAnalytics[]>;
|
||||||
|
|
||||||
// System
|
// System
|
||||||
testConnection(): Promise<boolean>;
|
testConnection(): Promise<boolean>;
|
||||||
}
|
}
|
||||||
@ -226,7 +232,32 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return history || undefined;
|
return history || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// System
|
// Network Analytics
|
||||||
|
async getAnalyticsByDateRange(startDate: Date, endDate: Date, hourly: boolean = false): Promise<NetworkAnalytics[]> {
|
||||||
|
const hourCondition = hourly
|
||||||
|
? sql`hour IS NOT NULL`
|
||||||
|
: sql`hour IS NULL`;
|
||||||
|
|
||||||
|
return await db
|
||||||
|
.select()
|
||||||
|
.from(networkAnalytics)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
gte(networkAnalytics.date, startDate),
|
||||||
|
sql`${networkAnalytics.date} <= ${endDate}`,
|
||||||
|
hourCondition
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(desc(networkAnalytics.date), desc(networkAnalytics.hour));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRecentAnalytics(days: number, hourly: boolean = false): Promise<NetworkAnalytics[]> {
|
||||||
|
const startDate = new Date();
|
||||||
|
startDate.setDate(startDate.getDate() - days);
|
||||||
|
|
||||||
|
return this.getAnalyticsByDateRange(startDate, new Date(), hourly);
|
||||||
|
}
|
||||||
|
|
||||||
async testConnection(): Promise<boolean> {
|
async testConnection(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await db.execute(sql`SELECT 1`);
|
await db.execute(sql`SELECT 1`);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { sql, relations } from "drizzle-orm";
|
import { sql, relations } from "drizzle-orm";
|
||||||
import { pgTable, text, varchar, integer, timestamp, decimal, boolean, index } from "drizzle-orm/pg-core";
|
import { pgTable, text, varchar, integer, timestamp, decimal, boolean, index, bigint } from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@ -89,6 +89,40 @@ export const trainingHistory = pgTable("training_history", {
|
|||||||
trainedAt: timestamp("trained_at").defaultNow().notNull(),
|
trainedAt: timestamp("trained_at").defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Network analytics - aggregazioni permanenti per statistiche long-term
|
||||||
|
export const networkAnalytics = pgTable("network_analytics", {
|
||||||
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
|
date: timestamp("date", { mode: 'date' }).notNull(),
|
||||||
|
hour: integer("hour"), // NULL = giornaliera, 0-23 = oraria
|
||||||
|
|
||||||
|
// Traffico totale
|
||||||
|
totalPackets: integer("total_packets").notNull().default(0),
|
||||||
|
totalBytes: bigint("total_bytes", { mode: 'number' }).notNull().default(0),
|
||||||
|
uniqueIps: integer("unique_ips").notNull().default(0),
|
||||||
|
|
||||||
|
// Traffico normale (non anomalo)
|
||||||
|
normalPackets: integer("normal_packets").notNull().default(0),
|
||||||
|
normalBytes: bigint("normal_bytes", { mode: 'number' }).notNull().default(0),
|
||||||
|
normalUniqueIps: integer("normal_unique_ips").notNull().default(0),
|
||||||
|
topNormalIps: text("top_normal_ips"), // JSON: [{ip, packets, bytes, country}]
|
||||||
|
|
||||||
|
// Attacchi/Anomalie
|
||||||
|
attackPackets: integer("attack_packets").notNull().default(0),
|
||||||
|
attackBytes: bigint("attack_bytes", { mode: 'number' }).notNull().default(0),
|
||||||
|
attackUniqueIps: integer("attack_unique_ips").notNull().default(0),
|
||||||
|
attacksByCountry: text("attacks_by_country"), // JSON: {IT: 5, RU: 30}
|
||||||
|
attacksByType: text("attacks_by_type"), // JSON: {ddos: 10, port_scan: 5}
|
||||||
|
topAttackers: text("top_attackers"), // JSON: [{ip, country, risk_score, packets}]
|
||||||
|
|
||||||
|
// Dettagli geografici (tutto il traffico)
|
||||||
|
trafficByCountry: text("traffic_by_country"), // JSON: {IT: {normal: 100, attacks: 5}}
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
}, (table) => ({
|
||||||
|
dateHourIdx: index("network_analytics_date_hour_idx").on(table.date, table.hour),
|
||||||
|
dateIdx: index("network_analytics_date_idx").on(table.date),
|
||||||
|
}));
|
||||||
|
|
||||||
// Schema version tracking for database migrations
|
// Schema version tracking for database migrations
|
||||||
export const schemaVersion = pgTable("schema_version", {
|
export const schemaVersion = pgTable("schema_version", {
|
||||||
id: integer("id").primaryKey().default(1),
|
id: integer("id").primaryKey().default(1),
|
||||||
@ -135,6 +169,11 @@ export const insertSchemaVersionSchema = createInsertSchema(schemaVersion).omit(
|
|||||||
appliedAt: true,
|
appliedAt: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const insertNetworkAnalyticsSchema = createInsertSchema(networkAnalytics).omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
});
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type Router = typeof routers.$inferSelect;
|
export type Router = typeof routers.$inferSelect;
|
||||||
export type InsertRouter = z.infer<typeof insertRouterSchema>;
|
export type InsertRouter = z.infer<typeof insertRouterSchema>;
|
||||||
@ -153,3 +192,6 @@ export type InsertTrainingHistory = z.infer<typeof insertTrainingHistorySchema>;
|
|||||||
|
|
||||||
export type SchemaVersion = typeof schemaVersion.$inferSelect;
|
export type SchemaVersion = typeof schemaVersion.$inferSelect;
|
||||||
export type InsertSchemaVersion = z.infer<typeof insertSchemaVersionSchema>;
|
export type InsertSchemaVersion = z.infer<typeof insertSchemaVersionSchema>;
|
||||||
|
|
||||||
|
export type NetworkAnalytics = typeof networkAnalytics.$inferSelect;
|
||||||
|
export type InsertNetworkAnalytics = z.infer<typeof insertNetworkAnalyticsSchema>;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user