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
220 lines
10 KiB
TypeScript
220 lines
10 KiB
TypeScript
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 { Input } from "@/components/ui/input";
|
|
import { AlertTriangle, Search, Shield, Eye, Globe, MapPin, Building2 } from "lucide-react";
|
|
import { format } from "date-fns";
|
|
import { useState } from "react";
|
|
import type { Detection } from "@shared/schema";
|
|
import { getFlag } from "@/lib/country-flags";
|
|
|
|
export default function Detections() {
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const { data: detections, isLoading } = useQuery<Detection[]>({
|
|
queryKey: ["/api/detections"],
|
|
refetchInterval: 5000,
|
|
});
|
|
|
|
const filteredDetections = detections?.filter((d) =>
|
|
d.sourceIp.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
d.anomalyType.toLowerCase().includes(searchQuery.toLowerCase())
|
|
);
|
|
|
|
const getRiskBadge = (riskScore: string) => {
|
|
const score = parseFloat(riskScore);
|
|
if (score >= 85) return <Badge variant="destructive">CRITICO</Badge>;
|
|
if (score >= 70) return <Badge className="bg-orange-500">ALTO</Badge>;
|
|
if (score >= 60) return <Badge className="bg-yellow-500">MEDIO</Badge>;
|
|
if (score >= 40) return <Badge variant="secondary">BASSO</Badge>;
|
|
return <Badge variant="outline">NORMALE</Badge>;
|
|
};
|
|
|
|
const getAnomalyTypeLabel = (type: string) => {
|
|
const labels: Record<string, string> = {
|
|
ddos: "DDoS Attack",
|
|
port_scan: "Port Scanning",
|
|
brute_force: "Brute Force",
|
|
botnet: "Botnet Activity",
|
|
suspicious: "Suspicious Activity"
|
|
};
|
|
return labels[type] || type;
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6 p-6" data-testid="page-detections">
|
|
<div>
|
|
<h1 className="text-3xl font-semibold" data-testid="text-page-title">Rilevamenti</h1>
|
|
<p className="text-muted-foreground" data-testid="text-page-subtitle">
|
|
Anomalie rilevate dal sistema IDS
|
|
</p>
|
|
</div>
|
|
|
|
{/* Search and Filters */}
|
|
<Card data-testid="card-filters">
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Cerca per IP o tipo anomalia..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-9"
|
|
data-testid="input-search"
|
|
/>
|
|
</div>
|
|
<Button variant="outline" data-testid="button-refresh">
|
|
Aggiorna
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Detections List */}
|
|
<Card data-testid="card-detections-list">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<AlertTriangle className="h-5 w-5" />
|
|
Rilevamenti ({filteredDetections?.length || 0})
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isLoading ? (
|
|
<div className="text-center py-8 text-muted-foreground" data-testid="text-loading">
|
|
Caricamento...
|
|
</div>
|
|
) : filteredDetections && filteredDetections.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{filteredDetections.map((detection) => (
|
|
<div
|
|
key={detection.id}
|
|
className="p-4 rounded-lg border hover-elevate"
|
|
data-testid={`detection-${detection.id}`}
|
|
>
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex-1 min-w-0">
|
|
<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}`}>
|
|
{detection.sourceIp}
|
|
</code>
|
|
{getRiskBadge(detection.riskScore)}
|
|
<Badge variant="outline" data-testid={`badge-type-${detection.id}`}>
|
|
{getAnomalyTypeLabel(detection.anomalyType)}
|
|
</Badge>
|
|
</div>
|
|
|
|
<p className="text-sm text-muted-foreground mb-3" data-testid={`text-reason-${detection.id}`}>
|
|
{detection.reason}
|
|
</p>
|
|
|
|
{/* Geolocation Info */}
|
|
{(detection.country || detection.organization || detection.asNumber) && (
|
|
<div className="flex flex-wrap gap-3 mb-3 text-sm" data-testid={`geo-info-${detection.id}`}>
|
|
{detection.country && (
|
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
|
<Globe className="h-3.5 w-3.5" />
|
|
<span data-testid={`text-country-${detection.id}`}>
|
|
{detection.city ? `${detection.city}, ${detection.country}` : detection.country}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{detection.organization && (
|
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
|
<Building2 className="h-3.5 w-3.5" />
|
|
<span data-testid={`text-org-${detection.id}`}>{detection.organization}</span>
|
|
</div>
|
|
)}
|
|
{detection.asNumber && (
|
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
|
<MapPin className="h-3.5 w-3.5" />
|
|
<span data-testid={`text-as-${detection.id}`}>
|
|
{detection.asNumber} {detection.asName && `- ${detection.asName}`}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<p className="text-muted-foreground text-xs">Risk Score</p>
|
|
<p className="font-medium" data-testid={`text-risk-${detection.id}`}>
|
|
{parseFloat(detection.riskScore).toFixed(1)}/100
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-muted-foreground text-xs">Confidence</p>
|
|
<p className="font-medium" data-testid={`text-confidence-${detection.id}`}>
|
|
{parseFloat(detection.confidence).toFixed(1)}%
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-muted-foreground text-xs">Log Count</p>
|
|
<p className="font-medium" data-testid={`text-logs-${detection.id}`}>
|
|
{detection.logCount}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-muted-foreground text-xs">Rilevato</p>
|
|
<p className="font-medium" data-testid={`text-detected-${detection.id}`}>
|
|
{format(new Date(detection.detectedAt), "dd/MM HH:mm")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4 mt-3 text-xs text-muted-foreground">
|
|
<span data-testid={`text-first-seen-${detection.id}`}>
|
|
Prima: {format(new Date(detection.firstSeen), "dd/MM HH:mm:ss")}
|
|
</span>
|
|
<span data-testid={`text-last-seen-${detection.id}`}>
|
|
Ultima: {format(new Date(detection.lastSeen), "dd/MM HH:mm:ss")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-end gap-2">
|
|
{detection.blocked ? (
|
|
<Badge variant="destructive" className="flex items-center gap-1" data-testid={`badge-blocked-${detection.id}`}>
|
|
<Shield className="h-3 w-3" />
|
|
Bloccato
|
|
</Badge>
|
|
) : (
|
|
<Badge variant="outline" data-testid={`badge-active-${detection.id}`}>
|
|
Attivo
|
|
</Badge>
|
|
)}
|
|
|
|
<Button variant="outline" size="sm" asChild data-testid={`button-details-${detection.id}`}>
|
|
<a href={`/logs?ip=${detection.sourceIp}`}>
|
|
<Eye className="h-3 w-3 mr-1" />
|
|
Dettagli
|
|
</a>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-12 text-muted-foreground" data-testid="text-no-results">
|
|
<AlertTriangle className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
|
<p>Nessun rilevamento trovato</p>
|
|
{searchQuery && (
|
|
<p className="text-sm">Prova con un altro termine di ricerca</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|