agent ??= new AgentClient; } protected function normalizeTabName(?string $tab): string { return match ($tab) { 'overview', 'firewall', 'fail2ban', 'antivirus', 'ssh', 'scanner' => $tab, default => 'overview', }; } public function setTab(string $tab): void { $this->activeTab = $this->normalizeTabName($tab); if ($this->activeTab === 'fail2ban') { $this->loadFail2banStatus(); } if ($this->activeTab === 'antivirus') { $this->loadClamavStatus(); $this->loadClamScanResults(); } if ($this->activeTab === 'scanner') { $this->checkScannerToolStatus(); $this->loadLastScans(); } } public function mount(): void { $this->activeTab = $this->normalizeTabName($this->activeTab); $this->loadFirewallStatus(); $this->loadFail2banStatusLight(); $this->loadClamavStatusLight(); $this->loadSshSettings(); $this->data = [ 'selectedWpSiteId' => null, 'selectedClamUser' => null, ]; if ($this->activeTab === 'fail2ban') { $this->loadFail2banStatus(); } elseif ($this->activeTab === 'antivirus') { $this->loadClamavStatus(); $this->loadClamScanResults(); } elseif ($this->activeTab === 'scanner') { $this->checkScannerToolStatus(); $this->loadLastScans(); } } #[On('refresh-security-data')] public function refreshSecurityData(): void { $this->loadFail2banStatus(); $this->loadClamavStatus(); } protected function getForms(): array { return [ 'securityForm', ]; } public function table(Table $table): Table { return $table ->records(fn () => $this->firewallRules) ->columns([ TextColumn::make('number') ->label(__('#')) ->sortable() ->width('60px'), TextColumn::make('action') ->label(__('Action')) ->badge() ->color(fn (string $state): string => match (strtoupper($state)) { 'ALLOW' => 'success', 'DENY' => 'danger', 'LIMIT' => 'warning', default => 'gray', }) ->formatStateUsing(fn (string $state): string => strtoupper($state)), TextColumn::make('to') ->label(__('To')) ->default('-'), TextColumn::make('from') ->label(__('From')) ->default(__('Anywhere')) ->formatStateUsing(fn (?string $state): string => $state ?: __('Anywhere')), TextColumn::make('direction') ->label(__('Direction')) ->badge() ->color('gray'), ]) ->recordAction(null) ->recordUrl(null) ->striped() ->emptyStateHeading(__('No firewall rules')) ->emptyStateDescription(__('Use the buttons above to add rules.')) ->emptyStateIcon('heroicon-o-shield-exclamation') ->actions([ TableAction::make('delete') ->label(__('Delete')) ->icon('heroicon-o-trash') ->color('danger') ->requiresConfirmation() ->action(fn (array $record) => $this->deleteRule($record['number'] ?? null)), ]); } public function securityForm(Schema $schema): Schema { return $schema ->schema([ View::make('filament.admin.components.security-tabs-nav'), ...$this->getTabContent(), ]); } protected function getTabContent(): array { return match ($this->activeTab) { 'overview' => $this->overviewTabContent(), 'firewall' => $this->firewallTabContent(), 'fail2ban' => $this->fail2banTabContent(), 'antivirus' => $this->antivirusTabContent(), 'ssh' => $this->sshTabContent(), 'scanner' => $this->scannerTabContent(), default => $this->overviewTabContent(), }; } protected function overviewTabContent(): array { return [ Grid::make(['default' => 1, 'sm' => 3]) ->schema([ Section::make($this->firewallEnabled ? __('Active') : __('Inactive')) ->description(__('Firewall')) ->icon('heroicon-o-shield-check') ->iconColor($this->firewallEnabled ? 'success' : 'danger'), Section::make($this->totalBanned !== null ? (string) $this->totalBanned : __('N/A')) ->description(__('IPs Banned')) ->icon('heroicon-o-lock-closed') ->iconColor($this->fail2banRunning ? 'success' : 'danger'), Section::make((string) count($this->recentThreats)) ->description(__('Threats Detected')) ->icon('heroicon-o-bug-ant') ->iconColor($this->clamavInstalled ? 'success' : 'gray'), ]), Section::make(__('Quick Actions')) ->icon('heroicon-o-bolt') ->schema([ FormActions::make([ FormAction::make('installFirewall') ->label(__('Install Firewall')) ->action('installFirewall') ->visible(fn () => ! $this->firewallInstalled), FormAction::make('installFail2ban') ->label(__('Install Fail2ban')) ->action('installFail2ban') ->visible(fn () => ! $this->fail2banInstalled), FormAction::make('installClamav') ->label(__('Install Antivirus')) ->action('installClamav') ->visible(fn () => ! $this->clamavInstalled), ])->visible(fn () => ! $this->firewallInstalled || ! $this->fail2banInstalled || ! $this->clamavInstalled), Text::make(__('All essential security tools are installed.')) ->visible(fn () => $this->firewallInstalled && $this->fail2banInstalled), ]), Section::make(__('Recent Audit Logs')) ->icon('heroicon-o-clipboard-document-list') ->schema([ EmbeddedTable::make(\App\Filament\Admin\Widgets\Security\AuditLogsTable::class), ]), ]; } protected function firewallTabContent(): array { return [ // Not installed state Section::make() ->schema([ Text::make(__('UFW Firewall is not installed.')), FormActions::make([ FormAction::make('installFirewall') ->label(__('Install Firewall')) ->action('installFirewall'), ])->alignment(Alignment::Center), ]) ->visible(fn () => ! $this->firewallInstalled), // Installed state Group::make([ Section::make(__('Firewall Status')) ->icon('heroicon-o-shield-check') ->iconColor($this->firewallEnabled ? 'success' : 'danger') ->schema([ Grid::make(['default' => 1, 'sm' => 2, 'lg' => 4]) ->schema([ Section::make($this->firewallEnabled ? __('ACTIVE') : __('INACTIVE')) ->description(__('Status')) ->icon('heroicon-o-signal') ->iconColor($this->firewallEnabled ? 'success' : 'danger'), Section::make(strtoupper($this->defaultIncoming)) ->description(__('Default Incoming')) ->icon('heroicon-o-arrow-down-circle') ->iconColor($this->defaultIncoming === 'deny' ? 'success' : 'warning'), Section::make(strtoupper($this->defaultOutgoing)) ->description(__('Default Outgoing')) ->icon('heroicon-o-arrow-up-circle') ->iconColor($this->defaultOutgoing === 'allow' ? 'success' : 'warning'), Section::make((string) count($this->firewallRules)) ->description(__('Active Rules')) ->icon('heroicon-o-queue-list') ->iconColor('gray'), ]), FormActions::make([ FormAction::make('toggleFirewall') ->label(fn () => $this->firewallEnabled ? __('Disable Firewall') : __('Enable Firewall')) ->icon(fn () => $this->firewallEnabled ? 'heroicon-o-x-circle' : 'heroicon-o-shield-check') ->color(fn () => $this->firewallEnabled ? 'danger' : 'success') ->action('toggleFirewall'), FormAction::make('reloadFirewall') ->label(__('Reload')) ->icon('heroicon-o-arrow-path') ->color('gray') ->outlined() ->action('reloadFirewall'), FormAction::make('resetFirewall') ->label(__('Reset All')) ->icon('heroicon-o-trash') ->color('danger') ->outlined() ->action('resetFirewall'), ]), ]), Section::make(__('Add Rule')) ->icon('heroicon-o-plus-circle') ->schema([ FormActions::make([ FormAction::make('allowPort') ->label(__('Allow Port')) ->icon('heroicon-o-plus-circle') ->color('success') ->action('openAllowPort'), FormAction::make('denyPort') ->label(__('Block Port')) ->icon('heroicon-o-x-circle') ->color('danger') ->action('openDenyPort'), FormAction::make('allowIp') ->label(__('Allow IP')) ->icon('heroicon-o-check-circle') ->color('success') ->action('openAllowIp'), FormAction::make('denyIp') ->label(__('Block IP')) ->icon('heroicon-o-no-symbol') ->color('danger') ->action('openDenyIp'), FormAction::make('allowService') ->label(__('Allow Service')) ->icon('heroicon-o-server') ->color('info') ->action('openAllowService'), FormAction::make('limitPort') ->label(__('Rate Limit')) ->icon('heroicon-o-clock') ->color('warning') ->action('openLimitPort'), ]), ]), Section::make(__('Firewall Rules')) ->icon('heroicon-o-queue-list') ->schema([ EmbeddedTable::make(), ]), Section::make(__('Quick Tips')) ->icon('heroicon-o-light-bulb') ->collapsible() ->collapsed() ->schema([ Text::make(__('Allow Port: Opens a port for all incoming connections (e.g., 80 for web, 443 for HTTPS)')), Text::make(__('Block IP: Denies all traffic from a specific IP address or subnet')), Text::make(__('Rate Limit: Limits connections to prevent brute-force attacks (6 connections in 30 seconds)')), Text::make(__('Default Policy: Controls what happens to traffic that doesn\'t match any rule')), Text::make(__('Important: Always ensure SSH (port 22) is allowed before enabling the firewall!')), ]), ])->visible(fn () => $this->firewallInstalled), ]; } protected function fail2banTabContent(): array { return [ // Not installed state Section::make() ->schema([ Text::make(__('Fail2ban is not installed.')), FormActions::make([ FormAction::make('installFail2ban') ->label(__('Install Fail2ban')) ->action('installFail2ban'), ])->alignment(Alignment::Center), ]) ->visible(fn () => ! $this->fail2banInstalled), // Installed state Group::make([ Section::make(__('Fail2ban Status')) ->icon('heroicon-o-lock-closed') ->headerActions([ FormAction::make('fail2banStatus') ->label(fn () => $this->fail2banRunning ? __('Running') : __('Stopped')) ->color(fn () => $this->fail2banRunning ? 'success' : 'danger') ->badge(), FormAction::make('toggleFail2ban') ->label(fn () => $this->fail2banRunning ? __('Stop') : __('Start')) ->color(fn () => $this->fail2banRunning ? 'danger' : 'success') ->size('sm') ->action(fn () => $this->fail2banRunning ? $this->stopFail2ban() : $this->startFail2ban()), ]) ->schema([ Grid::make(['default' => 1, 'md' => 3]) ->schema([ TextInput::make('maxRetry') ->label(__('Max Retry')) ->numeric() ->minValue(1) ->maxValue(20), TextInput::make('banTime') ->label(__('Ban Time (seconds)')) ->numeric() ->minValue(60), TextInput::make('findTime') ->label(__('Find Time (seconds)')) ->numeric() ->minValue(60), ]), FormActions::make([ FormAction::make('saveFail2banSettings') ->label(__('Save Settings')) ->action('saveFail2banSettings'), ]), ]), Section::make(__('Protection Modules')) ->icon('heroicon-o-shield-exclamation') ->description(__('Enable or disable protection modules for different services.')) ->schema([ EmbeddedTable::make(JailsTable::class, ['jails' => $this->availableJails]), ]), Section::make(__('Banned IPs').' ('.($this->totalBanned ?? __('N/A')).')') ->icon('heroicon-o-no-symbol') ->schema([ EmbeddedTable::make(BannedIpsTable::class, ['jails' => $this->jails]), ]) ->visible(fn () => ($this->totalBanned ?? 0) > 0), Section::make(__('Fail2ban Logs')) ->icon('heroicon-o-document-text') ->schema([ EmbeddedTable::make(Fail2banLogsTable::class, ['logs' => $this->fail2banLogs]), ]) ->collapsible(), ])->visible(fn () => $this->fail2banInstalled), ]; } protected function antivirusTabContent(): array { return [ // Warning for non-installed Section::make(__('Memory Requirements')) ->icon('heroicon-o-exclamation-triangle') ->iconColor('warning') ->description(__('ClamAV requires significant memory (~500MB+). Only install if your server has at least 2GB RAM available.')) ->visible(fn () => ! $this->clamavInstalled), // Not installed state Section::make() ->schema([ Text::make(__('ClamAV Antivirus is not installed.')), FormActions::make([ FormAction::make('installClamav') ->label(__('Install ClamAV')) ->action('installClamav') ->requiresConfirmation() ->modalDescription(__('ClamAV uses significant memory. Are you sure you want to install it?')), ])->alignment(Alignment::Center), ]) ->visible(fn () => ! $this->clamavInstalled), // Installed state Group::make([ Section::make(__('Resource Warning')) ->icon('heroicon-o-exclamation-triangle') ->iconColor('warning') ->description(__('ClamAV uses significant memory (~500MB+) and CPU resources. Running the daemon or real-time protection on low-resource servers may impact performance. Consider using on-demand scanning instead.')), Section::make(__('ClamAV Status')) ->icon('heroicon-o-shield-check') ->headerActions([ FormAction::make('daemonStatus') ->label(fn () => __('Daemon').' '.($this->clamavRunning ? __('Running') : __('Stopped'))) ->color(fn () => $this->clamavRunning ? 'success' : 'gray') ->badge(), FormAction::make('toggleClamav') ->label(fn () => $this->clamavRunning ? __('Stop') : __('Start')) ->color(fn () => $this->clamavRunning ? 'danger' : 'success') ->size('sm') ->action(fn () => $this->clamavRunning ? $this->stopClamav() : $this->startClamav()) ->requiresConfirmation(fn () => ! $this->clamavRunning) ->modalDescription(__('Starting ClamAV daemon uses ~500MB RAM. Continue?')), FormAction::make('updateSignatures') ->label(__('Update Signatures')) ->color('gray') ->size('sm') ->action('updateSignatures'), ]) ->schema([ Grid::make(['default' => 1, 'md' => 3]) ->schema([ Section::make($this->clamavVersion ?: __('Unknown')) ->description(__('Version')) ->icon('heroicon-o-code-bracket') ->iconColor('gray'), Section::make(number_format($this->signatureCount)) ->description(__('Signatures')) ->icon('heroicon-o-document-text') ->iconColor('gray'), Section::make($this->lastUpdate ?: __('Unknown')) ->description(__('Last Update')) ->icon('heroicon-o-clock') ->iconColor('gray'), ]), Grid::make(['default' => 1, 'md' => 2]) ->schema([ Section::make(__('Real-time Protection')) ->description(__('Monitors /home for new PHP, HTML, JS files')) ->icon($this->realtimeRunning ? 'heroicon-o-shield-check' : 'heroicon-o-shield-exclamation') ->iconColor($this->realtimeRunning ? 'success' : 'gray') ->schema([ FormActions::make([ FormAction::make('toggleRealtime') ->label(fn () => $this->realtimeRunning ? __('Disable') : __('Enable')) ->color(fn () => $this->realtimeRunning ? 'danger' : 'success') ->size('sm') ->action('toggleRealtime'), ]), ]), Section::make(__('Database Mode')) ->description(fn () => $this->clamavLightMode ? __('Web hosting only: PHP, email, scripts (~50K sigs)') : __('Full database: all malware signatures')) ->icon($this->clamavLightMode ? 'heroicon-o-bolt' : 'heroicon-o-circle-stack') ->iconColor($this->clamavLightMode ? 'warning' : 'gray') ->schema([ FormActions::make([ FormAction::make('toggleLightMode') ->label(fn () => $this->clamavLightMode ? __('Full') : __('Light')) ->color(fn () => $this->clamavLightMode ? 'warning' : 'gray') ->size('sm') ->action('toggleLightMode') ->requiresConfirmation() ->modalDescription(fn () => $this->clamavLightMode ? __('Switch to full database? This will download ~400MB of signatures.') : __('Switch to lightweight mode? This reduces signatures to web hosting essentials only.')), ]), ]), ]), ]), Section::make(__('On-Demand Scanner')) ->icon('heroicon-o-magnifying-glass') ->description(__('Manually scan user directories or the entire server for malware and threats.')) ->schema([ Grid::make(['default' => 1, 'md' => 2]) ->schema([ Section::make(__('Scan User Directory')) ->description(__('Scan a specific user\'s home directory for malware.')) ->icon('heroicon-o-user') ->iconColor('gray') ->schema([ Select::make('selectedClamUser') ->label(__('Select User')) ->options(fn () => $this->getClamScanUsers()) ->placeholder(__('Select a user to scan')) ->searchable() ->live(), FormActions::make([ FormAction::make('runClamScanUser') ->label(fn () => $this->isScanning && $this->currentScan === 'clamav' ? __('Scanning...') : __('Scan User')) ->icon('heroicon-o-play') ->action('runClamScanUser') ->disabled(fn () => $this->isScanning || ! $this->selectedClamUser), ]), ]), Section::make(__('Server-Wide Scan')) ->description(__('Scan all user directories (/home). This may take a long time.')) ->icon('heroicon-o-server') ->iconColor('warning') ->schema([ Text::make(__('Server-wide scans can take 30+ minutes depending on data size.')), FormActions::make([ FormAction::make('runClamScanServer') ->label(fn () => $this->isScanning && $this->currentScan === 'clamav' ? __('Scanning...') : __('Scan All Users')) ->icon('heroicon-o-server') ->color('warning') ->action('runClamScanServer') ->disabled(fn () => $this->isScanning) ->requiresConfirmation() ->modalDescription(__('This will scan all user directories and may take a long time. Continue?')), ]), ]), ]), Text::make($this->lastClamScan ? __('Last scan:').' '.$this->lastClamScan : '') ->visible(fn () => (bool) $this->lastClamScan), ]), Section::make(__('Scan Output')) ->icon('heroicon-o-command-line') ->collapsible() ->schema(fn () => $this->buildClamScanOutputSchema()) ->visible(fn () => $this->isScanning && $this->currentScan === 'clamav' || ! empty($this->clamScanResults['raw_output'] ?? '')), Section::make(__('Scan Results')) ->icon('heroicon-o-document-chart-bar') ->collapsible() ->schema(fn () => $this->buildClamavScanResultsSchema()) ->visible(fn () => ! empty($this->clamScanResults)), Section::make(__('Quarantined Files').' ('.count($this->quarantinedFiles).')') ->icon('heroicon-o-archive-box') ->schema([ EmbeddedTable::make(QuarantinedFilesTable::class, ['files' => $this->quarantinedFiles]), ]) ->visible(fn () => count($this->quarantinedFiles) > 0), Section::make(__('Recent Threats')) ->icon('heroicon-o-exclamation-triangle') ->schema([ EmbeddedTable::make(ThreatsTable::class, ['threats' => $this->recentThreats]), ]) ->visible(fn () => count($this->recentThreats) > 0), ])->visible(fn () => $this->clamavInstalled), ]; } protected function sshTabContent(): array { return [ // Current Configuration - 3 widgets on top Grid::make(['default' => 1, 'sm' => 3]) ->schema([ Section::make($this->sshPasswordAuth ? __('Enabled') : __('Disabled')) ->description(__('Password Auth')) ->icon('heroicon-o-key') ->iconColor($this->sshPasswordAuth ? 'warning' : 'success'), Section::make($this->sshPubkeyAuth ? __('Enabled') : __('Disabled')) ->description(__('Public Key Auth')) ->icon('heroicon-o-finger-print') ->iconColor($this->sshPubkeyAuth ? 'success' : 'danger'), Section::make((string) $this->sshPort) ->description(__('SSH Port')) ->icon('heroicon-o-server') ->iconColor($this->sshPort != 22 ? 'success' : 'gray'), ]), Section::make(__('Important Notice')) ->icon('heroicon-o-exclamation-triangle') ->iconColor('warning') ->description(__('Changing SSH settings can lock you out of the server. Ensure you have console access or an active SSH session before disabling password authentication.')), Section::make(__('SSH Authentication Settings')) ->icon('heroicon-o-command-line') ->schema([ Toggle::make('sshPasswordAuth') ->label(__('Password Authentication')) ->helperText(__('Allow users to log in using passwords. Disable for key-only access.')), Toggle::make('sshPubkeyAuth') ->label(__('Public Key Authentication')) ->helperText(__('Allow users to log in using SSH keys. Recommended for security.')), TextInput::make('sshPort') ->label(__('SSH Port')) ->numeric() ->minValue(1) ->maxValue(65535) ->helperText(__('Default is 22. Changing port can help reduce automated attacks.')) ->columnSpan(1), FormActions::make([ FormAction::make('saveSshSettings') ->label(__('Save SSH Settings')) ->action('saveSshSettings') ->requiresConfirmation() ->modalDescription(__('Are you sure? This will restart the SSH service immediately.')), ]), ]), Section::make(__('Security Recommendations')) ->icon('heroicon-o-light-bulb') ->schema(fn () => $this->buildSshRecommendationsSchema()), ]; } protected function scannerTabContent(): array { return [ Grid::make(['default' => 1, 'md' => 3]) ->schema([ // Lynis Section::make(__('Lynis')) ->icon('heroicon-o-shield-check') ->description(__('System security auditing tool. Checks configurations, permissions, and hardening settings.')) ->headerActions([ FormAction::make('lynisStatus') ->label(fn () => $this->lynisInstalled ? __('Installed') : __('Not Installed')) ->color(fn () => $this->lynisInstalled ? 'success' : 'danger') ->badge(), ]) ->schema([ Group::make([ Text::make($this->lynisVersion) ->visible(fn () => $this->lynisInstalled && $this->lynisVersion), Text::make($this->lastLynisScan ? __('Last:').' '.$this->lastLynisScan : '') ->visible(fn () => $this->lynisInstalled && $this->lastLynisScan), ])->visible(fn () => $this->lynisInstalled), FormActions::make([ FormAction::make('runLynisScan') ->label(fn () => $this->isScanning && $this->currentScan === 'lynis' ? __('Scanning...') : __('Run System Audit')) ->icon('heroicon-o-play') ->color('success') ->action('runLynisScan') ->disabled(fn () => $this->isScanning), ])->visible(fn () => $this->lynisInstalled), FormActions::make([ FormAction::make('installLynis') ->label(__('Install Lynis')) ->icon('heroicon-o-arrow-down-tray') ->action('installLynis'), ])->visible(fn () => ! $this->lynisInstalled), ]), // WPScan Section::make(__('WPScan')) ->icon('heroicon-o-globe-alt') ->description(__('WordPress vulnerability scanner. Checks for vulnerable plugins, themes, and core issues.')) ->headerActions([ FormAction::make('wpscanStatus') ->label(fn () => $this->wpscanInstalled ? __('Installed') : __('Not Installed')) ->color(fn () => $this->wpscanInstalled ? 'success' : 'danger') ->badge(), ]) ->schema([ Group::make([ Text::make($this->wpscanVersion) ->visible(fn () => $this->wpscanInstalled && $this->wpscanVersion), Text::make($this->lastWpscanScan ? __('Last:').' '.$this->lastWpscanScan : '') ->visible(fn () => $this->wpscanInstalled && $this->lastWpscanScan), Select::make('selectedWpSiteId') ->label(__('WordPress Site')) ->options(fn () => $this->getLocalWordPressSites()) ->placeholder(__('Select a WordPress site')) ->searchable() ->live() ->visible(fn () => count($this->getLocalWordPressSites()) > 0), Text::make(__('No WordPress sites found')) ->visible(fn () => count($this->getLocalWordPressSites()) === 0), ])->visible(fn () => $this->wpscanInstalled), FormActions::make([ FormAction::make('runWpscanOnSite') ->label(fn () => $this->isScanning && $this->currentScan === 'wpscan' ? __('Scanning...') : __('Scan WordPress Site')) ->icon('heroicon-o-play') ->color('info') ->action('runWpscanOnSite') ->disabled(fn () => $this->isScanning || ! $this->selectedWpSiteId), ])->visible(fn () => $this->wpscanInstalled && count($this->getLocalWordPressSites()) > 0), FormActions::make([ FormAction::make('installWpscan') ->label(__('Install WPScan')) ->icon('heroicon-o-arrow-down-tray') ->action('installWpscan'), ])->visible(fn () => ! $this->wpscanInstalled), ]), // Nikto Section::make(__('Nikto')) ->icon('heroicon-o-server') ->description(__('Web server scanner. Finds server misconfigurations and known vulnerabilities on localhost.')) ->headerActions([ FormAction::make('niktoStatus') ->label(fn () => $this->niktoInstalled ? __('Installed') : __('Not Installed')) ->color(fn () => $this->niktoInstalled ? 'success' : 'danger') ->badge(), ]) ->schema([ Group::make([ Text::make($this->niktoVersion) ->visible(fn () => $this->niktoInstalled && $this->niktoVersion), Text::make($this->lastNiktoScan ? __('Last:').' '.$this->lastNiktoScan : '') ->visible(fn () => $this->niktoInstalled && $this->lastNiktoScan), ])->visible(fn () => $this->niktoInstalled), FormActions::make([ FormAction::make('runNiktoScan') ->label(fn () => $this->isScanning && $this->currentScan === 'nikto' ? __('Scanning...') : __('Scan Local Server')) ->icon('heroicon-o-play') ->color('warning') ->action('runNiktoScan') ->disabled(fn () => $this->isScanning), ])->visible(fn () => $this->niktoInstalled), FormActions::make([ FormAction::make('installNikto') ->label(__('Install Nikto')) ->icon('heroicon-o-arrow-down-tray') ->action('installNikto'), ])->visible(fn () => ! $this->niktoInstalled), ]), ]), // Lynis Results Section::make(__('Lynis System Audit Results')) ->icon('heroicon-o-document-chart-bar') ->schema([ Grid::make(['default' => 1, 'md' => 3]) ->schema([ Section::make((string) ($this->lynisResults['hardening_index'] ?? 0)) ->description(__('Hardening Index')) ->icon('heroicon-o-shield-check') ->iconColor(fn () => ($this->lynisResults['hardening_index'] ?? 0) >= 70 ? 'success' : (($this->lynisResults['hardening_index'] ?? 0) >= 50 ? 'warning' : 'danger')), Section::make((string) count($this->lynisResults['warnings'] ?? [])) ->description(__('Warnings')) ->icon('heroicon-o-exclamation-triangle') ->iconColor('warning'), Section::make((string) count($this->lynisResults['suggestions'] ?? [])) ->description(__('Suggestions')) ->icon('heroicon-o-light-bulb') ->iconColor('primary'), ]), Section::make(__('Warnings')) ->icon('heroicon-o-exclamation-triangle') ->iconColor('warning') ->collapsible() ->schema([ EmbeddedTable::make(LynisResultsTable::class, ['results' => $this->lynisResults, 'type' => 'warnings']), ]) ->visible(fn () => ! empty($this->lynisResults['warnings'] ?? [])), Section::make(__('Suggestions')) ->icon('heroicon-o-light-bulb') ->iconColor('info') ->collapsible() ->schema([ EmbeddedTable::make(LynisResultsTable::class, ['results' => $this->lynisResults, 'type' => 'suggestions']), ]) ->visible(fn () => ! empty($this->lynisResults['suggestions'] ?? [])), ]) ->visible(fn () => ! empty($this->lynisResults)), // WPScan Results Section::make(__('WPScan Results')) ->icon('heroicon-o-globe-alt') ->schema([ Section::make('WordPress '.($this->wpscanResults['version']['number'] ?? __('Unknown'))) ->icon('heroicon-o-code-bracket') ->iconColor('info') ->visible(fn () => isset($this->wpscanResults['version']['number'])), EmbeddedTable::make(WpscanResultsTable::class, ['results' => $this->wpscanResults]), ]) ->visible(fn () => ! empty($this->wpscanResults) && ! isset($this->wpscanResults['error'])), // Nikto Results Section::make(__('Nikto Scan Results')) ->icon('heroicon-o-server') ->schema([ Section::make(__('Vulnerabilities')) ->icon('heroicon-o-exclamation-triangle') ->iconColor('danger') ->collapsible() ->schema([ EmbeddedTable::make(NiktoResultsTable::class, ['results' => $this->niktoResults, 'type' => 'vulnerabilities']), ]) ->visible(fn () => ! empty($this->niktoResults['vulnerabilities'] ?? [])), Section::make(__('Information')) ->icon('heroicon-o-information-circle') ->iconColor('info') ->collapsible() ->schema([ EmbeddedTable::make(NiktoResultsTable::class, ['results' => $this->niktoResults, 'type' => 'info']), ]) ->visible(fn () => ! empty($this->niktoResults['info'] ?? [])), ]) ->visible(fn () => ! empty($this->niktoResults)), // Scan Output Section::make(__('Scan Output')) ->icon('heroicon-o-command-line') ->schema(fn () => $this->buildScanOutputSchema()) ->visible(fn () => (bool) $this->scanOutput), ]; } protected function getHeaderActions(): array { return [ ]; } // Load methods protected function loadFirewallStatus(): void { try { $result = $this->getAgent()->send('ufw.status'); $this->firewallInstalled = true; $this->firewallEnabled = $result['active'] ?? false; $this->defaultIncoming = $result['default_incoming'] ?? 'deny'; $this->defaultOutgoing = $result['default_outgoing'] ?? 'allow'; $this->firewallStatusText = $result['status_text'] ?? ''; $rulesResult = $this->getAgent()->send('ufw.list_rules'); $this->firewallRules = $rulesResult['rules'] ?? []; } catch (Exception $e) { $this->firewallInstalled = false; } } protected function loadFail2banStatusLight(): void { try { $result = $this->getAgent()->send('fail2ban.status_light'); $this->fail2banInstalled = $result['installed'] ?? false; $this->fail2banRunning = $result['running'] ?? false; $this->fail2banVersion = $result['version'] ?? 'Unknown'; $this->jails = []; $this->availableJails = []; $this->totalBanned = null; $this->fail2banLogs = []; } catch (Exception $e) { $this->fail2banInstalled = false; $this->fail2banRunning = false; $this->fail2banVersion = ''; $this->jails = []; $this->availableJails = []; $this->totalBanned = null; $this->fail2banLogs = []; } } protected function loadFail2banStatus(): void { try { $result = $this->getAgent()->send('fail2ban.status'); $this->fail2banInstalled = $result['installed'] ?? false; if ($this->fail2banInstalled) { $this->fail2banRunning = $result['running'] ?? false; $this->fail2banVersion = $result['version'] ?? 'Unknown'; $this->jails = $result['jails'] ?? []; $this->totalBanned = $result['total_banned'] ?? 0; $this->maxRetry = $result['max_retry'] ?? 5; $this->banTime = $result['ban_time'] ?? 600; $this->findTime = $result['find_time'] ?? 600; $jailsResult = $this->getAgent()->send('fail2ban.list_jails'); $this->availableJails = $jailsResult['jails'] ?? []; $logsResult = $this->getAgent()->send('fail2ban.logs'); $this->fail2banLogs = $logsResult['logs'] ?? []; } else { $this->fail2banRunning = false; $this->fail2banVersion = ''; $this->jails = []; $this->availableJails = []; $this->totalBanned = null; $this->fail2banLogs = []; } } catch (Exception $e) { $this->fail2banInstalled = false; $this->fail2banRunning = false; $this->fail2banVersion = ''; $this->jails = []; $this->availableJails = []; $this->totalBanned = null; $this->fail2banLogs = []; } } protected function loadClamavStatusLight(): void { try { $result = $this->getAgent()->send('clamav.status_light'); $this->clamavInstalled = $result['installed'] ?? false; $this->clamavRunning = $result['running'] ?? false; $this->clamavVersion = $result['version'] ?? 'Unknown'; $this->realtimeEnabled = $result['realtime_enabled'] ?? false; $this->realtimeRunning = $result['realtime_running'] ?? false; $this->clamavLightMode = $result['light_mode'] ?? false; $this->signatureCount = 0; $this->lastUpdate = ''; $this->recentThreats = []; $this->quarantinedFiles = []; $this->signatureDatabases = []; } catch (Exception $e) { $this->clamavInstalled = false; $this->clamavRunning = false; $this->clamavVersion = ''; $this->realtimeEnabled = false; $this->realtimeRunning = false; $this->clamavLightMode = false; $this->signatureCount = 0; $this->lastUpdate = ''; $this->recentThreats = []; $this->quarantinedFiles = []; $this->signatureDatabases = []; } } protected function loadClamavStatus(): void { try { $result = $this->getAgent()->send('clamav.status'); $this->clamavInstalled = $result['installed'] ?? false; if ($this->clamavInstalled) { $this->clamavRunning = $result['running'] ?? false; $this->clamavVersion = $result['version'] ?? 'Unknown'; $this->signatureCount = $result['signature_count'] ?? 0; $this->lastUpdate = $result['last_update'] ?? ''; $this->recentThreats = $result['recent_threats'] ?? []; $this->quarantinedFiles = $result['quarantined_files'] ?? []; $this->realtimeEnabled = $result['realtime_enabled'] ?? false; $this->realtimeRunning = $result['realtime_running'] ?? false; $this->clamavLightMode = $result['light_mode'] ?? false; $this->signatureDatabases = $result['signature_databases'] ?? []; } else { $this->clamavRunning = false; $this->clamavVersion = ''; $this->signatureCount = 0; $this->lastUpdate = ''; $this->recentThreats = []; $this->quarantinedFiles = []; $this->realtimeEnabled = false; $this->realtimeRunning = false; $this->clamavLightMode = false; $this->signatureDatabases = []; } } catch (Exception $e) { $this->clamavInstalled = false; $this->clamavRunning = false; $this->clamavVersion = ''; $this->signatureCount = 0; $this->lastUpdate = ''; $this->recentThreats = []; $this->quarantinedFiles = []; $this->realtimeEnabled = false; $this->realtimeRunning = false; $this->clamavLightMode = false; $this->signatureDatabases = []; } } protected function loadSshSettings(): void { try { $result = $this->getAgent()->send('ssh.get_settings'); if ($result['success'] ?? false) { $this->sshPasswordAuth = $result['password_auth'] ?? false; $this->sshPubkeyAuth = $result['pubkey_auth'] ?? true; $this->sshPort = $result['port'] ?? 22; } } catch (Exception $e) { // Use defaults } } public function saveSshSettings(): void { try { if (! $this->sshPasswordAuth && ! $this->sshPubkeyAuth) { Notification::make() ->title(__('Invalid Configuration')) ->body(__('At least one authentication method must be enabled.')) ->danger() ->send(); return; } $result = $this->getAgent()->send('ssh.save_settings', [ 'password_auth' => $this->sshPasswordAuth, 'pubkey_auth' => $this->sshPubkeyAuth, 'port' => $this->sshPort, ]); if ($result['success'] ?? false) { Notification::make() ->title(__('SSH settings saved')) ->body(__('Changes will take effect immediately. Make sure you have key access if disabling passwords.')) ->success() ->send(); } else { throw new Exception($result['error'] ?? __('Failed to save settings')); } } catch (Exception $e) { Notification::make() ->title(__('Failed to save SSH settings')) ->body($e->getMessage()) ->danger() ->send(); } $this->loadSshSettings(); } // Firewall actions public function toggleFirewall(): void { try { $action = $this->firewallEnabled ? 'ufw.disable' : 'ufw.enable'; $result = $this->getAgent()->send($action); if ($result['success'] ?? false) { $this->firewallEnabled = ! $this->firewallEnabled; $auditAction = $this->firewallEnabled ? 'enabled' : 'disabled'; AuditLog::logFirewallAction($auditAction); Notification::make() ->title($this->firewallEnabled ? __('Firewall enabled') : __('Firewall disabled')) ->success() ->send(); } else { throw new Exception($result['message'] ?? __('Unknown error')); } } catch (Exception $e) { Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send(); } $this->loadFirewallStatus(); } public function installFirewall(): void { Notification::make()->title(__('Installing firewall...'))->info()->send(); try { $this->getAgent()->send('ufw.enable'); $this->getAgent()->send('ufw.allow_service', ['service' => 'ssh']); $this->getAgent()->send('ufw.allow_service', ['service' => 'http']); $this->getAgent()->send('ufw.allow_service', ['service' => 'https']); AuditLog::logFirewallAction('installed', 'default rules configured'); Notification::make()->title(__('Firewall configured with default rules'))->success()->send(); } catch (Exception $e) { Notification::make()->title(__('Failed to configure firewall'))->body($e->getMessage())->danger()->send(); } $this->loadFirewallStatus(); } public function deleteRule(int $ruleNumber): void { $this->ruleToDelete = $ruleNumber; $this->mountAction('deleteRuleAction'); } public function deleteRuleAction(): Action { return Action::make('deleteRuleAction') ->requiresConfirmation() ->modalHeading(__('Delete Firewall Rule')) ->modalDescription(fn () => __('Are you sure you want to delete rule #:number?', ['number' => $this->ruleToDelete])) ->modalSubmitActionLabel(__('Delete')) ->color('danger') ->action(function (): void { try { $result = $this->getAgent()->send('ufw.delete_rule', ['rule_number' => $this->ruleToDelete]); if ($result['success'] ?? false) { AuditLog::logFirewallAction('deleted', "rule #{$this->ruleToDelete}"); Notification::make()->title(__('Rule deleted'))->success()->send(); $this->loadFirewallStatus(); } else { throw new Exception($result['message'] ?? __('Unknown error')); } } catch (Exception $e) { Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send(); } }); } public function setDefaultPolicy(string $direction): void { $this->mountAction('setDefaultPolicyAction', ['direction' => $direction]); } public function setDefaultPolicyAction(): Action { return Action::make('setDefaultPolicyAction') ->modalHeading(__('Set Default Policy')) ->form([ Select::make('policy') ->label(__('Policy')) ->options([ 'allow' => __('Allow'), 'deny' => __('Deny'), 'reject' => __('Reject'), ]) ->required(), ]) ->action(function (array $data, array $arguments): void { try { $result = $this->getAgent()->send('ufw.set_default', [ 'direction' => $arguments['direction'] ?? 'incoming', 'policy' => $data['policy'], ]); if ($result['success'] ?? false) { Notification::make()->title(__('Default policy updated'))->success()->send(); $this->loadFirewallStatus(); } else { throw new Exception($result['message'] ?? __('Unknown error')); } } catch (Exception $e) { Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send(); } }); } public function reloadFirewall(): void { try { $result = $this->getAgent()->send('ufw.reload'); if ($result['success'] ?? false) { Notification::make()->title(__('Firewall reloaded'))->success()->send(); $this->loadFirewallStatus(); } else { throw new Exception($result['message'] ?? __('Unknown error')); } } catch (Exception $e) { Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send(); } } public function resetFirewall(): void { $this->mountAction('resetFirewallAction'); } public function resetFirewallAction(): Action { return Action::make('resetFirewallAction') ->requiresConfirmation() ->modalHeading(__('Reset Firewall')) ->modalDescription(__('This will delete ALL firewall rules and disable the firewall. Are you sure?')) ->modalSubmitActionLabel(__('Reset Everything')) ->color('danger') ->action(function (): void { try { $result = $this->getAgent()->send('ufw.reset'); if ($result['success'] ?? false) { AuditLog::logFirewallAction('reset', 'all rules deleted'); Notification::make()->title(__('Firewall reset'))->body(__('All rules have been deleted.'))->success()->send(); $this->loadFirewallStatus(); } else { throw new Exception($result['message'] ?? __('Unknown error')); } } catch (Exception $e) { Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send(); } }); } protected function allowPortAction(): Action { return Action::make('allowPort') ->label(__('Allow Port')) ->icon('heroicon-o-plus-circle') ->color('success') ->form([ TextInput::make('port') ->label(__('Port')) ->placeholder(__('e.g., 80, 443, 8000:8100')) ->required() ->helperText(__('Single port or range (e.g., 8000:8100)')), Select::make('protocol') ->label(__('Protocol')) ->options([ '' => __('Both (TCP & UDP)'), 'tcp' => __('TCP only'), 'udp' => __('UDP only'), ]) ->default(''), TextInput::make('comment') ->label(__('Comment (optional)')) ->placeholder(__('e.g., Web server')), ]) ->action(function (array $data): void { try { $result = $this->getAgent()->send('ufw.allow_port', $data); if ($result['success'] ?? false) { $rule = "allow port {$data['port']}".($data['protocol'] ? "/{$data['protocol']}" : ''); AuditLog::logFirewallAction('added', $rule, $data); Notification::make()->title(__('Port allowed'))->success()->send(); $this->loadFirewallStatus(); } else { throw new Exception($result['error'] ?? $result['message'] ?? __('Unknown error')); } } catch (Exception $e) { Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send(); } }); } protected function denyPortAction(): Action { return Action::make('denyPort') ->label(__('Block Port')) ->icon('heroicon-o-x-circle') ->color('danger') ->form([ TextInput::make('port') ->label(__('Port')) ->placeholder(__('e.g., 3306')) ->required(), Select::make('protocol') ->label(__('Protocol')) ->options([ '' => __('Both (TCP & UDP)'), 'tcp' => __('TCP only'), 'udp' => __('UDP only'), ]) ->default(''), TextInput::make('comment') ->label(__('Comment (optional)')), ]) ->action(function (array $data): void { try { $result = $this->getAgent()->send('ufw.deny_port', $data); if ($result['success'] ?? false) { $rule = "deny port {$data['port']}".($data['protocol'] ? "/{$data['protocol']}" : ''); AuditLog::logFirewallAction('added', $rule, $data); Notification::make()->title(__('Port blocked'))->success()->send(); $this->loadFirewallStatus(); } else { throw new Exception($result['error'] ?? $result['message'] ?? __('Unknown error')); } } catch (Exception $e) { Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send(); } }); } protected function allowIpAction(): Action { return Action::make('allowIp') ->label(__('Allow IP')) ->icon('heroicon-o-check-circle') ->color('success') ->form([ TextInput::make('ip') ->label(__('IP Address')) ->placeholder(__('e.g., 192.168.1.100 or 10.0.0.0/8')) ->required() ->helperText(__('Single IP or CIDR notation')), TextInput::make('port') ->label(__('Port (optional)')) ->placeholder(__('Leave empty to allow all ports')), Select::make('protocol') ->label(__('Protocol')) ->options([ '' => __('Any'), 'tcp' => __('TCP'), 'udp' => __('UDP'), ]) ->default(''), TextInput::make('comment') ->label(__('Comment (optional)')), ]) ->action(function (array $data): void { try { $result = $this->getAgent()->send('ufw.allow_ip', $data); if ($result['success'] ?? false) { $rule = "allow from {$data['ip']}".($data['port'] ? " to port {$data['port']}" : ''); AuditLog::logFirewallAction('added', $rule, $data); Notification::make()->title(__('IP allowed'))->success()->send(); $this->loadFirewallStatus(); } else { throw new Exception($result['error'] ?? $result['message'] ?? __('Unknown error')); } } catch (Exception $e) { Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send(); } }); } protected function denyIpAction(): Action { return Action::make('denyIp') ->label(__('Block IP')) ->icon('heroicon-o-no-symbol') ->color('danger') ->form([ TextInput::make('ip') ->label(__('IP Address')) ->placeholder(__('e.g., 192.168.1.100 or 10.0.0.0/8')) ->required(), TextInput::make('port') ->label(__('Port (optional)')) ->placeholder(__('Leave empty to block all ports')), Select::make('protocol') ->label(__('Protocol')) ->options([ '' => __('Any'), 'tcp' => __('TCP'), 'udp' => __('UDP'), ]) ->default(''), TextInput::make('comment') ->label(__('Comment (optional)')), ]) ->action(function (array $data): void { try { $result = $this->getAgent()->send('ufw.deny_ip', $data); if ($result['success'] ?? false) { $rule = "deny from {$data['ip']}".($data['port'] ? " to port {$data['port']}" : ''); AuditLog::logFirewallAction('added', $rule, $data); Notification::make()->title(__('IP blocked'))->success()->send(); $this->loadFirewallStatus(); } else { throw new Exception($result['error'] ?? $result['message'] ?? __('Unknown error')); } } catch (Exception $e) { Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send(); } }); } protected function allowServiceAction(): Action { return Action::make('allowService') ->label(__('Allow Service')) ->icon('heroicon-o-server') ->color('info') ->form([ Select::make('service') ->label(__('Service')) ->options([ 'ssh' => __('SSH (22)'), 'http' => __('HTTP (80)'), 'https' => __('HTTPS (443)'), 'ftp' => __('FTP (21)'), 'smtp' => __('SMTP (25)'), 'pop3' => __('POP3 (110)'), 'imap' => __('IMAP (143)'), 'dns' => __('DNS (53)'), 'mysql' => __('MySQL (3306)'), 'postgresql' => __('PostgreSQL (5432)'), ]) ->required() ->searchable(), ]) ->action(function (array $data): void { try { $result = $this->getAgent()->send('ufw.allow_service', $data); if ($result['success'] ?? false) { AuditLog::logFirewallAction('added', "allow service {$data['service']}", $data); Notification::make()->title(__('Service allowed'))->success()->send(); $this->loadFirewallStatus(); } else { throw new Exception($result['error'] ?? $result['message'] ?? __('Unknown error')); } } catch (Exception $e) { Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send(); } }); } protected function limitPortAction(): Action { return Action::make('limitPort') ->label(__('Rate Limit')) ->icon('heroicon-o-clock') ->color('warning') ->form([ TextInput::make('port') ->label(__('Port')) ->placeholder(__('e.g., 22')) ->required() ->helperText(__('Limit connections (6 in 30 seconds)')), Select::make('protocol') ->label(__('Protocol')) ->options([ 'tcp' => __('TCP'), 'udp' => __('UDP'), ]) ->default('tcp'), ]) ->action(function (array $data): void { try { $result = $this->getAgent()->send('ufw.limit_port', $data); if ($result['success'] ?? false) { AuditLog::logFirewallAction('added', "limit port {$data['port']}/{$data['protocol']}", $data); Notification::make()->title(__('Rate limit applied'))->success()->send(); $this->loadFirewallStatus(); } else { throw new Exception($result['error'] ?? $result['message'] ?? __('Unknown error')); } } catch (Exception $e) { Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send(); } }); } public function getActionColor(string $action): string { return match (strtoupper($action)) { 'ALLOW' => 'success', 'DENY' => 'danger', 'REJECT' => 'warning', 'LIMIT' => 'warning', default => 'gray', }; } // Firewall action mount helpers public function openAllowPort(): void { $this->mountAction('allowPort'); } public function openDenyPort(): void { $this->mountAction('denyPort'); } public function openAllowIp(): void { $this->mountAction('allowIp'); } public function openDenyIp(): void { $this->mountAction('denyIp'); } public function openAllowService(): void { $this->mountAction('allowService'); } public function openLimitPort(): void { $this->mountAction('limitPort'); } // Fail2ban actions public function installFail2ban(): void { Notification::make()->title(__('Installing Fail2ban...'))->info()->send(); try { $result = $this->getAgent()->send('fail2ban.install'); if ($result['success'] ?? false) { Notification::make()->title(__('Fail2ban installed'))->success()->send(); } else { throw new Exception($result['error'] ?? __('Installation failed')); } } catch (Exception $e) { Notification::make()->title(__('Failed to install Fail2ban'))->body($e->getMessage())->danger()->send(); } $this->loadFail2banStatus(); } public function startFail2ban(): void { try { $result = $this->getAgent()->send('fail2ban.start'); if ($result['success'] ?? false) { Notification::make()->title(__('Fail2ban started'))->success()->send(); } else { throw new Exception($result['error'] ?? __('Failed to start')); } } catch (Exception $e) { Notification::make()->title(__('Failed to start Fail2ban'))->body($e->getMessage())->danger()->send(); } $this->loadFail2banStatus(); } public function stopFail2ban(): void { try { $result = $this->getAgent()->send('fail2ban.stop'); if ($result['success'] ?? false) { Notification::make()->title(__('Fail2ban stopped'))->success()->send(); } else { throw new Exception($result['error'] ?? __('Failed to stop')); } } catch (Exception $e) { Notification::make()->title(__('Failed to stop Fail2ban'))->body($e->getMessage())->danger()->send(); } $this->loadFail2banStatus(); } public function saveFail2banSettings(): void { try { $result = $this->getAgent()->send('fail2ban.save_settings', [ 'max_retry' => $this->maxRetry, 'ban_time' => $this->banTime, 'find_time' => $this->findTime, ]); if ($result['success'] ?? false) { Notification::make()->title(__('Settings saved'))->success()->send(); } else { throw new Exception($result['error'] ?? __('Failed to save')); } } catch (Exception $e) { Notification::make()->title(__('Failed to save settings'))->body($e->getMessage())->danger()->send(); } $this->loadFail2banStatus(); } public function unbanIp(string $jail, string $ip): void { try { $result = $this->getAgent()->send('fail2ban.unban_ip', ['jail' => $jail, 'ip' => $ip]); if ($result['success'] ?? false) { Notification::make()->title(__('Unbanned :ip', ['ip' => $ip]))->success()->send(); } else { throw new Exception($result['error'] ?? __('Failed to unban')); } } catch (Exception $e) { Notification::make()->title(__('Failed to unban IP'))->body($e->getMessage())->danger()->send(); } $this->loadFail2banStatus(); } public function enableJail(string $jail): void { try { $result = $this->getAgent()->send('fail2ban.enable_jail', ['jail' => $jail]); if ($result['success'] ?? false) { Notification::make()->title(__('Jail :jail enabled', ['jail' => $jail]))->success()->send(); } else { throw new Exception($result['error'] ?? __('Failed to enable jail')); } } catch (Exception $e) { Notification::make()->title(__('Failed to enable jail'))->body($e->getMessage())->danger()->send(); } $this->loadFail2banStatus(); } public function disableJail(string $jail): void { try { $result = $this->getAgent()->send('fail2ban.disable_jail', ['jail' => $jail]); if ($result['success'] ?? false) { Notification::make()->title(__('Jail :jail disabled', ['jail' => $jail]))->success()->send(); } else { throw new Exception($result['error'] ?? __('Failed to disable jail')); } } catch (Exception $e) { Notification::make()->title(__('Failed to disable jail'))->body($e->getMessage())->danger()->send(); } $this->loadFail2banStatus(); } // ClamAV actions public function installClamav(): void { Notification::make()->title(__('Installing ClamAV...'))->body(__('This may take a few minutes.'))->info()->send(); try { $result = $this->getAgent()->send('clamav.install'); if ($result['success'] ?? false) { Notification::make()->title(__('ClamAV installed'))->body(__('Daemon disabled by default to save memory.'))->success()->send(); } else { throw new Exception($result['error'] ?? __('Installation failed')); } } catch (Exception $e) { Notification::make()->title(__('Failed to install ClamAV'))->body($e->getMessage())->danger()->send(); } $this->loadClamavStatus(); } public function updateSignatures(): void { try { $result = $this->getAgent()->send('clamav.update_signatures'); if ($result['success'] ?? false) { Notification::make()->title(__('Signatures updated'))->success()->send(); } else { Notification::make()->title(__('Update may have issues'))->body($result['output'] ?? '')->warning()->send(); } } catch (Exception $e) { Notification::make()->title(__('Failed to update signatures'))->body($e->getMessage())->danger()->send(); } $this->loadClamavStatus(); } public function startClamav(): void { try { $result = $this->getAgent()->send('clamav.start'); if ($result['success'] ?? false) { Notification::make()->title(__('ClamAV started'))->success()->send(); } else { throw new Exception($result['error'] ?? __('Failed to start')); } } catch (Exception $e) { Notification::make()->title(__('Failed to start ClamAV'))->body($e->getMessage())->danger()->send(); } $this->loadClamavStatus(); } public function stopClamav(): void { try { $result = $this->getAgent()->send('clamav.stop'); if ($result['success'] ?? false) { Notification::make()->title(__('ClamAV stopped'))->success()->send(); } else { throw new Exception($result['error'] ?? __('Failed to stop')); } } catch (Exception $e) { Notification::make()->title(__('Failed to stop ClamAV'))->body($e->getMessage())->danger()->send(); } $this->loadClamavStatus(); } public function toggleRealtime(): void { try { if ($this->realtimeRunning) { $result = $this->getAgent()->send('clamav.realtime_disable'); $message = __('Real-time protection disabled'); } else { $result = $this->getAgent()->send('clamav.realtime_enable'); $message = __('Real-time protection enabled'); } if ($result['success'] ?? false) { Notification::make()->title($message)->success()->send(); } else { throw new Exception($result['error'] ?? __('Failed')); } } catch (Exception $e) { Notification::make()->title(__('Failed to toggle real-time protection'))->body($e->getMessage())->danger()->send(); } $this->loadClamavStatus(); } public function toggleLightMode(): void { try { $action = $this->clamavLightMode ? 'clamav.set_full_mode' : 'clamav.set_light_mode'; $result = $this->getAgent()->send($action); if ($result['success'] ?? false) { $this->clamavLightMode = ! $this->clamavLightMode; $message = $this->clamavLightMode ? __('Switched to lightweight mode - web hosting signatures only') : __('Switched to full mode - all ClamAV signatures'); Notification::make() ->title($message) ->body(__('Signature count: :count', ['count' => number_format($result['signature_count'] ?? 0)])) ->success() ->send(); } else { throw new Exception($result['error'] ?? __('Failed to switch mode')); } } catch (Exception $e) { Notification::make() ->title(__('Failed to switch ClamAV mode')) ->body($e->getMessage()) ->danger() ->send(); } $this->loadClamavStatus(); } public function deleteQuarantined(string $filename): void { try { $result = $this->getAgent()->send('clamav.delete_quarantined', ['filename' => $filename]); if ($result['success'] ?? false) { Notification::make()->title(__('File deleted'))->success()->send(); } else { throw new Exception($result['error'] ?? __('Failed to delete')); } } catch (Exception $e) { Notification::make()->title(__('Failed to delete file'))->body($e->getMessage())->danger()->send(); } $this->loadClamavStatus(); } // Scanner methods protected function checkScannerToolStatus(): void { exec('which lynis 2>/dev/null', $output, $code); $this->lynisInstalled = $code === 0; if ($this->lynisInstalled) { exec('lynis --version 2>/dev/null | head -1', $versionOutput); $this->lynisVersion = trim($versionOutput[0] ?? 'Unknown'); } exec('which wpscan 2>/dev/null', $output2, $code2); $this->wpscanInstalled = $code2 === 0; if ($this->wpscanInstalled) { exec("wpscan --version 2>/dev/null | grep -i 'version' | tail -1", $versionOutput2); $this->wpscanVersion = trim($versionOutput2[0] ?? 'Unknown'); } exec('which nikto 2>/dev/null', $output3, $code3); $this->niktoInstalled = $code3 === 0; if ($this->niktoInstalled) { exec('nikto -Version 2>/dev/null | grep -i version | head -1', $versionOutput3); $this->niktoVersion = trim($versionOutput3[0] ?? 'Unknown'); } } protected function loadLastScans(): void { $scanDir = storage_path('app/security-scans'); if (file_exists("$scanDir/lynis-latest.json")) { $this->lastLynisScan = date('Y-m-d H:i:s', filemtime("$scanDir/lynis-latest.json")); $this->lynisResults = json_decode(file_get_contents("$scanDir/lynis-latest.json"), true) ?? []; } if (file_exists("$scanDir/wpscan-latest.json")) { $this->lastWpscanScan = date('Y-m-d H:i:s', filemtime("$scanDir/wpscan-latest.json")); $this->wpscanResults = json_decode(file_get_contents("$scanDir/wpscan-latest.json"), true) ?? []; } if (file_exists("$scanDir/nikto-latest.json")) { $this->lastNiktoScan = date('Y-m-d H:i:s', filemtime("$scanDir/nikto-latest.json")); $this->niktoResults = json_decode(file_get_contents("$scanDir/nikto-latest.json"), true) ?? []; } } public function installLynis(): void { Notification::make()->title(__('Installing Lynis...'))->info()->send(); exec('apt-get update && apt-get install -y lynis 2>&1', $output, $code); if ($code === 0) { Notification::make()->title(__('Lynis installed successfully'))->success()->send(); } else { Notification::make()->title(__('Installation failed'))->body(implode("\n", array_slice($output, -5)))->danger()->send(); } $this->checkScannerToolStatus(); } public function installWpscan(): void { Notification::make()->title(__('Installing WPScan...'))->body(__('This may take a few minutes.'))->info()->send(); exec('which ruby 2>/dev/null', $rubyCheck, $rubyCode); if ($rubyCode !== 0) { exec('apt-get update && apt-get install -y ruby ruby-dev build-essential libcurl4-openssl-dev libxml2 libxml2-dev libxslt1-dev 2>&1', $output, $code); } exec('gem install wpscan 2>&1', $output, $code); if ($code === 0) { exec('wpscan --update 2>&1'); Notification::make()->title(__('WPScan installed successfully'))->success()->send(); } else { Notification::make()->title(__('Installation failed'))->body(implode("\n", array_slice($output, -5)))->danger()->send(); } $this->checkScannerToolStatus(); } public function installNikto(): void { Notification::make()->title(__('Installing Nikto...'))->info()->send(); exec('apt-get update && apt-get install -y nikto 2>&1', $output, $code); if ($code === 0) { Notification::make()->title(__('Nikto installed successfully'))->success()->send(); } else { Notification::make()->title(__('Installation failed'))->body(implode("\n", array_slice($output, -5)))->danger()->send(); } $this->checkScannerToolStatus(); } public function runLynisScan(): void { if (! $this->lynisInstalled) { Notification::make()->title(__('Lynis not installed'))->danger()->send(); return; } $this->isScanning = true; $this->currentScan = 'lynis'; $this->scanOutput = __('Running Lynis system audit...')."\n"; $scanDir = storage_path('app/security-scans'); if (! is_dir($scanDir)) { mkdir($scanDir, 0755, true); } exec('lynis audit system --no-colors --quick 2>&1', $output, $code); $this->scanOutput = implode("\n", $output); $results = $this->parseLynisOutput($output); $results['scan_time'] = date('Y-m-d H:i:s'); $results['raw_output'] = $this->scanOutput; file_put_contents("$scanDir/lynis-latest.json", json_encode($results, JSON_PRETTY_PRINT)); $this->lynisResults = $results; $this->lastLynisScan = $results['scan_time']; $this->isScanning = false; $this->currentScan = ''; $warningCount = count($results['warnings'] ?? []); $suggestionCount = count($results['suggestions'] ?? []); Notification::make() ->title(__('Lynis scan completed')) ->body(__('Found :warnings warnings and :suggestions suggestions', ['warnings' => $warningCount, 'suggestions' => $suggestionCount])) ->success() ->send(); } protected function parseLynisOutput(array $output): array { $results = [ 'hardening_index' => 0, 'warnings' => [], 'suggestions' => [], 'tests_performed' => 0, ]; $fullOutput = implode("\n", $output); if (preg_match('/Hardening index\s*:\s*(\d+)/i', $fullOutput, $matches)) { $results['hardening_index'] = (int) $matches[1]; } if (preg_match('/Tests performed\s*:\s*(\d+)/i', $fullOutput, $matches)) { $results['tests_performed'] = (int) $matches[1]; } preg_match_all('/\[WARNING\]\s*(.+)$/m', $fullOutput, $warningMatches); $results['warnings'] = $warningMatches[1] ?? []; preg_match_all('/\[SUGGESTION\]\s*(.+)$/m', $fullOutput, $suggestionMatches); $results['suggestions'] = $suggestionMatches[1] ?? []; return $results; } public function getLocalWordPressSites(): array { $sites = []; try { $users = \App\Models\User::where('is_admin', false)->get(); foreach ($users as $user) { $result = $this->getAgent()->wpList($user->username); $userSites = $result['sites'] ?? []; foreach ($userSites as $site) { $siteId = $site['domain'].($site['path'] ?? ''); $sites[$siteId] = $site['domain'].($site['path'] !== '/' ? ($site['path'] ?? '') : ''); } } } catch (Exception $e) { // Return empty array on error } return $sites; } public function runWpscanOnSite(): void { if (! $this->wpscanInstalled) { Notification::make()->title(__('WPScan not installed'))->danger()->send(); return; } if (! $this->selectedWpSiteId) { Notification::make()->title(__('Please select a WordPress site'))->danger()->send(); return; } $url = 'https://'.$this->selectedWpSiteId; $this->isScanning = true; $this->currentScan = 'wpscan'; $this->scanOutput = __('Scanning WordPress site: :url', ['url' => $url])."\n"; $scanDir = storage_path('app/security-scans'); if (! is_dir($scanDir)) { mkdir($scanDir, 0755, true); } // Set HOME to writable directory for wpscan cache $wpscanCmd = 'HOME=/var/www wpscan --url '.escapeshellarg($url).' --format json --no-banner 2>&1'; exec($wpscanCmd, $output, $code); $jsonOutput = implode("\n", $output); $this->scanOutput = $jsonOutput; $results = json_decode($jsonOutput, true); if (! $results) { $results = [ 'error' => __('Failed to parse scan results'), 'raw_output' => $jsonOutput, ]; } $results['scan_time'] = date('Y-m-d H:i:s'); $results['target_url'] = $url; file_put_contents("$scanDir/wpscan-latest.json", json_encode($results, JSON_PRETTY_PRINT)); $this->wpscanResults = $results; $this->lastWpscanScan = $results['scan_time']; $this->isScanning = false; $this->currentScan = ''; Notification::make() ->title(__('WPScan completed')) ->body(__('Scan finished for :url', ['url' => $url])) ->success() ->send(); } public function runNiktoScan(): void { if (! $this->niktoInstalled) { Notification::make()->title(__('Nikto not installed'))->danger()->send(); return; } $target = 'localhost'; $this->isScanning = true; $this->currentScan = 'nikto'; $this->scanOutput = __('Scanning local web server...')."\n"; $scanDir = storage_path('app/security-scans'); if (! is_dir($scanDir)) { mkdir($scanDir, 0755, true); } $jsonFile = "$scanDir/nikto-".date('Y-m-d-His').'.json'; // Use full path for nikto since timeout command has restricted PATH $niktoPath = file_exists('/usr/bin/nikto') ? '/usr/bin/nikto' : '/usr/local/bin/nikto'; exec("timeout 300 {$niktoPath} -h localhost -Format json -output {$jsonFile} 2>&1", $output, $code); $this->scanOutput = implode("\n", $output); $results = []; if (file_exists($jsonFile)) { $results = json_decode(file_get_contents($jsonFile), true) ?? []; } if (empty($results)) { $results = $this->parseNiktoTextOutput($output); } $results['scan_time'] = date('Y-m-d H:i:s'); $results['target'] = $target; $results['raw_output'] = $this->scanOutput; file_put_contents("$scanDir/nikto-latest.json", json_encode($results, JSON_PRETTY_PRINT)); $this->niktoResults = $results; $this->lastNiktoScan = $results['scan_time']; $this->isScanning = false; $this->currentScan = ''; Notification::make() ->title(__('Nikto scan completed')) ->body(__('Local web server scan finished')) ->success() ->send(); } protected function parseNiktoTextOutput(array $output): array { $results = [ 'vulnerabilities' => [], 'info' => [], ]; foreach ($output as $line) { if (preg_match('/^\+\s*OSVDB-\d+:\s*(.+)/', $line, $matches)) { $results['vulnerabilities'][] = trim($matches[1]); } elseif (preg_match('/^\+\s*(.+)/', $line, $matches)) { $results['info'][] = trim($matches[1]); } } return $results; } // ClamAV on-demand scan methods public function getClamScanUsers(): array { $users = []; try { $systemUsers = \App\Models\User::where('is_admin', false)->get(); foreach ($systemUsers as $user) { $users[$user->username] = $user->username.' ('.$user->email.')'; } } catch (Exception $e) { // Return empty array on error } return $users; } public function runClamScanUser(): void { if (! $this->clamavInstalled) { Notification::make()->title(__('ClamAV not installed'))->danger()->send(); return; } if (! $this->selectedClamUser) { Notification::make()->title(__('Please select a user'))->danger()->send(); return; } $this->isScanning = true; $this->currentScan = 'clamav'; $this->scanOutput = __('Scanning user directory: /home/:user', ['user' => $this->selectedClamUser])."\n"; $scanDir = storage_path('app/security-scans'); if (! is_dir($scanDir)) { mkdir($scanDir, 0755, true); } $userDir = "/home/{$this->selectedClamUser}"; $logFile = "$scanDir/clamscan-{$this->selectedClamUser}-".date('Y-m-d-His').'.log'; $cmd = "clamscan -r --infected --log={$logFile} ". "--exclude-dir='^/home/{$this->selectedClamUser}/\\.cache' ". "--exclude-dir='^/home/{$this->selectedClamUser}/\\.local' ". escapeshellarg($userDir).' 2>&1'; exec($cmd, $output, $code); $this->scanOutput = implode("\n", $output); $results = $this->parseClamScanOutput($output); $results['scan_time'] = date('Y-m-d H:i:s'); $results['scan_type'] = 'user'; $results['target'] = $userDir; $results['username'] = $this->selectedClamUser; $results['raw_output'] = $this->scanOutput; file_put_contents("$scanDir/clamscan-latest.json", json_encode($results, JSON_PRETTY_PRINT)); $this->clamScanResults = $results; $this->lastClamScan = $results['scan_time']; $this->isScanning = false; $this->currentScan = ''; $infected = $results['infected_files'] ?? 0; $message = $infected > 0 ? __('Found :count infected file(s)', ['count' => $infected]) : __('No threats detected'); Notification::make() ->title(__('ClamAV scan completed')) ->body($message) ->color($infected > 0 ? 'danger' : 'success') ->send(); } public function runClamScanServer(): void { if (! $this->clamavInstalled) { Notification::make()->title(__('ClamAV not installed'))->danger()->send(); return; } $this->isScanning = true; $this->currentScan = 'clamav'; $this->scanOutput = __('Scanning server-wide: /home')."\n". __('This may take a while...')."\n"; $scanDir = storage_path('app/security-scans'); if (! is_dir($scanDir)) { mkdir($scanDir, 0755, true); } $logFile = "$scanDir/clamscan-server-".date('Y-m-d-His').'.log'; $cmd = "clamscan -r --infected --log={$logFile} ". "--exclude-dir='^\\.cache' ". "--exclude-dir='^\\.local' ". '/home 2>&1'; exec($cmd, $output, $code); $this->scanOutput = implode("\n", $output); $results = $this->parseClamScanOutput($output); $results['scan_time'] = date('Y-m-d H:i:s'); $results['scan_type'] = 'server'; $results['target'] = '/home'; $results['raw_output'] = $this->scanOutput; file_put_contents("$scanDir/clamscan-latest.json", json_encode($results, JSON_PRETTY_PRINT)); $this->clamScanResults = $results; $this->lastClamScan = $results['scan_time']; $this->isScanning = false; $this->currentScan = ''; $infected = $results['infected_files'] ?? 0; $message = $infected > 0 ? __('Found :count infected file(s)', ['count' => $infected]) : __('No threats detected'); Notification::make() ->title(__('Server-wide scan completed')) ->body($message) ->color($infected > 0 ? 'danger' : 'success') ->send(); } protected function parseClamScanOutput(array $output): array { $results = [ 'scanned_files' => 0, 'infected_files' => 0, 'threats' => [], ]; $fullOutput = implode("\n", $output); if (preg_match('/Scanned files:\s*(\d+)/i', $fullOutput, $matches)) { $results['scanned_files'] = (int) $matches[1]; } if (preg_match('/Infected files:\s*(\d+)/i', $fullOutput, $matches)) { $results['infected_files'] = (int) $matches[1]; } foreach ($output as $line) { if (preg_match('/^(.+?):\s*(.+?)\s*FOUND$/i', $line, $matches)) { $results['threats'][] = [ 'file' => trim($matches[1]), 'threat' => trim($matches[2]), ]; } } return $results; } protected function loadClamScanResults(): void { $scanDir = storage_path('app/security-scans'); if (file_exists("$scanDir/clamscan-latest.json")) { $this->lastClamScan = date('Y-m-d H:i:s', filemtime("$scanDir/clamscan-latest.json")); $this->clamScanResults = json_decode(file_get_contents("$scanDir/clamscan-latest.json"), true) ?? []; } } // Dynamic component builders for pure Filament UI protected function buildClamavScanResultsSchema(): array { if (empty($this->clamScanResults)) { return [Text::make(__('No scan results available'))]; } $scannedFiles = $this->clamScanResults['scanned_files'] ?? 0; $infectedFiles = $this->clamScanResults['infected_files'] ?? 0; $scanType = $this->clamScanResults['scan_type'] ?? 'unknown'; $target = $scanType === 'user' ? ($this->clamScanResults['username'] ?? '-') : __('Server'); $threats = $this->clamScanResults['threats'] ?? []; $components = [ Grid::make(['default' => 1, 'md' => 3]) ->schema([ Section::make((string) $scannedFiles) ->description(__('Files Scanned')) ->icon('heroicon-o-document-magnifying-glass') ->iconColor('primary'), Section::make((string) $infectedFiles) ->description(__('Infected Files')) ->icon('heroicon-o-exclamation-triangle') ->iconColor($infectedFiles > 0 ? 'danger' : 'success'), Section::make($target) ->description(__('Scan Target')) ->icon('heroicon-o-folder') ->iconColor('gray'), ]), ]; if (! empty($threats)) { $threatComponents = []; foreach ($threats as $threat) { $threatComponents[] = Text::make($threat['threat'].': '.basename($threat['file'])); } $components[] = Section::make(__('Threats Detected').' ('.count($threats).')') ->icon('heroicon-o-exclamation-triangle') ->iconColor('danger') ->schema($threatComponents); } else { $components[] = Section::make(__('No threats detected')) ->icon('heroicon-o-check-circle') ->iconColor('success') ->description(__('The scanned directory appears to be clean.')); } return $components; } protected function buildSshCurrentConfigSchema(): array { return [ Section::make($this->sshPasswordAuth ? __('Enabled') : __('Disabled')) ->description(__('Password Authentication')) ->icon('heroicon-o-key') ->iconColor($this->sshPasswordAuth ? 'warning' : 'success') ->aside(), Section::make($this->sshPubkeyAuth ? __('Enabled') : __('Disabled')) ->description(__('Public Key Authentication')) ->icon('heroicon-o-finger-print') ->iconColor($this->sshPubkeyAuth ? 'success' : 'danger') ->aside(), Section::make((string) $this->sshPort) ->description(__('SSH Port')) ->icon('heroicon-o-server') ->iconColor('gray') ->aside(), ]; } protected function buildSshRecommendationsSchema(): array { $keysOnly = ! $this->sshPasswordAuth && $this->sshPubkeyAuth; return [ Section::make(__('Disable password authentication')) ->description(__('Use SSH keys only for better security')) ->icon($keysOnly ? 'heroicon-o-check-circle' : 'heroicon-o-exclamation-triangle') ->iconColor($keysOnly ? 'success' : 'warning') ->aside(), Section::make(__('Enable Fail2ban protection')) ->description(__('Automatically block brute-force attempts')) ->icon($this->fail2banRunning ? 'heroicon-o-check-circle' : 'heroicon-o-exclamation-triangle') ->iconColor($this->fail2banRunning ? 'success' : 'warning') ->aside(), ]; } protected function buildClamScanOutputSchema(): array { $output = $this->clamScanResults['raw_output'] ?? ''; if ($this->isScanning && $this->currentScan === 'clamav') { $output = $this->scanOutput; } if (! $output) { return []; } return [ Text::make($output), ]; } protected function buildScanOutputSchema(): array { if (! $this->scanOutput) { return []; } return [ Text::make($this->scanOutput), ]; } }