mailwolt/app/Livewire/Ui/Security/Modal/Fail2BanJailModal.php

396 lines
13 KiB
PHP

<?php
namespace App\Livewire\Ui\Security\Modal;
use LivewireUI\Modal\ModalComponent;
class Fail2BanJailModal extends ModalComponent
{
public string $jail = '';
public array $rows = []; // [{ip,bantime,banned_at,remaining,until,time_text,meta_text,box_class}]
public static function modalMaxWidth(): string
{
return '4xl';
}
public function mount(string $jail): void
{
$this->jail = $jail;
$this->load();
}
public function render()
{
return view('livewire.ui.security.modal.fail2-ban-jail-modal');
}
public function refresh(): void
{
$this->load(true);
}
/* ---------------- intern ---------------- */
protected function load(bool $force = false): void
{
$jail = $this->jail;
// Aktuell gebannte IPs
[, $s] = $this->f2b('status ' . escapeshellarg($jail));
$ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
$ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : [];
$bantime = $this->getBantime($jail);
$rows = [];
foreach ($ips as $ip) {
$banAt = $this->lastBanTimestamp($jail, $ip); // Unix-Timestamp oder null
$remaining = null;
$until = null;
if ($bantime === -1) {
$remaining = -1;
} elseif ($banAt !== null) {
$remaining = max(0, $bantime - (time() - $banAt));
$until = $remaining > 0 ? ($banAt + $bantime) : null;
}
[$timeText, $metaText, $boxClass] = $this->present($remaining, $banAt, $until);
$rows[] = [
'ip' => $ip,
'bantime' => $bantime,
'banned_at' => $banAt,
'remaining' => $remaining,
'until' => $until,
'time_text' => $timeText,
'meta_text' => $metaText,
'box_class' => $boxClass,
];
}
$this->rows = $rows;
}
/** sudo + fail2ban-client */
private function f2b(string $args): array
{
$sudo = '/usr/bin/sudo';
$f2b = '/usr/bin/fail2ban-client';
$out = (string)@shell_exec("timeout 3 $sudo -n $f2b $args 2>&1");
$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;
}
/** robust: zgrep + journalctl via sudo */
private function lastBanTimestamp(string $jail, string $ip): ?int
{
$sudo = '/usr/bin/sudo';
$zgrep = '/usr/bin/zgrep';
$journal = '/usr/bin/journalctl';
$tail = '/usr/bin/tail';
$needle = sprintf('[%s] Ban %s', $jail, $ip);
$q = escapeshellarg($needle);
// Logs inkl. Rotation (gz)
$cmd1 = "$sudo -n $zgrep -h $q /var/log/fail2ban.log* 2>/dev/null | $tail -n 1";
$line = trim((string)@shell_exec($cmd1));
if ($line === '') { // Fallback: journald
$cmd2 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager -g $q 2>/dev/null | $tail -n 1";
$line = trim((string)@shell_exec($cmd2));
}
if ($line === '') return null;
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;
}
private function firstMatch(string $pattern, string $haystack): ?string
{
return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
}
private function present(?int $remaining, ?int $banAt, ?int $until): array
{
$amber = 'border-amber-400/30 bg-amber-500/10';
$rose = 'border-rose-400/30 bg-rose-500/10';
$muted = 'border-white/10 bg-white/5';
if ($remaining === -1) {
return ['permanent', $banAt ? ('seit ' . $this->fmtTs($banAt)) : '—', $rose];
}
if (is_int($remaining)) {
if ($remaining > 0) {
$meta = [];
if ($banAt) $meta[] = 'seit ' . $this->fmtTs($banAt);
if ($until) $meta[] = 'bis ' . $this->fmtTs($until);
return [$this->fmtSecs($remaining), $meta ? implode(' • ', $meta) : '—', $amber];
}
return ['abgelaufen', $banAt ? ('seit ' . $this->fmtTs($banAt)) : '—', $muted];
}
return ['—', '—', $muted];
}
private function fmtSecs(int $s): string
{
$h = intdiv($s, 3600);
$m = intdiv($s % 3600, 60);
$r = $s % 60;
if ($h > 0) return sprintf('%dh %02dm %02ds', $h, $m, $r);
if ($m > 0) return sprintf('%02dm %02ds', $m, $r);
return sprintf('%ds', $r);
}
private function fmtTs(int $ts): string
{
return date('d.m. H:i', $ts);
}
}
//
//namespace App\Livewire\Ui\Security\Modal;
//
//use LivewireUI\Modal\ModalComponent;
//
//class Fail2BanJailModal extends ModalComponent
//{
// public string $jail = '';
// /** @var array<int,array{
// * ip:string,
// * bantime:int, // Sek.; -1 = permanent
// * banned_at:?int, // Unix-Timestamp
// * remaining:?int, // -1=permanent, 0..Sekunden, null=unbekannt
// * until:?int, // Unix-Timestamp oder null
// * time_text:string, // "HHh MMm SSs", "permanent", "—"
// * meta_text:string, // "seit … • bis …" oder "seit …" oder "—"
// * box_class:string // Tailwind-Klassen für BG/Border
// * }]
// */
// public array $rows = [];
//
// public static function modalMaxWidth(): string
// {
// return '4xl';
// }
//
// public function mount(string $jail): void
// {
// $this->jail = $jail;
// $this->load();
// }
//
// public function refresh(): void
// {
// $this->load(true);
// }
//
// public function render()
// {
// return view('livewire.ui.security.modal.fail2-ban-jail-modal');
// }
//
// /* ---------------- intern ---------------- */
//
// protected function load(bool $force = false): void
// {
// $jail = $this->jail;
//
// // Aktuell gebannte IPs
// [, $s] = $this->f2b('status '.escapeshellarg($jail));
// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : [];
//
// // bantime für Jail
// $bantime = $this->getBantime($jail);
//
// $rows = [];
// foreach ($ips as $ip) {
// $banAt = $this->lastBanTimestamp($jail, $ip); // Unix-Timestamp oder null
//
// $remaining = null;
// $until = null;
// if ($banAt !== null) {
// if ($bantime === -1) {
// $remaining = -1;
// } else {
// $remaining = max(0, $bantime - (time() - $banAt));
// $until = $remaining > 0 ? $banAt + $bantime : null;
// }
// }
//
// // Darstellung
// [$timeText, $metaText, $boxClass] = $this->present($remaining, $banAt, $until);
//
// $rows[] = [
// 'ip' => $ip,
// 'bantime' => $bantime,
// 'banned_at' => $banAt,
// 'remaining' => $remaining,
// 'until' => $until,
// 'time_text' => $timeText,
// 'meta_text' => $metaText,
// 'box_class' => $boxClass,
// ];
// }
//
// $this->rows = $rows;
// }
//
// /** letzte "Ban <IP>"-Zeile -> Unix-Timestamp (robust über zgrep/journal/jail-tail) */
// private function lastBanTimestamp(string $jail, string $ip): ?int
// {
// $sudo = '/usr/bin/sudo';
// $zgrep = '/usr/bin/zgrep';
// $journal = '/usr/bin/journalctl';
// $tail = '/usr/bin/tail';
//
// $needle = sprintf('[%s] Ban %s', $jail, $ip);
// $q = escapeshellarg($needle);
//
// // 1) zgrep über alle fail2ban-Logs (inkl. Rotation .N, .gz)
// $cmd1 = "$sudo -n $zgrep -h $q /var/log/fail2ban.log* 2>/dev/null | $tail -n 1";
// $line = trim((string)@shell_exec($cmd1));
//
// // 2) Fallback: journald (ohne grep-Pipe, mit -g)
// if ($line === '') {
// $cmd2 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager -g $q 2>/dev/null | $tail -n 1";
// $line = trim((string)@shell_exec($cmd2));
// }
//
// if ($line === '') return null;
//
// // Zeitstempel extrahieren (YYYY-MM-DD HH:MM:SS)
// 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;
// }
//
// /** baut Details inkl. Restzeit; wenn kein Timestamp gefunden: ~bantime als Approximation */
// 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;
// } elseif ($banAt !== null) {
// $remaining = max(0, $bantime - ($now - $banAt));
// $until = $remaining > 0 ? ($banAt + $bantime) : null;
// } else {
// // kein Ban-Timestamp gefunden → Approximation anzeigen
// $remaining = -2; // Kennzeichen für "~bantime"
// }
//
// $out[] = [
// 'ip' => $ip,
// 'banned_at' => $banAt,
// 'remaining' => $remaining, // -1=permanent, -2≈approx, 0..Sek, null=unbekannt
// 'until' => $until,
// ];
// }
// return $out;
// }
//
// /** Darstellung (permanent / Restzeit / abgelaufen / approx / unbekannt) */
// private function present(?int $remaining, ?int $banAt, ?int $until): array
// {
// $green = 'border-emerald-400/30 bg-emerald-500/10';
// $amber = 'border-amber-400/30 bg-amber-500/10';
// $rose = 'border-rose-400/30 bg-rose-500/10';
// $muted = 'border-white/10 bg-white/5';
//
// if ($remaining === -1) {
// return ['permanent', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $rose];
// }
// if ($remaining === -2) {
// // Approximation: Bannzeit ohne Startzeitpunkt
// return ['~ '.$this->fmtSecs((int) $this->getApproxBantime()), '—', $amber];
// }
// if (is_int($remaining)) {
// if ($remaining > 0) {
// $meta = [];
// if ($banAt) $meta[] = 'seit '.$this->fmtTs($banAt);
// if ($until) $meta[] = 'bis '.$this->fmtTs($until);
// return [$this->fmtSecs($remaining), $meta ? implode(' • ', $meta) : '—', $amber];
// }
// return ['abgelaufen', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $muted];
// }
// return ['—', '—', $muted];
// }
//
//// Bantime als Approximation (du kannst das einfach aus dem aufrufenden Kontext übergeben;
//// hier fallback 600)
// private function getApproxBantime(): int { return 600; }
//
// /** Darstellung ableiten (Farbcode + Texte) */
//
// /** sudo + fail2ban-client */
// private function f2b(string $args): array
// {
// $sudo = '/usr/bin/sudo';
// $f2b = '/usr/bin/fail2ban-client';
// $out = (string) @shell_exec("timeout 3 $sudo -n $f2b $args 2>&1");
// $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;
// }
//
// /** letzte "Ban <IP>"-Zeile parsen -> Unix-Timestamp */
// /** letzte "Ban <IP>"-Zeile -> Unix-Timestamp aus /var/log/fail2ban.log */
//
// private function firstMatch(string $pattern, string $haystack): ?string
// {
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
// }
//
//
// private function fmtSecs(int $s): string
// {
// // kompakt: 1h 23m 45s / 05m 10s / 12s
// $h = intdiv($s, 3600);
// $m = intdiv($s % 3600, 60);
// $r = $s % 60;
// if ($h > 0) return sprintf('%dh %02dm %02ds', $h, $m, $r);
// if ($m > 0) return sprintf('%02dm %02ds', $m, $r);
// return sprintf('%ds', $r);
// }
//
// private function fmtTs(int $ts): string
// {
// // 29.10. 18:07
// return date('d.m. H:i', $ts);
// }
//}