parent
e77d9f64bb
commit
d9867db546
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
namespace App\Livewire\Ui\Security;
|
||||
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class Fail2BanCard extends Component
|
||||
|
|
@ -22,6 +23,7 @@ class Fail2BanCard extends Component
|
|||
return view('livewire.ui.security.fail2-ban-card');
|
||||
}
|
||||
|
||||
#[On('f2b:refresh-banlist')]
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->load(true);
|
||||
|
|
@ -35,67 +37,205 @@ class Fail2BanCard extends Component
|
|||
|
||||
/* ------------------- 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;
|
||||
// }
|
||||
//
|
||||
// [$ok, $raw] = $this->f2b('ping');
|
||||
// if (!$ok && stripos($raw, 'permission denied') !== false) {
|
||||
// $this->available = true;
|
||||
// $this->permDenied = true;
|
||||
// $this->activeBans = 0;
|
||||
// $this->jails = [];
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// [, $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) {
|
||||
// [, $s] = $this->f2b('status '.escapeshellarg($j));
|
||||
// $bantime = $this->getBantime($j);
|
||||
// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
|
||||
// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : [];
|
||||
//
|
||||
// $ipDetails = $this->buildIpDetails($j, $ips, $bantime);
|
||||
//
|
||||
// [, $s] = $this->f2b('status ' . escapeshellarg($j));
|
||||
// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||
// $bantime = $this->getBantime($j);
|
||||
// $rows[] = ['name' => $j, 'banned' => $banned, 'bantime' => $bantime];
|
||||
// $sum += $banned;
|
||||
// }
|
||||
//
|
||||
// $this->available = true;
|
||||
// $this->permDenied = false;
|
||||
// $this->activeBans = $sum;
|
||||
// $this->jails = $rows;
|
||||
// }
|
||||
|
||||
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->available = false;
|
||||
$this->permDenied = false;
|
||||
$this->activeBans = 0;
|
||||
$this->jails = [];
|
||||
$this->jails = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Rechte prüfen
|
||||
[$ok, $raw] = $this->f2b('ping');
|
||||
if (!$ok && stripos($raw, 'permission denied') !== false) {
|
||||
$this->available = true;
|
||||
$this->available = true;
|
||||
$this->permDenied = true;
|
||||
$this->activeBans = 0;
|
||||
$this->jails = [];
|
||||
$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))) : [];
|
||||
$jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : [];
|
||||
|
||||
$rows = [];
|
||||
$sum = 0;
|
||||
$sum = 0;
|
||||
|
||||
foreach ($jails as $j) {
|
||||
[, $s] = $this->f2b('status ' . escapeshellarg($j));
|
||||
$banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||
$bantime = $this->getBantime($j);
|
||||
$rows[] = ['name' => $j, 'banned' => $banned, 'bantime' => $bantime];
|
||||
$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->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';
|
||||
// $out = (string)@shell_exec("timeout 2 $sudo -n $f2b $args 2>&1");
|
||||
// $ok = str_contains($out, 'Status') || str_contains($out, 'Jail list') || str_contains($out, 'pong');
|
||||
// return [$ok, $out];
|
||||
// }
|
||||
|
||||
private function f2b(string $args): array
|
||||
{
|
||||
$sudo = '/usr/bin/sudo';
|
||||
$f2b = '/usr/bin/fail2ban-client';
|
||||
$out = (string)@shell_exec("timeout 2 $sudo -n $f2b $args 2>&1");
|
||||
$ok = str_contains($out, 'Status') || str_contains($out, 'Jail list') || str_contains($out, 'pong');
|
||||
$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');
|
||||
[, $out] = $this->f2b('get '.escapeshellarg($jail).' bantime');
|
||||
$val = trim($out);
|
||||
if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0];
|
||||
return 600;
|
||||
return 600; // konservativer Fallback
|
||||
}
|
||||
|
||||
// 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;
|
||||
// }
|
||||
|
||||
/** baut Detail-Liste inkl. Restzeit */
|
||||
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, 'until' => $until];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** letzte "Ban <IP>"-Zeile finden (Log + Rotation + journald), liefert Unix-Timestamp oder null */
|
||||
private function lastBanTimestamp(string $jail, string $ip): ?int
|
||||
{
|
||||
// 1) zgrep in /var/log/fail2ban.log*
|
||||
$line = (string)@shell_exec(
|
||||
"zgrep -h -E '\\[{$jail}\\]\\s+Ban\\s+{$ip}' /var/log/fail2ban.log* 2>/dev/null | tail -n 1"
|
||||
);
|
||||
$line = trim($line);
|
||||
|
||||
// 2) Fallback: journald
|
||||
if ($line === '') {
|
||||
$line = (string)@shell_exec(
|
||||
"journalctl -u fail2ban --since '14 days ago' 2>/dev/null | grep -F '[{$jail}] Ban {$ip}' | tail -n 1"
|
||||
);
|
||||
$line = trim($line);
|
||||
}
|
||||
if ($line === '') return null;
|
||||
|
||||
// "YYYY-MM-DD HH:MM:SS,mmm ..." oder ISO im Text
|
||||
if (preg_match('/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})/', $line, $m)
|
||||
|| 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 firstMatch(string $pattern, string $haystack): ?string
|
||||
// {
|
||||
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -146,15 +146,41 @@ class Fail2BanJailModal extends ModalComponent
|
|||
/** letzte "Ban <IP>"-Zeile parsen -> Unix-Timestamp */
|
||||
private function lastBanTimestamp(string $jail, string $ip): ?int
|
||||
{
|
||||
$cmd = 'grep -F ' . escapeshellarg("[$jail] Ban $ip")
|
||||
. ' /var/log/fail2ban.log 2>/dev/null | tail -n 1';
|
||||
$line = trim((string) @shell_exec($cmd));
|
||||
$jailQ = escapeshellarg($jail);
|
||||
$ipQ = escapeshellarg($ip);
|
||||
|
||||
// 1) Durchsuche fail2ban.log + rotierte + .gz
|
||||
$cmdFiles = "sh -c 'ls -1 /var/log/fail2ban.log* 2>/dev/null | wc -l'";
|
||||
$haveFiles = ((int) @shell_exec($cmdFiles)) > 0;
|
||||
|
||||
$line = '';
|
||||
if ($haveFiles) {
|
||||
// zgrep -h: ohne Dateinamen; -E für Regex; tail -n1: letzte Ban-Zeile
|
||||
$pattern = escapeshellarg(sprintf('\\[%s\\] Ban %s', $jail, $ip));
|
||||
$cmd = "zgrep -h -E \"\\[${jail}\\]\\s+Ban\\s+${ip}\" /var/log/fail2ban.log* 2>/dev/null | tail -n 1";
|
||||
$line = (string) @shell_exec($cmd);
|
||||
}
|
||||
|
||||
// 2) Fallback: journald (wenn kein Dateilog)
|
||||
if (trim($line) === '') {
|
||||
// seit 14 Tagen scannen, Format wie im file-log
|
||||
$cmdJ = "journalctl -u fail2ban --since '14 days ago' 2>/dev/null | grep -F \"[{$jail}] Ban {$ip}\" | tail -n 1";
|
||||
$line = (string) @shell_exec($cmdJ);
|
||||
}
|
||||
|
||||
$line = trim($line);
|
||||
if ($line === '') return null;
|
||||
|
||||
// Datum am Anfang: 2025-10-29 18:07:11,436 ...
|
||||
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;
|
||||
}
|
||||
// journald kann anderes Präfix haben; versuche generischen ISO-Zeitstempel irgendwo in der Zeile
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +189,31 @@ class Fail2BanJailModal extends ModalComponent
|
|||
return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||
}
|
||||
|
||||
private function buildIpDetails(string $jail, array $ips, int $bantime): array
|
||||
{
|
||||
$out = [];
|
||||
$now = time();
|
||||
|
||||
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=unbekannt, 0..N Sekunden
|
||||
'until' => $until,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function fmtSecs(int $s): string
|
||||
{
|
||||
// kompakt: 1h 23m 45s / 05m 10s / 12s
|
||||
|
|
|
|||
|
|
@ -32,7 +32,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">
|
||||
<button wire:click="refresh" wire:loading.attr="disabled"
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue