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

main v1.0.102
boban 2025-10-30 23:55:07 +01:00
parent 8b4f2d9fe8
commit 67b6e1fa02
3 changed files with 421 additions and 46 deletions

View File

@ -18,7 +18,7 @@ class GuestOnlyMiddleware
{ {
if (Auth::check()) { if (Auth::check()) {
// Eingeloggt → z. B. Dashboard weiterleiten // Eingeloggt → z. B. Dashboard weiterleiten
return redirect()->route('dashboard'); return redirect()->route('ui.dashboard');
} }
return $next($request); return $next($request);

View File

@ -9,7 +9,7 @@ class Fail2BanCard extends Component
public bool $available = true; // fail2ban-client vorhanden? public bool $available = true; // fail2ban-client vorhanden?
public bool $permDenied = false; // Socket/Root-Rechte fehlen? public bool $permDenied = false; // Socket/Root-Rechte fehlen?
public int $activeBans = 0; // Summe gebannter IPs über alle Jails public int $activeBans = 0; // Summe gebannter IPs über alle Jails
public array $jails = []; // [['name'=>'sshd','banned'=>2,'ips'=>['1.2.3.4',...]], ...] public array $jails = []; // [['name','banned','bantime','ips'=>[['ip','remaining','until'],...]],...]
public array $topIps = []; // [['ip'=>'x.x.x.x','count'=>N], ...] public array $topIps = []; // [['ip'=>'x.x.x.x','count'=>N], ...]
public function mount(): void public function mount(): void
@ -22,7 +22,7 @@ class Fail2BanCard extends Component
return view('livewire.ui.security.fail2-ban-card'); return view('livewire.ui.security.fail2-ban-card');
} }
// Wird vom Button "Neu prüfen" genutzt /** Button „Neu prüfen“ */
public function refresh(): void public function refresh(): void
{ {
$this->load(true); $this->load(true);
@ -59,13 +59,46 @@ class Fail2BanCard extends Component
$jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $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) { foreach ($jails as $j) {
$bantimeSecs = $this->getBantime($j); // Sek., -1 = permanent
[, $s] = $this->f2b('status ' . escapeshellarg($j)); [, $s] = $this->f2b('status ' . escapeshellarg($j));
$banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0); $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
$ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
$ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : []; $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : [];
$rows[] = ['name'=>$j,'banned'=>$banned,'ips'=>array_slice($ips, 0, 8)];
// 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; $total += $banned;
} }
@ -91,36 +124,61 @@ class Fail2BanCard extends Component
return [$ok, $out]; 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 private function firstMatch(string $pattern, string $haystack): ?string
{ {
return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; 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 private function collectTopIps(): array
{ {
// 1. Versuch: IPs direkt aus den Jails $map = [];
$rows = [];
foreach ($this->jails as $jail) { foreach ($this->jails as $jail) {
foreach ($jail['ips'] as $ip) { foreach ($jail['ips'] as $row) {
$rows[$ip] = ($rows[$ip] ?? 0) + 1; $ip = $row['ip'] ?? null;
if (!$ip) continue;
$map[$ip] = ($map[$ip] ?? 0) + 1;
} }
} }
if (!empty($rows)) { if (!empty($map)) {
arsort($rows); arsort($map);
return collect($rows) $out = [];
->map(fn($count, $ip) => ['ip' => $ip, 'count' => $count]) foreach (array_slice($map, 0, 5, true) as $ip => $count) {
->values() $out[] = ['ip' => $ip, 'count' => $count];
->take(5) }
->toArray(); 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' $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'; . ' | sort | uniq -c | sort -nr | head -5';
$log = (string)@shell_exec($cmd); $log = (string)@shell_exec($cmd);
$rows = []; $rows = [];
if ($log !== '') { if ($log !== '') {
foreach (preg_split('/\R+/', trim($log)) as $l) { foreach (preg_split('/\R+/', trim($log)) as $l) {
@ -132,3 +190,216 @@ class Fail2BanCard extends Component
return $rows; 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 <IP>"-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;
// }
//}

View File

@ -26,14 +26,36 @@
<div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2"> <div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="text-white/85 font-medium">{{ $j['name'] }}</div> <div class="text-white/85 font-medium">{{ $j['name'] }}</div>
<div class="flex items-center gap-2">
<span class="text-[11px] text-white/50">
Bannzeit:
@if($j['bantime'] === -1) permanent
@else {{ $j['bantime'] }}s
@endif
</span>
<span class="px-2 py-0.5 rounded-full border text-[11px] <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']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">
{{ $j['banned'] }} gebannt {{ $j['banned'] }} gebannt
</span> </span>
</div> </div>
</div>
@if(!empty($j['ips'])) @if(!empty($j['ips']))
<div class="mt-1 text-[12px] text-white/65 font-mono break-words"> <div class="mt-2 grid gap-1">
{{ implode(', ', $j['ips']) }} @foreach($j['ips'] as $ip)
<div class="flex items-center gap-2 text-[12px] text-white/80 font-mono">
<span>{{ $ip['ip'] }}</span>
@if($ip['remaining'] === -1)
<span class="px-1.5 py-0.5 rounded border border-rose-400/30 bg-rose-500/10 text-rose-200">
permanent
</span>
@elseif(is_int($ip['remaining']) && $ip['remaining'] > 0)
<span class="px-1.5 py-0.5 rounded border border-amber-400/30 bg-amber-500/10 text-amber-100">
{{ gmdate('H\h i\m s\s', $ip['remaining']) }}
</span>
@endif
</div>
@endforeach
</div> </div>
@endif @endif
</div> </div>
@ -43,7 +65,7 @@
</div> </div>
{{-- Top IPs aus Ban-Events --}} {{-- Top IPs aus Ban-Events --}}
<div class="mt-3"> <div class="mt-4">
<div class="text-sm text-white/70">Top IPs (letzte Fail2Ban-Logs):</div> <div class="text-sm text-white/70">Top IPs (letzte Fail2Ban-Logs):</div>
<ul class="mt-2 space-y-1 text-sm"> <ul class="mt-2 space-y-1 text-sm">
@forelse($topIps as $i) @forelse($topIps as $i)
@ -67,3 +89,85 @@
</div> </div>
@endif @endif
</div> </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>--}}
{{-- @else--}}
{{-- --}}{{-- Jails --}}
{{-- <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>--}}
{{-- <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>--}}
{{-- </div>--}}
{{-- @if(!empty($j['ips']))--}}
{{-- <div class="mt-1 text-[12px] text-white/65 font-mono break-words">--}}
{{-- {{ implode(', ', $j['ips']) }}--}}
{{-- </div>--}}
{{-- @endif--}}
{{-- </div>--}}
{{-- @empty--}}
{{-- <div class="text-sm text-white/60">Keine Jails gefunden.</div>--}}
{{-- @endforelse--}}
{{-- </div>--}}
{{-- --}}{{-- Top IPs aus Ban-Events --}}
{{-- <div class="mt-3">--}}
{{-- <div class="text-sm text-white/70">Top IPs (letzte Fail2Ban-Logs):</div>--}}
{{-- <ul class="mt-2 space-y-1 text-sm">--}}
{{-- @foreach($j['ips'] as $ip)--}}
{{-- <div class="flex items-center gap-2 text-[12px] text-white/80 font-mono">--}}
{{-- <span>{{ $ip['ip'] }}</span>--}}
{{-- @if($ip['remaining'] === -1)--}}
{{-- <span class="px-1.5 py-0.5 rounded border border-rose-400/30 bg-rose-500/10 text-rose-200">permanent</span>--}}
{{-- @elseif(is_int($ip['remaining']) && $ip['remaining'] > 0)--}}
{{-- <span class="px-1.5 py-0.5 rounded border border-amber-400/30 bg-amber-500/10 text-amber-100">--}}
{{-- {{ gmdate('H\h i\m s\s', $ip['remaining']) }}--}}
{{-- </span>--}}
{{-- @endif--}}
{{-- </div>--}}
{{-- @endforeach--}}
{{-- @forelse($topIps as $i)--}}
{{-- <li class="flex justify-between">--}}
{{-- <span class="text-white/80 font-mono">{{ $i['ip'] }}</span>--}}
{{-- <span class="text-white/60">{{ $i['count'] }}</span>--}}
{{-- </li>--}}
{{-- @empty--}}
{{-- <li class="text-white/50"></li>--}}
{{-- @endforelse--}}
{{-- </ul>--}}
{{-- </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>--}}