Compare commits

..

3 Commits

Author SHA1 Message Date
Marco Lanzara
14645c520b 🚀 Release v1.0.113
- Tipo: patch
- Database schema: database-schema/schema.sql (solo struttura)
- Data: 2026-02-16 11:32:42
2026-02-16 11:32:42 +00:00
marco370
c62b41d624 Saved progress at the end of the loop
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 95131e28-728c-4c85-9a81-76c89430618b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/MmMtYN7
2026-02-16 11:29:58 +00:00
marco370
c8efe5c942 Improve ML stats display and add database fallback
Add backend logic for ML stats to use database when unavailable and update frontend to show offline status.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: b69b4d7d-5491-401d-a003-d99b33ae655d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/MmMtYN7
2026-02-16 11:29:12 +00:00
5 changed files with 150 additions and 77 deletions

View File

@ -33,10 +33,13 @@ import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
interface MLStatsResponse {
source?: string;
ml_backend_status?: string;
logs?: { total: number; last_hour: number };
detections?: { total: number; blocked: number };
detections?: { total: number; blocked: number; critical?: number; unique_ips?: number };
routers?: { active: number };
latest_training?: any;
logs_24h?: number;
}
const trainFormSchema = z.object({
@ -147,21 +150,43 @@ export default function TrainingPage() {
</p>
</div>
{/* ML Backend Status Warning */}
{mlStats?.ml_backend_status === "offline" && (
<Card className="border-orange-300 dark:border-orange-700" data-testid="card-ml-offline-warning">
<CardContent className="flex items-center gap-3 py-3">
<XCircle className="h-5 w-5 text-orange-500 shrink-0" />
<div>
<p className="font-medium text-sm">ML Backend Python offline</p>
<p className="text-xs text-muted-foreground">
Le statistiche mostrate provengono dal database. Training e detection manuali non sono disponibili fino al riavvio del servizio.
</p>
</div>
</CardContent>
</Card>
)}
{/* ML Stats */}
{mlStats && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card data-testid="card-ml-logs">
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Log Totali</CardTitle>
<CardTitle className="text-sm font-medium">
{mlStats.source === "database_fallback" ? "Log Ultime 24h" : "Log Totali"}
</CardTitle>
<Brain className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold" data-testid="text-ml-logs-total">
{mlStats.logs?.total?.toLocaleString() || 0}
{(mlStats.source === "database_fallback"
? mlStats.logs_24h
: mlStats.logs?.total
)?.toLocaleString() || 0}
</div>
<p className="text-xs text-muted-foreground mt-1">
Ultima ora: {mlStats.logs?.last_hour?.toLocaleString() || 0}
</p>
{mlStats.source !== "database_fallback" && (
<p className="text-xs text-muted-foreground mt-1">
Ultima ora: {mlStats.logs?.last_hour?.toLocaleString() || 0}
</p>
)}
</CardContent>
</Card>
@ -172,22 +197,30 @@ export default function TrainingPage() {
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold" data-testid="text-ml-detections-total">
{mlStats.detections?.total || 0}
{mlStats.detections?.total?.toLocaleString() || 0}
</div>
<p className="text-xs text-muted-foreground mt-1">
Bloccati: {mlStats.detections?.blocked || 0}
Bloccati: {mlStats.detections?.blocked?.toLocaleString() || 0}
{mlStats.detections?.critical !== undefined && (
<span> | Critici: {mlStats.detections.critical.toLocaleString()}</span>
)}
</p>
</CardContent>
</Card>
<Card data-testid="card-ml-routers">
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Router Attivi</CardTitle>
<CardTitle className="text-sm font-medium">
{mlStats.source === "database_fallback" ? "IP Unici" : "Router Attivi"}
</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold" data-testid="text-ml-routers-active">
{mlStats.routers?.active || 0}
{(mlStats.source === "database_fallback"
? mlStats.detections?.unique_ips
: mlStats.routers?.active
)?.toLocaleString() || 0}
</div>
</CardContent>
</Card>
@ -214,9 +247,9 @@ export default function TrainingPage() {
</p>
<Dialog open={isTrainDialogOpen} onOpenChange={setIsTrainDialogOpen}>
<DialogTrigger asChild>
<Button className="w-full" data-testid="button-start-training">
<Button className="w-full" disabled={mlStats?.ml_backend_status === "offline"} data-testid="button-start-training">
<Play className="h-4 w-4 mr-2" />
Avvia Training
{mlStats?.ml_backend_status === "offline" ? "ML Backend Offline" : "Avvia Training"}
</Button>
</DialogTrigger>
<DialogContent data-testid="dialog-training">
@ -265,7 +298,7 @@ export default function TrainingPage() {
>
Annulla
</Button>
<Button type="submit" disabled={trainMutation.isPending} data-testid="button-confirm-training">
<Button type="submit" disabled={trainMutation.isPending || mlStats?.ml_backend_status === "offline"} data-testid="button-confirm-training">
{trainMutation.isPending ? "Avvio..." : "Avvia Training"}
</Button>
</DialogFooter>
@ -294,9 +327,9 @@ export default function TrainingPage() {
</p>
<Dialog open={isDetectDialogOpen} onOpenChange={setIsDetectDialogOpen}>
<DialogTrigger asChild>
<Button variant="secondary" className="w-full" data-testid="button-start-detection">
<Button variant="secondary" className="w-full" disabled={mlStats?.ml_backend_status === "offline"} data-testid="button-start-detection">
<Search className="h-4 w-4 mr-2" />
Avvia Detection
{mlStats?.ml_backend_status === "offline" ? "ML Backend Offline" : "Avvia Detection"}
</Button>
</DialogTrigger>
<DialogContent data-testid="dialog-detection">
@ -377,7 +410,7 @@ export default function TrainingPage() {
>
Annulla
</Button>
<Button type="submit" disabled={detectMutation.isPending} data-testid="button-confirm-detection">
<Button type="submit" disabled={detectMutation.isPending || mlStats?.ml_backend_status === "offline"} data-testid="button-confirm-detection">
{detectMutation.isPending ? "Avvio..." : "Avvia Detection"}
</Button>
</DialogFooter>

View File

@ -2,7 +2,7 @@
-- PostgreSQL database dump
--
\restrict aCgEgb8lET7e1jInisWq8PxX1FjLAEzvVLqvthf8YBIYG3k2DGoUIepNBg4w74z
\restrict 57h0JKERpuPHgeauMl1aA00ae8u7sLKZJ9awV7GNEIwcfDuB4SO4rwGdBTcE4Xm
-- Dumped from database version 16.11 (df20cf9)
-- Dumped by pg_dump version 16.10
@ -387,5 +387,5 @@ ALTER TABLE ONLY public.public_blacklist_ips
-- PostgreSQL database dump complete
--
\unrestrict aCgEgb8lET7e1jInisWq8PxX1FjLAEzvVLqvthf8YBIYG3k2DGoUIepNBg4w74z
\unrestrict 57h0JKERpuPHgeauMl1aA00ae8u7sLKZJ9awV7GNEIwcfDuB4SO4rwGdBTcE4Xm

View File

@ -28,7 +28,7 @@ The IDS employs a React-based frontend for real-time monitoring, detection visua
- **Automated Blocking**: Critical IPs (score >= 80) are automatically blocked in parallel across configured MikroTik routers via their REST API. **Auto-unblock on whitelist**: When an IP is added to the whitelist, it is automatically removed from all router blocklists. Manual unblock button available in Detections page.
- **Public Lists Integration (v2.0.0 - CIDR Complete)**: Automatic fetcher syncs blacklist/whitelist feeds every 10 minutes (Spamhaus, Talos, AWS, GCP, Cloudflare, IANA, NTP Pool). **Full CIDR support** using PostgreSQL INET/CIDR types with `<<=` containment operators for network range matching. Priority-based merge logic: Manual whitelist > Public whitelist > Blacklist (CIDR-aware). Detections created for blacklisted IPs/ranges (excluding whitelisted ranges). CRUD API + UI for list management. See `deployment/docs/PUBLIC_LISTS_V2_CIDR.md` for implementation details.
- **Automatic Cleanup**: An hourly systemd timer (`cleanup_detections.py`) removes old detections (48h) and auto-unblocks IPs (2h).
- **Service Monitoring & Management**: A dashboard provides real-time status (ML Backend, Database, Syslog Parser). API endpoints, secured with API key authentication and Systemd integration, allow for service management (start/stop/restart) of Python services.
- **Service Monitoring & Management**: A dashboard provides real-time status (ML Backend, Database, Syslog Parser, Analytics Aggregator). **Syslog Parser check is database-based** (counts logs in last 30 minutes) and independent of ML Backend availability. ML Stats endpoint has database fallback when Python backend is offline. Training UI shows offline warning and disables actions when ML Backend is unavailable. API endpoints, secured with API key authentication and Systemd integration, allow for service management (start/stop/restart) of Python services.
- **IP Geolocation**: Integration with `ip-api.com` enriches detection data with geographical and AS information, utilizing intelligent caching.
- **Database Management**: PostgreSQL is used for all persistent data. An intelligent database versioning system ensures efficient SQL migrations (v8 with forced INET/CIDR column types for network range matching). Migration 008 unconditionally recreates INET columns to fix type mismatches. Dual-mode database drivers (`@neondatabase/serverless` for Replit, `pg` for AlmaLinux) ensure environment compatibility.
- **Microservices**: Clear separation of concerns between the Python ML backend and the Node.js API backend.

View File

@ -1,7 +1,7 @@
import type { Express } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
import { insertRouterSchema, insertDetectionSchema, insertWhitelistSchema, insertPublicListSchema, networkAnalytics, routers, detections, networkLogs } from "@shared/schema";
import { insertRouterSchema, insertDetectionSchema, insertWhitelistSchema, insertPublicListSchema, networkAnalytics, routers, detections, networkLogs, trainingHistory } from "@shared/schema";
import { db } from "./db";
import { desc, eq, gte, sql } from "drizzle-orm";
import { blockIpOnAllRouters, unblockIpOnAllRouters, bulkBlockIps, testRouterConnection } from "./mikrotik";
@ -627,7 +627,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
app.post("/api/ml/block-all-critical", async (req, res) => {
try {
const { min_score = 80, list_name = "ddos_blocked" } = req.body;
const { min_score = 80, list_name = "ddos_blocked", limit = 100 } = req.body;
const maxIps = Math.min(Number(limit) || 100, 500);
const allRouters = await storage.getAllRouters();
const enabledRouters = allRouters.filter(r => r.enabled);
@ -643,23 +644,35 @@ export async function registerRoutes(app: Express): Promise<Server> {
AND blocked = false
AND source_ip NOT IN (SELECT ip_address FROM whitelist WHERE active = true)
GROUP BY source_ip
ORDER BY max_score DESC`
ORDER BY max_score DESC
LIMIT ${maxIps}`
);
const rows = (unblockedDetections as any).rows || unblockedDetections;
const totalUnblockedResult = await db.execute(
sql`SELECT COUNT(DISTINCT source_ip) as count
FROM detections
WHERE CAST(risk_score AS FLOAT) >= ${min_score}
AND blocked = false
AND source_ip NOT IN (SELECT ip_address FROM whitelist WHERE active = true)`
);
const totalUnblockedRows = (totalUnblockedResult as any).rows || totalUnblockedResult;
const totalUnblocked = parseInt(totalUnblockedRows[0]?.count || "0");
if (!rows || rows.length === 0) {
return res.json({
message: "Nessun IP critico da bloccare",
blocked: 0,
failed: 0,
total_critical: 0,
remaining: 0,
skipped: 0
});
}
const ipList = rows.map((r: any) => r.source_ip);
console.log(`[BLOCK-ALL] Avvio blocco massivo: ${ipList.length} IP con score >= ${min_score} su ${enabledRouters.length} router`);
console.log(`[BLOCK-ALL] Avvio blocco massivo: ${ipList.length}/${totalUnblocked} IP con score >= ${min_score} su ${enabledRouters.length} router`);
const result = await bulkBlockIps(
enabledRouters as any,
@ -686,12 +699,15 @@ export async function registerRoutes(app: Express): Promise<Server> {
console.log(`[BLOCK-ALL] Database aggiornato: ${blockedIps.length} IP marcati come bloccati`);
}
const remaining = totalUnblocked - ipList.length;
res.json({
message: `Blocco massivo completato: ${result.blocked} IP bloccati, ${result.failed} falliti, ${result.skipped} già bloccati`,
message: `Blocco massivo completato: ${result.blocked} IP bloccati, ${result.failed} falliti, ${result.skipped} già bloccati` +
(remaining > 0 ? `. Rimangono ${remaining} IP da bloccare.` : ''),
blocked: result.blocked,
failed: result.failed,
skipped: result.skipped,
total_critical: ipList.length,
remaining,
details: result.details.slice(0, 100)
});
} catch (error: any) {
@ -703,7 +719,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
app.get("/api/ml/stats", async (req, res) => {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout for stats
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch(`${ML_BACKEND_URL}/stats`, {
headers: getMLBackendHeaders(),
@ -713,23 +729,49 @@ export async function registerRoutes(app: Express): Promise<Server> {
clearTimeout(timeout);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return res.status(response.status).json({
error: errorData.detail || "Failed to fetch ML stats",
status: response.status,
});
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
res.json(data);
} catch (error: any) {
if (error.name === 'AbortError') {
return res.status(504).json({ error: "Stats timeout" });
try {
const latestTraining = await db
.select()
.from(trainingHistory)
.orderBy(desc(trainingHistory.trainedAt))
.limit(1);
const detectionStats = await db.execute(
sql`SELECT
COUNT(*) as total_detections,
COUNT(*) FILTER (WHERE blocked = true) as blocked_count,
COUNT(*) FILTER (WHERE CAST(risk_score AS FLOAT) >= 80) as critical_count,
COUNT(DISTINCT source_ip) as unique_ips
FROM detections`
);
const statsRows = (detectionStats as any).rows || detectionStats;
const logCount = await db.execute(
sql`SELECT COUNT(*) as count FROM network_logs WHERE timestamp > NOW() - INTERVAL '24 hours'`
);
const logRows = (logCount as any).rows || logCount;
res.json({
source: "database_fallback",
ml_backend_status: "offline",
latest_training: latestTraining[0] || null,
detections: {
total: parseInt(statsRows[0]?.total_detections || "0"),
blocked: parseInt(statsRows[0]?.blocked_count || "0"),
critical: parseInt(statsRows[0]?.critical_count || "0"),
unique_ips: parseInt(statsRows[0]?.unique_ips || "0"),
},
logs_24h: parseInt(logRows[0]?.count || "0"),
});
} catch (dbError: any) {
res.status(503).json({ error: "ML backend offline and database fallback failed" });
}
if (error.code === 'ECONNREFUSED') {
return res.status(503).json({ error: "ML backend not available" });
}
res.status(500).json({ error: error.message || "Failed to fetch ML stats" });
}
});
@ -784,45 +826,43 @@ export async function registerRoutes(app: Express): Promise<Server> {
services.database.details = { error: error.message };
}
// Check Python Services via authenticated endpoint
// Check Syslog Parser via database (independent of ML Backend)
try {
const controller2 = new AbortController();
const timeout2 = setTimeout(() => controller2.abort(), 5000);
const recentLogsResult = await db.execute(
sql`SELECT COUNT(*) as count, MAX(timestamp) as last_log
FROM network_logs
WHERE timestamp > NOW() - INTERVAL '30 minutes'`
);
const logRows = (recentLogsResult as any).rows || recentLogsResult;
const recentLogCount = parseInt(logRows[0]?.count || "0");
const lastLogTime = logRows[0]?.last_log;
const servicesResponse = await fetch(`${ML_BACKEND_URL}/services/status`, {
headers: getMLBackendHeaders(),
signal: controller2.signal,
});
clearTimeout(timeout2);
if (servicesResponse.ok) {
const servicesData = await servicesResponse.json();
// Update syslog parser status
const parserInfo = servicesData.services?.syslog_parser;
if (parserInfo) {
services.syslogParser.status = parserInfo.running ? "running" : "offline";
services.syslogParser.healthy = parserInfo.running;
services.syslogParser.details = {
systemd_unit: parserInfo.systemd_unit,
pid: parserInfo.details?.pid,
error: parserInfo.error,
};
}
} else if (servicesResponse.status === 403) {
services.syslogParser.status = "error";
services.syslogParser.healthy = false;
services.syslogParser.details = { error: "Authentication failed" };
if (recentLogCount > 0) {
services.syslogParser.status = "running";
services.syslogParser.healthy = true;
services.syslogParser.details = {
recentLogs30min: recentLogCount,
lastLog: lastLogTime,
};
} else {
throw new Error(`HTTP ${servicesResponse.status}`);
const lastLogEverResult = await db.execute(
sql`SELECT MAX(timestamp) as last_log FROM network_logs`
);
const lastLogEverRows = (lastLogEverResult as any).rows || lastLogEverResult;
const lastLogEver = lastLogEverRows[0]?.last_log;
services.syslogParser.status = "offline";
services.syslogParser.healthy = false;
services.syslogParser.details = {
recentLogs30min: 0,
lastLog: lastLogEver || "Never",
warning: "No logs received in last 30 minutes",
};
}
} catch (error: any) {
services.syslogParser.status = "error";
services.syslogParser.healthy = false;
services.syslogParser.details = {
error: error.code === 'ECONNREFUSED' ? "ML Backend offline" : error.message
};
services.syslogParser.details = { error: error.message };
}
// Check Analytics Aggregator (via last record timestamp)

View File

@ -1,7 +1,13 @@
{
"version": "1.0.112",
"lastUpdate": "2026-02-16T11:06:31.030Z",
"version": "1.0.113",
"lastUpdate": "2026-02-16T11:32:42.766Z",
"changelog": [
{
"version": "1.0.113",
"date": "2026-02-16",
"type": "patch",
"description": "Deployment automatico v1.0.113"
},
{
"version": "1.0.112",
"date": "2026-02-16",
@ -295,12 +301,6 @@
"date": "2025-11-24",
"type": "patch",
"description": "Deployment automatico v1.0.64"
},
{
"version": "1.0.63",
"date": "2025-11-24",
"type": "patch",
"description": "Deployment automatico v1.0.63"
}
]
}