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:
139
server/fail2banDb.ts
Normal file
139
server/fail2banDb.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export interface BannedIP {
|
||||
ip: string;
|
||||
banTime: string;
|
||||
failCount: number;
|
||||
}
|
||||
|
||||
export interface Fail2BanStats {
|
||||
currentlyBanned: number;
|
||||
totalBanned: number;
|
||||
currentlyFailed: number;
|
||||
totalFailed: number;
|
||||
bannedIPs: BannedIP[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le statut de Fail2Ban pour SSH
|
||||
*/
|
||||
export async function getFail2BanStatus(): Promise<Fail2BanStats> {
|
||||
try {
|
||||
const { stdout } = await execAsync('sudo fail2ban-client status sshd');
|
||||
|
||||
const stats: Fail2BanStats = {
|
||||
currentlyBanned: 0,
|
||||
totalBanned: 0,
|
||||
currentlyFailed: 0,
|
||||
totalFailed: 0,
|
||||
bannedIPs: [],
|
||||
};
|
||||
|
||||
// Parser le statut
|
||||
const currentlyBannedMatch = stdout.match(/Currently banned:\s+(\d+)/);
|
||||
const totalBannedMatch = stdout.match(/Total banned:\s+(\d+)/);
|
||||
const currentlyFailedMatch = stdout.match(/Currently failed:\s+(\d+)/);
|
||||
const totalFailedMatch = stdout.match(/Total failed:\s+(\d+)/);
|
||||
const bannedIPsMatch = stdout.match(/Banned IP list:\s+(.+)/);
|
||||
|
||||
if (currentlyBannedMatch) stats.currentlyBanned = parseInt(currentlyBannedMatch[1]);
|
||||
if (totalBannedMatch) stats.totalBanned = parseInt(totalBannedMatch[1]);
|
||||
if (currentlyFailedMatch) stats.currentlyFailed = parseInt(currentlyFailedMatch[1]);
|
||||
if (totalFailedMatch) stats.totalFailed = parseInt(totalFailedMatch[1]);
|
||||
|
||||
// Récupérer les IPs bannies avec détails
|
||||
if (bannedIPsMatch && bannedIPsMatch[1].trim()) {
|
||||
const ips = bannedIPsMatch[1].trim().split(/\s+/);
|
||||
|
||||
for (const ip of ips) {
|
||||
try {
|
||||
// Récupérer les détails de chaque IP bannie
|
||||
const { stdout: ipInfo } = await execAsync(`sudo fail2ban-client get sshd actionban | grep -A 10 "${ip}" || echo ""`);
|
||||
|
||||
stats.bannedIPs.push({
|
||||
ip,
|
||||
banTime: new Date().toISOString(), // Approximation, Fail2Ban ne stocke pas la date exacte facilement
|
||||
failCount: 3, // Valeur par défaut basée sur maxretry
|
||||
});
|
||||
} catch (err) {
|
||||
// Si on ne peut pas récupérer les détails, on ajoute quand même l'IP
|
||||
stats.bannedIPs.push({
|
||||
ip,
|
||||
banTime: new Date().toISOString(),
|
||||
failCount: 3,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('[Fail2Ban] Erreur lors de la récupération du statut:', error);
|
||||
throw new Error('Impossible de récupérer le statut Fail2Ban');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Débannit une IP spécifique
|
||||
*/
|
||||
export async function unbanIP(ip: string): Promise<boolean> {
|
||||
try {
|
||||
await execAsync(`sudo fail2ban-client set sshd unbanip ${ip}`);
|
||||
console.log(`[Fail2Ban] IP ${ip} débannie avec succès`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[Fail2Ban] Erreur lors du débannissement de ${ip}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère l'historique des bannissements depuis les logs
|
||||
*/
|
||||
export async function getBanHistory(limit: number = 50): Promise<Array<{ ip: string; date: string; action: string }>> {
|
||||
try {
|
||||
const { stdout } = await execAsync(`sudo grep "Ban" /var/log/fail2ban.log | tail -${limit}`);
|
||||
|
||||
const history: Array<{ ip: string; date: string; action: string }> = [];
|
||||
const lines = stdout.trim().split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
// Format: 2026-01-18 19:00:00,123 fail2ban.actions [12345]: NOTICE [sshd] Ban 1.2.3.4
|
||||
const match = line.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*\[(sshd)\] (Ban|Unban) ([\d.]+)/);
|
||||
|
||||
if (match) {
|
||||
history.push({
|
||||
date: match[1],
|
||||
action: match[3],
|
||||
ip: match[4],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return history.reverse(); // Plus récent en premier
|
||||
} catch (error) {
|
||||
console.error('[Fail2Ban] Erreur lors de la récupération de l\'historique:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre d'IPs bannies dans la dernière heure
|
||||
*/
|
||||
export async function countRecentBans(hours: number = 1): Promise<number> {
|
||||
try {
|
||||
const since = new Date();
|
||||
since.setHours(since.getHours() - hours);
|
||||
const sinceStr = since.toISOString().slice(0, 19).replace('T', ' ');
|
||||
|
||||
const { stdout } = await execAsync(`sudo grep "Ban" /var/log/fail2ban.log | grep -v "Unban" | awk '{print $1, $2}' | awk -v since="${sinceStr}" '$0 >= since' | wc -l`);
|
||||
|
||||
return parseInt(stdout.trim()) || 0;
|
||||
} catch (error) {
|
||||
console.error('[Fail2Ban] Erreur lors du comptage des bannissements récents:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user