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:
@@ -32,6 +32,7 @@ import AdminNotifications from "./pages/admin/AdminNotifications";
|
||||
import AdminAttestations from "./pages/admin/AdminAttestations";
|
||||
import HistoriqueAttestations from "./pages/HistoriqueAttestations";
|
||||
import AdminGestionAttestations from "./pages/AdminGestionAttestations";
|
||||
import AdminFail2Ban from "./pages/admin/AdminFail2Ban";
|
||||
import AdminParametres from "./pages/AdminParametres";
|
||||
import QuestionnaireReponse from "./pages/QuestionnaireReponse";
|
||||
import Inscription from "./pages/Inscription";
|
||||
@@ -71,6 +72,7 @@ function Router() {
|
||||
<Route path={"/admin/rappels"} component={AdminRappels} />
|
||||
<Route path={"/admin/rappels/historique"} component={AdminRappelsHistorique} />
|
||||
<Route path={"/admin/rappels/statistiques"} component={AdminRappelsStats} />
|
||||
<Route path={"/admin/fail2ban"} component={AdminFail2Ban} />
|
||||
<Route path={"/admin/analytics"} component={AdminAnalytics} />
|
||||
<Route path={"/admin/etablissements"} component={AdminEtablissements} />
|
||||
<Route path={"/admin/import-excel"} component={AdminImportExcel} />
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
} from "@/components/ui/sidebar";
|
||||
import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const";
|
||||
import { useIsMobile } from "@/hooks/useMobile";
|
||||
import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, CalendarDays, Bell, BarChart3, UserCog, Mail, Settings, TrendingUp, Building2, FileSpreadsheet, ClipboardList, ChevronDown, User, FileText, QrCode } from "lucide-react";
|
||||
import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, CalendarDays, Bell, BarChart3, UserCog, Mail, Settings, TrendingUp, Building2, FileSpreadsheet, ClipboardList, ChevronDown, User, FileText, QrCode, Shield } from "lucide-react";
|
||||
import { CSSProperties, useEffect, useRef, useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
|
||||
@@ -69,6 +69,7 @@ const menuSections = [
|
||||
{ icon: Settings, label: "Configuration SMTP", path: "/admin/email-config" },
|
||||
{ icon: Settings, label: "Paramètres", path: "/admin/parametres" },
|
||||
{ icon: FileText, label: "Attestations de formation", path: "/admin/attestations" },
|
||||
{ icon: Shield, label: "Sécurité SSH (Fail2Ban)", path: "/admin/fail2ban" },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
239
client/src/pages/admin/AdminFail2Ban.tsx
Normal file
239
client/src/pages/admin/AdminFail2Ban.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user