Sync pending admin UI and security panel updates
This commit is contained in:
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 [
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
151
app/Filament/Admin/Widgets/Settings/DatabaseTuningTable.php
Normal file
151
app/Filament/Admin/Widgets/Settings/DatabaseTuningTable.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
693
app/Livewire/Admin/SecurityWafPanel.php
Normal file
693
app/Livewire/Admin/SecurityWafPanel.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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'],
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
@livewire('admin.security-waf-panel')
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
36
resources/views/livewire/admin/security-waf-panel.blade.php
Normal file
36
resources/views/livewire/admin/security-waf-panel.blade.php
Normal 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>
|
||||||
Reference in New Issue
Block a user