Checkpoint: Système complet de monitoring Fail2Ban déployé avec succès. Tableau de bord admin affichant en temps réel les IPs bannies, statistiques d'attaques, et historique des bannissements. Fonctionnalité de débannissement manuel d'IPs. Système d'alertes email automatiques configuré : envoie une notification au propriétaire lorsque plus de 10 IPs sont bannies en 1 heure. Vérification toutes les 15 minutes. Déployé et testé sur le VPS de production avec logs confirmant l'initialisation correcte.

This commit is contained in:
Manus
2026-01-18 13:26:55 -05:00
parent cd18e2873a
commit 4b407b9c83
8 changed files with 610 additions and 1 deletions

View File

@@ -0,0 +1,239 @@
import { useState } from "react";
import { trpc } from "@/lib/trpc";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Shield, ShieldAlert, Ban, CheckCircle2, AlertTriangle, RefreshCw } from "lucide-react";
import { toast } from "sonner";
export default function AdminFail2Ban() {
const [refreshKey, setRefreshKey] = useState(0);
// Récupérer le statut Fail2Ban
const { data: status, isLoading: statusLoading, refetch: refetchStatus } = trpc.fail2ban.getStatus.useQuery(undefined, {
refetchInterval: 30000, // Rafraîchir toutes les 30 secondes
});
// Récupérer l'historique
const { data: history, isLoading: historyLoading, refetch: refetchHistory } = trpc.fail2ban.getHistory.useQuery({ limit: 50 });
// Compter les bannissements récents
const { data: recentBans } = trpc.fail2ban.countRecentBans.useQuery({ hours: 1 });
// Mutation pour débannir une IP
const unbanMutation = trpc.fail2ban.unbanIP.useMutation({
onSuccess: () => {
toast.success("IP débannie avec succès");
refetchStatus();
refetchHistory();
},
onError: (error) => {
toast.error(`Erreur: ${error.message}`);
},
});
const handleRefresh = () => {
refetchStatus();
refetchHistory();
setRefreshKey(prev => prev + 1);
toast.info("Données actualisées");
};
const handleUnban = (ip: string) => {
if (confirm(`Êtes-vous sûr de vouloir débannir l'IP ${ip} ?`)) {
unbanMutation.mutate({ ip });
}
};
if (statusLoading) {
return (
<div className="container mx-auto py-8">
<div className="flex items-center justify-center h-64">
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
</div>
);
}
const alertLevel = recentBans && recentBans >= 10 ? "danger" : recentBans && recentBans >= 5 ? "warning" : "safe";
return (
<div className="container mx-auto py-8 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Monitoring Fail2Ban</h1>
<p className="text-muted-foreground">Surveillance des tentatives d'intrusion SSH</p>
</div>
<Button onClick={handleRefresh} variant="outline">
<RefreshCw className="h-4 w-4 mr-2" />
Actualiser
</Button>
</div>
{/* Alerte si attaque massive */}
{alertLevel === "danger" && (
<Alert variant="destructive">
<ShieldAlert className="h-4 w-4" />
<AlertDescription>
<strong>Attaque massive détectée !</strong> {recentBans} IPs bannies dans la dernière heure.
</AlertDescription>
</Alert>
)}
{alertLevel === "warning" && (
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Activité suspecte détectée.</strong> {recentBans} IPs bannies dans la dernière heure.
</AlertDescription>
</Alert>
)}
{/* Statistiques globales */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">IPs Bannies</CardTitle>
<Ban className="h-4 w-4 text-destructive" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{status?.currentlyBanned || 0}</div>
<p className="text-xs text-muted-foreground">Actuellement bloquées</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Banni</CardTitle>
<ShieldAlert className="h-4 w-4 text-orange-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{status?.totalBanned || 0}</div>
<p className="text-xs text-muted-foreground">Depuis le démarrage</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Tentatives Échouées</CardTitle>
<AlertTriangle className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{status?.currentlyFailed || 0}</div>
<p className="text-xs text-muted-foreground">En cours</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Dernière Heure</CardTitle>
<Shield className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{recentBans || 0}</div>
<p className="text-xs text-muted-foreground">Bannissements récents</p>
</CardContent>
</Card>
</div>
{/* Liste des IPs bannies */}
<Card>
<CardHeader>
<CardTitle>IPs Actuellement Bannies</CardTitle>
<CardDescription>
{status?.currentlyBanned || 0} adresse(s) IP bloquée(s)
</CardDescription>
</CardHeader>
<CardContent>
{status?.bannedIPs && status.bannedIPs.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>Adresse IP</TableHead>
<TableHead>Tentatives</TableHead>
<TableHead>Date de bannissement</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{status.bannedIPs.map((ban) => (
<TableRow key={ban.ip}>
<TableCell className="font-mono">{ban.ip}</TableCell>
<TableCell>
<Badge variant="destructive">{ban.failCount}</Badge>
</TableCell>
<TableCell>
{new Date(ban.banTime).toLocaleString('fr-FR')}
</TableCell>
<TableCell className="text-right">
<Button
size="sm"
variant="outline"
onClick={() => handleUnban(ban.ip)}
disabled={unbanMutation.isPending}
>
<CheckCircle2 className="h-4 w-4 mr-1" />
Débannir
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="text-center py-8 text-muted-foreground">
<Shield className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Aucune IP bannie actuellement</p>
</div>
)}
</CardContent>
</Card>
{/* Historique des bannissements */}
<Card>
<CardHeader>
<CardTitle>Historique des Bannissements</CardTitle>
<CardDescription>
50 dernières actions Fail2Ban
</CardDescription>
</CardHeader>
<CardContent>
{historyLoading ? (
<div className="text-center py-8">
<RefreshCw className="h-8 w-8 animate-spin mx-auto text-muted-foreground" />
</div>
) : history && history.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Action</TableHead>
<TableHead>Adresse IP</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{history.map((entry, index) => (
<TableRow key={index}>
<TableCell>{entry.date}</TableCell>
<TableCell>
<Badge variant={entry.action === "Ban" ? "destructive" : "default"}>
{entry.action}
</Badge>
</TableCell>
<TableCell className="font-mono">{entry.ip}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="text-center py-8 text-muted-foreground">
<p>Aucun historique disponible</p>
</div>
)}
</CardContent>
</Card>
</div>
);
}