Fix: Mailbox Stats über Dovecot mit config/mailpool.php

main v1.0.112
boban 2025-10-31 01:38:00 +01:00
parent 8690067d9c
commit 46591669d6
4 changed files with 666 additions and 248 deletions

View File

@ -1,17 +1,18 @@
<?php
namespace App\Livewire\Ui\Security;
use Illuminate\Support\Facades\Log;
use Livewire\Attributes\On;
use Livewire\Component;
class Fail2BanCard extends Component
{
public bool $available = true;
public bool $permDenied = false;
public bool $available = true; // fail2ban-client vorhanden?
public bool $permDenied = false; // sudo / Socket-Rechte fehlen?
public bool $error = false; // anderer Fehler (Output unerwartet)
public int $activeBans = 0;
public array $jails = [];
public array $jails = []; // [['name','banned','bantime','ips'=>[...]]]
public function mount(): void
{
@ -31,7 +32,7 @@ class Fail2BanCard extends Component
public function openDetails(string $jail): void
{
// KORREKTER DISPATCH für wire-elements/modal
// wire-elements/modal (v2): Event-Namen + Component + Params
$this->dispatch('openModal', component: 'ui.security.modal.fail2-ban-jail-modal', arguments: ['jail' => $jail]);
}
@ -39,65 +40,76 @@ class Fail2BanCard extends Component
protected function load(bool $force = false): void
{
$this->available = true;
$this->permDenied = false;
$this->error = false;
$this->activeBans = 0;
$this->jails = [];
// 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->available = false;
return;
}
// Rechte prüfen
[$ok, $raw] = $this->f2b('ping');
if (!$ok && stripos($raw, 'permission denied') !== false) {
$this->available = true;
// Rechte / Erreichbarkeit
[, $ping] = $this->f2b('ping');
if ($this->looksDenied($ping)) {
$this->permDenied = true;
$this->activeBans = 0;
$this->jails = [];
return;
}
// Jail-Liste
// Jails lesen
[, $status] = $this->f2b('status');
$jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status);
$jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : [];
if ($this->looksDenied($status)) {
$this->permDenied = true;
return;
}
if (!preg_match('/Jail list:\s*(.+)$/mi', $status, $mm)) {
// etwas stimmt nicht loggen und „error“ zeigen
$this->error = true;
Log::warning('Fail2BanCard: unexpected status output', ['status' => $status]);
return;
}
$jails = array_filter(array_map('trim', preg_split('/\s*,\s*/', $mm[1] ?? '')));
$sum = 0;
$rows = [];
$sum = 0;
foreach ($jails as $j) {
$jEsc = escapeshellarg($j);
[, $s] = $this->f2b("status {$jEsc}");
$banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
$bantime = $this->getBantime($j);
$ipListLine = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
$ips = $ipListLine !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipListLine)))) : [];
[, $s] = $this->f2b("status {$jEsc}");
if ($this->looksDenied($s)) {
$this->permDenied = true;
return;
}
// Details inkl. Restzeit je IP
$ipDetails = $this->buildIpDetails($j, $ips, $bantime);
$banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
$bantime = $this->getBantime($j);
$ipLine = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
$ips = $ipLine !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipLine)))) : [];
$rows[] = [
'name' => $j,
'banned' => $banned,
'bantime' => $bantime, // Sek. (-1 = permanent)
'ips' => $ipDetails, // [['ip'=>..., 'remaining'=>..., 'until'=>...], ...]
'name' => $j,
'banned' => $banned,
'bantime' => $bantime,
// wir zeigen IPs NICHT mehr in der Card; Details sind im Modal
'ips' => [],
];
$sum += $banned;
}
$this->available = true;
$this->permDenied = false;
$this->activeBans = $sum;
$this->jails = $rows;
$this->jails = $rows;
}
private function f2b(string $args): array
{
$sudo = '/usr/bin/sudo';
$f2b = '/usr/bin/fail2ban-client';
$cmd = "timeout 3 $sudo -n $f2b $args 2>&1";
$out = (string)@shell_exec($cmd);
$f2b = '/usr/bin/fail2ban-client';
$cmd = "timeout 3 $sudo -n $f2b $args 2>&1";
$out = (string)@shell_exec($cmd);
$ok = stripos($out, 'Status') !== false
|| stripos($out, 'Jail list') !== false
@ -106,81 +118,209 @@ class Fail2BanCard extends Component
return [$ok, $out];
}
/** konfig. Bantime des Jails in Sekunden (-1 = permanent) */
private function getBantime(string $jail): int
{
[, $out] = $this->f2b('get '.escapeshellarg($jail).' bantime');
[, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime');
if ($this->looksDenied($out)) {
$this->permDenied = true;
return 600;
}
$val = trim($out);
if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0];
return 600; // konservativer Fallback
return 600;
}
/** Letzten Ban-Zeitpunkt (Unix-Timestamp) aus /var/log/fail2ban.log ermitteln. */
private function lastBanTimestamp(string $jail, string $ip): ?int
private function looksDenied(string $out): bool
{
$file = '/var/log/fail2ban.log';
if (!is_readable($file)) return null;
// nur das Ende der Datei lesen (Performance, auch bei Rotation groß genug wählen)
$tailBytes = 400000; // 400 KB
$size = @filesize($file) ?: 0;
$seek = max(0, $size - $tailBytes);
$fh = @fopen($file, 'rb');
if (!$fh) return null;
if ($seek > 0) fseek($fh, $seek);
$data = stream_get_contents($fh) ?: '';
fclose($fh);
// Beispielzeile:
// 2025-10-30 22:34:20,797 fail2ban.actions [...] NOTICE [sshd] Ban 193.46.255.244
$j = preg_quote($jail, '/');
$p = preg_quote($ip, '/');
$pattern = '/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2}),\d+.*\['.$j.'\]\s+Ban\s+'.$p.'\s*$/m';
if (preg_match_all($pattern, $data, $m) && !empty($m[1])) {
$date = end($m[1]); // YYYY-MM-DD
$time = end($m[2]); // HH:MM:SS
$dt = \DateTime::createFromFormat('Y-m-d H:i:s', "$date $time", new \DateTimeZone(date_default_timezone_get()));
return $dt ? $dt->getTimestamp() : null;
}
return null;
return preg_match('/(permission denied|not allowed to execute|a password is required)/i', $out) === 1;
}
/** Baut Details inkl. Restzeit (Sekunden; -1 = permanent). */
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; // permanent
} elseif ($banAt !== null) {
$remaining = max(0, $bantime - ($now - $banAt));
$until = $remaining > 0 ? ($banAt + $bantime) : null;
}
$out[] = [
'ip' => $ip,
'remaining' => $remaining, // -1 = permanent, null = Ban-Zeitpunkt nicht gefunden, >=0 = Sekunden
'until' => $until, // Unix-Timestamp oder null
];
}
return $out;
}
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\Attributes\On;
//use Livewire\Component;
//
//class Fail2BanCard extends Component
//{
// public bool $available = true;
// public bool $permDenied = false;
// public int $activeBans = 0;
// public array $jails = [];
//
// public function mount(): void
// {
// $this->load();
// }
//
// public function render()
// {
// return view('livewire.ui.security.fail2-ban-card');
// }
//
// #[On('f2b:refresh-banlist')]
// public function refresh(): void
// {
// $this->load(true);
// }
//
// public function openDetails(string $jail): void
// {
// // KORREKTER DISPATCH für wire-elements/modal
// $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;
// }
//
// // Rechte prüfen
// [$ok, $raw] = $this->f2b('ping');
// if (!$ok && stripos($raw, 'permission denied') !== false) {
// $this->available = true;
// $this->permDenied = true;
// $this->activeBans = 0;
// $this->jails = [];
// return;
// }
//
// // Jail-Liste
// [, $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) {
// $jEsc = escapeshellarg($j);
// [, $s] = $this->f2b("status {$jEsc}");
// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
// $bantime = $this->getBantime($j);
// $ipListLine = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
// $ips = $ipListLine !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipListLine)))) : [];
//
// // Details inkl. Restzeit je IP
// $ipDetails = $this->buildIpDetails($j, $ips, $bantime);
//
// $rows[] = [
// 'name' => $j,
// 'banned' => $banned,
// 'bantime' => $bantime, // Sek. (-1 = permanent)
// 'ips' => $ipDetails, // [['ip'=>..., 'remaining'=>..., 'until'=>...], ...]
// ];
// $sum += $banned;
// }
//
// $this->available = true;
// $this->permDenied = false;
// $this->activeBans = $sum;
// $this->jails = $rows;
// }
//
// private function f2b(string $args): array
// {
// $sudo = '/usr/bin/sudo';
// $f2b = '/usr/bin/fail2ban-client';
// $cmd = "timeout 3 $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];
// }
//
// /** konfig. Bantime des Jails in Sekunden (-1 = permanent) */
// 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; // konservativer Fallback
// }
//
// /** Letzten Ban-Zeitpunkt (Unix-Timestamp) aus /var/log/fail2ban.log ermitteln. */
// private function lastBanTimestamp(string $jail, string $ip): ?int
// {
// $file = '/var/log/fail2ban.log';
// if (!is_readable($file)) return null;
//
// // nur das Ende der Datei lesen (Performance, auch bei Rotation groß genug wählen)
// $tailBytes = 400000; // 400 KB
// $size = @filesize($file) ?: 0;
// $seek = max(0, $size - $tailBytes);
//
// $fh = @fopen($file, 'rb');
// if (!$fh) return null;
// if ($seek > 0) fseek($fh, $seek);
// $data = stream_get_contents($fh) ?: '';
// fclose($fh);
//
// // Beispielzeile:
// // 2025-10-30 22:34:20,797 fail2ban.actions [...] NOTICE [sshd] Ban 193.46.255.244
// $j = preg_quote($jail, '/');
// $p = preg_quote($ip, '/');
// $pattern = '/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2}),\d+.*\['.$j.'\]\s+Ban\s+'.$p.'\s*$/m';
//
// if (preg_match_all($pattern, $data, $m) && !empty($m[1])) {
// $date = end($m[1]); // YYYY-MM-DD
// $time = end($m[2]); // HH:MM:SS
// $dt = \DateTime::createFromFormat('Y-m-d H:i:s', "$date $time", new \DateTimeZone(date_default_timezone_get()));
// return $dt ? $dt->getTimestamp() : null;
// }
// return null;
// }
//
// /** Baut Details inkl. Restzeit (Sekunden; -1 = permanent). */
// 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; // permanent
// } elseif ($banAt !== null) {
// $remaining = max(0, $bantime - ($now - $banAt));
// $until = $remaining > 0 ? ($banAt + $bantime) : null;
// }
//
// $out[] = [
// 'ip' => $ip,
// 'remaining' => $remaining, // -1 = permanent, null = Ban-Zeitpunkt nicht gefunden, >=0 = Sekunden
// 'until' => $until, // Unix-Timestamp oder null
// ];
// }
// return $out;
// }
//
//
// private function firstMatch(string $pattern, string $haystack): ?string
// {
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
// }
//}
//namespace App\Livewire\Ui\Security;
//

