Remove the "Dettagli" button and the Eye icon import from the Detections page as they are no longer needed. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 62bbad53-aa3a-4887-b48d-7203ea4974de Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/L6QSDnx
311 lines
14 KiB
TypeScript
311 lines
14 KiB
TypeScript
import { useQuery, useMutation } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Slider } from "@/components/ui/slider";
|
|
import { AlertTriangle, Search, Shield, Globe, MapPin, Building2, ShieldPlus } from "lucide-react";
|
|
import { format } from "date-fns";
|
|
import { useState } from "react";
|
|
import type { Detection } from "@shared/schema";
|
|
import { getFlag } from "@/lib/country-flags";
|
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
|
|
export default function Detections() {
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [anomalyTypeFilter, setAnomalyTypeFilter] = useState<string>("all");
|
|
const [minScore, setMinScore] = useState(0);
|
|
const [maxScore, setMaxScore] = useState(100);
|
|
const { toast } = useToast();
|
|
|
|
// Build query params
|
|
const queryParams = new URLSearchParams();
|
|
queryParams.set("limit", "5000");
|
|
if (anomalyTypeFilter !== "all") {
|
|
queryParams.set("anomalyType", anomalyTypeFilter);
|
|
}
|
|
if (minScore > 0) {
|
|
queryParams.set("minScore", minScore.toString());
|
|
}
|
|
if (maxScore < 100) {
|
|
queryParams.set("maxScore", maxScore.toString());
|
|
}
|
|
|
|
const { data: detections, isLoading } = useQuery<Detection[]>({
|
|
queryKey: ["/api/detections", anomalyTypeFilter, minScore, maxScore],
|
|
queryFn: () => fetch(`/api/detections?${queryParams.toString()}`).then(r => r.json()),
|
|
refetchInterval: 5000,
|
|
});
|
|
|
|
const filteredDetections = detections?.filter((d) =>
|
|
d.sourceIp.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
d.anomalyType.toLowerCase().includes(searchQuery.toLowerCase())
|
|
);
|
|
|
|
// Mutation per aggiungere a whitelist
|
|
const addToWhitelistMutation = useMutation({
|
|
mutationFn: async (detection: Detection) => {
|
|
return await apiRequest("POST", "/api/whitelist", {
|
|
ipAddress: detection.sourceIp,
|
|
reason: `Auto-added from detection: ${detection.anomalyType} (Risk: ${parseFloat(detection.riskScore).toFixed(1)})`
|
|
});
|
|
},
|
|
onSuccess: (_, detection) => {
|
|
toast({
|
|
title: "IP aggiunto alla whitelist",
|
|
description: `${detection.sourceIp} è stato aggiunto alla whitelist con successo.`,
|
|
});
|
|
queryClient.invalidateQueries({ queryKey: ["/api/whitelist"] });
|
|
queryClient.invalidateQueries({ queryKey: ["/api/detections"] });
|
|
},
|
|
onError: (error: any, detection) => {
|
|
toast({
|
|
title: "Errore",
|
|
description: error.message || `Impossibile aggiungere ${detection.sourceIp} alla whitelist.`,
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
});
|
|
|
|
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 flex-col gap-4">
|
|
<div className="flex items-center gap-4 flex-wrap">
|
|
<div className="relative flex-1 min-w-[200px]">
|
|
<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>
|
|
|
|
<Select value={anomalyTypeFilter} onValueChange={setAnomalyTypeFilter}>
|
|
<SelectTrigger className="w-[200px]" data-testid="select-anomaly-type">
|
|
<SelectValue placeholder="Tipo attacco" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Tutti i tipi</SelectItem>
|
|
<SelectItem value="ddos">DDoS Attack</SelectItem>
|
|
<SelectItem value="port_scan">Port Scanning</SelectItem>
|
|
<SelectItem value="brute_force">Brute Force</SelectItem>
|
|
<SelectItem value="botnet">Botnet Activity</SelectItem>
|
|
<SelectItem value="suspicious">Suspicious Activity</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-muted-foreground">Risk Score:</span>
|
|
<span className="font-medium" data-testid="text-score-range">
|
|
{minScore} - {maxScore}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-xs text-muted-foreground w-8">0</span>
|
|
<Slider
|
|
min={0}
|
|
max={100}
|
|
step={5}
|
|
value={[minScore, maxScore]}
|
|
onValueChange={([min, max]) => {
|
|
setMinScore(min);
|
|
setMaxScore(max);
|
|
}}
|
|
className="flex-1"
|
|
data-testid="slider-risk-score"
|
|
/>
|
|
<span className="text-xs text-muted-foreground w-8">100</span>
|
|
</div>
|
|
</div>
|
|
</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"
|
|
onClick={() => addToWhitelistMutation.mutate(detection)}
|
|
disabled={addToWhitelistMutation.isPending}
|
|
className="w-full"
|
|
data-testid={`button-whitelist-${detection.id}`}
|
|
>
|
|
<ShieldPlus className="h-3 w-3 mr-1" />
|
|
Whitelist
|
|
</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>
|
|
);
|
|
}
|