Feat: Fail2ban-Banlist zeigt Restlaufzeit und lesbaren Dienst-Namen

main v1.1.421
boban 2026-04-27 04:03:01 +02:00
parent aaeea2bb86
commit fc0ede4840
2 changed files with 67 additions and 40 deletions

View File

@ -4,6 +4,7 @@ namespace App\Livewire\Ui\Security;
use Livewire\Attributes\On;
use Livewire\Component;
use SQLite3;
class Fail2banBanlist extends Component
{
@ -15,22 +16,9 @@ class Fail2banBanlist extends Component
public ?string $jail = null;
/**
* 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<int,array{
* ip:string,jail:string,permanent:bool,label:string,box:string,badge:string,btn:string
* ip:string,jail:string,service:string,permanent:bool,label:string,
* remaining:string,box:string,badge:string,dot:string,btn:string
* }>
*/
public array $rows = [];
@ -75,7 +63,9 @@ class Fail2banBanlist extends Component
continue;
}
$permanent = $this->isPermanent($j, $ip);
$banInfo = $this->getBanInfo($j, $ip);
$permanent = $banInfo['permanent'];
$remaining = $banInfo['remaining'];
if ($permanent) {
$box = 'border-rose-400/30 bg-rose-500/5';
@ -94,9 +84,11 @@ class Fail2banBanlist extends Component
$rows[] = [
'ip' => $ip,
'jail' => $j,
'service' => $this->serviceLabel($j),
'permanent' => $permanent,
'style' => $style,
'label' => $label,
'remaining' => $remaining,
'box' => $box,
'badge' => $badge,
'dot' => $dot,
@ -140,35 +132,67 @@ class Fail2banBanlist extends Component
/* ================= helpers ================= */
/** Prüft via SQLite, ob der **letzte** Ban für (jail, ip) permanent ist (bantime < 0). */
private function isPermanent(string $jail, string $ip): bool
/** Gibt permanent-Flag und verbleibende Zeit für (jail, ip) zurück. */
private function getBanInfo(string $jail, string $ip): array
{
$db = $this->getDbFile();
$fallbackPermanent = ($jail === 'mailwolt-blacklist');
if ($db === '' || !is_readable($db)) {
// Fallback: Blacklist-Jail ist per Design permanent
return $jail === 'mailwolt-blacklist';
return ['permanent' => $fallbackPermanent, 'remaining' => ''];
}
$q = <<<SQL
WITH last AS (
SELECT MAX(timeofban) AS t
FROM bans
WHERE jail = '$jail' AND ip = '$ip'
)
SELECT bantime
FROM bans, last
WHERE jail = '$jail' AND ip = '$ip' AND timeofban = last.t
LIMIT 1;
SQL;
$cmd = sprintf(
'sudo -n /usr/bin/sqlite3 -readonly %s %s 2>&1',
escapeshellarg($db),
escapeshellarg($q)
$q = sprintf(
"WITH last AS (SELECT MAX(timeofban) AS t FROM bans WHERE jail='%s' AND ip='%s') ".
"SELECT timeofban, bantime FROM bans, last ".
"WHERE jail='%s' AND ip='%s' AND timeofban=last.t LIMIT 1;",
SQLite3::escapeString($jail), SQLite3::escapeString($ip),
SQLite3::escapeString($jail), SQLite3::escapeString($ip)
);
$cmd = sprintf('sudo -n /usr/bin/sqlite3 -readonly %s %s 2>&1',
escapeshellarg($db), escapeshellarg($q));
$out = trim((string)@shell_exec($cmd));
if ($out === '') return ($jail === 'mailwolt-blacklist'); // Fallback
return ((int)$out) < 0;
if ($out === '') {
return ['permanent' => $fallbackPermanent, 'remaining' => ''];
}
[$timeofban, $bantime] = array_pad(explode('|', $out), 2, '0');
$timeofban = (int)$timeofban;
$bantime = (int)$bantime;
if ($bantime < 0) {
return ['permanent' => true, 'remaining' => ''];
}
$remaining = max(0, ($timeofban + $bantime) - time());
return [
'permanent' => false,
'remaining' => $this->formatRemaining($remaining),
];
}
private function formatRemaining(int $seconds): string
{
if ($seconds <= 0) return 'läuft ab';
if ($seconds < 60) return "noch {$seconds} Sek.";
if ($seconds < 3600) return 'noch ' . (int)ceil($seconds / 60) . ' Min.';
if ($seconds < 86400) return 'noch ' . round($seconds / 3600, 1) . ' Std.';
return 'noch ' . (int)ceil($seconds / 86400) . ' Tage';
}
private function serviceLabel(string $jail): string
{
return match(true) {
$jail === 'sshd' => 'SSH',
$jail === 'dovecot' => 'IMAP/POP3',
str_starts_with($jail, 'postfix') => 'SMTP',
str_starts_with($jail, 'nginx') => 'Web',
$jail === 'recidive' => 'Recidive',
str_contains($jail, 'blacklist') => 'Blacklist',
default => $jail,
};
}
/** Liste aller Jails */

View File

@ -21,11 +21,14 @@
<div style="padding:10px 12px">
@foreach($rows as $r)
<div class="f2b-row {{ $r['style'] }}">
<div style="display:flex;align-items:center;gap:10px;min-width:0">
<div style="display:flex;align-items:center;gap:10px;min-width:0;flex:1">
<div class="f2b-dot {{ $r['style'] }}"></div>
<code style="font-size:12.5px;color:var(--mw-t1);letter-spacing:.02em;flex-shrink:0">{{ $r['ip'] }}</code>
<span class="f2b-jail-badge">{{ $r['jail'] }}</span>
<span class="f2b-jail-badge" title="{{ $r['jail'] }}">{{ $r['service'] }}</span>
<span style="font-size:10.5px;color:{{ $r['style'] === 'permanent' ? 'rgba(252,165,165,.8)' : 'rgba(252,211,77,.8)' }}">{{ $r['label'] }}</span>
@if($r['remaining'] !== '')
<span style="font-size:10.5px;color:var(--mw-t4)">· {{ $r['remaining'] }}</span>
@endif
</div>
<button wire:click="unban('{{ $r['ip'] }}','{{ $r['jail'] }}')"
class="mbx-act-btn mbx-act-danger" title="Entbannen"