diff --git a/app/Livewire/Ui/Security/Fail2BanCard.php b/app/Livewire/Ui/Security/Fail2BanCard.php index 5251949..a3c2045 100644 --- a/app/Livewire/Ui/Security/Fail2BanCard.php +++ b/app/Livewire/Ui/Security/Fail2BanCard.php @@ -31,9 +31,8 @@ class Fail2BanCard extends Component // Optional: öffnet später dein Detail-Modal/Tab public function openDetails(string $jail): void { - $this->dispatch('security:open-f2b', jail: $jail); + $this->dispatch('openModal', component: 'ui.security.modal.fail2-ban-jail-modal', arguments: ['jail' => $jail]); } - /* ---------------- intern ---------------- */ protected function load(bool $force = false): void diff --git a/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php new file mode 100644 index 0000000..304fef0 --- /dev/null +++ b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php @@ -0,0 +1,182 @@ +jail = $jail; + $this->load(); + } + + public function refresh(): void + { + $this->load(true); + } + + public function render() + { + return view('livewire.ui.security.modal.fail2ban-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; + } + + /** Darstellung ableiten (Farbcode + Texte) */ + private function present(?int $remaining, ?int $banAt, ?int $until): array + { + // Farben an dein Schema angelehnt + $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) { + $time = 'permanent'; + $meta = $banAt ? ('seit '.$this->fmtTs($banAt)) : '—'; + return [$time, $meta, $rose]; + } + + if (is_int($remaining)) { + if ($remaining > 0) { + $time = $this->fmtSecs($remaining); + $parts = []; + if ($banAt) $parts[] = 'seit '.$this->fmtTs($banAt); + if ($until) $parts[] = 'bis '.$this->fmtTs($until); + $meta = $parts ? implode(' • ', $parts) : '—'; + return [$time, $meta, $amber]; + } + + // 0 Sekunden -> theoretisch abgelaufen + $time = 'abgelaufen'; + $meta = $banAt ? ('seit '.$this->fmtTs($banAt)) : '—'; + return [$time, $meta, $muted]; + } + + // keine Info + return ['—', '—', $muted]; + } + + /** 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 */ + 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)); + if ($line === '') return null; + + 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; + } + return null; + } + + 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/app/Livewire/Ui/Security/Modal/Fail2BanJailModal/Php.php b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal/Php.php new file mode 100644 index 0000000..b5a347b --- /dev/null +++ b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal/Php.php @@ -0,0 +1,13 @@ +{{ $j['name'] }}
- - Bannzeit: - @if($j['bantime'] === -1) - permanent - @else - {{ $j['bantime'] }}s - @endif - {{ $j['banned'] }} gebannt 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 new file mode 100644 index 0000000..f7feb3e --- /dev/null +++ b/resources/views/livewire/ui/security/modal/fail2-ban-jail-modal.blade.php @@ -0,0 +1,47 @@ +@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