View File

@ -7,18 +7,7 @@ use LivewireUI\Modal\ModalComponent;
class Fail2BanJailModal extends ModalComponent
{
public string $jail = '';
/** @var array<int,array{
* ip:string,
* bantime:int, // Sek.; -1 = permanent
* banned_at:?int, // Unix-Timestamp
* remaining:?int, // -1=permanent, 0..Sekunden, null=unbekannt
* until:?int, // Unix-Timestamp oder null
* time_text:string, // "HHh MMm SSs", "permanent", "—"
* meta_text:string, // "seit … • bis …" oder "seit …" oder "—"
* box_class:string // Tailwind-Klassen für BG/Border
* }]
*/
public array $rows = [];
public array $rows = []; // [{ip,bantime,banned_at,remaining,until,time_text,meta_text,box_class}]
public static function modalMaxWidth(): string
{
@ -31,16 +20,16 @@ class Fail2BanJailModal extends ModalComponent
$this->load();
}
public function refresh(): void
{
$this->load(true);
}
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
@ -48,11 +37,10 @@ class Fail2BanJailModal extends ModalComponent
$jail = $this->jail;
// Aktuell gebannte IPs
[, $s] = $this->f2b('status '.escapeshellarg($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)))) : [];
// bantime für Jail
$bantime = $this->getBantime($jail);
$rows = [];
@ -61,24 +49,22 @@ class Fail2BanJailModal extends ModalComponent
$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;
}
if ($bantime === -1) {
$remaining = -1;
} elseif ($banAt !== null) {
$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,
'ip' => $ip,
'bantime' => $bantime,
'banned_at' => $banAt,
'remaining' => $remaining,
'until' => $until,
'until' => $until,
'time_text' => $timeText,
'meta_text' => $metaText,
'box_class' => $boxClass,
@ -88,107 +74,13 @@ class Fail2BanJailModal extends ModalComponent
$this->rows = $rows;
}
/** letzte "Ban <IP>"-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
$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];
@ -196,24 +88,68 @@ class Fail2BanJailModal extends ModalComponent
private function getBantime(string $jail): int
{
[, $out] = $this->f2b('get '.escapeshellarg($jail).' bantime');
[, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime');
$val = trim($out);
if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0];
return 600;
}
/** letzte "Ban <IP>"-Zeile parsen -> Unix-Timestamp */
/** letzte "Ban <IP>"-Zeile -> Unix-Timestamp aus /var/log/fail2ban.log */
/** 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
{
// kompakt: 1h 23m 45s / 05m 10s / 12s
$h = intdiv($s, 3600);
$m = intdiv($s % 3600, 60);
$r = $s % 60;
@ -224,7 +160,236 @@ class Fail2BanJailModal extends ModalComponent
private function fmtTs(int $ts): string
{
// 29.10. 18:07
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 array<int,array{
// * ip:string,
// * bantime:int, // Sek.; -1 = permanent
// * banned_at:?int, // Unix-Timestamp
// * remaining:?int, // -1=permanent, 0..Sekunden, null=unbekannt
// * until:?int, // Unix-Timestamp oder null
// * time_text:string, // "HHh MMm SSs", "permanent", "—"
// * meta_text:string, // "seit … • bis …" oder "seit …" oder "—"
// * box_class:string // Tailwind-Klassen für BG/Border
// * }]
// */
// 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
// [, $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 <IP>"-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 <IP>"-Zeile parsen -> Unix-Timestamp */
// /** letzte "Ban <IP>"-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);
// }
//}

