406 lines
14 KiB
PHP
406 lines
14 KiB
PHP
<?php
|
|
|
|
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','banned','bantime','ips'=>[['ip','remaining','until'],...]],...]
|
|
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');
|
|
}
|
|
|
|
/** Button „Neu prüfen“ */
|
|
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 = [];
|
|
|
|
foreach ($jails as $j) {
|
|
$bantimeSecs = $this->getBantime($j); // Sek., -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,
|
|
'bantime' => (int)$bantimeSecs,
|
|
];
|
|
$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');
|
|
$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
|
|
{
|
|
return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
|
}
|
|
|
|
/** Top-IPs grob zählen (aus der aktuellen Jail-Liste; Fallback: Log) */
|
|
private function collectTopIps(): array
|
|
{
|
|
$map = [];
|
|
foreach ($this->jails as $jail) {
|
|
foreach ($jail['ips'] as $row) {
|
|
$ip = $row['ip'] ?? null;
|
|
if (!$ip) continue;
|
|
$map[$ip] = ($map[$ip] ?? 0) + 1;
|
|
}
|
|
}
|
|
|
|
if (!empty($map)) {
|
|
arsort($map);
|
|
$out = [];
|
|
foreach (array_slice($map, 0, 5, true) as $ip => $count) {
|
|
$out[] = ['ip' => $ip, 'count' => $count];
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
// Fallback: aus fail2ban.log
|
|
$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;
|
|
}
|
|
}
|
|
|
|
//
|
|
//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;
|
|
// }
|
|
//}
|