diff --git a/app/Http/Middleware/GuestOnlyMiddleware.php b/app/Http/Middleware/GuestOnlyMiddleware.php index b9a4cc7..c2e935f 100644 --- a/app/Http/Middleware/GuestOnlyMiddleware.php +++ b/app/Http/Middleware/GuestOnlyMiddleware.php @@ -18,7 +18,7 @@ class GuestOnlyMiddleware { if (Auth::check()) { // Eingeloggt → z. B. Dashboard weiterleiten - return redirect()->route('dashboard'); + return redirect()->route('ui.dashboard'); } return $next($request); diff --git a/app/Livewire/Ui/Security/Fail2BanCard.php b/app/Livewire/Ui/Security/Fail2BanCard.php index 185673f..3cb1180 100644 --- a/app/Livewire/Ui/Security/Fail2BanCard.php +++ b/app/Livewire/Ui/Security/Fail2BanCard.php @@ -8,8 +8,8 @@ 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 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 @@ -22,7 +22,7 @@ class Fail2BanCard extends Component return view('livewire.ui.security.fail2-ban-card'); } - // Wird vom Button "Neu prüfen" genutzt + /** Button „Neu prüfen“ */ public function refresh(): void { $this->load(true); @@ -33,56 +33,89 @@ class Fail2BanCard extends Component protected function load(bool $force = false): void { // existiert fail2ban-client? - $bin = trim((string) @shell_exec('command -v fail2ban-client 2>/dev/null')) ?: ''; + $bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: ''; if ($bin === '') { - $this->available = false; + $this->available = false; $this->permDenied = false; $this->activeBans = 0; - $this->jails = []; - $this->topIps = []; + $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->available = true; $this->permDenied = true; $this->activeBans = 0; - $this->jails = []; - $this->topIps = $this->collectTopIps(); + $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))) : []; + $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : []; + + $total = 0; + $rows = []; - $total = 0; $rows = []; foreach ($jails as $j) { - [, $s] = $this->f2b('status '.escapeshellarg($j)); - $banned = (int) ($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0); + $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)))) : []; - $rows[] = ['name'=>$j,'banned'=>$banned,'ips'=>array_slice($ips, 0, 8)]; + $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->available = true; $this->permDenied = false; $this->activeBans = $total; - $this->jails = $rows; - $this->topIps = $this->collectTopIps(); + $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); + $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 @@ -91,44 +124,282 @@ class Fail2BanCard extends Component 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; } - /** Zählt die häufigsten IPs aus den letzten Fail2Ban-Logs (ban/unban Events) */ + /** Top-IPs grob zählen (aus der aktuellen Jail-Liste; Fallback: Log) */ private function collectTopIps(): array { - // 1. Versuch: IPs direkt aus den Jails - $rows = []; + $map = []; foreach ($this->jails as $jail) { - foreach ($jail['ips'] as $ip) { - $rows[$ip] = ($rows[$ip] ?? 0) + 1; + foreach ($jail['ips'] as $row) { + $ip = $row['ip'] ?? null; + if (!$ip) continue; + $map[$ip] = ($map[$ip] ?? 0) + 1; } } - if (!empty($rows)) { - arsort($rows); - return collect($rows) - ->map(fn($count, $ip) => ['ip' => $ip, 'count' => $count]) - ->values() - ->take(5) - ->toArray(); + if (!empty($map)) { + arsort($map); + $out = []; + foreach (array_slice($map, 0, 5, true) as $ip => $count) { + $out[] = ['ip' => $ip, 'count' => $count]; + } + return $out; } - // 2. Fallback: Falls keine Jails/IPs → Logdatei + // 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); - + $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]]; + $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; +// } +//} diff --git a/resources/views/livewire/ui/security/fail2-ban-card.blade.php b/resources/views/livewire/ui/security/fail2-ban-card.blade.php index f46f043..87b9e2f 100644 --- a/resources/views/livewire/ui/security/fail2-ban-card.blade.php +++ b/resources/views/livewire/ui/security/fail2-ban-card.blade.php @@ -26,14 +26,36 @@
{{ $j['name'] }}
- - {{ $j['banned'] }} gebannt - +
+ + Bannzeit: + @if($j['bantime'] === -1) permanent + @else {{ $j['bantime'] }}s + @endif + + + {{ $j['banned'] }} gebannt + +
+ @if(!empty($j['ips'])) -
- {{ implode(', ', $j['ips']) }} +
+ @foreach($j['ips'] as $ip) +
+ {{ $ip['ip'] }} + @if($ip['remaining'] === -1) + + permanent + + @elseif(is_int($ip['remaining']) && $ip['remaining'] > 0) + + {{ gmdate('H\h i\m s\s', $ip['remaining']) }} + + @endif +
+ @endforeach
@endif
@@ -43,7 +65,7 @@
{{-- Top IPs aus Ban-Events --}} -
+
Top IPs (letzte Fail2Ban-Logs):
    @forelse($topIps as $i) @@ -67,3 +89,85 @@
@endif
+ +{{--
--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{-- Fail2Ban--}} +{{--
--}} + +{{-- @if($available)--}} +{{-- --}} +{{-- {{ $activeBans }} aktuell--}} +{{-- --}} +{{-- @else--}} +{{-- --}} +{{-- nicht installiert--}} +{{-- --}} +{{-- @endif--}} +{{--
--}} + +{{-- @if(!$available)--}} +{{--
fail2ban-client wurde nicht gefunden.
--}} +{{-- @else--}} +{{-- --}}{{-- Jails --}} +{{--
--}} +{{-- @forelse($jails as $j)--}} +{{--
--}} +{{--
--}} +{{--
{{ $j['name'] }}
--}} +{{-- --}} +{{-- {{ $j['banned'] }} gebannt--}} +{{-- --}} +{{--
--}} +{{-- @if(!empty($j['ips']))--}} +{{--
--}} +{{-- {{ implode(', ', $j['ips']) }}--}} +{{--
--}} +{{-- @endif--}} +{{--
--}} +{{-- @empty--}} +{{--
Keine Jails gefunden.
--}} +{{-- @endforelse--}} +{{--
--}} + +{{-- --}}{{-- Top IPs aus Ban-Events --}} +{{--
--}} +{{--
Top IPs (letzte Fail2Ban-Logs):
--}} +{{--
    --}} +{{-- @foreach($j['ips'] as $ip)--}} +{{--
    --}} +{{-- {{ $ip['ip'] }}--}} +{{-- @if($ip['remaining'] === -1)--}} +{{-- permanent--}} +{{-- @elseif(is_int($ip['remaining']) && $ip['remaining'] > 0)--}} +{{-- --}} +{{-- {{ gmdate('H\h i\m s\s', $ip['remaining']) }}--}} +{{-- --}} +{{-- @endif--}} +{{--
    --}} +{{-- @endforeach--}} +{{-- @forelse($topIps as $i)--}} +{{--
  • --}} +{{-- {{ $i['ip'] }}--}} +{{-- {{ $i['count'] }}--}} +{{--
  • --}} +{{-- @empty--}} +{{--
  • --}} +{{-- @endforelse--}} +{{--
--}} +{{--
--}} + +{{--
--}} +{{-- --}} +{{--
--}} +{{-- @endif--}} +{{--
--}}