*/ public array $jails = []; public function mount(): void { $this->load(); } public function render() { return view('livewire.ui.security.fail2-ban-card'); } public function refresh(): void { $this->load(true); } // Optional: öffnet später dein Detail-Modal/Tab public function openDetails(string $jail): void { $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; } // Rechtecheck [$ok, $raw] = $this->f2b('ping'); if (!$ok && stripos($raw, 'permission denied') !== false) { $this->available = true; $this->permDenied = true; $this->activeBans = 0; $this->jails = []; return; } // Jails laden [, $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) { [, $s] = $this->f2b('status ' . escapeshellarg($j)); $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0); $bantime = $this->getBantime($j); // Sek.; -1 = permanent $rows[] = ['name' => $j, 'banned' => $banned, 'bantime' => $bantime]; $sum += $banned; } $this->available = true; $this->permDenied = false; $this->activeBans = $sum; $this->jails = $rows; } /** sudo + fail2ban-client ausführen; [ok, output] */ private function f2b(string $args): array { $sudo = '/usr/bin/sudo'; $f2b = '/usr/bin/fail2ban-client'; $out = (string)@shell_exec("timeout 2 $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; // defensiver Default } 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\Component; // //class Fail2BanCard extends Component //{ // public bool $available = true; // fail2ban-client vorhanden? // public bool $permDenied = false; // Socket/Root-Rechte fehlen? // public int $activeBans = 0; // Summe gebannter IPs über alle Jails // public array $jails = []; // [['name','banned','bantime','ips'=>[['ip','remaining','until'],...]],...] // public array $topIps = []; // [['ip'=>'x.x.x.x','count'=>N], ...] // // public function mount(): void // { // $this->load(); // } // // public function render() // { // return view('livewire.ui.security.fail2-ban-card'); // } // // /** Button „Neu prüfen“ */ // public function refresh(): void // { // $this->load(true); // } // // /* --------------------- intern --------------------- */ // // protected function load(bool $force = false): void // { // // 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->topIps = []; // return; // } // // // ping → prüft zugleich Rechte (bei Permission-Fehler kommt Klartext) // [$ok, $raw] = $this->f2b('ping'); // ok == "pong" erkannt // if (!$ok && stripos($raw, 'permission denied') !== false) { // $this->available = true; // $this->permDenied = true; // $this->activeBans = 0; // $this->jails = []; // $this->topIps = $this->collectTopIps(); // return; // } // // // Jails auflisten // [, $status] = $this->f2b('status'); // $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status); // $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : []; // // $total = 0; // $rows = []; // // foreach ($jails as $j) { // $bantimeSecs = $this->getBantime($j); // Sek., -1 = permanent // // [, $s] = $this->f2b('status ' . escapeshellarg($j)); // $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0); // $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; // $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : []; // // // Restzeiten je IP bestimmen (aus /var/log/fail2ban.log) // $ipDetails = []; // foreach (array_slice($ips, 0, 50) as $ip) { // $banAt = $this->lastBanTimestamp($j, $ip); // Unix-Timestamp oder null // $remaining = null; // $until = null; // // if ($banAt !== null) { // if ((int)$bantimeSecs === -1) { // $remaining = -1; // permanent // } else { // $remaining = max(0, $bantimeSecs - (time() - $banAt)); // $until = $remaining > 0 ? ($banAt + $bantimeSecs) : null; // } // } // // $ipDetails[] = [ // 'ip' => $ip, // 'remaining' => $remaining, // -1 = permanent, 0 = abgelaufen, >0 Sek. // 'until' => $until, // Unix-Timestamp oder null // ]; // } // // $rows[] = [ // 'name' => $j, // 'banned' => $banned, // 'ips' => $ipDetails, // 'bantime' => (int)$bantimeSecs, // ]; // $total += $banned; // } // // $this->available = true; // $this->permDenied = false; // $this->activeBans = $total; // $this->jails = $rows; // $this->topIps = $this->collectTopIps(); // } // // /** führt fail2ban-client via sudo aus; gibt [ok, output] zurück */ // private function f2b(string $args): array // { // $sudo = '/usr/bin/sudo'; // $f2b = '/usr/bin/fail2ban-client'; // $cmd = "timeout 2 $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'); // $val = trim($out); // if (preg_match('/-?\d+/', $val, $m)) { // return (int)$m[0]; // } // return 600; // defensiver Default // } // // /** letzte Ban-Zeile aus /var/log/fail2ban.log → Unix-Timestamp */ // private function lastBanTimestamp(string $jail, string $ip): ?int // { // $cmd = "grep -F \"[{$jail}] Ban {$ip}\" /var/log/fail2ban.log 2>/dev/null | tail -n 1"; // $line = trim((string)@shell_exec($cmd)); // if ($line === '') return null; // // // "YYYY-MM-DD HH:MM:SS,mmm ..." // 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; // } // // /** Top-IPs grob zählen (aus der aktuellen Jail-Liste; Fallback: Log) */ // private function collectTopIps(): array // { // $map = []; // foreach ($this->jails as $jail) { // foreach ($jail['ips'] as $row) { // $ip = $row['ip'] ?? null; // if (!$ip) continue; // $map[$ip] = ($map[$ip] ?? 0) + 1; // } // } // // if (!empty($map)) { // arsort($map); // $out = []; // foreach (array_slice($map, 0, 5, true) as $ip => $count) { // $out[] = ['ip' => $ip, 'count' => $count]; // } // return $out; // } // // // Fallback: aus fail2ban.log // $cmd = 'grep -Eo "([0-9]{1,3}\.){3}[0-9]{1,3}" /var/log/fail2ban.log 2>/dev/null' // . ' | sort | uniq -c | sort -nr | head -5'; // $log = (string)@shell_exec($cmd); // $rows = []; // if ($log !== '') { // foreach (preg_split('/\R+/', trim($log)) as $l) { // if (preg_match('/^\s*(\d+)\s+(\d+\.\d+\.\d+\.\d+)/', $l, $m)) { // $rows[] = ['ip' => $m[2], 'count' => (int)$m[1]]; // } // } // } // return $rows; // } //} // //namespace App\Livewire\Ui\Security; // //use Livewire\Component; // //class Fail2BanCard extends Component //{ // public bool $available = true; // fail2ban-client vorhanden? // public bool $permDenied = false; // Socket/Root-Rechte fehlen? // public int $activeBans = 0; // Summe gebannter IPs über alle Jails // public array $jails = []; // [['name'=>'sshd','banned'=>2,'ips'=>['1.2.3.4',...]], ...] // public array $topIps = []; // [['ip'=>'x.x.x.x','count'=>N], ...] // // public function mount(): void // { // $this->load(); // } // // public function render() // { // return view('livewire.ui.security.fail2-ban-card'); // } // // // Wird vom Button "Neu prüfen" genutzt // public function refresh(): void // { // $this->load(true); // } // // /* --------------------- intern --------------------- */ // // protected function load(bool $force = false): void // { // // 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->topIps = []; // return; // } // // // ping → prüft zugleich Rechte (bei Permission-Fehler kommt Klartext) // [$ok, $raw] = $this->f2b('ping'); // ok == "pong" erkannt // if (!$ok && stripos($raw, 'permission denied') !== false) { // $this->available = true; // $this->permDenied = true; // $this->activeBans = 0; // $this->jails = []; // $this->topIps = $this->collectTopIps(); // return; // } // // // Jails auflisten // [, $status] = $this->f2b('status'); // $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status); // $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : []; // // $total = 0; $rows = []; //// ... in load() NACH dem Einlesen der Jail-Liste: // $rows = []; // foreach ($jails as $j) { // $bantimeSecs = $this->getBantime($j); // konfigurierter Wert (Sekunden, -1 = permanent) // // [, $s] = $this->f2b('status '.escapeshellarg($j)); // $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0); // $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; // $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : []; // // // Restzeiten je IP bestimmen (aus /var/log/fail2ban.log) // $ipDetails = []; // foreach (array_slice($ips, 0, 50) as $ip) { // $banAt = $this->lastBanTimestamp($j, $ip); // Unix-Timestamp oder null // $remaining = null; // $until = null; // // if ($banAt !== null) { // if ((int)$bantimeSecs === -1) { // $remaining = -1; // permanent // } else { // $remaining = max(0, $bantimeSecs - (time() - $banAt)); // $until = $remaining > 0 ? ($banAt + $bantimeSecs) : null; // } // } // // $ipDetails[] = [ // 'ip' => $ip, // 'remaining' => $remaining, // -1 = permanent, 0 = abgelaufen, >0 Sek. // 'until' => $until, // Unix-Timestamp oder null // ]; // } // // $rows[] = [ // 'name' => $j, // 'banned' => $banned, // 'ips' => $ipDetails, // jetzt mit Details // 'bantime' => (int)$bantimeSecs, // ]; // $total += $banned; // } // // // foreach ($jails as $j) { //// [, $s] = $this->f2b('status '.escapeshellarg($j)); //// $banned = (int) ($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0); //// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; //// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : []; //// $rows[] = ['name'=>$j,'banned'=>$banned,'ips'=>array_slice($ips, 0, 8)]; //// $total += $banned; //// } // // // // $this->available = true; // $this->permDenied = false; // $this->activeBans = $total; // $this->jails = $rows; // $this->topIps = $this->collectTopIps(); // } // // /** führt fail2ban-client via sudo aus; gibt [ok, output] zurück */ // private function f2b(string $args): array // { // $sudo = '/usr/bin/sudo'; // $f2b = '/usr/bin/fail2ban-client'; // $cmd = "timeout 2 $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'); // // fail2ban liefert Seconds als Zahl (oder mit Newline) // $val = trim($out); // // Fallback: manche Versionen geben nur Zahl ohne Kontext zurück, // // sonst aus jail.local ermitteln wäre overkill -> einfache Zahl extrahieren: // if (preg_match('/-?\d+/', $val, $m)) { // return (int)$m[0]; // } // // wenn nicht ermittelbar: 600 Sekunden als conservative default // return 600; // } // // /** Sucht die letzte "Ban "-Zeile für Jail in /var/log/fail2ban.log und gibt Unix-Timestamp zurück. */ // private function lastBanTimestamp(string $jail, string $ip): ?int // { // // Beispiel-Logzeilen: // // 2025-10-29 18:07:11,436 fail2ban.actions [12345]: NOTICE [sshd] Ban 1.2.3.4 // // Wir holen die letzte passende Zeile (tail mit grep), dann parsen Datum. // $pattern = escapeshellarg(sprintf('\\[%s\\] Ban %s', $jail, $ip)); // $cmd = "grep -F \"[{$jail}] Ban {$ip}\" /var/log/fail2ban.log 2>/dev/null | tail -n 1"; // $line = (string)@shell_exec($cmd); // $line = trim($line); // if ($line === '') { // return null; // } // // Datumsformat am Anfang: "YYYY-MM-DD HH:MM:SS,mmm" // 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; // } // // /** Zählt die häufigsten IPs aus den letzten Fail2Ban-Logs (ban/unban Events) */ // private function collectTopIps(): array // { // // 1. Versuch: IPs direkt aus den Jails // $rows = []; // foreach ($this->jails as $jail) { // foreach ($jail['ips'] as $ip) { // $rows[$ip] = ($rows[$ip] ?? 0) + 1; // } // } // // if (!empty($rows)) { // arsort($rows); // return collect($rows) // ->map(fn($count, $ip) => ['ip' => $ip, 'count' => $count]) // ->values() // ->take(5) // ->toArray(); // } // // // 2. Fallback: Falls keine Jails/IPs → Logdatei // $cmd = 'grep -Eo "([0-9]{1,3}\.){3}[0-9]{1,3}" /var/log/fail2ban.log 2>/dev/null' // . ' | sort | uniq -c | sort -nr | head -5'; // $log = (string) @shell_exec($cmd); // // $rows = []; // if ($log !== '') { // foreach (preg_split('/\R+/', trim($log)) as $l) { // if (preg_match('/^\s*(\d+)\s+(\d+\.\d+\.\d+\.\d+)/', $l, $m)) { // $rows[] = ['ip'=>$m[2],'count'=>(int)$m[1]]; // } // } // } // return $rows; // } //}