View File

@ -19,11 +19,19 @@
@if(!$available)
<div class="text-sm text-white/60">fail2ban-client wurde nicht gefunden.</div>
@elseif($permDenied)
<div class="text-sm text-amber-200">
Keine Berechtigung auf <code class="font-mono">/var/run/fail2ban/fail2ban.sock</code>.
Keine Berechtigung (sudo) auf <code class="font-mono">fail2ban-client</code>/<code class="font-mono">journalctl</code>/<code class="font-mono">zgrep</code>.
<span class="opacity-80">Sudo-Regel prüfen.</span>
</div>
@elseif($error)
<div class="text-sm text-amber-200">
Unerwartete Ausgabe von <code class="font-mono">fail2ban-client status</code>.
<span class="opacity-80">Details in <code>storage/logs/laravel.log</code>.</span>
</div>
@else
<div class="space-y-2">
@forelse($jails as $j)
@ -32,19 +40,13 @@
<div class="text-white/85 font-medium">{{ $j['name'] }}</div>
<div class="flex items-center gap-2">
<span class="text-[11px] text-white/60">
Bannzeit:
@if($j['bantime'] === -1)
permanent
@else
{{ $j['bantime'] }}s
@endif
Bannzeit: {{ $j['bantime'] === -1 ? 'permanent' : ($j['bantime'].'s') }}
</span>
<span class="px-2 py-0.5 rounded-full border text-[11px]
{{ $j['banned']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">
{{ $j['banned'] }} gebannt
</span>
{{-- fix: stop event bubbling --}}
<button wire:click.stop="openDetails('{{ $j['name'] }}')"
class="text-[11px] px-2 py-0.5 rounded border border-white/15 bg-white/5 hover:bg-white/10">
Details
@ -87,6 +89,76 @@
{{-- @endif--}}
{{-- </div>--}}
{{-- @if(!$available)--}}
{{-- <div class="text-sm text-white/60">fail2ban-client wurde nicht gefunden.</div>--}}
{{-- @elseif($permDenied)--}}
{{-- <div class="text-sm text-amber-200">--}}
{{-- Keine Berechtigung auf <code class="font-mono">/var/run/fail2ban/fail2ban.sock</code>.--}}
{{-- <span class="opacity-80">Sudo-Regel prüfen.</span>--}}
{{-- </div>--}}
{{-- @else--}}
{{-- <div class="space-y-2">--}}
{{-- @forelse($jails as $j)--}}
{{-- <div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2">--}}
{{-- <div class="flex items-center justify-between">--}}
{{-- <div class="text-white/85 font-medium">{{ $j['name'] }}</div>--}}
{{-- <div class="flex items-center gap-2">--}}
{{-- <span class="text-[11px] text-white/60">--}}
{{-- Bannzeit:--}}
{{-- @if($j['bantime'] === -1)--}}
{{-- permanent--}}
{{-- @else--}}
{{-- {{ $j['bantime'] }}s--}}
{{-- @endif--}}
{{-- </span>--}}
{{-- <span class="px-2 py-0.5 rounded-full border text-[11px]--}}
{{-- {{ $j['banned']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">--}}
{{-- {{ $j['banned'] }} gebannt--}}
{{-- </span>--}}
{{-- --}}{{-- fix: stop event bubbling --}}
{{-- <button wire:click.stop="openDetails('{{ $j['name'] }}')"--}}
{{-- class="text-[11px] px-2 py-0.5 rounded border border-white/15 bg-white/5 hover:bg-white/10">--}}
{{-- Details--}}
{{-- </button>--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- @empty--}}
{{-- <div class="text-sm text-white/60">Keine Jails gefunden.</div>--}}
{{-- @endforelse--}}
{{-- </div>--}}
{{-- <div class="mt-4 flex justify-end">--}}
{{-- <button wire:click="refresh" wire:loading.attr="disabled"--}}
{{-- class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">--}}
{{-- <i class="ph ph-arrows-clockwise text-[13px]"></i>--}}
{{-- <span wire:loading.remove>Neu prüfen</span>--}}
{{-- <span wire:loading>prüfe…</span>--}}
{{-- </button>--}}
{{-- </div>--}}
{{-- @endif--}}
{{--</div>--}}
{{--<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
{{-- <div class="flex items-center justify-between mb-3">--}}
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
{{-- <i class="ph ph-shield-checkered text-white/70 text-[13px]"></i>--}}
{{-- <span class="text-[11px] uppercase text-white/70">Fail2Ban</span>--}}
{{-- </div>--}}
{{-- @if($available)--}}
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
{{-- {{ $activeBans>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' }}">--}}
{{-- {{ $activeBans }} aktuell--}}
{{-- </span>--}}
{{-- @else--}}
{{-- <span class="px-2 py-0.5 rounded-full border text-xs text-rose-300 border-rose-400/30 bg-rose-500/10">--}}
{{-- nicht installiert--}}
{{-- </span>--}}
{{-- @endif--}}
{{-- </div>--}}
{{-- @if(!$available)--}}
{{-- <div class="text-sm text-white/60">fail2ban-client wurde nicht gefunden.</div>--}}
{{-- @elseif($permDenied)--}}

View File

@ -1,11 +1,7 @@
@push('modal.header')
<div class="px-5 pt-5 pb-3 border-b border-white/10 backdrop-blur rounded-t-2xl">
<h2 class="text-[18px] font-semibold text-slate-100">
Fail2Ban {{ $jail }}
</h2>
<p class="text-[13px] text-slate-300/80">
Aktuell gebannte IPs und Restlaufzeiten.
</p>
<h2 class="text-[18px] font-semibold text-slate-100">Fail2Ban {{ $jail }}</h2>
<p class="text-[13px] text-slate-300/80">Aktuell gebannte IPs und Restlaufzeiten.</p>
</div>
@endpush
@ -14,13 +10,9 @@
<div class="rounded-xl border px-4 py-3 {{ $r['box_class'] }}">
<div class="flex items-center justify-between">
<div class="text-white/90 font-mono text-[14px]">{{ $r['ip'] }}</div>
<div class="text-[12px] text-white/80">
{{ $r['time_text'] }}
</div>
</div>
<div class="mt-1 text-[12px] text-white/55">
{{ $r['meta_text'] }}
<div class="text-[12px] text-white/80">{{ $r['time_text'] }}</div>
</div>
<div class="mt-1 text-[12px] text-white/55">{{ $r['meta_text'] }}</div>
</div>
@empty
<div class="rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/70">
@ -32,6 +24,7 @@
@push('modal.footer')
<div class="px-5 py-3 border-t border-white/10 backdrop-blur rounded-b-2xl">
<div class="flex items-center gap-2 justify-end">
{{-- WICHTIG: refresht NUR das Modal --}}
<button wire:click="$dispatch('f2b:refresh-banlist')" wire:loading.attr="disabled"
class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">
<i class="ph ph-arrows-clockwise text-[13px]"></i>
@ -45,3 +38,51 @@
</div>
</div>
@endpush
{{--@push('modal.header')--}}
{{-- <div class="px-5 pt-5 pb-3 border-b border-white/10 backdrop-blur rounded-t-2xl">--}}
{{-- <h2 class="text-[18px] font-semibold text-slate-100">--}}
{{-- Fail2Ban {{ $jail }}--}}
{{-- </h2>--}}
{{-- <p class="text-[13px] text-slate-300/80">--}}
{{-- Aktuell gebannte IPs und Restlaufzeiten.--}}
{{-- </p>--}}
{{-- </div>--}}
{{--@endpush--}}
{{--<div class="p-5 space-y-3">--}}
{{-- @forelse($rows as $r)--}}
{{-- <div class="rounded-xl border px-4 py-3 {{ $r['box_class'] }}">--}}
{{-- <div class="flex items-center justify-between">--}}
{{-- <div class="text-white/90 font-mono text-[14px]">{{ $r['ip'] }}</div>--}}
{{-- <div class="text-[12px] text-white/80">--}}
{{-- {{ $r['time_text'] }}--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- <div class="mt-1 text-[12px] text-white/55">--}}
{{-- {{ $r['meta_text'] }}--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- @empty--}}
{{-- <div class="rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/70">--}}
{{-- Keine gebannten IPs in diesem Jail.--}}
{{-- </div>--}}
{{-- @endforelse--}}
{{--</div>--}}
{{--@push('modal.footer')--}}
{{-- <div class="px-5 py-3 border-t border-white/10 backdrop-blur rounded-b-2xl">--}}
{{-- <div class="flex items-center gap-2 justify-end">--}}
{{-- <button wire:click="$dispatch('f2b:refresh-banlist')" wire:loading.attr="disabled"--}}
{{-- class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">--}}
{{-- <i class="ph ph-arrows-clockwise text-[13px]"></i>--}}
{{-- <span wire:loading.remove>Neu prüfen</span>--}}
{{-- <span wire:loading>prüfe…</span>--}}
{{-- </button>--}}
{{-- <button wire:click="$dispatch('closeModal')"--}}
{{-- class="px-3 py-1.5 rounded-lg text-sm bg-emerald-500/20 text-emerald-300 border border-emerald-400/30 hover:bg-emerald-500/30">--}}
{{-- Fertig--}}
{{-- </button>--}}
{{-- </div>--}}
{{-- </div>--}}
{{--@endpush--}}