Sync pending admin UI and security panel updates

This commit is contained in:
root
2026-02-01 01:08:46 +02:00
parent 868f60d1a3
commit b382d13c34
20 changed files with 1027 additions and 282 deletions

View File

@@ -27,7 +27,7 @@ class Dashboard extends Page implements HasActions, HasForms
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-home'; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-home';
protected static ?int $navigationSort = 1; protected static ?int $navigationSort = 0;
protected static ?string $slug = 'dashboard'; protected static ?string $slug = 'dashboard';

View File

@@ -30,6 +30,8 @@ class DatabaseTuning extends Page implements HasActions, HasTable
protected static ?string $slug = 'database-tuning'; protected static ?string $slug = 'database-tuning';
protected static bool $shouldRegisterNavigation = false;
protected string $view = 'filament.admin.pages.database-tuning'; protected string $view = 'filament.admin.pages.database-tuning';
public array $variables = []; public array $variables = [];

View File

@@ -13,6 +13,7 @@ use App\Filament\Admin\Widgets\Security\QuarantinedFilesTable;
use App\Filament\Admin\Widgets\Security\ThreatsTable; use App\Filament\Admin\Widgets\Security\ThreatsTable;
use App\Filament\Admin\Widgets\Security\WpscanResultsTable; use App\Filament\Admin\Widgets\Security\WpscanResultsTable;
use App\Models\AuditLog; use App\Models\AuditLog;
use App\Models\Setting;
use App\Services\Agent\AgentClient; use App\Services\Agent\AgentClient;
use BackedEnum; use BackedEnum;
use Exception; use Exception;
@@ -76,6 +77,11 @@ class Security extends Page implements HasActions, HasForms, HasTable
public ?int $ruleToDelete = null; public ?int $ruleToDelete = null;
// WAF
public bool $wafInstalled = false;
public bool $wafEnabled = false;
// Fail2ban // Fail2ban
public bool $fail2banInstalled = false; public bool $fail2banInstalled = false;
@@ -190,7 +196,7 @@ class Security extends Page implements HasActions, HasForms, HasTable
protected function normalizeTabName(?string $tab): string protected function normalizeTabName(?string $tab): string
{ {
return match ($tab) { return match ($tab) {
'overview', 'firewall', 'fail2ban', 'antivirus', 'ssh', 'scanner' => $tab, 'overview', 'firewall', 'waf', 'fail2ban', 'antivirus', 'ssh', 'scanner' => $tab,
default => 'overview', default => 'overview',
}; };
} }
@@ -218,6 +224,7 @@ class Security extends Page implements HasActions, HasForms, HasTable
{ {
$this->activeTab = $this->normalizeTabName($this->activeTab); $this->activeTab = $this->normalizeTabName($this->activeTab);
$this->loadFirewallStatus(); $this->loadFirewallStatus();
$this->loadWafStatus();
$this->loadFail2banStatusLight(); $this->loadFail2banStatusLight();
$this->loadClamavStatusLight(); $this->loadClamavStatusLight();
$this->loadSshSettings(); $this->loadSshSettings();
@@ -241,6 +248,7 @@ class Security extends Page implements HasActions, HasForms, HasTable
#[On('refresh-security-data')] #[On('refresh-security-data')]
public function refreshSecurityData(): void public function refreshSecurityData(): void
{ {
$this->loadWafStatus();
$this->loadFail2banStatus(); $this->loadFail2banStatus();
$this->loadClamavStatus(); $this->loadClamavStatus();
} }
@@ -313,6 +321,7 @@ class Security extends Page implements HasActions, HasForms, HasTable
return match ($this->activeTab) { return match ($this->activeTab) {
'overview' => $this->overviewTabContent(), 'overview' => $this->overviewTabContent(),
'firewall' => $this->firewallTabContent(), 'firewall' => $this->firewallTabContent(),
'waf' => $this->wafTabContent(),
'fail2ban' => $this->fail2banTabContent(), 'fail2ban' => $this->fail2banTabContent(),
'antivirus' => $this->antivirusTabContent(), 'antivirus' => $this->antivirusTabContent(),
'ssh' => $this->sshTabContent(), 'ssh' => $this->sshTabContent(),
@@ -321,6 +330,13 @@ class Security extends Page implements HasActions, HasForms, HasTable
}; };
} }
protected function wafTabContent(): array
{
return [
View::make('filament.admin.components.security-waf'),
];
}
protected function overviewTabContent(): array protected function overviewTabContent(): array
{ {
return [ return [
@@ -330,6 +346,18 @@ class Security extends Page implements HasActions, HasForms, HasTable
->description(__('Firewall')) ->description(__('Firewall'))
->icon('heroicon-o-shield-check') ->icon('heroicon-o-shield-check')
->iconColor($this->firewallEnabled ? 'success' : 'danger'), ->iconColor($this->firewallEnabled ? 'success' : 'danger'),
Section::make($this->wafInstalled
? ($this->wafEnabled ? __('Enabled') : __('Disabled'))
: __('Not Installed'))
->description(__('WAF'))
->icon('heroicon-o-shield-exclamation')
->iconColor($this->wafInstalled
? ($this->wafEnabled ? 'success' : 'danger')
: 'gray'),
Section::make($this->fail2banRunning ? __('Running') : __('Stopped'))
->description(__('Fail2ban'))
->icon('heroicon-o-lock-closed')
->iconColor($this->fail2banRunning ? 'success' : 'danger'),
Section::make($this->totalBanned !== null ? (string) $this->totalBanned : __('N/A')) Section::make($this->totalBanned !== null ? (string) $this->totalBanned : __('N/A'))
->description(__('IPs Banned')) ->description(__('IPs Banned'))
->icon('heroicon-o-lock-closed') ->icon('heroicon-o-lock-closed')
@@ -338,6 +366,10 @@ class Security extends Page implements HasActions, HasForms, HasTable
->description(__('Threats Detected')) ->description(__('Threats Detected'))
->icon('heroicon-o-bug-ant') ->icon('heroicon-o-bug-ant')
->iconColor($this->clamavInstalled ? 'success' : 'gray'), ->iconColor($this->clamavInstalled ? 'success' : 'gray'),
Section::make($this->clamavRunning ? __('Running') : __('Stopped'))
->description(__('Antivirus'))
->icon('heroicon-o-shield-check')
->iconColor($this->clamavRunning ? 'success' : 'danger'),
]), ]),
Section::make(__('Quick Actions')) Section::make(__('Quick Actions'))
->icon('heroicon-o-bolt') ->icon('heroicon-o-bolt')
@@ -1008,6 +1040,30 @@ class Security extends Page implements HasActions, HasForms, HasTable
} }
} }
protected function loadWafStatus(): void
{
$this->wafInstalled = $this->detectWaf();
$this->wafEnabled = $this->wafInstalled && Setting::get('waf_enabled', '0') === '1';
}
protected function detectWaf(): bool
{
$paths = [
'/etc/nginx/modsec/main.conf',
'/etc/nginx/modsecurity.conf',
'/etc/modsecurity/modsecurity.conf',
'/etc/modsecurity/modsecurity.conf-recommended',
];
foreach ($paths as $path) {
if (file_exists($path)) {
return true;
}
}
return false;
}
protected function loadFail2banStatusLight(): void protected function loadFail2banStatusLight(): void
{ {
try { try {

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Filament\Admin\Pages; namespace App\Filament\Admin\Pages;
use App\Filament\Admin\Widgets\Settings\DatabaseTuningTable;
use App\Filament\Admin\Widgets\Settings\DnssecTable; use App\Filament\Admin\Widgets\Settings\DnssecTable;
use App\Filament\Admin\Widgets\Settings\NotificationLogTable; use App\Filament\Admin\Widgets\Settings\NotificationLogTable;
use App\Models\DnsSetting; use App\Models\DnsSetting;
@@ -78,18 +79,6 @@ class ServerSettings extends Page implements HasActions, HasForms
public ?array $phpFpmData = []; public ?array $phpFpmData = [];
// Version info (non-form) // Version info (non-form)
public string $currentVersion = '';
public string $latestVersion = '';
public int $updatesAvailable = 0;
public bool $isChecking = false;
public bool $isUpgrading = false;
public string $upgradeLog = '';
public bool $isSystemdResolved = false; public bool $isSystemdResolved = false;
public ?string $currentLogo = null; public ?string $currentLogo = null;
@@ -119,7 +108,7 @@ class ServerSettings extends Page implements HasActions, HasForms
protected function normalizeTabName(?string $tab): string protected function normalizeTabName(?string $tab): string
{ {
return match ($tab) { return match ($tab) {
'general', 'dns', 'storage', 'email', 'notifications', 'php-fpm' => $tab, 'general', 'dns', 'storage', 'email', 'notifications', 'php-fpm', 'database' => $tab,
default => 'general', default => 'general',
}; };
} }
@@ -239,7 +228,6 @@ class ServerSettings extends Page implements HasActions, HasForms
'memory_limit' => $settings['fpm_memory_limit'] ?? '512M', 'memory_limit' => $settings['fpm_memory_limit'] ?? '512M',
]; ];
$this->loadVersionInfo();
} }
public function settingsForm(Schema $schema): Schema public function settingsForm(Schema $schema): Schema
@@ -260,6 +248,7 @@ class ServerSettings extends Page implements HasActions, HasForms
'email' => $this->emailTabContent(), 'email' => $this->emailTabContent(),
'notifications' => $this->notificationsTabContent(), 'notifications' => $this->notificationsTabContent(),
'php-fpm' => $this->phpFpmTabContent(), 'php-fpm' => $this->phpFpmTabContent(),
'database' => $this->databaseTabContent(),
default => $this->generalTabContent(), default => $this->generalTabContent(),
}; };
} }
@@ -267,25 +256,6 @@ class ServerSettings extends Page implements HasActions, HasForms
protected function generalTabContent(): array protected function generalTabContent(): array
{ {
return [ return [
Section::make(__('Panel Version & Updates'))
->description($this->currentVersion ?: __('Unknown'))
->icon('heroicon-o-arrow-up-tray')
->schema([
Actions::make([
FormAction::make('checkForUpdates')
->label(__('Check for Updates'))
->icon('heroicon-o-arrow-path')
->color('gray')
->action('checkForUpdates'),
FormAction::make('performUpgrade')
->label(__('Upgrade Now'))
->icon('heroicon-o-arrow-up-tray')
->color('success')
->requiresConfirmation()
->action('performUpgrade')
->visible(fn () => $this->updatesAvailable > 0),
]),
]),
Section::make(__('Panel Branding')) Section::make(__('Panel Branding'))
->icon('heroicon-o-paint-brush') ->icon('heroicon-o-paint-brush')
->schema([ ->schema([
@@ -678,6 +648,18 @@ class ServerSettings extends Page implements HasActions, HasForms
]; ];
} }
protected function databaseTabContent(): array
{
return [
Section::make(__('Database Tuning'))
->description(__('Adjust MariaDB/MySQL global variables.'))
->icon('heroicon-o-circle-stack')
->schema([
EmbeddedTable::make(DatabaseTuningTable::class),
]),
];
}
protected function getForms(): array protected function getForms(): array
{ {
return [ return [
@@ -1133,78 +1115,6 @@ class ServerSettings extends Page implements HasActions, HasForms
} }
} }
protected function loadVersionInfo(): void
{
$versionFile = base_path('VERSION');
if (File::exists($versionFile)) {
$content = File::get($versionFile);
if (preg_match('/VERSION=(.+)/', $content, $matches)) {
$this->currentVersion = trim($matches[1]);
if (preg_match('/BUILD=(\d+)/', $content, $buildMatches)) {
$this->currentVersion .= ' ('.__('build').' '.trim($buildMatches[1]).')';
}
}
} else {
$this->currentVersion = __('Unknown');
}
}
public function checkForUpdates(): void
{
$this->isChecking = true;
$this->updatesAvailable = 0;
try {
$basePath = base_path();
if (! is_dir("{$basePath}/.git")) {
throw new Exception(__('Not a git repository.'));
}
exec("cd {$basePath} && timeout 30 git fetch origin main 2>&1", $fetchOutput, $fetchCode);
if ($fetchCode !== 0) {
throw new Exception(__('Failed to fetch from repository.'));
}
$behindCount = trim(shell_exec("cd {$basePath} && git rev-list HEAD..origin/main --count 2>&1") ?? '0');
$this->updatesAvailable = (int) $behindCount;
if ($this->updatesAvailable > 0) {
$this->latestVersion = trim(shell_exec("cd {$basePath} && git log origin/main -1 --format='%s' 2>&1") ?? '');
Notification::make()->title(__('Updates Available'))->body(__(':count update(s) available', ['count' => $this->updatesAvailable]))->warning()->send();
} else {
Notification::make()->title(__('Up to Date'))->body(__('Running the latest version'))->success()->send();
}
} catch (Exception $e) {
Notification::make()->title(__('Update Check Failed'))->body($e->getMessage())->danger()->send();
}
$this->isChecking = false;
}
public function performUpgrade(): void
{
$this->isUpgrading = true;
$this->upgradeLog = __('Starting upgrade...')."\n";
try {
$exitCode = Artisan::call('jabali:upgrade', ['--force' => true]);
$this->upgradeLog .= Artisan::output();
if ($exitCode !== 0) {
throw new Exception(__('Upgrade failed. Check the log for details.'));
}
$this->loadVersionInfo();
$this->updatesAvailable = 0;
Notification::make()->title(__('Upgrade Complete'))->body(__('Refresh to see changes.'))->success()->send();
} catch (Exception $e) {
$this->upgradeLog .= "\n".__('Error').': '.$e->getMessage();
Notification::make()->title(__('Upgrade Failed'))->body($e->getMessage())->danger()->send();
}
$this->isUpgrading = false;
}
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [

View File

@@ -180,8 +180,7 @@ class Services extends Page implements HasActions, HasForms, HasTable
}) })
->iconColor(fn (array $record): string => $record['is_active'] ? 'success' : 'danger') ->iconColor(fn (array $record): string => $record['is_active'] ? 'success' : 'danger')
->description(fn (array $record): string => $record['description'] ?? '') ->description(fn (array $record): string => $record['description'] ?? '')
->weight('medium') ->weight('medium'),
->searchable(),
TextColumn::make('is_active') TextColumn::make('is_active')
->label(__('Status')) ->label(__('Status'))
->badge() ->badge()

View File

@@ -37,6 +37,8 @@ class Waf extends Page implements HasForms, HasTable
protected string $view = 'filament.admin.pages.waf'; protected string $view = 'filament.admin.pages.waf';
protected static bool $shouldRegisterNavigation = false;
public bool $wafInstalled = false; public bool $wafInstalled = false;
public array $wafFormData = []; public array $wafFormData = [];

View File

@@ -22,7 +22,7 @@ class HostingPackageResource extends Resource
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCube; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCube;
protected static ?int $navigationSort = 13; protected static ?int $navigationSort = 2;
public static function getNavigationLabel(): string public static function getNavigationLabel(): string
{ {

View File

@@ -20,7 +20,7 @@ class UserResource extends Resource
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack; protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
protected static ?int $navigationSort = 2; protected static ?int $navigationSort = 1;
public static function getNavigationLabel(): string public static function getNavigationLabel(): string
{ {

View File

@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace App\Filament\Admin\Widgets\Settings;
use App\Services\Agent\AgentClient;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Support\Contracts\TranslatableContentDriver;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Livewire\Component;
class DatabaseTuningTable extends Component implements HasTable, HasSchemas, HasActions
{
use InteractsWithActions;
use InteractsWithSchemas;
use InteractsWithTable;
public array $variables = [];
protected ?AgentClient $agent = null;
public function makeFilamentTranslatableContentDriver(): ?TranslatableContentDriver
{
return null;
}
public function mount(): void
{
$this->loadVariables();
}
protected function getAgent(): AgentClient
{
return $this->agent ??= new AgentClient();
}
public function loadVariables(): void
{
$names = [
'innodb_buffer_pool_size',
'max_connections',
'tmp_table_size',
'max_heap_table_size',
'innodb_log_file_size',
'innodb_flush_log_at_trx_commit',
];
try {
$result = $this->getAgent()->databaseGetVariables($names);
if (! ($result['success'] ?? false)) {
throw new \RuntimeException($result['error'] ?? __('Unable to load variables'));
}
$this->variables = collect($result['variables'] ?? [])->map(function (array $row) {
return [
'name' => $row['name'] ?? '',
'value' => $row['value'] ?? '',
];
})->toArray();
} catch (\Exception $e) {
$this->variables = [];
Notification::make()
->title(__('Unable to load database variables'))
->body($e->getMessage())
->warning()
->send();
}
$this->resetTable();
}
public function table(Table $table): Table
{
return $table
->records(fn () => $this->variables)
->columns([
TextColumn::make('name')
->label(__('Variable'))
->fontFamily('mono'),
TextColumn::make('value')
->label(__('Current Value'))
->fontFamily('mono'),
])
->recordActions([
Action::make('update')
->label(__('Update'))
->icon('heroicon-o-pencil-square')
->form([
TextInput::make('value')
->label(__('New Value'))
->required(),
])
->action(function (array $record, array $data): void {
try {
$agent = $this->getAgent();
$setResult = $agent->databaseSetGlobal($record['name'], (string) $data['value']);
if (! ($setResult['success'] ?? false)) {
throw new \RuntimeException($setResult['error'] ?? __('Update failed'));
}
$persistResult = $agent->databasePersistTuning($record['name'], (string) $data['value']);
if (! ($persistResult['success'] ?? false)) {
Notification::make()
->title(__('Variable updated, but not persisted'))
->body($persistResult['error'] ?? __('Unable to persist value'))
->warning()
->send();
} else {
Notification::make()
->title(__('Variable updated'))
->success()
->send();
}
} catch (\Exception $e) {
Notification::make()
->title(__('Update failed'))
->body($e->getMessage())
->danger()
->send();
}
$this->loadVariables();
}),
])
->headerActions([
Action::make('refresh')
->label(__('Refresh'))
->icon('heroicon-o-arrow-path')
->color('gray')
->action(fn () => $this->loadVariables()),
])
->striped()
->emptyStateHeading(__('No variables found'))
->emptyStateDescription(__('Database variables could not be loaded.'));
}
public function render()
{
return $this->getTable()->render();
}
}

View File

@@ -6,13 +6,16 @@ namespace App\Filament\Admin\Widgets;
use App\Models\Domain; use App\Models\Domain;
use App\Models\SslCertificate; use App\Models\SslCertificate;
use Filament\Widgets\StatsOverviewWidget as BaseWidget; use Filament\Widgets\Widget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class SslStatsOverview extends BaseWidget class SslStatsOverview extends Widget
{ {
protected ?string $pollingInterval = '30s'; protected ?string $pollingInterval = '30s';
protected int|string|array $columnSpan = 'full';
protected string $view = 'filament.admin.widgets.dashboard-stats';
protected function getStats(): array protected function getStats(): array
{ {
$totalDomains = Domain::count(); $totalDomains = Domain::count();
@@ -30,35 +33,36 @@ class SslStatsOverview extends BaseWidget
$withoutSsl = $totalDomains - $domainsWithSsl; $withoutSsl = $totalDomains - $domainsWithSsl;
return [ return [
Stat::make(__('Total Domains'), (string) $totalDomains) [
->description(__('All registered domains')) 'value' => $domainsWithSsl,
->descriptionIcon('heroicon-m-globe-alt') 'label' => __('With SSL'),
->color('gray'), 'icon' => 'heroicon-m-shield-check',
'color' => 'success',
Stat::make(__('With SSL'), (string) $domainsWithSsl) ],
->description(__('Active certificates')) [
->descriptionIcon('heroicon-m-shield-check') 'value' => $withoutSsl,
->color('success'), 'label' => __('Without SSL'),
'icon' => 'heroicon-m-shield-exclamation',
Stat::make(__('Without SSL'), (string) $withoutSsl) 'color' => 'gray',
->description(__('No certificate')) ],
->descriptionIcon('heroicon-m-shield-exclamation') [
->color('gray'), 'value' => $expiringSoon,
'label' => __('Expiring Soon'),
Stat::make(__('Expiring Soon'), (string) $expiringSoon) 'icon' => 'heroicon-m-clock',
->description(__('Within 30 days')) 'color' => $expiringSoon > 0 ? 'warning' : 'success',
->descriptionIcon('heroicon-m-clock') ],
->color($expiringSoon > 0 ? 'warning' : 'success'), [
'value' => $expired,
Stat::make(__('Expired'), (string) $expired) 'label' => __('Expired'),
->description(__('Need renewal')) 'icon' => 'heroicon-m-x-circle',
->descriptionIcon('heroicon-m-x-circle') 'color' => $expired > 0 ? 'danger' : 'success',
->color($expired > 0 ? 'danger' : 'success'), ],
[
Stat::make(__('Failed'), (string) $failed) 'value' => $failed,
->description(__('Issuance failed')) 'label' => __('Failed'),
->descriptionIcon('heroicon-m-exclamation-triangle') 'icon' => 'heroicon-m-exclamation-triangle',
->color($failed > 0 ? 'danger' : 'success'), 'color' => $failed > 0 ? 'danger' : 'success',
],
]; ];
} }
} }

View File

@@ -0,0 +1,693 @@
<?php
declare(strict_types=1);
namespace App\Livewire\Admin;
use App\Models\Setting;
use App\Services\Agent\AgentClient;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Contracts\TranslatableContentDriver;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Str;
use Livewire\Attributes\On;
use Livewire\Component;
class SecurityWafPanel extends Component implements HasForms, HasTable, HasActions
{
use InteractsWithActions;
use InteractsWithForms;
use InteractsWithTable;
public bool $wafInstalled = false;
public array $wafFormData = [];
public array $auditEntries = [];
public bool $auditLoaded = false;
public function makeFilamentTranslatableContentDriver(): ?TranslatableContentDriver
{
return null;
}
protected function getForms(): array
{
return ['wafForm'];
}
public function mount(): void
{
$this->wafInstalled = $this->detectWaf();
$this->wafFormData = [
'enabled' => Setting::get('waf_enabled', '0') === '1',
'paranoia' => Setting::get('waf_paranoia', '1'),
'audit_log' => Setting::get('waf_audit_log', '1') === '1',
];
$this->loadAuditLogs(false);
}
protected function detectWaf(): bool
{
$paths = [
'/etc/nginx/modsec/main.conf',
'/etc/nginx/modsecurity.conf',
'/etc/modsecurity/modsecurity.conf',
'/etc/modsecurity/modsecurity.conf-recommended',
];
foreach ($paths as $path) {
if (file_exists($path)) {
return true;
}
}
return false;
}
public function wafForm(Schema $schema): Schema
{
return $schema
->statePath('wafFormData')
->schema([
Section::make(__('WAF Settings'))
->headerActions([
Action::make('saveWafSettings')
->label(__('Save WAF Settings'))
->icon('heroicon-o-check')
->color('primary')
->action('saveWafSettings'),
])
->extraAttributes(['class' => 'mb-8'])
->schema([
Toggle::make('enabled')
->label(__('Enable ModSecurity'))
->disabled(fn () => ! $this->wafInstalled)
->helperText(fn () => $this->wafInstalled ? null : __('ModSecurity is not installed. Install it to enable WAF.')),
Select::make('paranoia')
->label(__('Paranoia Level'))
->options([
'1' => '1 - Basic',
'2' => '2 - Moderate',
'3' => '3 - Strict',
'4' => '4 - Very Strict',
])
->default('1'),
Toggle::make('audit_log')
->label(__('Enable Audit Log')),
])
->columns(2),
]);
}
public function saveWafSettings(): void
{
$data = $this->wafForm->getState();
$requestedEnabled = ! empty($data['enabled']);
if ($requestedEnabled && ! $this->wafInstalled) {
$requestedEnabled = false;
}
Setting::set('waf_enabled', $requestedEnabled ? '1' : '0');
Setting::set('waf_paranoia', (string) ($data['paranoia'] ?? '1'));
Setting::set('waf_audit_log', ! empty($data['audit_log']) ? '1' : '0');
$whitelistRules = $this->getWhitelistRules();
try {
$agent = new AgentClient;
$agent->wafApplySettings(
$requestedEnabled,
(string) ($data['paranoia'] ?? '1'),
! empty($data['audit_log']),
$whitelistRules
);
if (! $this->wafInstalled && ! empty($data['enabled'])) {
Notification::make()
->title(__('ModSecurity is not installed'))
->body(__('WAF was disabled automatically. Install ModSecurity to enable it.'))
->warning()
->send();
return;
}
Notification::make()
->title(__('WAF settings applied'))
->success()
->send();
} catch (Exception $e) {
Notification::make()
->title(__('WAF settings saved, but apply failed'))
->body($e->getMessage())
->warning()
->send();
}
}
public function loadAuditLogs(bool $notify = true): void
{
try {
$agent = new AgentClient;
$response = $agent->wafAuditLogList();
$entries = $response['entries'] ?? [];
if (! is_array($entries)) {
$entries = [];
}
$this->auditEntries = $this->normalizeAuditEntries($this->markWhitelisted($entries));
$this->auditLoaded = true;
if ($notify) {
Notification::make()
->title(__('WAF logs refreshed'))
->success()
->send();
}
} catch (Exception $e) {
Notification::make()
->title(__('Failed to load WAF logs'))
->body($e->getMessage())
->warning()
->send();
}
}
protected function getWhitelistRules(): array
{
$raw = Setting::get('waf_whitelist_rules', '[]');
$rules = json_decode($raw, true);
if (! is_array($rules)) {
return [];
}
$changed = false;
foreach ($rules as &$rule) {
if (! is_array($rule)) {
$rule = [];
$changed = true;
continue;
}
if (array_key_exists('enabled', $rule)) {
unset($rule['enabled']);
$changed = true;
}
$label = (string) ($rule['label'] ?? '');
if ($label === '' || str_contains($label, '{rule}') || str_contains($label, ':rule')) {
$rule['label'] = $this->defaultWhitelistLabel($rule);
$changed = true;
}
}
$rules = array_values(array_filter($rules, fn ($rule) => is_array($rule) && ($rule !== [])));
if ($changed) {
Setting::set('waf_whitelist_rules', json_encode($rules, JSON_UNESCAPED_SLASHES));
}
return $rules;
}
protected function defaultWhitelistLabel(array $rule): string
{
$ids = trim((string) ($rule['rule_ids'] ?? ''));
if ($ids !== '') {
return __('Rule :id', ['id' => $ids]);
}
$matchValue = trim((string) ($rule['match_value'] ?? ''));
if ($matchValue !== '') {
return $matchValue;
}
return __('Whitelist rule');
}
protected function markWhitelisted(array $entries): array
{
$rules = $this->getWhitelistRules();
foreach ($entries as &$entry) {
$entry['whitelisted'] = $this->matchesWhitelist($entry, $rules);
}
return $entries;
}
protected function normalizeAuditEntries(array $entries): array
{
return array_map(function (array $entry): array {
$entry['__key'] = md5(implode('|', [
(string) ($entry['timestamp'] ?? ''),
(string) ($entry['rule_id'] ?? ''),
(string) ($entry['uri'] ?? ''),
(string) ($entry['remote_ip'] ?? ''),
(string) ($entry['host'] ?? ''),
]));
return $entry;
}, $entries);
}
protected function matchesWhitelist(array $entry, array $rules): bool
{
$ruleId = (string) ($entry['rule_id'] ?? '');
$uri = (string) ($entry['uri'] ?? '');
$uriPath = $this->stripQueryString($uri);
$host = (string) ($entry['host'] ?? '');
$ip = (string) ($entry['remote_ip'] ?? '');
foreach ($rules as $rule) {
if (! is_array($rule)) {
continue;
}
$idsRaw = (string) ($rule['rule_ids'] ?? '');
$ids = preg_split('/[,\s]+/', $idsRaw, -1, PREG_SPLIT_NO_EMPTY) ?: [];
$ids = array_map('trim', $ids);
if ($ruleId !== '' && ! empty($ids) && ! in_array($ruleId, $ids, true)) {
continue;
}
$matchType = (string) ($rule['match_type'] ?? '');
$matchValue = (string) ($rule['match_value'] ?? '');
if ($matchType === 'ip' && $matchValue !== '' && $this->ipMatches($ip, $matchValue)) {
return true;
}
if ($matchType === 'uri_exact' && $matchValue !== '' && ($uri === $matchValue || $uriPath === $matchValue)) {
return true;
}
if ($matchType === 'uri_prefix' && $matchValue !== '' && (str_starts_with($uri, $matchValue) || str_starts_with($uriPath, $matchValue))) {
return true;
}
if ($matchType === 'host' && $matchValue !== '' && $host === $matchValue) {
return true;
}
}
return false;
}
protected function ruleMatchesEntry(array $rule, array $entry): bool
{
if (! is_array($rule)) {
return false;
}
$ruleId = (string) ($entry['rule_id'] ?? '');
$uri = (string) ($entry['uri'] ?? '');
$uriPath = $this->stripQueryString($uri);
$host = (string) ($entry['host'] ?? '');
$ip = (string) ($entry['remote_ip'] ?? '');
$idsRaw = (string) ($rule['rule_ids'] ?? '');
$ids = preg_split('/[,\s]+/', $idsRaw, -1, PREG_SPLIT_NO_EMPTY) ?: [];
$ids = array_map('trim', $ids);
if ($ruleId !== '' && ! empty($ids) && ! in_array($ruleId, $ids, true)) {
return false;
}
$matchType = (string) ($rule['match_type'] ?? '');
$matchValue = (string) ($rule['match_value'] ?? '');
if ($matchType === 'ip' && $matchValue !== '' && $this->ipMatches($ip, $matchValue)) {
return true;
}
if ($matchType === 'uri_exact' && $matchValue !== '' && ($uri === $matchValue || $uriPath === $matchValue)) {
return true;
}
if ($matchType === 'uri_prefix' && $matchValue !== '' && (str_starts_with($uri, $matchValue) || str_starts_with($uriPath, $matchValue))) {
return true;
}
if ($matchType === 'host' && $matchValue !== '' && $host === $matchValue) {
return true;
}
return false;
}
protected function ipMatches(string $ip, string $rule): bool
{
if ($ip === '' || $rule === '') {
return false;
}
if (! str_contains($rule, '/')) {
return $ip === $rule;
}
[$subnet, $bits] = array_pad(explode('/', $rule, 2), 2, null);
$bits = is_numeric($bits) ? (int) $bits : null;
if ($bits === null || $bits < 0 || $bits > 32) {
return $ip === $rule;
}
$ipLong = ip2long($ip);
$subnetLong = ip2long($subnet);
if ($ipLong === false || $subnetLong === false) {
return $ip === $rule;
}
$mask = -1 << (32 - $bits);
return ($ipLong & $mask) === ($subnetLong & $mask);
}
protected function formatUriForDisplay(array $record): string
{
$host = (string) ($record['host'] ?? '');
$uri = (string) ($record['uri'] ?? '');
if ($host !== '' && $uri !== '') {
return $host.$uri;
}
return $uri !== '' ? $uri : $host;
}
public function whitelistEntry(array $record): void
{
$rules = $this->getWhitelistRules();
$matchType = 'uri_prefix';
$rawUri = (string) ($record['uri'] ?? '');
$matchValue = $this->stripQueryString($rawUri);
if ($matchValue === '') {
$matchType = 'ip';
$matchValue = (string) ($record['remote_ip'] ?? '');
}
if ($matchValue === '' || empty($record['rule_id'])) {
Notification::make()
->title(__('Unable to whitelist entry'))
->body(__('Missing URI/IP or rule ID for this entry.'))
->warning()
->send();
return;
}
$rules[] = [
'label' => __('Rule :rule', ['rule' => $record['rule_id'] ?? '']),
'match_type' => $matchType,
'match_value' => $matchValue,
'rule_ids' => $record['rule_id'] ?? '',
];
Setting::set('waf_whitelist_rules', json_encode(array_values($rules), JSON_UNESCAPED_SLASHES));
try {
$agent = new AgentClient;
$agent->wafApplySettings(
Setting::get('waf_enabled', '0') === '1',
(string) Setting::get('waf_paranoia', '1'),
Setting::get('waf_audit_log', '1') === '1',
$rules
);
} catch (Exception $e) {
Notification::make()
->title(__('Whitelist saved, but apply failed'))
->body($e->getMessage())
->warning()
->send();
}
$this->loadAuditLogs(false);
$this->resetTable();
$this->dispatch('waf-whitelist-updated');
$this->dispatch('waf-blocked-updated');
Notification::make()
->title(__('Rule whitelisted'))
->success()
->send();
}
protected function stripQueryString(string $uri): string
{
$pos = strpos($uri, '?');
if ($pos === false) {
return $uri;
}
return substr($uri, 0, $pos);
}
public function removeWhitelistEntry(array $record): void
{
$rules = $this->getWhitelistRules();
$beforeCount = count($rules);
$rules = array_values(array_filter($rules, function (array $rule) use ($record): bool {
return ! $this->ruleMatchesEntry($rule, $record);
}));
if (count($rules) === $beforeCount) {
Notification::make()
->title(__('No matching whitelist rule found'))
->warning()
->send();
return;
}
Setting::set('waf_whitelist_rules', json_encode(array_values($rules), JSON_UNESCAPED_SLASHES));
try {
$agent = new AgentClient;
$agent->wafApplySettings(
Setting::get('waf_enabled', '0') === '1',
(string) Setting::get('waf_paranoia', '1'),
Setting::get('waf_audit_log', '1') === '1',
$rules
);
} catch (Exception $e) {
Notification::make()
->title(__('Whitelist updated, but apply failed'))
->body($e->getMessage())
->warning()
->send();
}
$this->loadAuditLogs(false);
$this->resetTable();
$this->dispatch('waf-whitelist-updated');
$this->dispatch('waf-blocked-updated');
Notification::make()
->title(__('Whitelist removed'))
->success()
->send();
}
public function table(Table $table): Table
{
return $table
->persistColumnsInSession(false)
->paginated([25, 50, 100])
->defaultPaginationPageOption(25)
->records(function (?array $filters, ?string $search, int|string $page, int|string $recordsPerPage, ?string $sortColumn, ?string $sortDirection) {
if (! $this->auditLoaded) {
$this->loadAuditLogs(false);
}
$records = $this->auditEntries;
$records = $this->filterRecords($records, $search);
$records = $this->sortRecords($records, $sortColumn, $sortDirection);
return $this->paginateRecords($records, $page, $recordsPerPage);
})
->columns([
TextColumn::make('timestamp')
->label(__('Time'))
->formatStateUsing(function (array $record): string {
$timestamp = (int) ($record['timestamp'] ?? 0);
return $timestamp > 0 ? date('Y-m-d H:i:s', $timestamp) : '';
})
->sortable(),
TextColumn::make('rule_id')
->label(__('Rule ID'))
->fontFamily('mono')
->sortable()
->searchable(),
TextColumn::make('event_type')
->label(__('Type'))
->badge()
->getStateUsing(function (array $record): string {
if (! empty($record['blocked'])) {
return __('Blocked');
}
$severity = (int) ($record['severity'] ?? 0);
if ($severity >= 4) {
return __('Error');
}
return __('Warning');
})
->color(function (array $record): string {
if (! empty($record['blocked'])) {
return 'danger';
}
$severity = (int) ($record['severity'] ?? 0);
if ($severity >= 4) {
return 'warning';
}
return 'gray';
})
->sortable()
->toggleable(),
TextColumn::make('message')
->label(__('Message'))
->wrap()
->limit(80)
->searchable(),
TextColumn::make('uri')
->label(__('URI'))
->getStateUsing(fn (array $record): string => $this->formatUriForDisplay($record))
->formatStateUsing(fn (string $state): string => Str::limit($state, 52, '…'))
->tooltip(fn (array $record): string => $this->formatUriForDisplay($record))
->copyable()
->copyableState(fn (array $record): string => $this->formatUriForDisplay($record))
->extraAttributes([
'class' => 'inline-block max-w-[240px] truncate',
'style' => 'max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;',
])
->wrap(false)
->searchable(),
TextColumn::make('remote_ip')
->label(__('Source IP'))
->fontFamily('mono')
->copyable()
->toggleable(isToggledHiddenByDefault: false),
TextColumn::make('host')
->label(__('Host'))
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('whitelisted')
->label(__('Whitelisted'))
->badge()
->formatStateUsing(fn (array $record): string => ! empty($record['whitelisted']) ? __('Yes') : __('No'))
->color(fn (array $record): string => ! empty($record['whitelisted']) ? 'success' : 'gray'),
])
->recordActions([
\Filament\Actions\Action::make('whitelist')
->label(__('Whitelist'))
->icon('heroicon-o-check-badge')
->color('primary')
->visible(fn (array $record): bool => empty($record['whitelisted']))
->action(fn (array $record) => $this->whitelistEntry($record)),
\Filament\Actions\Action::make('removeWhitelist')
->label(__('Remove whitelist'))
->icon('heroicon-o-x-mark')
->color('danger')
->visible(fn (array $record): bool => ! empty($record['whitelisted']))
->requiresConfirmation()
->action(fn (array $record) => $this->removeWhitelistEntry($record)),
])
->emptyStateHeading(__('No blocked rules found'))
->emptyStateDescription(__('No ModSecurity denials found in the audit log.'))
->headerActions([
\Filament\Actions\Action::make('refresh')
->label(__('Refresh Logs'))
->icon('heroicon-o-arrow-path')
->action(fn () => $this->loadAuditLogs()),
]);
}
#[On('waf-blocked-updated')]
public function refreshBlockedTable(): void
{
$this->loadAuditLogs(false);
$this->resetTable();
}
protected function filterRecords(array $records, ?string $search): array
{
if (! $search) {
return $records;
}
return array_values(array_filter($records, function (array $record) use ($search) {
$haystack = implode(' ', array_filter([
(string) ($record['rule_id'] ?? ''),
(string) ($record['message'] ?? ''),
(string) ($record['uri'] ?? ''),
(string) ($record['remote_ip'] ?? ''),
(string) ($record['host'] ?? ''),
]));
return str_contains(Str::lower($haystack), Str::lower($search));
}));
}
protected function sortRecords(array $records, ?string $sortColumn, ?string $sortDirection): array
{
$direction = $sortDirection === 'asc' ? 'asc' : 'desc';
if (! $sortColumn) {
return $records;
}
usort($records, function (array $a, array $b) use ($sortColumn, $direction): int {
$aValue = $a[$sortColumn] ?? null;
$bValue = $b[$sortColumn] ?? null;
if (is_numeric($aValue) && is_numeric($bValue)) {
$result = (float) $aValue <=> (float) $bValue;
} else {
$result = strcmp((string) $aValue, (string) $bValue);
}
return $direction === 'asc' ? $result : -$result;
});
return $records;
}
protected function paginateRecords(array $records, int|string $page, int|string $recordsPerPage): LengthAwarePaginator
{
$page = max(1, (int) $page);
$perPage = max(1, (int) $recordsPerPage);
$total = count($records);
$items = array_slice($records, ($page - 1) * $perPage, $perPage);
return new LengthAwarePaginator(
$items,
$total,
$perPage,
$page,
[
'path' => request()->url(),
'pageName' => $this->getTablePaginationPageName(),
],
);
}
public function render()
{
return view('livewire.admin.security-waf-panel');
}
}

View File

@@ -43,8 +43,7 @@ class AdminPanelProvider extends PanelProvider
->renderHook( ->renderHook(
PanelsRenderHook::HEAD_END, PanelsRenderHook::HEAD_END,
fn () => $this->getOpenGraphTags('Jabali Admin', 'Server administration panel for Jabali - Manage your hosting infrastructure'). fn () => $this->getOpenGraphTags('Jabali Admin', 'Server administration panel for Jabali - Manage your hosting infrastructure').
'<link rel="stylesheet" href="'.asset('css/filament-custom.css').'">'. \Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/css/app.css', 'resources/js/server-charts.js'])->toHtml().
\Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/js/server-charts.js'])->toHtml().
$this->getRtlScript() $this->getRtlScript()
) )
->renderHook( ->renderHook(

View File

@@ -46,8 +46,7 @@ class JabaliPanelProvider extends PanelProvider
->renderHook( ->renderHook(
PanelsRenderHook::HEAD_END, PanelsRenderHook::HEAD_END,
fn () => $this->getOpenGraphTags('Jabali Panel', 'Web hosting control panel - Manage your domains, emails, databases and more'). fn () => $this->getOpenGraphTags('Jabali Panel', 'Web hosting control panel - Manage your domains, emails, databases and more').
'<link rel="stylesheet" href="'.asset('css/filament-custom.css').'?v='.filemtime(public_path('css/filament-custom.css')).'">'. \Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/css/app.css', 'resources/js/server-charts.js'])->toHtml().
\Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/js/server-charts.js'])->toHtml().
$this->getRtlScript() $this->getRtlScript()
) )
->renderHook( ->renderHook(

View File

@@ -1329,9 +1329,9 @@ class AgentClient
} }
// Server updates // Server updates
public function updatesList(): array public function updatesList(bool $refresh = false): array
{ {
return $this->send('updates.list'); return $this->send('updates.list', ['refresh' => $refresh]);
} }
public function updatesRun(): array public function updatesRun(): array

View File

@@ -3781,6 +3781,11 @@ function geoApplyRules(array $params): array
$httpConf = [ $httpConf = [
'# Managed by Jabali', '# Managed by Jabali',
'real_ip_header X-Forwarded-For;',
'real_ip_recursive on;',
'set_real_ip_from 127.0.0.1;',
'set_real_ip_from ::1;',
'',
"geoip2 {$mmdb} {", "geoip2 {$mmdb} {",
" {$countryVar} country iso_code;", " {$countryVar} country iso_code;",
'}', '}',
@@ -25028,6 +25033,18 @@ function fail2banLogs(array $params): array
function updatesList(array $params): array function updatesList(array $params): array
{ {
$warnings = [];
$refresh = (bool) ($params['refresh'] ?? false);
$refreshOutput = [];
$refreshSuccess = null;
if ($refresh) {
exec('apt-get update -y 2>&1', $refreshOutput, $codeUpdate);
$refreshSuccess = $codeUpdate === 0;
if ($codeUpdate !== 0) {
$warnings[] = implode("\n", $refreshOutput);
}
}
$output = []; $output = [];
exec('apt list --upgradable 2>/dev/null', $output, $code); exec('apt list --upgradable 2>/dev/null', $output, $code);
if ($code !== 0) { if ($code !== 0) {
@@ -25049,7 +25066,13 @@ function updatesList(array $params): array
} }
} }
return ['success' => true, 'packages' => $packages]; return [
'success' => true,
'packages' => $packages,
'warnings' => $warnings,
'refresh_output' => $refreshOutput,
'refresh_success' => $refreshSuccess,
];
} }
function updatesRun(array $params): array function updatesRun(array $params): array

View File

@@ -1,132 +0,0 @@
/**
* Jabali Panel - Custom Styling
* Removes all rounded corners for a sharp, professional look
*/
/* Reset all border-radius to 0 */
*,
*::before,
*::after {
border-radius: 0 !important;
}
/* Filament specific overrides */
.fi-sidebar,
.fi-sidebar-nav,
.fi-topbar,
.fi-main,
.fi-section,
.fi-card,
.fi-modal,
.fi-dropdown,
.fi-btn,
.fi-badge,
.fi-avatar,
.fi-input-wrapper,
.fi-select,
.fi-checkbox,
.fi-radio,
.fi-toggle,
.fi-tabs,
.fi-tab,
.fi-notification,
.fi-pagination,
.fi-table,
.fi-table-cell,
.fi-widget,
.fi-breadcrumbs,
.fi-header,
.fi-footer {
border-radius: 0 !important;
}
/* Form inputs */
input,
select,
textarea,
button,
.form-input,
.form-select,
.form-textarea,
.form-checkbox,
.form-radio {
border-radius: 0 !important;
}
/* Buttons */
[class*="rounded"],
.rounded,
.rounded-sm,
.rounded-md,
.rounded-lg,
.rounded-xl,
.rounded-2xl,
.rounded-3xl,
.rounded-full {
border-radius: 0 !important;
}
/* Cards and containers */
.bg-white,
.bg-gray-50,
.bg-gray-100,
[class*="shadow"] {
border-radius: 0 !important;
}
/* Dropdown menus */
[x-float],
[x-menu],
[x-dropdown] {
border-radius: 0 !important;
}
/* Modal dialogs */
[x-dialog],
.fi-modal-window {
border-radius: 0 !important;
}
/* Notifications/Toasts */
.fi-notification-body,
[class*="toast"] {
border-radius: 0 !important;
}
/* Progress bars */
.fi-progress,
progress {
border-radius: 0 !important;
}
/* Images and avatars */
img,
.fi-avatar-image {
border-radius: 0 !important;
}
/* Specific Filament 4 component overrides */
.fi-simple-layout,
.fi-simple-main,
.fi-simple-main-ctn {
border-radius: 0 !important;
}
/* Table cells */
.fi-ta-cell,
.fi-ta-header-cell {
border-radius: 0 !important;
}
/* Action buttons */
.fi-ac-btn,
.fi-ac-action,
.fi-ac-icon-btn {
border-radius: 0 !important;
}
/* Widgets */
.fi-wi-stats-overview-stat,
.fi-wi-chart {
border-radius: 0 !important;
}

View File

@@ -2,6 +2,7 @@
$tabs = [ $tabs = [
'overview' => ['label' => __('Overview'), 'icon' => 'heroicon-o-home'], 'overview' => ['label' => __('Overview'), 'icon' => 'heroicon-o-home'],
'firewall' => ['label' => __('Firewall'), 'icon' => 'heroicon-o-shield-check'], 'firewall' => ['label' => __('Firewall'), 'icon' => 'heroicon-o-shield-check'],
'waf' => ['label' => __('ModSecurity / WAF'), 'icon' => 'heroicon-o-shield-exclamation'],
'fail2ban' => ['label' => __('Fail2ban'), 'icon' => 'heroicon-o-lock-closed'], 'fail2ban' => ['label' => __('Fail2ban'), 'icon' => 'heroicon-o-lock-closed'],
'antivirus' => ['label' => __('Antivirus'), 'icon' => 'heroicon-o-bug-ant'], 'antivirus' => ['label' => __('Antivirus'), 'icon' => 'heroicon-o-bug-ant'],
'ssh' => ['label' => __('SSH'), 'icon' => 'heroicon-o-command-line'], 'ssh' => ['label' => __('SSH'), 'icon' => 'heroicon-o-command-line'],

View File

@@ -0,0 +1 @@
@livewire('admin.security-waf-panel')

View File

@@ -6,6 +6,7 @@
'email' => ['label' => __('Email'), 'icon' => 'heroicon-o-envelope'], 'email' => ['label' => __('Email'), 'icon' => 'heroicon-o-envelope'],
'notifications' => ['label' => __('Notifications'), 'icon' => 'heroicon-o-bell'], 'notifications' => ['label' => __('Notifications'), 'icon' => 'heroicon-o-bell'],
'php-fpm' => ['label' => __('PHP-FPM'), 'icon' => 'heroicon-o-cpu-chip'], 'php-fpm' => ['label' => __('PHP-FPM'), 'icon' => 'heroicon-o-cpu-chip'],
'database' => ['label' => __('Database Tuning'), 'icon' => 'heroicon-o-circle-stack'],
]; ];
@endphp @endphp

View File

@@ -0,0 +1,36 @@
<div>
@if(! $wafInstalled)
<x-filament::section icon="heroicon-o-exclamation-triangle" icon-color="warning">
<x-slot name="heading">{{ __('ModSecurity not detected') }}</x-slot>
<x-slot name="description">
{{ __('Install ModSecurity on the server to enable WAF controls. Settings can be saved now and applied later.') }}
</x-slot>
</x-filament::section>
@endif
<div class="mt-6 mb-8">
{{ $this->wafForm }}
</div>
<div class="mt-8">
<x-filament::section icon="heroicon-o-list-bullet" icon-color="primary">
<x-slot name="heading">{{ __('Whitelist Rules') }}</x-slot>
<x-slot name="description">
{{ __('Exclude trusted traffic from specific ModSecurity rule IDs.') }}
</x-slot>
@livewire('admin.waf-whitelist-table')
</x-filament::section>
</div>
<div class="mt-8">
<x-filament::section icon="heroicon-o-document-text" icon-color="primary">
<x-slot name="heading">{{ __('Blocked Requests') }}</x-slot>
<x-slot name="description">
{{ __('Recent ModSecurity denials from the audit log. Use whitelist to allow trusted traffic.') }}
</x-slot>
{{ $this->table }}
</x-filament::section>
</div>
<x-filament-actions::modals />
</div>