From 46591669d64d0251843eb2523f3e836f1c30d053 Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 31 Oct 2025 01:38:00 +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 | 332 ++++++++++---- .../Ui/Security/Modal/Fail2BanJailModal.php | 429 ++++++++++++------ .../ui/security/fail2-ban-card.blade.php | 88 +++- .../modal/fail2-ban-jail-modal.blade.php | 65 ++- 4 files changed, 666 insertions(+), 248 deletions(-) diff --git a/app/Livewire/Ui/Security/Fail2BanCard.php b/app/Livewire/Ui/Security/Fail2BanCard.php index 87d9f06..ccd0c1d 100644 --- a/app/Livewire/Ui/Security/Fail2BanCard.php +++ b/app/Livewire/Ui/Security/Fail2BanCard.php @@ -1,17 +1,18 @@ [...]]] public function mount(): void { @@ -31,7 +32,7 @@ class Fail2BanCard extends Component public function openDetails(string $jail): void { - // KORREKTER DISPATCH für wire-elements/modal + // wire-elements/modal (v2): Event-Namen + Component + Params $this->dispatch('openModal', component: 'ui.security.modal.fail2-ban-jail-modal', arguments: ['jail' => $jail]); } @@ -39,65 +40,76 @@ class Fail2BanCard extends Component protected function load(bool $force = false): void { + $this->available = true; + $this->permDenied = false; + $this->error = false; + $this->activeBans = 0; + $this->jails = []; + + // existiert fail2ban-client? $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 = []; + $this->available = false; return; } - // Rechte prüfen - [$ok, $raw] = $this->f2b('ping'); - if (!$ok && stripos($raw, 'permission denied') !== false) { - $this->available = true; + // Rechte / Erreichbarkeit + [, $ping] = $this->f2b('ping'); + if ($this->looksDenied($ping)) { $this->permDenied = true; - $this->activeBans = 0; - $this->jails = []; return; } - // Jail-Liste + // Jails lesen [, $status] = $this->f2b('status'); - $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status); - $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : []; + if ($this->looksDenied($status)) { + $this->permDenied = true; + return; + } + if (!preg_match('/Jail list:\s*(.+)$/mi', $status, $mm)) { + // etwas stimmt nicht – loggen und „error“ zeigen + $this->error = true; + Log::warning('Fail2BanCard: unexpected status output', ['status' => $status]); + return; + } + $jails = array_filter(array_map('trim', preg_split('/\s*,\s*/', $mm[1] ?? ''))); + $sum = 0; $rows = []; - $sum = 0; foreach ($jails as $j) { $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)))) : []; + [, $s] = $this->f2b("status {$jEsc}"); + if ($this->looksDenied($s)) { + $this->permDenied = true; + return; + } - // Details inkl. Restzeit je IP - $ipDetails = $this->buildIpDetails($j, $ips, $bantime); + $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0); + $bantime = $this->getBantime($j); + $ipLine = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; + $ips = $ipLine !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipLine)))) : []; $rows[] = [ - 'name' => $j, - 'banned' => $banned, - 'bantime' => $bantime, // Sek. (-1 = permanent) - 'ips' => $ipDetails, // [['ip'=>..., 'remaining'=>..., 'until'=>...], ...] + 'name' => $j, + 'banned' => $banned, + 'bantime' => $bantime, + // wir zeigen IPs NICHT mehr in der Card; Details sind im Modal + 'ips' => [], ]; $sum += $banned; } - $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'; - $cmd = "timeout 3 $sudo -n $f2b $args 2>&1"; - $out = (string)@shell_exec($cmd); + $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 @@ -106,81 +118,209 @@ class Fail2BanCard extends Component 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'); + if ($this->looksDenied($out)) { + $this->permDenied = true; + return 600; + } $val = trim($out); if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0]; - return 600; // konservativer Fallback + return 600; } - /** Letzten Ban-Zeitpunkt (Unix-Timestamp) aus /var/log/fail2ban.log ermitteln. */ - private function lastBanTimestamp(string $jail, string $ip): ?int + private function looksDenied(string $out): bool { - $file = '/var/log/fail2ban.log'; - if (!is_readable($file)) return null; - - // nur das Ende der Datei lesen (Performance, auch bei Rotation groß genug wählen) - $tailBytes = 400000; // 400 KB - $size = @filesize($file) ?: 0; - $seek = max(0, $size - $tailBytes); - - $fh = @fopen($file, 'rb'); - if (!$fh) return null; - if ($seek > 0) fseek($fh, $seek); - $data = stream_get_contents($fh) ?: ''; - fclose($fh); - - // Beispielzeile: - // 2025-10-30 22:34:20,797 fail2ban.actions [...] NOTICE [sshd] Ban 193.46.255.244 - $j = preg_quote($jail, '/'); - $p = preg_quote($ip, '/'); - $pattern = '/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2}),\d+.*\['.$j.'\]\s+Ban\s+'.$p.'\s*$/m'; - - if (preg_match_all($pattern, $data, $m) && !empty($m[1])) { - $date = end($m[1]); // YYYY-MM-DD - $time = end($m[2]); // HH:MM:SS - $dt = \DateTime::createFromFormat('Y-m-d H:i:s', "$date $time", new \DateTimeZone(date_default_timezone_get())); - return $dt ? $dt->getTimestamp() : null; - } - return null; + return preg_match('/(permission denied|not allowed to execute|a password is required)/i', $out) === 1; } - /** Baut Details inkl. Restzeit (Sekunden; -1 = permanent). */ - 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, // -1 = permanent, null = Ban-Zeitpunkt nicht gefunden, >=0 = Sekunden - 'until' => $until, // Unix-Timestamp oder null - ]; - } - return $out; - } - - private function firstMatch(string $pattern, string $haystack): ?string { return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; } } +//namespace App\Livewire\Ui\Security; +// +//use Livewire\Attributes\On; +//use Livewire\Component; +// +//class Fail2BanCard extends Component +//{ +// public bool $available = true; +// public bool $permDenied = false; +// public int $activeBans = 0; +// public array $jails = []; +// +// public function mount(): void +// { +// $this->load(); +// } +// +// public function render() +// { +// return view('livewire.ui.security.fail2-ban-card'); +// } +// +// #[On('f2b:refresh-banlist')] +// public function refresh(): void +// { +// $this->load(true); +// } +// +// public function openDetails(string $jail): void +// { +// // KORREKTER DISPATCH für wire-elements/modal +// $this->dispatch('openModal', component: 'ui.security.modal.fail2-ban-jail-modal', arguments: ['jail' => $jail]); +// } +// +// /* ------------------- 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; +// } +// +// // Rechte prüfen +// [$ok, $raw] = $this->f2b('ping'); +// if (!$ok && stripos($raw, 'permission denied') !== false) { +// $this->available = true; +// $this->permDenied = true; +// $this->activeBans = 0; +// $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))) : []; +// +// $rows = []; +// $sum = 0; +// +// foreach ($jails as $j) { +// $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->permDenied = false; +// $this->activeBans = $sum; +// $this->jails = $rows; +// } +// +// private function f2b(string $args): array +// { +// $sudo = '/usr/bin/sudo'; +// $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'); +// $val = trim($out); +// if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0]; +// return 600; // konservativer Fallback +// } +// +// /** Letzten Ban-Zeitpunkt (Unix-Timestamp) aus /var/log/fail2ban.log ermitteln. */ +// private function lastBanTimestamp(string $jail, string $ip): ?int +// { +// $file = '/var/log/fail2ban.log'; +// if (!is_readable($file)) return null; +// +// // nur das Ende der Datei lesen (Performance, auch bei Rotation groß genug wählen) +// $tailBytes = 400000; // 400 KB +// $size = @filesize($file) ?: 0; +// $seek = max(0, $size - $tailBytes); +// +// $fh = @fopen($file, 'rb'); +// if (!$fh) return null; +// if ($seek > 0) fseek($fh, $seek); +// $data = stream_get_contents($fh) ?: ''; +// fclose($fh); +// +// // Beispielzeile: +// // 2025-10-30 22:34:20,797 fail2ban.actions [...] NOTICE [sshd] Ban 193.46.255.244 +// $j = preg_quote($jail, '/'); +// $p = preg_quote($ip, '/'); +// $pattern = '/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2}),\d+.*\['.$j.'\]\s+Ban\s+'.$p.'\s*$/m'; +// +// if (preg_match_all($pattern, $data, $m) && !empty($m[1])) { +// $date = end($m[1]); // YYYY-MM-DD +// $time = end($m[2]); // HH:MM:SS +// $dt = \DateTime::createFromFormat('Y-m-d H:i:s', "$date $time", new \DateTimeZone(date_default_timezone_get())); +// return $dt ? $dt->getTimestamp() : null; +// } +// return null; +// } +// +// /** Baut Details inkl. Restzeit (Sekunden; -1 = permanent). */ +// 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, // -1 = permanent, null = Ban-Zeitpunkt nicht gefunden, >=0 = Sekunden +// 'until' => $until, // Unix-Timestamp oder null +// ]; +// } +// return $out; +// } +// +// +// private function firstMatch(string $pattern, string $haystack): ?string +// { +// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; +// } +//} + //namespace App\Livewire\Ui\Security; // diff --git a/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php index 778caa1..54ba644 100644 --- a/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php +++ b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php @@ -7,18 +7,7 @@ use LivewireUI\Modal\ModalComponent; class Fail2BanJailModal extends ModalComponent { public string $jail = ''; - /** @var arrayload(); } - public function refresh(): void - { - $this->load(true); - } - public function render() { return view('livewire.ui.security.modal.fail2-ban-jail-modal'); } + public function refresh(): void + { + $this->load(true); + } + /* ---------------- intern ---------------- */ protected function load(bool $force = false): void @@ -48,11 +37,10 @@ class Fail2BanJailModal extends ModalComponent $jail = $this->jail; // Aktuell gebannte IPs - [, $s] = $this->f2b('status '.escapeshellarg($jail)); + [, $s] = $this->f2b('status ' . escapeshellarg($jail)); $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : []; - // bantime für Jail $bantime = $this->getBantime($jail); $rows = []; @@ -61,24 +49,22 @@ class Fail2BanJailModal extends ModalComponent $remaining = null; $until = null; - if ($banAt !== null) { - if ($bantime === -1) { - $remaining = -1; - } else { - $remaining = max(0, $bantime - (time() - $banAt)); - $until = $remaining > 0 ? $banAt + $bantime : null; - } + + if ($bantime === -1) { + $remaining = -1; + } elseif ($banAt !== null) { + $remaining = max(0, $bantime - (time() - $banAt)); + $until = $remaining > 0 ? ($banAt + $bantime) : null; } - // Darstellung [$timeText, $metaText, $boxClass] = $this->present($remaining, $banAt, $until); $rows[] = [ - 'ip' => $ip, - 'bantime' => $bantime, + 'ip' => $ip, + 'bantime' => $bantime, 'banned_at' => $banAt, 'remaining' => $remaining, - 'until' => $until, + 'until' => $until, 'time_text' => $timeText, 'meta_text' => $metaText, 'box_class' => $boxClass, @@ -88,107 +74,13 @@ class Fail2BanJailModal extends ModalComponent $this->rows = $rows; } - /** letzte "Ban "-Zeile -> Unix-Timestamp (robust über zgrep/journal/jail-tail) */ - private function lastBanTimestamp(string $jail, string $ip): ?int - { - $sudo = '/usr/bin/sudo'; - $zgrep = '/usr/bin/zgrep'; - $journal = '/usr/bin/journalctl'; - $tail = '/usr/bin/tail'; - - $needle = sprintf('[%s] Ban %s', $jail, $ip); - $q = escapeshellarg($needle); - - // 1) zgrep über alle fail2ban-Logs (inkl. Rotation .N, .gz) - $cmd1 = "$sudo -n $zgrep -h $q /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; - $line = trim((string)@shell_exec($cmd1)); - - // 2) Fallback: journald (ohne grep-Pipe, mit -g) - if ($line === '') { - $cmd2 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager -g $q 2>/dev/null | $tail -n 1"; - $line = trim((string)@shell_exec($cmd2)); - } - - if ($line === '') return null; - - // Zeitstempel extrahieren (YYYY-MM-DD HH:MM:SS) - 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; - } - - /** baut Details inkl. Restzeit; wenn kein Timestamp gefunden: ~bantime als Approximation */ - 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; - } elseif ($banAt !== null) { - $remaining = max(0, $bantime - ($now - $banAt)); - $until = $remaining > 0 ? ($banAt + $bantime) : null; - } else { - // kein Ban-Timestamp gefunden → Approximation anzeigen - $remaining = -2; // Kennzeichen für "~bantime" - } - - $out[] = [ - 'ip' => $ip, - 'banned_at' => $banAt, - 'remaining' => $remaining, // -1=permanent, -2≈approx, 0..Sek, null=unbekannt - 'until' => $until, - ]; - } - return $out; - } - - /** Darstellung (permanent / Restzeit / abgelaufen / approx / unbekannt) */ - private function present(?int $remaining, ?int $banAt, ?int $until): array - { - $green = 'border-emerald-400/30 bg-emerald-500/10'; - $amber = 'border-amber-400/30 bg-amber-500/10'; - $rose = 'border-rose-400/30 bg-rose-500/10'; - $muted = 'border-white/10 bg-white/5'; - - if ($remaining === -1) { - return ['permanent', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $rose]; - } - if ($remaining === -2) { - // Approximation: Bannzeit ohne Startzeitpunkt - return ['~ '.$this->fmtSecs((int) $this->getApproxBantime()), '—', $amber]; - } - if (is_int($remaining)) { - if ($remaining > 0) { - $meta = []; - if ($banAt) $meta[] = 'seit '.$this->fmtTs($banAt); - if ($until) $meta[] = 'bis '.$this->fmtTs($until); - return [$this->fmtSecs($remaining), $meta ? implode(' • ', $meta) : '—', $amber]; - } - return ['abgelaufen', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $muted]; - } - return ['—', '—', $muted]; - } - -// Bantime als Approximation (du kannst das einfach aus dem aufrufenden Kontext übergeben; -// hier fallback 600) - private function getApproxBantime(): int { return 600; } - - /** Darstellung ableiten (Farbcode + Texte) */ - /** sudo + fail2ban-client */ private function f2b(string $args): array { $sudo = '/usr/bin/sudo'; - $f2b = '/usr/bin/fail2ban-client'; - $out = (string) @shell_exec("timeout 3 $sudo -n $f2b $args 2>&1"); - $ok = stripos($out, 'Status') !== false + $f2b = '/usr/bin/fail2ban-client'; + $out = (string)@shell_exec("timeout 3 $sudo -n $f2b $args 2>&1"); + $ok = stripos($out, 'Status') !== false || stripos($out, 'Jail list') !== false || stripos($out, 'pong') !== false; return [$ok, $out]; @@ -196,24 +88,68 @@ class Fail2BanJailModal extends ModalComponent 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; } - /** letzte "Ban "-Zeile parsen -> Unix-Timestamp */ - /** letzte "Ban "-Zeile -> Unix-Timestamp aus /var/log/fail2ban.log */ + /** robust: zgrep + journalctl via sudo */ + private function lastBanTimestamp(string $jail, string $ip): ?int + { + $sudo = '/usr/bin/sudo'; + $zgrep = '/usr/bin/zgrep'; + $journal = '/usr/bin/journalctl'; + $tail = '/usr/bin/tail'; + + $needle = sprintf('[%s] Ban %s', $jail, $ip); + $q = escapeshellarg($needle); + + // Logs inkl. Rotation (gz) + $cmd1 = "$sudo -n $zgrep -h $q /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; + $line = trim((string)@shell_exec($cmd1)); + + if ($line === '') { // Fallback: journald + $cmd2 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager -g $q 2>/dev/null | $tail -n 1"; + $line = trim((string)@shell_exec($cmd2)); + } + if ($line === '') return null; + + 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; + } private function firstMatch(string $pattern, string $haystack): ?string { return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; } + private function present(?int $remaining, ?int $banAt, ?int $until): array + { + $amber = 'border-amber-400/30 bg-amber-500/10'; + $rose = 'border-rose-400/30 bg-rose-500/10'; + $muted = 'border-white/10 bg-white/5'; + + if ($remaining === -1) { + return ['permanent', $banAt ? ('seit ' . $this->fmtTs($banAt)) : '—', $rose]; + } + if (is_int($remaining)) { + if ($remaining > 0) { + $meta = []; + if ($banAt) $meta[] = 'seit ' . $this->fmtTs($banAt); + if ($until) $meta[] = 'bis ' . $this->fmtTs($until); + return [$this->fmtSecs($remaining), $meta ? implode(' • ', $meta) : '—', $amber]; + } + return ['abgelaufen', $banAt ? ('seit ' . $this->fmtTs($banAt)) : '—', $muted]; + } + return ['—', '—', $muted]; + } private function fmtSecs(int $s): string { - // kompakt: 1h 23m 45s / 05m 10s / 12s $h = intdiv($s, 3600); $m = intdiv($s % 3600, 60); $r = $s % 60; @@ -224,7 +160,236 @@ class Fail2BanJailModal extends ModalComponent private function fmtTs(int $ts): string { - // 29.10. 18:07 return date('d.m. H:i', $ts); } } + +// +//namespace App\Livewire\Ui\Security\Modal; +// +//use LivewireUI\Modal\ModalComponent; +// +//class Fail2BanJailModal extends ModalComponent +//{ +// public string $jail = ''; +// /** @var arrayjail = $jail; +// $this->load(); +// } +// +// public function refresh(): void +// { +// $this->load(true); +// } +// +// public function render() +// { +// return view('livewire.ui.security.modal.fail2-ban-jail-modal'); +// } +// +// /* ---------------- intern ---------------- */ +// +// protected function load(bool $force = false): void +// { +// $jail = $this->jail; +// +// // Aktuell gebannte IPs +// [, $s] = $this->f2b('status '.escapeshellarg($jail)); +// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; +// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : []; +// +// // bantime für Jail +// $bantime = $this->getBantime($jail); +// +// $rows = []; +// foreach ($ips as $ip) { +// $banAt = $this->lastBanTimestamp($jail, $ip); // Unix-Timestamp oder null +// +// $remaining = null; +// $until = null; +// if ($banAt !== null) { +// if ($bantime === -1) { +// $remaining = -1; +// } else { +// $remaining = max(0, $bantime - (time() - $banAt)); +// $until = $remaining > 0 ? $banAt + $bantime : null; +// } +// } +// +// // Darstellung +// [$timeText, $metaText, $boxClass] = $this->present($remaining, $banAt, $until); +// +// $rows[] = [ +// 'ip' => $ip, +// 'bantime' => $bantime, +// 'banned_at' => $banAt, +// 'remaining' => $remaining, +// 'until' => $until, +// 'time_text' => $timeText, +// 'meta_text' => $metaText, +// 'box_class' => $boxClass, +// ]; +// } +// +// $this->rows = $rows; +// } +// +// /** letzte "Ban "-Zeile -> Unix-Timestamp (robust über zgrep/journal/jail-tail) */ +// private function lastBanTimestamp(string $jail, string $ip): ?int +// { +// $sudo = '/usr/bin/sudo'; +// $zgrep = '/usr/bin/zgrep'; +// $journal = '/usr/bin/journalctl'; +// $tail = '/usr/bin/tail'; +// +// $needle = sprintf('[%s] Ban %s', $jail, $ip); +// $q = escapeshellarg($needle); +// +// // 1) zgrep über alle fail2ban-Logs (inkl. Rotation .N, .gz) +// $cmd1 = "$sudo -n $zgrep -h $q /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; +// $line = trim((string)@shell_exec($cmd1)); +// +// // 2) Fallback: journald (ohne grep-Pipe, mit -g) +// if ($line === '') { +// $cmd2 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager -g $q 2>/dev/null | $tail -n 1"; +// $line = trim((string)@shell_exec($cmd2)); +// } +// +// if ($line === '') return null; +// +// // Zeitstempel extrahieren (YYYY-MM-DD HH:MM:SS) +// 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; +// } +// +// /** baut Details inkl. Restzeit; wenn kein Timestamp gefunden: ~bantime als Approximation */ +// 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; +// } elseif ($banAt !== null) { +// $remaining = max(0, $bantime - ($now - $banAt)); +// $until = $remaining > 0 ? ($banAt + $bantime) : null; +// } else { +// // kein Ban-Timestamp gefunden → Approximation anzeigen +// $remaining = -2; // Kennzeichen für "~bantime" +// } +// +// $out[] = [ +// 'ip' => $ip, +// 'banned_at' => $banAt, +// 'remaining' => $remaining, // -1=permanent, -2≈approx, 0..Sek, null=unbekannt +// 'until' => $until, +// ]; +// } +// return $out; +// } +// +// /** Darstellung (permanent / Restzeit / abgelaufen / approx / unbekannt) */ +// private function present(?int $remaining, ?int $banAt, ?int $until): array +// { +// $green = 'border-emerald-400/30 bg-emerald-500/10'; +// $amber = 'border-amber-400/30 bg-amber-500/10'; +// $rose = 'border-rose-400/30 bg-rose-500/10'; +// $muted = 'border-white/10 bg-white/5'; +// +// if ($remaining === -1) { +// return ['permanent', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $rose]; +// } +// if ($remaining === -2) { +// // Approximation: Bannzeit ohne Startzeitpunkt +// return ['~ '.$this->fmtSecs((int) $this->getApproxBantime()), '—', $amber]; +// } +// if (is_int($remaining)) { +// if ($remaining > 0) { +// $meta = []; +// if ($banAt) $meta[] = 'seit '.$this->fmtTs($banAt); +// if ($until) $meta[] = 'bis '.$this->fmtTs($until); +// return [$this->fmtSecs($remaining), $meta ? implode(' • ', $meta) : '—', $amber]; +// } +// return ['abgelaufen', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $muted]; +// } +// return ['—', '—', $muted]; +// } +// +//// Bantime als Approximation (du kannst das einfach aus dem aufrufenden Kontext übergeben; +//// hier fallback 600) +// private function getApproxBantime(): int { return 600; } +// +// /** Darstellung ableiten (Farbcode + Texte) */ +// +// /** sudo + fail2ban-client */ +// private function f2b(string $args): array +// { +// $sudo = '/usr/bin/sudo'; +// $f2b = '/usr/bin/fail2ban-client'; +// $out = (string) @shell_exec("timeout 3 $sudo -n $f2b $args 2>&1"); +// $ok = stripos($out, 'Status') !== false +// || stripos($out, 'Jail list') !== false +// || stripos($out, 'pong') !== false; +// return [$ok, $out]; +// } +// +// 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; +// } +// +// /** letzte "Ban "-Zeile parsen -> Unix-Timestamp */ +// /** letzte "Ban "-Zeile -> Unix-Timestamp aus /var/log/fail2ban.log */ +// +// private function firstMatch(string $pattern, string $haystack): ?string +// { +// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; +// } +// +// +// private function fmtSecs(int $s): string +// { +// // kompakt: 1h 23m 45s / 05m 10s / 12s +// $h = intdiv($s, 3600); +// $m = intdiv($s % 3600, 60); +// $r = $s % 60; +// if ($h > 0) return sprintf('%dh %02dm %02ds', $h, $m, $r); +// if ($m > 0) return sprintf('%02dm %02ds', $m, $r); +// return sprintf('%ds', $r); +// } +// +// private function fmtTs(int $ts): string +// { +// // 29.10. 18:07 +// return date('d.m. H:i', $ts); +// } +//} diff --git a/resources/views/livewire/ui/security/fail2-ban-card.blade.php b/resources/views/livewire/ui/security/fail2-ban-card.blade.php index 9d5daca..2e2e3b7 100644 --- a/resources/views/livewire/ui/security/fail2-ban-card.blade.php +++ b/resources/views/livewire/ui/security/fail2-ban-card.blade.php @@ -19,11 +19,19 @@ @if(!$available)
fail2ban-client wurde nicht gefunden.
+ @elseif($permDenied)
- Keine Berechtigung auf /var/run/fail2ban/fail2ban.sock. + Keine Berechtigung (sudo) auf fail2ban-client/journalctl/zgrep. Sudo-Regel prüfen.
+ + @elseif($error) +
+ Unerwartete Ausgabe von fail2ban-client status. + Details in storage/logs/laravel.log. +
+ @else
@forelse($jails as $j) @@ -32,19 +40,13 @@
{{ $j['name'] }}
- Bannzeit: - @if($j['bantime'] === -1) - permanent - @else - {{ $j['bantime'] }}s - @endif + Bannzeit: {{ $j['bantime'] === -1 ? 'permanent' : ($j['bantime'].'s') }} {{ $j['banned'] }} gebannt - {{-- fix: stop event bubbling --}}
--}} +{{-- @if(!$available)--}} +{{--
fail2ban-client wurde nicht gefunden.
--}} +{{-- @elseif($permDenied)--}} +{{--
--}} +{{-- Keine Berechtigung auf /var/run/fail2ban/fail2ban.sock.--}} +{{-- Sudo-Regel prüfen.--}} +{{--
--}} +{{-- @else--}} +{{--
--}} +{{-- @forelse($jails as $j)--}} +{{--
--}} +{{--
--}} +{{--
{{ $j['name'] }}
--}} +{{--
--}} +{{-- --}} +{{-- Bannzeit:--}} +{{-- @if($j['bantime'] === -1)--}} +{{-- permanent--}} +{{-- @else--}} +{{-- {{ $j['bantime'] }}s--}} +{{-- @endif--}} +{{-- --}} +{{-- --}} +{{-- {{ $j['banned'] }} gebannt--}} +{{-- --}} + +{{-- --}}{{-- fix: stop event bubbling --}} +{{-- --}} +{{--
--}} +{{--
--}} +{{--
--}} +{{-- @empty--}} +{{--
Keine Jails gefunden.
--}} +{{-- @endforelse--}} +{{--
--}} + +{{--
--}} +{{-- --}} +{{--
--}} +{{-- @endif--}} +{{--
--}} + +{{--
--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{-- Fail2Ban--}} +{{--
--}} + +{{-- @if($available)--}} +{{-- --}} +{{-- {{ $activeBans }} aktuell--}} +{{-- --}} +{{-- @else--}} +{{-- --}} +{{-- nicht installiert--}} +{{-- --}} +{{-- @endif--}} +{{--
--}} + {{-- @if(!$available)--}} {{--
fail2ban-client wurde nicht gefunden.
--}} {{-- @elseif($permDenied)--}} 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 9cbb64e..b2f868d 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 @@ -1,11 +1,7 @@ @push('modal.header')
-

- Fail2Ban – {{ $jail }} -

-

- Aktuell gebannte IPs und Restlaufzeiten. -

+

Fail2Ban – {{ $jail }}

+

Aktuell gebannte IPs und Restlaufzeiten.

@endpush @@ -14,13 +10,9 @@
{{ $r['ip'] }}
-
- {{ $r['time_text'] }} -
-
-
- {{ $r['meta_text'] }} +
{{ $r['time_text'] }}
+
{{ $r['meta_text'] }}
@empty
@@ -32,6 +24,7 @@ @push('modal.footer')
+ {{-- WICHTIG: refresht NUR das Modal --}}
@endpush + +{{--@push('modal.header')--}} +{{--
--}} +{{--

--}} +{{-- Fail2Ban – {{ $jail }}--}} +{{--

--}} +{{--

--}} +{{-- Aktuell gebannte IPs und Restlaufzeiten.--}} +{{--

--}} +{{--
--}} +{{--@endpush--}} + +{{--
--}} +{{-- @forelse($rows as $r)--}} +{{--
--}} +{{--
--}} +{{--
{{ $r['ip'] }}
--}} +{{--
--}} +{{-- {{ $r['time_text'] }}--}} +{{--
--}} +{{--
--}} +{{--
--}} +{{-- {{ $r['meta_text'] }}--}} +{{--
--}} +{{--
--}} +{{-- @empty--}} +{{--
--}} +{{-- Keine gebannten IPs in diesem Jail.--}} +{{--
--}} +{{-- @endforelse--}} +{{--
--}} + +{{--@push('modal.footer')--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{-- --}} +{{--
--}} +{{--
--}} +{{--@endpush--}}