parent
6b0dd7d176
commit
4d1fd64158
|
|
@ -31,9 +31,8 @@ class Fail2BanCard extends Component
|
|||
// Optional: öffnet später dein Detail-Modal/Tab
|
||||
public function openDetails(string $jail): void
|
||||
{
|
||||
$this->dispatch('security:open-f2b', jail: $jail);
|
||||
$this->dispatch('openModal', component: 'ui.security.modal.fail2-ban-jail-modal', arguments: ['jail' => $jail]);
|
||||
}
|
||||
|
||||
/* ---------------- intern ---------------- */
|
||||
|
||||
protected function load(bool $force = false): void
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
<?php
|
||||
|
||||
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.fail2ban-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;
|
||||
}
|
||||
|
||||
/** Darstellung ableiten (Farbcode + Texte) */
|
||||
private function present(?int $remaining, ?int $banAt, ?int $until): array
|
||||
{
|
||||
// Farben an dein Schema angelehnt
|
||||
$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) {
|
||||
$time = 'permanent';
|
||||
$meta = $banAt ? ('seit '.$this->fmtTs($banAt)) : '—';
|
||||
return [$time, $meta, $rose];
|
||||
}
|
||||
|
||||
if (is_int($remaining)) {
|
||||
if ($remaining > 0) {
|
||||
$time = $this->fmtSecs($remaining);
|
||||
$parts = [];
|
||||
if ($banAt) $parts[] = 'seit '.$this->fmtTs($banAt);
|
||||
if ($until) $parts[] = 'bis '.$this->fmtTs($until);
|
||||
$meta = $parts ? implode(' • ', $parts) : '—';
|
||||
return [$time, $meta, $amber];
|
||||
}
|
||||
|
||||
// 0 Sekunden -> theoretisch abgelaufen
|
||||
$time = 'abgelaufen';
|
||||
$meta = $banAt ? ('seit '.$this->fmtTs($banAt)) : '—';
|
||||
return [$time, $meta, $muted];
|
||||
}
|
||||
|
||||
// keine Info
|
||||
return ['—', '—', $muted];
|
||||
}
|
||||
|
||||
/** 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 */
|
||||
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));
|
||||
if ($line === '') return null;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Ui\Security\Modal\Fail2BanJailModal;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class Php extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.security.modal.fail2-ban-jail-modal.php');
|
||||
}
|
||||
}
|
||||
|
|
@ -32,14 +32,6 @@
|
|||
<div class="text-white/85 font-medium">{{ $j['name'] }}</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[11px] text-white/60">
|
||||
Bannzeit:
|
||||
@if($j['bantime'] === -1)
|
||||
permanent
|
||||
@else
|
||||
{{ $j['bantime'] }}s
|
||||
@endif
|
||||
</span>
|
||||
<span class="px-2 py-0.5 rounded-full border text-[11px]
|
||||
{{ $j['banned']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">
|
||||
{{ $j['banned'] }} gebannt
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
@push('modal.header')
|
||||
<div class="px-5 pt-5 pb-3 border-b border-white/10 backdrop-blur rounded-t-2xl">
|
||||
<h2 class="text-[18px] font-semibold text-slate-100">
|
||||
Fail2Ban – {{ $jail }}
|
||||
</h2>
|
||||
<p class="text-[13px] text-slate-300/80">
|
||||
Aktuell gebannte IPs und Restlaufzeiten.
|
||||
</p>
|
||||
</div>
|
||||
@endpush
|
||||
|
||||
<div class="p-5 space-y-3">
|
||||
@forelse($rows as $r)
|
||||
<div class="rounded-xl border px-4 py-3 {{ $r['box_class'] }}">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-white/90 font-mono text-[14px]">{{ $r['ip'] }}</div>
|
||||
<div class="text-[12px] text-white/80">
|
||||
{{ $r['time_text'] }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 text-[12px] text-white/55">
|
||||
{{ $r['meta_text'] }}
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/70">
|
||||
Keine gebannten IPs in diesem Jail.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
@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"
|
||||
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>
|
||||
<span wire:loading>prüfe…</span>
|
||||
</button>
|
||||
<button wire:click="$dispatch('closeModal')"
|
||||
class="px-3 py-1.5 rounded-lg text-sm bg-emerald-500/20 text-emerald-300 border border-emerald-400/30 hover:bg-emerald-500/30">
|
||||
Fertig
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
Loading…
Reference in New Issue