jail = $jail; $this->load(); } public function refresh(): void { $this->load(true); } public function render() { return view('livewire.ui.security.modal.fail2-ban-jail-modal'); } /* ---------------- intern ---------------- */ protected function load(bool $force = false): void { $jail = $this->jail; [, $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)))) : []; $defaultBantime = $this->getBantime($jail); $rows = []; foreach ($ips as $ip) { $banAt = null; $until = null; $remaining = null; // 1) Primärquelle: DB if ($info = $this->banInfoFromDb($jail, $ip)) { $banAt = $info['banned_at']; if ((int)$info['expire'] === -1) { $remaining = -1; // permanent } elseif ((int)$info['expire'] > 0) { $until = (int)$info['expire']; $remaining = max(0, $until - time()); } } // 2) Fallback: Log + Jail-Bantime if ($remaining === null) { $banAt = $banAt ?? $this->lastBanTimestamp($jail, $ip); if ($banAt !== null) { $remaining = max(0, $defaultBantime - (time() - $banAt)); $until = $remaining > 0 ? $banAt + $defaultBantime : null; } else { $remaining = -2; // ~approx } } // 3) Wenn 0 Sekunden, aber Fail2Ban hält die IP noch → „verlängert/unbekannt“ if ($remaining === 0 && $this->isStillBanned($jail, $ip)) { $remaining = -2; // markiere als „~ unbekannt/verlängert“ } [$timeText, $metaText, $boxClass] = $this->present($remaining, $banAt, $until); $rows[] = [ 'ip' => $ip, 'bantime' => $defaultBantime, 'banned_at' => $banAt, 'remaining' => $remaining, 'until' => $until, 'time_text' => $timeText, 'meta_text' => $metaText, 'box_class' => $boxClass, ]; } $this->rows = $rows; } /** ROBUST: findet Binaries automatisch */ private function bin(string $name): string { $p = trim((string)@shell_exec("command -v ".escapeshellarg($name)." 2>/dev/null")); return $p !== '' ? $p : $name; } /** letzte "Ban "-Zeile → Unix-Timestamp (mit Fallbacks) */ private function lastBanTimestamp(string $jail, string $ip): ?int { $sudo = $this->bin('sudo'); $zgrep = $this->bin('zgrep'); $grep = $this->bin('grep'); $tail = $this->bin('tail'); $journal = $this->bin('journalctl'); // 1) exakter Treffer: "[jail] Ban IP" $needleExact = sprintf('[%s] Ban %s', $jail, $ip); $qExact = escapeshellarg($needleExact); $cmd1 = "$sudo -n $zgrep -h -F $qExact /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; $line = trim((string)@shell_exec($cmd1)); // 2) Fallback: nur "Ban IP" if ($line === '') { $qLoose = escapeshellarg("Ban $ip"); $cmd2 = "$sudo -n $zgrep -h -F $qLoose /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; $line = trim((string)@shell_exec($cmd2)); } // 3) Fallback: unkomprimiertes Log if ($line === '') { $cmd3 = "$sudo -n $grep -h -F $qExact /var/log/fail2ban.log 2>/dev/null | $tail -n 1"; $line = trim((string)@shell_exec($cmd3)); if ($line === '') { $cmd3b = "$sudo -n $grep -h -F ".escapeshellarg("Ban $ip")." /var/log/fail2ban.log 2>/dev/null | $tail -n 1"; $line = trim((string)@shell_exec($cmd3b)); } } // 4) Fallback: journald if ($line === '') { $cmd4 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager | $grep -F $qExact | $tail -n 1"; $line = trim((string)@shell_exec($cmd4)); if ($line === '') { $cmd4b = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager | $grep -F ".escapeshellarg("Ban $ip")." | $tail -n 1"; $line = trim((string)@shell_exec($cmd4b)); } } if ($line === '') return null; // "2025-10-25 11:22:05,958 ..." → Millisekunden ignorieren 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; } private function getDbFile(): string { [, $out] = $this->f2b('get dbfile'); $path = trim($out); return $path !== '' ? $path : '/var/lib/fail2ban/fail2ban.sqlite3'; } private function sql(string $s): string { return "'".str_replace("'", "''", $s)."'"; } /** Darstellung: permanent / Restzeit / abgelaufen / ~approx / unbekannt */ private function present(?int $remaining, ?int $banAt, ?int $until): array { $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) { return ['permanent', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $rose]; } if ($remaining === -2) { return ['~ '.$this->fmtSecs($this->getApproxBantime()), '—', $amber]; } if (is_int($remaining)) { if ($remaining > 0) { $meta = []; if ($banAt) $meta[] = 'seit '.$this->fmtTs($banAt); if ($until) $meta[] = 'bis '.$this->fmtTs($until); return [$this->fmtSecs($remaining), $meta ? implode(' • ', $meta) : '—', $amber]; } return ['abgelaufen', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $muted]; } return ['—', '—', $muted]; } private function getApproxBantime(): int { return 600; } private function f2b(string $args): array { $sudo = $this->bin('sudo'); $f2b = $this->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 banInfoFromDb(string $jail, string $ip): ?array { $sudo = $this->bin('sudo'); $sqlite = $this->bin('sqlite3'); $db = $this->getDbFile(); // 1) Spalten ermitteln $cmdCols = "$sudo -n $sqlite -readonly ".escapeshellarg($db)." ".escapeshellarg("PRAGMA table_info(bans);"); $colsRaw = trim((string)@shell_exec($cmdCols)); if ($colsRaw === '') return null; $hasExpire = strpos($colsRaw, "|expiretime|") !== false; $hasBantime= strpos($colsRaw, "|bantime|") !== false; // 2) Query bauen nach Schema if ($hasExpire) { $q = sprintf( "SELECT timeofban, expiretime FROM bans WHERE jail=%s AND ip=%s ORDER BY timeofban DESC LIMIT 1", $this->sql($jail), $this->sql($ip) ); $cmd = "$sudo -n $sqlite -readonly ".escapeshellarg($db).' '.escapeshellarg($q); $out = trim((string)@shell_exec($cmd)); if ($out === '') return null; [$timeofban, $expire] = array_map('intval', explode('|', $out)) + [null, null]; return ['banned_at' => $timeofban ?: null, 'expire' => $expire ?? null]; } if ($hasBantime) { $q = sprintf( "SELECT timeofban, bantime FROM bans WHERE jail=%s AND ip=%s ORDER BY timeofban DESC LIMIT 1", $this->sql($jail), $this->sql($ip) ); $cmd = "$sudo -n $sqlite -readonly ".escapeshellarg($db).' '.escapeshellarg($q); $out = trim((string)@shell_exec($cmd)); if ($out === '') return null; [$timeofban, $bantime] = array_map('intval', explode('|', $out)) + [null, null]; $expire = ($timeofban && $bantime) ? ($timeofban + $bantime) : null; return ['banned_at' => $timeofban ?: null, 'expire' => $expire]; } // Fallback: nur timeofban vorhanden → aktuelles Jail-Bantime verwenden $q = sprintf( "SELECT timeofban FROM bans WHERE jail=%s AND ip=%s ORDER BY timeofban DESC LIMIT 1", $this->sql($jail), $this->sql($ip) ); $cmd = "$sudo -n $sqlite -readonly ".escapeshellarg($db).' '.escapeshellarg($q); $out = trim((string)@shell_exec($cmd)); if ($out === '') return null; $timeofban = (int)$out; $bantime = $this->getBantime($jail); // aktuell konfiguriert (z. B. 3600) $expire = $timeofban ? ($timeofban + max(0, $bantime)) : null; return ['banned_at' => $timeofban ?: null, 'expire' => $expire]; } private function isStillBanned(string $jail, string $ip): bool { // 1) Direkt: banip [, $out1] = $this->f2b('get '.escapeshellarg($jail).' banip '.escapeshellarg($ip)); if (preg_match('/\b1\b/', $out1)) return true; // 2) Liste: banlist (Rohliste, whitespace-separiert) [, $out2] = $this->f2b('get '.escapeshellarg($jail).' banlist'); if ($out2 !== '') { $list = array_filter(array_map('trim', preg_split('/\s+/', trim($out2)))); if (in_array($ip, $list, true)) return true; } // 3) Notfall: status-Parsing [, $out3] = $this->f2b('status '.escapeshellarg($jail)); $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $out3) ?: ''; return $ipList !== '' && preg_match('/(^|\s)'.preg_quote($ip,'/').'(\s|$)/', $ipList) === 1; } private function getBantime(string $jail): int { [, $out] = $this->f2b('get '.escapeshellarg($jail).' bantime'); $val = trim($out); return preg_match('/-?\d+/', $val, $m) ? (int)$m[0] : 600; } private function firstMatch(string $pattern, string $haystack): ?string { return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; } private function fmtSecs(int $s): string { $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 { return date('d.m. H:i', $ts); } } //namespace App\Livewire\Ui\Security\Modal; // //use LivewireUI\Modal\ModalComponent; // //class Fail2BanJailModal extends ModalComponent //{ // public string $jail = ''; // public array $rows = []; // // public static function modalMaxWidth(): string // { // return '4xl'; // } // // public function mount(string $jail): void // { // $this->jail = $jail; // $this->load(); // } // // public function refresh(): void // { // $this->load(true); // } // // public function render() // { // return view('livewire.ui.security.modal.fail2-ban-jail-modal'); // } // // /* ---------------- intern ---------------- */ // // protected function load(bool $force = false): void // { // $jail = $this->jail; // // // Aktuell gebannte IPs + Bantime // [, $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 = $this->getBantime($jail); // Sek., -1 = permanent // // $rows = []; // foreach ($ips as $ip) { // $banAt = $this->lastBanTimestamp($jail, $ip); // Unix-TS oder null // // $remaining = null; // $until = null; // if ($bantime === -1) { // $remaining = -1; // } elseif ($banAt !== null) { // $remaining = max(0, $bantime - (time() - $banAt)); // $until = $remaining > 0 ? ($banAt + $bantime) : null; // } else { // // kein exakter Timestamp auffindbar → als Approximation ausweisen // $remaining = -2; // Kennzeichen für "~ Bantime" // } // // [$timeText, $metaText, $boxClass] = $this->present($remaining, $banAt, $until, $bantime); // // $rows[] = [ // 'ip' => $ip, // 'bantime' => $bantime, // 'banned_at' => $banAt, // 'remaining' => $remaining, // -1=permanent, -2≈approx, 0..Sek, null=unbekannt // 'until' => $until, // 'time_text' => $timeText, // 'meta_text' => $metaText, // 'box_class' => $boxClass, // ]; // } // // $this->rows = $rows; // } // // /** robust: findet Pfade automatisch, mehrere Fallbacks */ // private function lastBanTimestamp(string $jail, string $ip): ?int // { // $sudo = $this->bin('sudo'); // $zgrep = $this->bin('zgrep'); // $grep = $this->bin('grep'); // $tail = $this->bin('tail'); // $journal = $this->bin('journalctl'); // // // 1) exakte Suche: "[jail] Ban IP" // $needleExact = sprintf('[%s] Ban %s', $jail, $ip); // $qExact = escapeshellarg($needleExact); // $cmd1 = "$sudo -n $zgrep -h -F $qExact /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; // $line = trim((string)@shell_exec($cmd1)); // // // 2) locker: nur "Ban IP" (falls der Jail-Tag im Log anders ist) // if ($line === '') { // $qLoose = escapeshellarg("Ban $ip"); // $cmd2 = "$sudo -n $zgrep -h -F $qLoose /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; // $line = trim((string)@shell_exec($cmd2)); // } // // // 3) unkomprimiertes Log als Fallback // if ($line === '') { // $cmd3 = "$sudo -n $grep -h -F $qExact /var/log/fail2ban.log 2>/dev/null | $tail -n 1"; // $line = trim((string)@shell_exec($cmd3)); // if ($line === '') { // $cmd3b = "$sudo -n $grep -h -F " . escapeshellarg("Ban $ip") . " /var/log/fail2ban.log 2>/dev/null | $tail -n 1"; // $line = trim((string)@shell_exec($cmd3b)); // } // } // // // 4) journald // if ($line === '') { // $cmd4 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager | $grep -F $qExact | $tail -n 1"; // $line = trim((string)@shell_exec($cmd4)); // if ($line === '') { // $cmd4b = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager | $grep -F " . escapeshellarg("Ban $ip") . " | $tail -n 1"; // $line = trim((string)@shell_exec($cmd4b)); // } // } // // if ($line === '') return null; // // // 2025-10-30 22:34:59,491 ... -> Datum/Zeit extrahieren // 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; // } // // /** Anzeige-Logik */ // private function present(?int $remaining, ?int $banAt, ?int $until, int $bantime): array // { // $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) { // return ['permanent', $banAt ? ('seit ' . $this->fmtTs($banAt)) : '—', $rose]; // } // if ($remaining === -2) { // // nur ungefähre Bantime verfügbar // return ['~ ' . $this->fmtSecs($bantime), '—', $amber]; // } // if (is_int($remaining)) { // if ($remaining > 0) { // $meta = []; // if ($banAt) $meta[] = 'seit ' . $this->fmtTs($banAt); // if ($until) $meta[] = 'bis ' . $this->fmtTs($until); // return [$this->fmtSecs($remaining), $meta ? implode(' • ', $meta) : '—', $amber]; // } // return ['abgelaufen', $banAt ? ('seit ' . $this->fmtTs($banAt)) : '—', $muted]; // } // return ['—', '—', $muted]; // } // // /** sudo + fail2ban-client */ // private function f2b(string $args): array // { // $sudo = $this->bin('sudo'); // $f2b = $this->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; // } // // private function firstMatch(string $pattern, string $haystack): ?string // { // return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; // } // // /** findet den Pfad zu einem Binary robust */ // private function bin(string $name): string // { // $p = trim((string)@shell_exec('command -v ' . escapeshellarg($name) . ' 2>/dev/null')); // return $p !== '' ? $p : $name; // } // // private function fmtSecs(int $s): string // { // $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 // { // return date('d.m. H:i', $ts); // } //} //namespace App\Livewire\Ui\Security\Modal; // //use LivewireUI\Modal\ModalComponent; // //class Fail2BanJailModal extends ModalComponent //{ // public string $jail = ''; // public array $rows = []; // // public static function modalMaxWidth(): string // { // return '4xl'; // } // // public function mount(string $jail): void // { // $this->jail = $jail; // $this->load(); // } // // public function render() // { // return view('livewire.ui.security.modal.fail2-ban-jail-modal'); // } // // public function refresh(): void // { // $this->load(true); // } // // /* ---------------- intern ---------------- */ // // protected function load(bool $force = false): void // { // $jail = $this->jail; // // [, $s] = $this->f2b('status ' . escapeshellarg($jail)); // $ipLine = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; // $ips = $ipLine !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipLine)))) : []; // // $bantime = $this->getBantime($jail); // // $rows = []; // foreach ($ips as $ip) { // $banAt = $this->lastBanTimestamp($jail, $ip); // null wenn nicht gefunden // // $remaining = null; // $until = null; // if ($bantime === -1) { // $remaining = -1; // } elseif ($banAt !== null) { // $remaining = max(0, $bantime - (time() - $banAt)); // $until = $remaining > 0 ? ($banAt + $bantime) : null; // } // // [$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; // } // // /** ---- helpers ---- */ // // private function f2b(string $args): array // { // $sudo = $this->bin('sudo'); // $f2b = $this->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'); // if (preg_match('/-?\d+/', trim($out), $m)) return (int)$m[0]; // return 600; // } // // /** findet die letzte "Ban "-Zeile in log / rotierte .gz / journald */ // private function lastBanTimestamp(string $jail, string $ip): ?int // { // $sudo = $this->bin('sudo'); // $zgrep = $this->bin('zgrep'); // findet /bin/zgrep ODER /usr/bin/zgrep // $grep = $this->bin('grep'); // $tail = $this->bin('tail'); // $journal = $this->bin('journalctl'); // // // 1) exakter Treffer: "[jail] Ban IP" // $needleExact = sprintf('[%s] Ban %s', $jail, $ip); // $qExact = escapeshellarg($needleExact); // // $cmd1 = "$sudo -n $zgrep -h -F $qExact /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; // $line = trim((string) @shell_exec($cmd1)); // // // 2) Fallback: nur "Ban IP" (falls Jail-Name im Log abweicht) // if ($line === '') { // $needleLoose = sprintf('Ban %s', $ip); // $qLoose = escapeshellarg($needleLoose); // $cmd2 = "$sudo -n $zgrep -h -F $qLoose /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; // $line = trim((string) @shell_exec($cmd2)); // } // // // 3) Fallback: unkomprimiertes Log mit grep // if ($line === '') { // $cmd3 = "$sudo -n $grep -h -F $qExact /var/log/fail2ban.log 2>/dev/null | $tail -n 1"; // $line = trim((string) @shell_exec($cmd3)); // if ($line === '') { // $cmd3b = "$sudo -n $grep -h -F ".escapeshellarg("Ban $ip")." /var/log/fail2ban.log 2>/dev/null | $tail -n 1"; // $line = trim((string) @shell_exec($cmd3b)); // } // } // // // 4) Fallback: journald // if ($line === '') { // $cmd4 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager | $grep -F $qExact | $tail -n 1"; // $line = trim((string) @shell_exec($cmd4)); // if ($line === '') { // $cmd4b = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager | $grep -F ".escapeshellarg("Ban $ip")." | $tail -n 1"; // $line = trim((string) @shell_exec($cmd4b)); // } // } // // if ($line === '') return null; // // // Zeitstempel extrahieren (YYYY-MM-DD HH:MM:SS[,ms] egal) // 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; // } // // private function firstMatch(string $pattern, string $haystack): ?string // { // return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; // } // // private function present(?int $remaining, ?int $banAt, ?int $until): array // { // $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) { // return ['permanent', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $rose]; // } // if ($remaining === -2) { // // zeigt ungefähre Bantime, falls Timestamp nicht gefunden wurde // return ['~ '.$this->fmtSecs($this->getApproxBantime()), '—', $amber]; // } // if (is_int($remaining)) { // if ($remaining > 0) { // $meta = []; // if ($banAt) $meta[] = 'seit '.$this->fmtTs($banAt); // if ($until) $meta[] = 'bis '.$this->fmtTs($until); // return [$this->fmtSecs($remaining), $meta ? implode(' • ', $meta) : '—', $amber]; // } // return ['abgelaufen', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $muted]; // } // return ['—', '—', $muted]; // } // // private function getApproxBantime(): int { return 600; } // // private function fmtSecs(int $s): string // { // $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 // { // return date('d.m. H:i', $ts); // } // // private function bin(string $name): string // { // $p = trim((string)@shell_exec("command -v ".escapeshellarg($name)." 2>/dev/null")); // return $p !== '' ? $p : $name; // } //} //namespace App\Livewire\Ui\Security\Modal; // //use LivewireUI\Modal\ModalComponent; // //class Fail2BanJailModal extends ModalComponent //{ // public string $jail = ''; // public array $rows = []; // [{ip,bantime,banned_at,remaining,until,time_text,meta_text,box_class}] // // public static function modalMaxWidth(): string // { // return '4xl'; // } // // public function mount(string $jail): void // { // $this->jail = $jail; // $this->load(); // } // // public function render() // { // return view('livewire.ui.security.modal.fail2-ban-jail-modal'); // } // // public function refresh(): void // { // $this->load(true); // } // // /* ---------------- 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 = $this->getBantime($jail); // // $rows = []; // foreach ($ips as $ip) { // $banAt = $this->lastBanTimestamp($jail, $ip); // Unix-Timestamp oder null // // $remaining = null; // $until = null; // // if ($bantime === -1) { // $remaining = -1; // } elseif ($banAt !== null) { // $remaining = max(0, $bantime - (time() - $banAt)); // $until = $remaining > 0 ? ($banAt + $bantime) : null; // } // // [$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; // } // // /** 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; // } // // /** robust: zgrep + journalctl via sudo */ // private function lastBanTimestamp(string $jail, string $ip): ?int // { // $sudo = '/usr/bin/sudo'; // $zgrep = '/usr/bin/zgrep'; // $journal = '/usr/bin/journalctl'; // $tail = '/usr/bin/tail'; // // $needle = sprintf('[%s] Ban %s', $jail, $ip); // $q = escapeshellarg($needle); // // // Logs inkl. Rotation (gz) // $cmd1 = "$sudo -n $zgrep -h $q /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; // $line = trim((string)@shell_exec($cmd1)); // // if ($line === '') { // Fallback: journald // $cmd2 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager -g $q 2>/dev/null | $tail -n 1"; // $line = trim((string)@shell_exec($cmd2)); // } // if ($line === '') return null; // // 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; // } // // private function firstMatch(string $pattern, string $haystack): ?string // { // return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; // } // // private function present(?int $remaining, ?int $banAt, ?int $until): array // { // $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) { // return ['permanent', $banAt ? ('seit ' . $this->fmtTs($banAt)) : '—', $rose]; // } // if (is_int($remaining)) { // if ($remaining > 0) { // $meta = []; // if ($banAt) $meta[] = 'seit ' . $this->fmtTs($banAt); // if ($until) $meta[] = 'bis ' . $this->fmtTs($until); // return [$this->fmtSecs($remaining), $meta ? implode(' • ', $meta) : '—', $amber]; // } // return ['abgelaufen', $banAt ? ('seit ' . $this->fmtTs($banAt)) : '—', $muted]; // } // return ['—', '—', $muted]; // } // // private function fmtSecs(int $s): string // { // $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 // { // return date('d.m. H:i', $ts); // } //} // //namespace App\Livewire\Ui\Security\Modal; // //use LivewireUI\Modal\ModalComponent; // //class Fail2BanJailModal extends ModalComponent //{ // public string $jail = ''; // /** @var arrayjail = $jail; // $this->load(); // } // // public function refresh(): void // { // $this->load(true); // } // // public function render() // { // return view('livewire.ui.security.modal.fail2-ban-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; // } // // /** letzte "Ban "-Zeile -> Unix-Timestamp (robust über zgrep/journal/jail-tail) */ // private function lastBanTimestamp(string $jail, string $ip): ?int // { // $sudo = '/usr/bin/sudo'; // $zgrep = '/usr/bin/zgrep'; // $journal = '/usr/bin/journalctl'; // $tail = '/usr/bin/tail'; // // $needle = sprintf('[%s] Ban %s', $jail, $ip); // $q = escapeshellarg($needle); // // // 1) zgrep über alle fail2ban-Logs (inkl. Rotation .N, .gz) // $cmd1 = "$sudo -n $zgrep -h $q /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; // $line = trim((string)@shell_exec($cmd1)); // // // 2) Fallback: journald (ohne grep-Pipe, mit -g) // if ($line === '') { // $cmd2 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager -g $q 2>/dev/null | $tail -n 1"; // $line = trim((string)@shell_exec($cmd2)); // } // // if ($line === '') return null; // // // Zeitstempel extrahieren (YYYY-MM-DD HH:MM:SS) // 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 // { // $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) { // 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) { // $meta = []; // if ($banAt) $meta[] = 'seit '.$this->fmtTs($banAt); // if ($until) $meta[] = 'bis '.$this->fmtTs($until); // return [$this->fmtSecs($remaining), $meta ? implode(' • ', $meta) : '—', $amber]; // } // return ['abgelaufen', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $muted]; // } // 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 // { // $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 */ // /** letzte "Ban "-Zeile -> Unix-Timestamp aus /var/log/fail2ban.log */ // // 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); // } //}