clusev/app/Livewire/Modals/Fail2banConfig.php

140 lines
4.3 KiB
PHP

<?php
namespace App\Livewire\Modals;
use App\Models\AuditEvent;
use App\Models\Server;
use App\Services\MaintenanceService;
use Illuminate\Support\Facades\Auth;
use LivewireUI\Modal\ModalComponent;
use Throwable;
/**
* Tune the fail2ban [DEFAULT] policy (Sperrdauer / Max. Fehlversuche / Zeitfenster)
* via a small German form — no shell commands are ever shown. mount() loads the
* current values; save() clamps + validates the integers, writes jail.local, AUDITs,
* notifies and closes.
*/
class Fail2banConfig extends ModalComponent
{
public int $serverId;
public string $serverName = '';
/** fail2ban duration grammar (e.g. 600, 10m, 1h 30m, -1 = dauerhaft) — kept verbatim. */
public string $bantime = '10m';
public int $maxretry = 5;
public string $findtime = '10m';
public ?string $error = null;
/** True only once the current policy was read — guards save() from overwriting with unseen defaults. */
public bool $loaded = false;
public function mount(int $serverId, MaintenanceService $maintenance): void
{
$this->serverId = $serverId;
$server = Server::find($serverId);
if (! $server) {
$this->error = 'Server nicht gefunden.';
return;
}
$this->serverName = $server->name;
try {
$current = $maintenance->readFail2ban($server);
$this->bantime = $current['bantime'];
$this->maxretry = $current['maxretry'];
$this->findtime = $current['findtime'];
$this->loaded = true;
} catch (Throwable $e) {
$this->error = $e->getMessage();
}
}
public static function modalMaxWidth(): string
{
return 'lg';
}
public function save(MaintenanceService $maintenance): void
{
// Never overwrite the remote policy with defaults the operator never saw —
// saving is only allowed once the current values were read successfully.
if (! $this->loaded) {
$this->error = 'Aktuelle Konfiguration konnte nicht gelesen werden — Speichern ist gesperrt.';
return;
}
$this->error = null;
// fail2ban duration grammar: a number (seconds) optionally with a unit
// (s/m/h/d/w/mo/y), space-separated composites, or -1 for a permanent ban.
$duration = ['required', 'string', 'max:40', 'regex:/^(?:-1|\d+(?:\.\d+)?\s*(?:s|m|h|d|w|mo|y)?(?:\s+\d+(?:\.\d+)?\s*(?:s|m|h|d|w|mo|y)?)*)$/i'];
$validated = $this->validate([
'bantime' => $duration,
'maxretry' => ['required', 'integer', 'min:1', 'max:100'],
'findtime' => $duration,
], [
'bantime.regex' => 'Ungültige Dauer (z. B. 600, 10m, 1h, -1 für dauerhaft).',
'findtime.regex' => 'Ungültige Dauer (z. B. 600, 10m, 1h).',
], [
'bantime' => 'Sperrdauer',
'maxretry' => 'Max. Fehlversuche',
'findtime' => 'Zeitfenster',
]);
$server = Server::find($this->serverId);
if (! $server) {
$this->error = 'Server nicht gefunden.';
return;
}
try {
// Values are already validated/clamped to the allowed ranges above.
$result = $maintenance->writeFail2ban(
$server,
(string) $validated['bantime'],
(int) $validated['maxretry'],
(string) $validated['findtime'],
);
} catch (Throwable $e) {
$this->error = $e->getMessage();
return;
}
if (! $result['ok']) {
$this->error = 'Speichern fehlgeschlagen. '.$result['output'];
return;
}
AuditEvent::create([
'user_id' => Auth::id(),
'server_id' => $server->id,
'actor' => Auth::user()?->name ?? 'system',
'action' => 'fail2ban.configure',
'target' => $server->name.' · bantime '.$validated['bantime'].', maxretry '.$validated['maxretry'],
'ip' => request()->ip(),
]);
$this->dispatch('hardeningApplied');
$this->dispatch('notify', message: 'fail2ban konfiguriert.');
$this->closeModal();
}
public function render()
{
return view('livewire.modals.fail2ban-config');
}
}