From 792f0e352899ec81227a5566667e6309fff620f0 Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 31 Oct 2025 03:01:17 +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 | 187 ++++- .../Ui/Security/Modal/Fail2BanJailModal.php | 766 +++++++++++++++--- 2 files changed, 832 insertions(+), 121 deletions(-) diff --git a/app/Livewire/Ui/Security/Fail2BanCard.php b/app/Livewire/Ui/Security/Fail2BanCard.php index ccd0c1d..1ffbfd8 100644 --- a/app/Livewire/Ui/Security/Fail2BanCard.php +++ b/app/Livewire/Ui/Security/Fail2BanCard.php @@ -1,5 +1,6 @@ [...]]] + public array $jails = []; public function mount(): void { @@ -32,42 +33,36 @@ class Fail2BanCard extends Component public function openDetails(string $jail): void { - // wire-elements/modal (v2): Event-Namen + Component + Params $this->dispatch('openModal', component: 'ui.security.modal.fail2-ban-jail-modal', arguments: ['jail' => $jail]); } - /* ------------------- intern ------------------- */ + /* ---------------- intern ---------------- */ protected function load(bool $force = false): void { - $this->available = true; - $this->permDenied = false; - $this->error = false; + $this->available = $this->permDenied = $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; return; } + $this->available = true; - // Rechte / Erreichbarkeit [, $ping] = $this->f2b('ping'); if ($this->looksDenied($ping)) { $this->permDenied = true; return; } - // Jails lesen [, $status] = $this->f2b('status'); 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; @@ -87,16 +82,8 @@ class Fail2BanCard extends Component $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, - // wir zeigen IPs NICHT mehr in der Card; Details sind im Modal - 'ips' => [], - ]; + $rows[] = ['name' => $j, 'banned' => $banned, 'bantime' => $bantime, 'ips' => []]; $sum += $banned; } @@ -106,8 +93,8 @@ class Fail2BanCard extends Component private function f2b(string $args): array { - $sudo = '/usr/bin/sudo'; - $f2b = '/usr/bin/fail2ban-client'; + $sudo = $this->bin('sudo'); + $f2b = $this->bin('fail2ban-client'); $cmd = "timeout 3 $sudo -n $f2b $args 2>&1"; $out = (string)@shell_exec($cmd); @@ -125,22 +112,168 @@ class Fail2BanCard extends Component $this->permDenied = true; return 600; } - $val = trim($out); - if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0]; + if (preg_match('/-?\d+/', trim($out), $m)) return (int)$m[0]; return 600; } private function looksDenied(string $out): bool { - return preg_match('/(permission denied|not allowed to execute|a password is required)/i', $out) === 1; + return (bool)preg_match('/(permission denied|not allowed to execute|a password is required)/i', $out); } private function firstMatch(string $pattern, string $haystack): ?string { return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; } + + private function bin(string $name): string + { + $p = trim((string)@shell_exec("command -v " . escapeshellarg($name) . " 2>/dev/null")); + return $p !== '' ? $p : $name; + } } +//namespace App\Livewire\Ui\Security; +// +//use Illuminate\Support\Facades\Log; +//use Livewire\Attributes\On; +//use Livewire\Component; +// +//class Fail2BanCard extends Component +//{ +// public bool $available = true; // fail2ban-client vorhanden? +// public bool $permDenied = false; // sudo / Socket-Rechte fehlen? +// public bool $error = false; // anderer Fehler (Output unerwartet) +// public int $activeBans = 0; +// public array $jails = []; // [['name','banned','bantime','ips'=>[...]]] +// +// 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 +// { +// // wire-elements/modal (v2): Event-Namen + Component + Params +// $this->dispatch('openModal', component: 'ui.security.modal.fail2-ban-jail-modal', arguments: ['jail' => $jail]); +// } +// +// /* ------------------- intern ------------------- */ +// +// 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; +// return; +// } +// +// // Rechte / Erreichbarkeit +// [, $ping] = $this->f2b('ping'); +// if ($this->looksDenied($ping)) { +// $this->permDenied = true; +// return; +// } +// +// // Jails lesen +// [, $status] = $this->f2b('status'); +// 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 = []; +// +// foreach ($jails as $j) { +// $jEsc = escapeshellarg($j); +// [, $s] = $this->f2b("status {$jEsc}"); +// if ($this->looksDenied($s)) { +// $this->permDenied = true; +// return; +// } +// +// $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, +// // wir zeigen IPs NICHT mehr in der Card; Details sind im Modal +// 'ips' => [], +// ]; +// $sum += $banned; +// } +// +// $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]; +// } +// +// private function getBantime(string $jail): int +// { +// [, $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; +// } +// +// private function looksDenied(string $out): bool +// { +// return preg_match('/(permission denied|not allowed to execute|a password is required)/i', $out) === 1; +// } +// +// 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; diff --git a/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php index 54ba644..790a448 100644 --- a/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php +++ b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php @@ -7,28 +7,13 @@ use LivewireUI\Modal\ModalComponent; class Fail2BanJailModal extends ModalComponent { public string $jail = ''; - public array $rows = []; // [{ip,bantime,banned_at,remaining,until,time_text,meta_text,box_class}] + public array $rows = []; - public static function modalMaxWidth(): string - { - return '4xl'; - } + public static function modalMaxWidth(): string { return '4xl'; } - public function mount(string $jail): void - { - $this->jail = $jail; - $this->load(); - } - - public function render() - { - return view('livewire.ui.security.modal.fail2-ban-jail-modal'); - } - - public function refresh(): void - { - $this->load(true); - } + public function mount(string $jail): void { $this->jail = $jail; $this->load(); } + public function refresh(): void { $this->load(true); } + public function render() { return view('livewire.ui.security.modal.fail2-ban-jail-modal'); } /* ---------------- intern ---------------- */ @@ -36,8 +21,7 @@ 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)))) : []; @@ -47,24 +31,24 @@ class Fail2BanJailModal extends ModalComponent foreach ($ips as $ip) { $banAt = $this->lastBanTimestamp($jail, $ip); // Unix-Timestamp oder null - $remaining = null; - $until = null; - + $remaining = null; $until = null; if ($bantime === -1) { - $remaining = -1; + $remaining = -1; // permanent } elseif ($banAt !== null) { $remaining = max(0, $bantime - (time() - $banAt)); - $until = $remaining > 0 ? ($banAt + $bantime) : null; + $until = $remaining > 0 ? $banAt + $bantime : null; + } else { + $remaining = -2; // ≈ Approximation (Bantime bekannt, Start unbekannt) } [$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, @@ -74,13 +58,98 @@ class Fail2BanJailModal extends ModalComponent $this->rows = $rows; } - /** sudo + fail2ban-client */ + /** ROBUST: findet Binaries automatisch */ + private function bin(string $name): string + { + $p = trim((string)@shell_exec("command -v ".escapeshellarg($name)." 2>/dev/null")); + return $p !== '' ? $p : $name; + } + + /** letzte "Ban "-Zeile → Unix-Timestamp (mit Fallbacks) */ + private function lastBanTimestamp(string $jail, string $ip): ?int + { + $sudo = $this->bin('sudo'); + $zgrep = $this->bin('zgrep'); + $grep = $this->bin('grep'); + $tail = $this->bin('tail'); + $journal = $this->bin('journalctl'); + + // 1) exakter Treffer: "[jail] Ban IP" + $needleExact = sprintf('[%s] Ban %s', $jail, $ip); + $qExact = escapeshellarg($needleExact); + $cmd1 = "$sudo -n $zgrep -h -F $qExact /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; + $line = trim((string)@shell_exec($cmd1)); + + // 2) Fallback: nur "Ban IP" + if ($line === '') { + $qLoose = escapeshellarg("Ban $ip"); + $cmd2 = "$sudo -n $zgrep -h -F $qLoose /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; + $line = trim((string)@shell_exec($cmd2)); + } + + // 3) Fallback: unkomprimiertes Log + if ($line === '') { + $cmd3 = "$sudo -n $grep -h -F $qExact /var/log/fail2ban.log 2>/dev/null | $tail -n 1"; + $line = trim((string)@shell_exec($cmd3)); + if ($line === '') { + $cmd3b = "$sudo -n $grep -h -F ".escapeshellarg("Ban $ip")." /var/log/fail2ban.log 2>/dev/null | $tail -n 1"; + $line = trim((string)@shell_exec($cmd3b)); + } + } + + // 4) Fallback: journald + if ($line === '') { + $cmd4 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager | $grep -F $qExact | $tail -n 1"; + $line = trim((string)@shell_exec($cmd4)); + if ($line === '') { + $cmd4b = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager | $grep -F ".escapeshellarg("Ban $ip")." | $tail -n 1"; + $line = trim((string)@shell_exec($cmd4b)); + } + } + + if ($line === '') return null; + + // "2025-10-25 11:22:05,958 ..." → Millisekunden ignorieren + 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; + } + + /** Darstellung: permanent / Restzeit / abgelaufen / ~approx / unbekannt */ + 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 ($remaining === -2) { + return ['~ '.$this->fmtSecs($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]; + } + + private function getApproxBantime(): int { return 600; } + 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 + $sudo = $this->bin('sudo'); + $f2b = $this->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]; @@ -88,38 +157,9 @@ 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; - } - - /** 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; + return preg_match('/-?\d+/', $val, $m) ? (int)$m[0] : 600; } private function firstMatch(string $pattern, string $haystack): ?string @@ -127,43 +167,581 @@ class Fail2BanJailModal extends ModalComponent 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 { - $h = intdiv($s, 3600); - $m = intdiv($s % 3600, 60); - $r = $s % 60; + $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 - { - return date('d.m. H:i', $ts); - } + private function fmtTs(int $ts): string { return date('d.m. H:i', $ts); } } +//namespace App\Livewire\Ui\Security\Modal; +// +//use LivewireUI\Modal\ModalComponent; +// +//class Fail2BanJailModal extends ModalComponent +//{ +// public string $jail = ''; +// public array $rows = []; +// +// public static function modalMaxWidth(): string +// { +// return '4xl'; +// } +// +// public function mount(string $jail): void +// { +// $this->jail = $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 + Bantime +// [, $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 = $this->getBantime($jail); // Sek., -1 = permanent +// +// $rows = []; +// foreach ($ips as $ip) { +// $banAt = $this->lastBanTimestamp($jail, $ip); // Unix-TS oder null +// +// $remaining = null; +// $until = null; +// if ($bantime === -1) { +// $remaining = -1; +// } elseif ($banAt !== null) { +// $remaining = max(0, $bantime - (time() - $banAt)); +// $until = $remaining > 0 ? ($banAt + $bantime) : null; +// } else { +// // kein exakter Timestamp auffindbar → als Approximation ausweisen +// $remaining = -2; // Kennzeichen für "~ Bantime" +// } +// +// [$timeText, $metaText, $boxClass] = $this->present($remaining, $banAt, $until, $bantime); +// +// $rows[] = [ +// 'ip' => $ip, +// 'bantime' => $bantime, +// 'banned_at' => $banAt, +// 'remaining' => $remaining, // -1=permanent, -2≈approx, 0..Sek, null=unbekannt +// 'until' => $until, +// 'time_text' => $timeText, +// 'meta_text' => $metaText, +// 'box_class' => $boxClass, +// ]; +// } +// +// $this->rows = $rows; +// } +// +// /** robust: findet Pfade automatisch, mehrere Fallbacks */ +// private function lastBanTimestamp(string $jail, string $ip): ?int +// { +// $sudo = $this->bin('sudo'); +// $zgrep = $this->bin('zgrep'); +// $grep = $this->bin('grep'); +// $tail = $this->bin('tail'); +// $journal = $this->bin('journalctl'); +// +// // 1) exakte Suche: "[jail] Ban IP" +// $needleExact = sprintf('[%s] Ban %s', $jail, $ip); +// $qExact = escapeshellarg($needleExact); +// $cmd1 = "$sudo -n $zgrep -h -F $qExact /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; +// $line = trim((string)@shell_exec($cmd1)); +// +// // 2) locker: nur "Ban IP" (falls der Jail-Tag im Log anders ist) +// if ($line === '') { +// $qLoose = escapeshellarg("Ban $ip"); +// $cmd2 = "$sudo -n $zgrep -h -F $qLoose /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; +// $line = trim((string)@shell_exec($cmd2)); +// } +// +// // 3) unkomprimiertes Log als Fallback +// if ($line === '') { +// $cmd3 = "$sudo -n $grep -h -F $qExact /var/log/fail2ban.log 2>/dev/null | $tail -n 1"; +// $line = trim((string)@shell_exec($cmd3)); +// if ($line === '') { +// $cmd3b = "$sudo -n $grep -h -F " . escapeshellarg("Ban $ip") . " /var/log/fail2ban.log 2>/dev/null | $tail -n 1"; +// $line = trim((string)@shell_exec($cmd3b)); +// } +// } +// +// // 4) journald +// if ($line === '') { +// $cmd4 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager | $grep -F $qExact | $tail -n 1"; +// $line = trim((string)@shell_exec($cmd4)); +// if ($line === '') { +// $cmd4b = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager | $grep -F " . escapeshellarg("Ban $ip") . " | $tail -n 1"; +// $line = trim((string)@shell_exec($cmd4b)); +// } +// } +// +// if ($line === '') return null; +// +// // 2025-10-30 22:34:59,491 ... -> Datum/Zeit extrahieren +// 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; +// } +// +// /** Anzeige-Logik */ +// private function present(?int $remaining, ?int $banAt, ?int $until, int $bantime): 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 ($remaining === -2) { +// // nur ungefähre Bantime verfügbar +// return ['~ ' . $this->fmtSecs($bantime), '—', $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]; +// } +// +// /** sudo + fail2ban-client */ +// private function f2b(string $args): array +// { +// $sudo = $this->bin('sudo'); +// $f2b = $this->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; +// } +// +// private function firstMatch(string $pattern, string $haystack): ?string +// { +// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; +// } +// +// /** findet den Pfad zu einem Binary robust */ +// private function bin(string $name): string +// { +// $p = trim((string)@shell_exec('command -v ' . escapeshellarg($name) . ' 2>/dev/null')); +// return $p !== '' ? $p : $name; +// } +// +// private function fmtSecs(int $s): string +// { +// $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 +// { +// return date('d.m. H:i', $ts); +// } +//} + +//namespace App\Livewire\Ui\Security\Modal; +// +//use LivewireUI\Modal\ModalComponent; +// +//class Fail2BanJailModal extends ModalComponent +//{ +// public string $jail = ''; +// public array $rows = []; +// +// public static function modalMaxWidth(): string +// { +// return '4xl'; +// } +// +// public function mount(string $jail): void +// { +// $this->jail = $jail; +// $this->load(); +// } +// +// 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 +// { +// $jail = $this->jail; +// +// [, $s] = $this->f2b('status ' . escapeshellarg($jail)); +// $ipLine = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; +// $ips = $ipLine !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipLine)))) : []; +// +// $bantime = $this->getBantime($jail); +// +// $rows = []; +// foreach ($ips as $ip) { +// $banAt = $this->lastBanTimestamp($jail, $ip); // null wenn nicht gefunden +// +// $remaining = null; +// $until = null; +// if ($bantime === -1) { +// $remaining = -1; +// } elseif ($banAt !== null) { +// $remaining = max(0, $bantime - (time() - $banAt)); +// $until = $remaining > 0 ? ($banAt + $bantime) : null; +// } +// +// [$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; +// } +// +// /** ---- helpers ---- */ +// +// private function f2b(string $args): array +// { +// $sudo = $this->bin('sudo'); +// $f2b = $this->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'); +// if (preg_match('/-?\d+/', trim($out), $m)) return (int)$m[0]; +// return 600; +// } +// +// /** findet die letzte "Ban "-Zeile in log / rotierte .gz / journald */ +// private function lastBanTimestamp(string $jail, string $ip): ?int +// { +// $sudo = $this->bin('sudo'); +// $zgrep = $this->bin('zgrep'); // findet /bin/zgrep ODER /usr/bin/zgrep +// $grep = $this->bin('grep'); +// $tail = $this->bin('tail'); +// $journal = $this->bin('journalctl'); +// +// // 1) exakter Treffer: "[jail] Ban IP" +// $needleExact = sprintf('[%s] Ban %s', $jail, $ip); +// $qExact = escapeshellarg($needleExact); +// +// $cmd1 = "$sudo -n $zgrep -h -F $qExact /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; +// $line = trim((string) @shell_exec($cmd1)); +// +// // 2) Fallback: nur "Ban IP" (falls Jail-Name im Log abweicht) +// if ($line === '') { +// $needleLoose = sprintf('Ban %s', $ip); +// $qLoose = escapeshellarg($needleLoose); +// $cmd2 = "$sudo -n $zgrep -h -F $qLoose /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; +// $line = trim((string) @shell_exec($cmd2)); +// } +// +// // 3) Fallback: unkomprimiertes Log mit grep +// if ($line === '') { +// $cmd3 = "$sudo -n $grep -h -F $qExact /var/log/fail2ban.log 2>/dev/null | $tail -n 1"; +// $line = trim((string) @shell_exec($cmd3)); +// if ($line === '') { +// $cmd3b = "$sudo -n $grep -h -F ".escapeshellarg("Ban $ip")." /var/log/fail2ban.log 2>/dev/null | $tail -n 1"; +// $line = trim((string) @shell_exec($cmd3b)); +// } +// } +// +// // 4) Fallback: journald +// if ($line === '') { +// $cmd4 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager | $grep -F $qExact | $tail -n 1"; +// $line = trim((string) @shell_exec($cmd4)); +// if ($line === '') { +// $cmd4b = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager | $grep -F ".escapeshellarg("Ban $ip")." | $tail -n 1"; +// $line = trim((string) @shell_exec($cmd4b)); +// } +// } +// +// if ($line === '') return null; +// +// // Zeitstempel extrahieren (YYYY-MM-DD HH:MM:SS[,ms] egal) +// 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 ($remaining === -2) { +// // zeigt ungefähre Bantime, falls Timestamp nicht gefunden wurde +// return ['~ '.$this->fmtSecs($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]; +// } +// +// private function getApproxBantime(): int { return 600; } +// +// private function fmtSecs(int $s): string +// { +// $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 +// { +// return date('d.m. H:i', $ts); +// } +// +// private function bin(string $name): string +// { +// $p = trim((string)@shell_exec("command -v ".escapeshellarg($name)." 2>/dev/null")); +// return $p !== '' ? $p : $name; +// } +//} + +//namespace App\Livewire\Ui\Security\Modal; +// +//use LivewireUI\Modal\ModalComponent; +// +//class Fail2BanJailModal extends ModalComponent +//{ +// public string $jail = ''; +// public array $rows = []; // [{ip,bantime,banned_at,remaining,until,time_text,meta_text,box_class}] +// +// public static function modalMaxWidth(): string +// { +// return '4xl'; +// } +// +// public function mount(string $jail): void +// { +// $this->jail = $jail; +// $this->load(); +// } +// +// 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 +// { +// $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 = $this->getBantime($jail); +// +// $rows = []; +// foreach ($ips as $ip) { +// $banAt = $this->lastBanTimestamp($jail, $ip); // Unix-Timestamp oder null +// +// $remaining = null; +// $until = null; +// +// if ($bantime === -1) { +// $remaining = -1; +// } elseif ($banAt !== null) { +// $remaining = max(0, $bantime - (time() - $banAt)); +// $until = $remaining > 0 ? ($banAt + $bantime) : null; +// } +// +// [$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; +// } +// +// /** 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; +// } +// +// /** 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 +// { +// $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 +// { +// return date('d.m. H:i', $ts); +// } +//} + // //namespace App\Livewire\Ui\Security\Modal; //