From d9867db54690d87b11b2ee5e0bd3a9f2a6fbecc8 Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 31 Oct 2025 00:43:16 +0100 Subject: [PATCH] =?UTF-8?q?Fix:=20Mailbox=20Stats=20=C3=BCber=20Dovecot=20?= =?UTF-8?q?mit=20config/mailpool.php?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Livewire/Ui/Security/Fail2BanCard.php | 174 ++++++++++++++++-- .../Ui/Security/Modal/Fail2BanJailModal.php | 57 +++++- .../modal/fail2-ban-jail-modal.blade.php | 2 +- 3 files changed, 212 insertions(+), 21 deletions(-) diff --git a/app/Livewire/Ui/Security/Fail2BanCard.php b/app/Livewire/Ui/Security/Fail2BanCard.php index ef41d80..b4f4ccd 100644 --- a/app/Livewire/Ui/Security/Fail2BanCard.php +++ b/app/Livewire/Ui/Security/Fail2BanCard.php @@ -3,6 +3,7 @@ namespace App\Livewire\Ui\Security; +use Livewire\Attributes\On; use Livewire\Component; class Fail2BanCard extends Component @@ -22,6 +23,7 @@ class Fail2BanCard extends Component return view('livewire.ui.security.fail2-ban-card'); } + #[On('f2b:refresh-banlist')] public function refresh(): void { $this->load(true); @@ -35,67 +37,205 @@ class Fail2BanCard extends Component /* ------------------- intern ------------------- */ +// protected function load(bool $force = false): void +// { +// $bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: ''; +// if ($bin === '') { +// $this->available = false; +// $this->permDenied = false; +// $this->activeBans = 0; +// $this->jails = []; +// return; +// } +// +// [$ok, $raw] = $this->f2b('ping'); +// if (!$ok && stripos($raw, 'permission denied') !== false) { +// $this->available = true; +// $this->permDenied = true; +// $this->activeBans = 0; +// $this->jails = []; +// return; +// } +// +// [, $status] = $this->f2b('status'); +// $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status); +// $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : []; +// +// $rows = []; +// $sum = 0; +// foreach ($jails as $j) { +// [, $s] = $this->f2b('status '.escapeshellarg($j)); +// $bantime = $this->getBantime($j); +// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; +// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : []; +// +// $ipDetails = $this->buildIpDetails($j, $ips, $bantime); +// +// [, $s] = $this->f2b('status ' . escapeshellarg($j)); +// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0); +// $bantime = $this->getBantime($j); +// $rows[] = ['name' => $j, 'banned' => $banned, 'bantime' => $bantime]; +// $sum += $banned; +// } +// +// $this->available = true; +// $this->permDenied = false; +// $this->activeBans = $sum; +// $this->jails = $rows; +// } + protected function load(bool $force = false): void { $bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: ''; if ($bin === '') { - $this->available = false; + $this->available = false; $this->permDenied = false; $this->activeBans = 0; - $this->jails = []; + $this->jails = []; return; } + // Rechte prüfen [$ok, $raw] = $this->f2b('ping'); if (!$ok && stripos($raw, 'permission denied') !== false) { - $this->available = true; + $this->available = true; $this->permDenied = true; $this->activeBans = 0; - $this->jails = []; + $this->jails = []; return; } + // Jail-Liste [, $status] = $this->f2b('status'); $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status); - $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : []; + $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : []; $rows = []; - $sum = 0; + $sum = 0; + foreach ($jails as $j) { - [, $s] = $this->f2b('status ' . escapeshellarg($j)); - $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0); - $bantime = $this->getBantime($j); - $rows[] = ['name' => $j, 'banned' => $banned, 'bantime' => $bantime]; + $jEsc = escapeshellarg($j); + [, $s] = $this->f2b("status {$jEsc}"); + $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0); + $bantime = $this->getBantime($j); + $ipListLine = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; + $ips = $ipListLine !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipListLine)))) : []; + + // Details inkl. Restzeit je IP + $ipDetails = $this->buildIpDetails($j, $ips, $bantime); + + $rows[] = [ + 'name' => $j, + 'banned' => $banned, + 'bantime' => $bantime, // Sek. (-1 = permanent) + 'ips' => $ipDetails, // [['ip'=>..., 'remaining'=>..., 'until'=>...], ...] + ]; $sum += $banned; } - $this->available = true; + $this->available = true; $this->permDenied = false; $this->activeBans = $sum; - $this->jails = $rows; + $this->jails = $rows; } +// private function f2b(string $args): array +// { +// $sudo = '/usr/bin/sudo'; +// $f2b = '/usr/bin/fail2ban-client'; +// $out = (string)@shell_exec("timeout 2 $sudo -n $f2b $args 2>&1"); +// $ok = str_contains($out, 'Status') || str_contains($out, 'Jail list') || str_contains($out, 'pong'); +// return [$ok, $out]; +// } + private function f2b(string $args): array { $sudo = '/usr/bin/sudo'; - $f2b = '/usr/bin/fail2ban-client'; - $out = (string)@shell_exec("timeout 2 $sudo -n $f2b $args 2>&1"); - $ok = str_contains($out, 'Status') || str_contains($out, 'Jail list') || str_contains($out, 'pong'); + $f2b = '/usr/bin/fail2ban-client'; + $cmd = "timeout 3 $sudo -n $f2b $args 2>&1"; + $out = (string)@shell_exec($cmd); + + $ok = stripos($out, 'Status') !== false + || stripos($out, 'Jail list') !== false + || stripos($out, 'pong') !== false; + return [$ok, $out]; } + /** konfig. Bantime des Jails in Sekunden (-1 = permanent) */ private function getBantime(string $jail): int { - [, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime'); + [, $out] = $this->f2b('get '.escapeshellarg($jail).' bantime'); $val = trim($out); if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0]; - return 600; + return 600; // konservativer Fallback + } + +// private function getBantime(string $jail): int +// { +// [, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime'); +// $val = trim($out); +// if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0]; +// return 600; +// } + + /** baut Detail-Liste inkl. Restzeit */ + private function buildIpDetails(string $jail, array $ips, int $bantime): array + { + $now = time(); $out = []; + foreach ($ips as $ip) { + $banAt = $this->lastBanTimestamp($jail, $ip); + $remaining = null; $until = null; + + if ($bantime === -1) { + $remaining = -1; // permanent + } elseif ($banAt !== null) { + $remaining = max(0, $bantime - ($now - $banAt)); + $until = $remaining > 0 ? ($banAt + $bantime) : null; + } + + $out[] = ['ip' => $ip, 'remaining' => $remaining, 'until' => $until]; + } + return $out; + } + + /** letzte "Ban "-Zeile finden (Log + Rotation + journald), liefert Unix-Timestamp oder null */ + private function lastBanTimestamp(string $jail, string $ip): ?int + { + // 1) zgrep in /var/log/fail2ban.log* + $line = (string)@shell_exec( + "zgrep -h -E '\\[{$jail}\\]\\s+Ban\\s+{$ip}' /var/log/fail2ban.log* 2>/dev/null | tail -n 1" + ); + $line = trim($line); + + // 2) Fallback: journald + if ($line === '') { + $line = (string)@shell_exec( + "journalctl -u fail2ban --since '14 days ago' 2>/dev/null | grep -F '[{$jail}] Ban {$ip}' | tail -n 1" + ); + $line = trim($line); + } + if ($line === '') return null; + + // "YYYY-MM-DD HH:MM:SS,mmm ..." oder ISO im Text + if (preg_match('/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})/', $line, $m) + || preg_match('/(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})/', $line, $m)) { + $ts = strtotime($m[1].' '.$m[2]); + return $ts ?: null; + } + return null; } private function firstMatch(string $pattern, string $haystack): ?string { return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; } + + +// private function firstMatch(string $pattern, string $haystack): ?string +// { +// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; +// } } diff --git a/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php index c818dbf..dcb256c 100644 --- a/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php +++ b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php @@ -146,15 +146,41 @@ class Fail2BanJailModal extends ModalComponent /** letzte "Ban "-Zeile parsen -> Unix-Timestamp */ private function lastBanTimestamp(string $jail, string $ip): ?int { - $cmd = 'grep -F ' . escapeshellarg("[$jail] Ban $ip") - . ' /var/log/fail2ban.log 2>/dev/null | tail -n 1'; - $line = trim((string) @shell_exec($cmd)); + $jailQ = escapeshellarg($jail); + $ipQ = escapeshellarg($ip); + + // 1) Durchsuche fail2ban.log + rotierte + .gz + $cmdFiles = "sh -c 'ls -1 /var/log/fail2ban.log* 2>/dev/null | wc -l'"; + $haveFiles = ((int) @shell_exec($cmdFiles)) > 0; + + $line = ''; + if ($haveFiles) { + // zgrep -h: ohne Dateinamen; -E für Regex; tail -n1: letzte Ban-Zeile + $pattern = escapeshellarg(sprintf('\\[%s\\] Ban %s', $jail, $ip)); + $cmd = "zgrep -h -E \"\\[${jail}\\]\\s+Ban\\s+${ip}\" /var/log/fail2ban.log* 2>/dev/null | tail -n 1"; + $line = (string) @shell_exec($cmd); + } + + // 2) Fallback: journald (wenn kein Dateilog) + if (trim($line) === '') { + // seit 14 Tagen scannen, Format wie im file-log + $cmdJ = "journalctl -u fail2ban --since '14 days ago' 2>/dev/null | grep -F \"[{$jail}] Ban {$ip}\" | tail -n 1"; + $line = (string) @shell_exec($cmdJ); + } + + $line = trim($line); if ($line === '') return null; + // Datum am Anfang: 2025-10-29 18:07:11,436 ... if (preg_match('/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})/', $line, $m)) { $ts = strtotime($m[1].' '.$m[2]); return $ts ?: null; } + // journald kann anderes Präfix haben; versuche generischen ISO-Zeitstempel irgendwo in der Zeile + if (preg_match('/(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})/', $line, $m)) { + $ts = strtotime($m[1].' '.$m[2]); + return $ts ?: null; + } return null; } @@ -163,6 +189,31 @@ class Fail2BanJailModal extends ModalComponent return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; } + private function buildIpDetails(string $jail, array $ips, int $bantime): array + { + $out = []; + $now = time(); + + foreach ($ips as $ip) { + $banAt = $this->lastBanTimestamp($jail, $ip); + $remaining = null; $until = null; + + if ($bantime === -1) { + $remaining = -1; // permanent + } elseif ($banAt !== null) { + $remaining = max(0, $bantime - ($now - $banAt)); + $until = $remaining > 0 ? ($banAt + $bantime) : null; + } + + $out[] = [ + 'ip' => $ip, + 'remaining' => $remaining, // -1=permanent, null=unbekannt, 0..N Sekunden + 'until' => $until, + ]; + } + return $out; + } + private function fmtSecs(int $s): string { // kompakt: 1h 23m 45s / 05m 10s / 12s diff --git a/resources/views/livewire/ui/security/modal/fail2-ban-jail-modal.blade.php b/resources/views/livewire/ui/security/modal/fail2-ban-jail-modal.blade.php index f7feb3e..9cbb64e 100644 --- a/resources/views/livewire/ui/security/modal/fail2-ban-jail-modal.blade.php +++ b/resources/views/livewire/ui/security/modal/fail2-ban-jail-modal.blade.php @@ -32,7 +32,7 @@ @push('modal.footer')
-