diff --git a/app/Livewire/Ui/Security/Fail2banBanlist.php b/app/Livewire/Ui/Security/Fail2banBanlist.php index 4ccaa6a..e0994eb 100644 --- a/app/Livewire/Ui/Security/Fail2banBanlist.php +++ b/app/Livewire/Ui/Security/Fail2banBanlist.php @@ -14,20 +14,37 @@ class Fail2banBanlist extends Component */ public ?string $jail = null; - /** @var array */ - public array $banned = []; + /** + * Struktur für Blade (reine Ausgabe, keine Logik im Blade): + * [ + * [ + * 'ip' => '1.2.3.4', + * 'jail' => 'recidive', + * 'permanent' => false, + * 'label' => 'Temporär', // oder 'Permanent' + * 'box' => 'border-amber-400/20 bg-white/3', // Kartenstil + * 'badge' => 'border-amber-400/30 bg-amber-500/10 text-amber-200', + * 'btn' => 'border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50', + * ], + * ... + * ] + * + * @var array + */ + public array $rows = []; #[On('f2b:refresh')] public function refreshList(): void { - $this->loadBannedIps(); + $this->loadBanned(); } public function mount(?string $jail = null): void { - // erlaubt optionalen Param via $this->jail = $jail; - $this->loadBannedIps(); + $this->loadBanned(); } public function render() @@ -37,61 +54,119 @@ class Fail2banBanlist extends Component /* ================= core ================= */ - private function loadBannedIps(): void + private function loadBanned(): void { $jails = $this->jailList(); - // ggf. nur ein bestimmtes Jail anzeigen + // ggf. nur ein bestimmtes Jail if (is_string($this->jail) && $this->jail !== '' && $this->jail !== '*') { $jails = in_array($this->jail, $jails, true) ? [$this->jail] : []; } - $ips = []; + $rows = []; foreach ($jails as $j) { - $out = $this->f2b("status ".escapeshellarg($j)); - if (preg_match('/IP list:\s*(.+)$/mi', $out, $m)) { - $parts = preg_split('/\s+/', trim($m[1])); - foreach ($parts as $ip) { - if (filter_var($ip, FILTER_VALIDATE_IP)) $ips[] = $ip; + $out = $this->f2b("status " . escapeshellarg($j)); + if (!preg_match('/IP list:\s*(.+)$/mi', $out, $m)) { + continue; + } + $ips = preg_split('/\s+/', trim($m[1])) ?: []; + foreach ($ips as $ip) { + if (!filter_var($ip, FILTER_VALIDATE_IP)) { + continue; } + + $permanent = $this->isPermanent($j, $ip); + + // Präsentationsklassen zentral definieren + if ($permanent) { + $box = 'border-rose-400/30 bg-rose-500/5'; + $badge = 'border-rose-400/30 bg-rose-500/10 text-rose-200'; + $label = 'Permanent'; + } else { + $box = 'border-amber-400/20 bg-white/3'; + $badge = 'border-amber-400/30 bg-amber-500/10 text-amber-200'; + $label = 'Temporär'; + } + + $rows[] = [ + 'ip' => $ip, + 'jail' => $j, + 'permanent' => $permanent, + 'label' => $label, + 'box' => $box, + 'badge' => $badge, + 'btn' => 'border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50', + ]; } } - $this->banned = array_values(array_unique($ips)); + // Sortierung: permanent oben, dann nach Jail, dann IP + usort($rows, function ($a, $b) { + if ($a['permanent'] !== $b['permanent']) return $a['permanent'] ? -1 : 1; + if ($a['jail'] !== $b['jail']) return strcmp($a['jail'], $b['jail']); + return strcmp($a['ip'], $b['ip']); + }); + + $this->rows = $rows; } - /** Entbannt IP in allen passenden Jails */ - public function unban(string $ip): void + /** Entbannt eine IP **im angegebenen Jail** (Button gibt Jail mit) */ + public function unban(string $ip, string $jail): void { if (!filter_var($ip, FILTER_VALIDATE_IP)) return; - $jails = $this->jailList(); - if (is_string($this->jail) && $this->jail !== '' && $this->jail !== '*') { - $jails = in_array($this->jail, $jails, true) ? [$this->jail] : []; - } + $cmd = sprintf( + 'sudo -n /usr/bin/fail2ban-client set %s unbanip %s 2>&1', + escapeshellarg($jail), + escapeshellarg($ip) + ); + @shell_exec($cmd); - $cnt = 0; - foreach ($jails as $j) { - $this->f2b("set ".escapeshellarg($j)." unbanip ".escapeshellarg($ip)); - $cnt++; - } - - $this->loadBannedIps(); + $this->loadBanned(); $this->dispatch('toast', type: 'done', badge: 'Fail2Ban', title: 'IP entbannt', - text: ($this->jail && $this->jail !== '*') - ? "IP {$ip} in Jail „{$this->jail}“ entbannt." - : "IP {$ip} in {$cnt} Jail(s) entbannt.", + text: "IP {$ip} in Jail „{$jail}“ entbannt.", duration: 5000, ); } /* ================= helpers ================= */ - /** Liefert Liste aller Jails über fail2ban-client */ + /** Prüft via SQLite, ob der **letzte** Ban für (jail, ip) permanent ist (bantime < 0). */ + private function isPermanent(string $jail, string $ip): bool + { + $db = $this->getDbFile(); + if ($db === '' || !is_readable($db)) { + // Fallback: Blacklist-Jail ist per Design permanent + return $jail === 'mailwolt-blacklist'; + } + + $q = <<&1', + escapeshellarg($db), + escapeshellarg($q) + ); + $out = trim((string)@shell_exec($cmd)); + if ($out === '') return ($jail === 'mailwolt-blacklist'); // Fallback + return ((int)$out) < 0; + } + + /** Liste aller Jails */ private function jailList(): array { $out = $this->f2b('status'); @@ -102,9 +177,19 @@ class Fail2banBanlist extends Component return []; } - /** führt fail2ban-client via sudo aus und gibt stdout zurück */ + /** fail2ban-client über sudo aufrufen */ private function f2b(string $args): string { return (string) @shell_exec('sudo -n /usr/bin/fail2ban-client '.$args.' 2>&1'); } + + /** Pfad zur Fail2Ban-SQLite-DB holen */ + private function getDbFile(): string + { + $out = $this->f2b('get dbfile'); + $lines = array_values(array_filter(array_map('trim', preg_split('/\r?\n/', $out)))); + $path = end($lines) ?: ''; + $path = preg_replace('/^`?-?\s*/', '', $path); + return $path ?: '/var/lib/fail2ban/fail2ban.sqlite3'; + } } diff --git a/app/Livewire/Ui/Security/Fail2banSettings.php b/app/Livewire/Ui/Security/Fail2banSettings.php index 66f950c..b16abcd 100644 --- a/app/Livewire/Ui/Security/Fail2banSettings.php +++ b/app/Livewire/Ui/Security/Fail2banSettings.php @@ -3,6 +3,7 @@ namespace App\Livewire\Ui\Security; +use Livewire\Attributes\On; use Livewire\Component; use App\Models\Fail2banSetting; use App\Models\Fail2banIpList; diff --git a/resources/views/livewire/ui/security/fail2ban-banlist.blade.php b/resources/views/livewire/ui/security/fail2ban-banlist.blade.php index 65cf654..bc0c44a 100644 --- a/resources/views/livewire/ui/security/fail2ban-banlist.blade.php +++ b/resources/views/livewire/ui/security/fail2ban-banlist.blade.php @@ -1,39 +1,34 @@
-

Aktuell gebannte IPs

+

Aktuell gebannte IPs

- @if (empty($banned)) -
- Keine aktiven Banns vorhanden. -
+ @if (empty($rows)) +
Keine aktiven Banns vorhanden.
@else -
- - - - - - - - - @foreach ($banned as $ip) - - - - - @endforeach - -
IP-AdresseAktion
{{ $ip }} - -
+
+ @foreach ($rows as $r) +
+
+
{{ $r['ip'] }}
+ + {{ $r['style'] === 'permanent' ? 'Permanent' : 'Temporär' }} + + Jail: {{ $r['jail'] }} +
+ + +
+ @endforeach
@endif
diff --git a/resources/views/livewire/ui/security/fail2ban-settings.blade.php b/resources/views/livewire/ui/security/fail2ban-settings.blade.php index c95736e..b0bb751 100644 --- a/resources/views/livewire/ui/security/fail2ban-settings.blade.php +++ b/resources/views/livewire/ui/security/fail2ban-settings.blade.php @@ -8,7 +8,7 @@ Fail2Ban Konfiguration