diff --git a/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php index 1d55acd..b6a9568 100644 --- a/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php +++ b/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php @@ -88,41 +88,110 @@ class Fail2BanJailModal extends ModalComponent $this->rows = $rows; } - /** Darstellung ableiten (Farbcode + Texte) */ + /** letzte "Ban "-Zeile -> Unix-Timestamp (robust über zgrep/journal/jail-tail) */ + private function lastBanTimestamp(string $jail, string $ip): ?int + { + // 1) zgrep über alle Logfiles (inkl. Rotation .N und .gz) + $j = escapeshellarg($jail); + $p = escapeshellarg($ip); + $cmd = "zgrep -h \"[${jail}] Ban ${ip}\" /var/log/fail2ban.log* 2>/dev/null | tail -n 1"; + $line = trim((string) @shell_exec($cmd)); + + // 2) Fallback: journald der letzten 14 Tage + if ($line === '') { + $cmdJ = "journalctl -u fail2ban --since '14 days ago' 2>/dev/null | grep -F \"[{$jail}] Ban {$ip}\" | tail -n 1"; + $line = trim((string) @shell_exec($cmdJ)); + } + + // 3) Fallback: PHP-Tail nur aktuelles file + if ($line === '') { + $file = '/var/log/fail2ban.log'; + if (!is_readable($file)) return null; + $tailBytes = 400000; + $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); + if (preg_match('/^.*\['.preg_quote($jail,'/').'\]\s+Ban\s+'.preg_quote($ip,'/').'.*$/m', $data, $m)) { + $line = trim($m[0]); + } + } + + if ($line === '') return null; + + // Zeitstempel irgendwo in der Zeile (ISO-ähnlich) nehmen + 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 { - // 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]; + 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) { - $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]; + $meta = []; + if ($banAt) $meta[] = 'seit '.$this->fmtTs($banAt); + if ($until) $meta[] = 'bis '.$this->fmtTs($until); + return [$this->fmtSecs($remaining), $meta ? implode(' • ', $meta) : '—', $amber]; } - - // 0 Sekunden -> theoretisch abgelaufen - $time = 'abgelaufen'; - $meta = $banAt ? ('seit '.$this->fmtTs($banAt)) : '—'; - return [$time, $meta, $muted]; + return ['abgelaufen', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $muted]; } - - // keine Info 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 { @@ -145,66 +214,12 @@ class Fail2BanJailModal extends ModalComponent /** letzte "Ban "-Zeile parsen -> Unix-Timestamp */ /** letzte "Ban "-Zeile -> Unix-Timestamp aus /var/log/fail2ban.log */ - private function lastBanTimestamp(string $jail, string $ip): ?int - { - $file = '/var/log/fail2ban.log'; - if (!is_readable($file)) return null; - - // nur das Dateiende lesen (performant, auch bei großen Logs) - $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); - - // Beispiel: 2025-10-30 22:34:20,797 ... [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]); - $time = end($m[2]); - $dt = \DateTime::createFromFormat('Y-m-d H:i:s', "$date $time", - new \DateTimeZone(date_default_timezone_get())); - return $dt ? $dt->getTimestamp() : null; - } - return null; - } private function firstMatch(string $pattern, string $haystack): ?string { 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 {