mailwolt/app/Livewire/Ui/Security/Fail2BanCard.php

135 lines
4.5 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'=>'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 = [];
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 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;
}
}