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

122 lines
3.9 KiB
PHP

<?php
namespace App\Livewire\Ui\Security;
use Livewire\Component;
class Fail2BanCard extends Component
{
public bool $available = true; // ob fail2ban vorhanden ist
public int $activeBans = 0; // Summe über alle Jails
/** @var array<int,array{name:string,banned:int,ips:array<int,string>}> */
public array $jails = []; // je Jail: Name, Anzahl, IPs (gekürzt)
/** @var array<int,array{ip:string,count:int}> */
public array $topIps = []; // Top IPs aus Log/Journal (Ban-Events)
public function mount(): void
{
$this->load();
}
public function render()
{
return view('livewire.ui.security.fail2-ban-card');
}
public function refresh(): void
{
$this->load(true);
}
protected function load(bool $force = false): void
{
// 0) vorhanden?
$bin = trim((string) @shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
if ($bin === '') {
$this->available = false;
$this->activeBans = 0;
$this->jails = [];
$this->topIps = [];
return;
}
// 1) Jails ermitteln
$status = (string) (@shell_exec("timeout 2 $bin status 2>/dev/null") ?? '');
$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 = (string) (@shell_exec("timeout 2 $bin status ".escapeshellarg($j)." 2>/dev/null") ?? '');
$banned = (int) ($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
// „Banned IP list:“ kann fehlen → leeres Array
$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,
// nur die ersten 8 zur UI-Anzeige
'ips' => array_slice($ips, 0, 8),
];
$total += $banned;
}
$this->available = true;
$this->activeBans = $total;
$this->jails = $rows;
// 2) Top-IPs aus den letzten Ban-Events
$this->topIps = $this->collectTopIps();
}
/** Extrahiert erste Regex-Gruppe oder null */
private function firstMatch(string $pattern, string $haystack): ?string
{
return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
}
/** Zählt „Ban <IP>“ aus fail2ban.log (Fallback: journalctl) */
private function collectTopIps(): array
{
$lines = '';
if (is_readable('/var/log/fail2ban.log')) {
// letzte 500 Zeilen reichen völlig
$lines = (string) (@shell_exec('tail -n 500 /var/log/fail2ban.log 2>/dev/null') ?? '');
}
if ($lines === '') {
// Fallback: Journal (falls rsyslog die Datei nicht schreibt)
$lines = (string) (@shell_exec('timeout 2 journalctl -u fail2ban -n 500 --no-pager 2>/dev/null') ?? '');
}
if ($lines === '') {
return [];
}
// Nur Ban-Events, IP extrahieren
$ips = [];
foreach (preg_split('/\R+/', $lines) as $ln) {
if (stripos($ln, 'Ban ') === false) continue;
if (preg_match('/\b(\d{1,3}(?:\.\d{1,3}){3})\b/', $ln, $m)) {
$ip = $m[1];
$ips[$ip] = ($ips[$ip] ?? 0) + 1;
}
}
if (!$ips) return [];
arsort($ips);
$top = array_slice($ips, 0, 5, true);
$out = [];
foreach ($top as $ip => $cnt) {
$out[] = ['ip' => $ip, 'count' => (int)$cnt];
}
return $out;
}
}