Compare commits
3 Commits
40f8f05e87
...
14645c520b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14645c520b | ||
|
|
c62b41d624 | ||
|
|
c8efe5c942 |
@ -33,10 +33,13 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
|
||||||
interface MLStatsResponse {
|
interface MLStatsResponse {
|
||||||
|
source?: string;
|
||||||
|
ml_backend_status?: string;
|
||||||
logs?: { total: number; last_hour: number };
|
logs?: { total: number; last_hour: number };
|
||||||
detections?: { total: number; blocked: number };
|
detections?: { total: number; blocked: number; critical?: number; unique_ips?: number };
|
||||||
routers?: { active: number };
|
routers?: { active: number };
|
||||||
latest_training?: any;
|
latest_training?: any;
|
||||||
|
logs_24h?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trainFormSchema = z.object({
|
const trainFormSchema = z.object({
|
||||||
@ -147,21 +150,43 @@ export default function TrainingPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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 */}
|
{/* ML Stats */}
|
||||||
{mlStats && (
|
{mlStats && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<Card data-testid="card-ml-logs">
|
<Card data-testid="card-ml-logs">
|
||||||
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-2">
|
<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" />
|
<Brain className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-semibold" data-testid="text-ml-logs-total">
|
<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>
|
</div>
|
||||||
|
{mlStats.source !== "database_fallback" && (
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Ultima ora: {mlStats.logs?.last_hour?.toLocaleString() || 0}
|
Ultima ora: {mlStats.logs?.last_hour?.toLocaleString() || 0}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -172,22 +197,30 @@ export default function TrainingPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-semibold" data-testid="text-ml-detections-total">
|
<div className="text-2xl font-semibold" data-testid="text-ml-detections-total">
|
||||||
{mlStats.detections?.total || 0}
|
{mlStats.detections?.total?.toLocaleString() || 0}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<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>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card data-testid="card-ml-routers">
|
<Card data-testid="card-ml-routers">
|
||||||
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-2">
|
<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" />
|
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-semibold" data-testid="text-ml-routers-active">
|
<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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -214,9 +247,9 @@ export default function TrainingPage() {
|
|||||||
</p>
|
</p>
|
||||||
<Dialog open={isTrainDialogOpen} onOpenChange={setIsTrainDialogOpen}>
|
<Dialog open={isTrainDialogOpen} onOpenChange={setIsTrainDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<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" />
|
<Play className="h-4 w-4 mr-2" />
|
||||||
Avvia Training
|
{mlStats?.ml_backend_status === "offline" ? "ML Backend Offline" : "Avvia Training"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent data-testid="dialog-training">
|
<DialogContent data-testid="dialog-training">
|
||||||
@ -265,7 +298,7 @@ export default function TrainingPage() {
|
|||||||
>
|
>
|
||||||
Annulla
|
Annulla
|
||||||
</Button>
|
</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"}
|
{trainMutation.isPending ? "Avvio..." : "Avvia Training"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@ -294,9 +327,9 @@ export default function TrainingPage() {
|
|||||||
</p>
|
</p>
|
||||||
<Dialog open={isDetectDialogOpen} onOpenChange={setIsDetectDialogOpen}>
|
<Dialog open={isDetectDialogOpen} onOpenChange={setIsDetectDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<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" />
|
<Search className="h-4 w-4 mr-2" />
|
||||||
Avvia Detection
|
{mlStats?.ml_backend_status === "offline" ? "ML Backend Offline" : "Avvia Detection"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent data-testid="dialog-detection">
|
<DialogContent data-testid="dialog-detection">
|
||||||
@ -377,7 +410,7 @@ export default function TrainingPage() {
|
|||||||
>
|
>
|
||||||
Annulla
|
Annulla
|
||||||
</Button>
|
</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"}
|
{detectMutation.isPending ? "Avvio..." : "Avvia Detection"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
-- PostgreSQL database dump
|
-- PostgreSQL database dump
|
||||||
--
|
--
|
||||||
|
|
||||||
\restrict aCgEgb8lET7e1jInisWq8PxX1FjLAEzvVLqvthf8YBIYG3k2DGoUIepNBg4w74z
|
\restrict 57h0JKERpuPHgeauMl1aA00ae8u7sLKZJ9awV7GNEIwcfDuB4SO4rwGdBTcE4Xm
|
||||||
|
|
||||||
-- Dumped from database version 16.11 (df20cf9)
|
-- Dumped from database version 16.11 (df20cf9)
|
||||||
-- Dumped by pg_dump version 16.10
|
-- Dumped by pg_dump version 16.10
|
||||||
@ -387,5 +387,5 @@ ALTER TABLE ONLY public.public_blacklist_ips
|
|||||||
-- PostgreSQL database dump complete
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|
||||||
\unrestrict aCgEgb8lET7e1jInisWq8PxX1FjLAEzvVLqvthf8YBIYG3k2DGoUIepNBg4w74z
|
\unrestrict 57h0JKERpuPHgeauMl1aA00ae8u7sLKZJ9awV7GNEIwcfDuB4SO4rwGdBTcE4Xm
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
- **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.
|
- **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).
|
- **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.
|
- **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.
|
- **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.
|
- **Microservices**: Clear separation of concerns between the Python ML backend and the Node.js API backend.
|
||||||
|
|||||||
134
server/routes.ts
134
server/routes.ts
@ -1,7 +1,7 @@
|
|||||||
import type { Express } from "express";
|
import type { Express } from "express";
|
||||||
import { createServer, type Server } from "http";
|
import { createServer, type Server } from "http";
|
||||||
import { storage } from "./storage";
|
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 { db } from "./db";
|
||||||
import { desc, eq, gte, sql } from "drizzle-orm";
|
import { desc, eq, gte, sql } from "drizzle-orm";
|
||||||
import { blockIpOnAllRouters, unblockIpOnAllRouters, bulkBlockIps, testRouterConnection } from "./mikrotik";
|
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) => {
|
app.post("/api/ml/block-all-critical", async (req, res) => {
|
||||||
try {
|
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 allRouters = await storage.getAllRouters();
|
||||||
const enabledRouters = allRouters.filter(r => r.enabled);
|
const enabledRouters = allRouters.filter(r => r.enabled);
|
||||||
@ -643,23 +644,35 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
AND blocked = false
|
AND blocked = false
|
||||||
AND source_ip NOT IN (SELECT ip_address FROM whitelist WHERE active = true)
|
AND source_ip NOT IN (SELECT ip_address FROM whitelist WHERE active = true)
|
||||||
GROUP BY source_ip
|
GROUP BY source_ip
|
||||||
ORDER BY max_score DESC`
|
ORDER BY max_score DESC
|
||||||
|
LIMIT ${maxIps}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const rows = (unblockedDetections as any).rows || unblockedDetections;
|
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) {
|
if (!rows || rows.length === 0) {
|
||||||
return res.json({
|
return res.json({
|
||||||
message: "Nessun IP critico da bloccare",
|
message: "Nessun IP critico da bloccare",
|
||||||
blocked: 0,
|
blocked: 0,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
total_critical: 0,
|
total_critical: 0,
|
||||||
|
remaining: 0,
|
||||||
skipped: 0
|
skipped: 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ipList = rows.map((r: any) => r.source_ip);
|
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(
|
const result = await bulkBlockIps(
|
||||||
enabledRouters as any,
|
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`);
|
console.log(`[BLOCK-ALL] Database aggiornato: ${blockedIps.length} IP marcati come bloccati`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const remaining = totalUnblocked - ipList.length;
|
||||||
res.json({
|
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,
|
blocked: result.blocked,
|
||||||
failed: result.failed,
|
failed: result.failed,
|
||||||
skipped: result.skipped,
|
skipped: result.skipped,
|
||||||
total_critical: ipList.length,
|
total_critical: ipList.length,
|
||||||
|
remaining,
|
||||||
details: result.details.slice(0, 100)
|
details: result.details.slice(0, 100)
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -703,7 +719,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
app.get("/api/ml/stats", async (req, res) => {
|
app.get("/api/ml/stats", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
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`, {
|
const response = await fetch(`${ML_BACKEND_URL}/stats`, {
|
||||||
headers: getMLBackendHeaders(),
|
headers: getMLBackendHeaders(),
|
||||||
@ -713,23 +729,49 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
throw new Error(`HTTP ${response.status}`);
|
||||||
return res.status(response.status).json({
|
|
||||||
error: errorData.detail || "Failed to fetch ML stats",
|
|
||||||
status: response.status,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
res.json(data);
|
res.json(data);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.name === 'AbortError') {
|
try {
|
||||||
return res.status(504).json({ error: "Stats timeout" });
|
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 };
|
services.database.details = { error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Python Services via authenticated endpoint
|
// Check Syslog Parser via database (independent of ML Backend)
|
||||||
try {
|
try {
|
||||||
const controller2 = new AbortController();
|
const recentLogsResult = await db.execute(
|
||||||
const timeout2 = setTimeout(() => controller2.abort(), 5000);
|
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`, {
|
if (recentLogCount > 0) {
|
||||||
headers: getMLBackendHeaders(),
|
services.syslogParser.status = "running";
|
||||||
signal: controller2.signal,
|
services.syslogParser.healthy = true;
|
||||||
});
|
|
||||||
|
|
||||||
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 = {
|
services.syslogParser.details = {
|
||||||
systemd_unit: parserInfo.systemd_unit,
|
recentLogs30min: recentLogCount,
|
||||||
pid: parserInfo.details?.pid,
|
lastLog: lastLogTime,
|
||||||
error: parserInfo.error,
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
} else if (servicesResponse.status === 403) {
|
|
||||||
services.syslogParser.status = "error";
|
|
||||||
services.syslogParser.healthy = false;
|
|
||||||
services.syslogParser.details = { error: "Authentication failed" };
|
|
||||||
} else {
|
} 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) {
|
} catch (error: any) {
|
||||||
services.syslogParser.status = "error";
|
services.syslogParser.status = "error";
|
||||||
services.syslogParser.healthy = false;
|
services.syslogParser.healthy = false;
|
||||||
services.syslogParser.details = {
|
services.syslogParser.details = { error: error.message };
|
||||||
error: error.code === 'ECONNREFUSED' ? "ML Backend offline" : error.message
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Analytics Aggregator (via last record timestamp)
|
// Check Analytics Aggregator (via last record timestamp)
|
||||||
|
|||||||
16
version.json
16
version.json
@ -1,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.112",
|
"version": "1.0.113",
|
||||||
"lastUpdate": "2026-02-16T11:06:31.030Z",
|
"lastUpdate": "2026-02-16T11:32:42.766Z",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
|
{
|
||||||
|
"version": "1.0.113",
|
||||||
|
"date": "2026-02-16",
|
||||||
|
"type": "patch",
|
||||||
|
"description": "Deployment automatico v1.0.113"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "1.0.112",
|
"version": "1.0.112",
|
||||||
"date": "2026-02-16",
|
"date": "2026-02-16",
|
||||||
@ -295,12 +301,6 @@
|
|||||||
"date": "2025-11-24",
|
"date": "2025-11-24",
|
||||||
"type": "patch",
|
"type": "patch",
|
||||||
"description": "Deployment automatico v1.0.64"
|
"description": "Deployment automatico v1.0.64"
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.63",
|
|
||||||
"date": "2025-11-24",
|
|
||||||
"type": "patch",
|
|
||||||
"description": "Deployment automatico v1.0.63"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user