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); } }