diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php b/extensions/Servers/Proxmox/Livewire/Firewall.php index f234588d8..05dae636c 100644 --- a/extensions/Servers/Proxmox/Livewire/Firewall.php +++ b/extensions/Servers/Proxmox/Livewire/Firewall.php @@ -6,6 +6,7 @@ use Illuminate\Http\Client\RequestException; use Illuminate\Support\Facades\Http; use Paymenter\Extensions\Servers\Proxmox\Models\Server; use Paymenter\Extensions\Servers\Proxmox\Proxmox; +use Carbon\Carbon; class Firewall extends Component { @@ -803,8 +804,8 @@ class Firewall extends Component 'get', ['limit' => $this->logLimit] ); - - if (!$response->successful()) { + + if (! $response->successful()) { $errorData = []; try { $errorData = $response->json(); @@ -817,45 +818,60 @@ class Firewall extends Component 'status' => $response->status(), 'body' => substr($response->body(), 0, 500), 'server_id' => $this->server->id, - 'vm_id' => $this->server->vm_id + 'vm_id' => $this->server->vm_id, ]); $this->firewallLogs = []; return; } - + // Proxmox firewall log endpoint returns text/plain format // Each log entry is on a new line $body = $response->body(); $rawLogs = []; - - if (!empty($body)) { + + if (! empty($body)) { // Split by newlines and filter empty lines $lines = explode("\n", trim($body)); - $rawLogs = array_filter($lines, function($line) { - return !empty(trim($line)); + $rawLogs = array_filter($lines, static function ($line) { + return ! empty(trim($line)); }); } - + \Log::debug('Firewall logs fetched from Proxmox API', [ 'raw_count' => count($rawLogs), 'body_length' => strlen($body), 'content_type' => $response->header('Content-Type'), - 'sample' => !empty($rawLogs) ? substr($rawLogs[0], 0, 200) : null + 'sample' => ! empty($rawLogs) ? substr($rawLogs[array_key_first($rawLogs)], 0, 200) : null, ]); - + $this->firewallLogs = []; - - // Store raw log entries exactly as received from Proxmox (no parsing/sanitization) + foreach ($rawLogs as $logEntry) { + $logText = ''; + if (is_string($logEntry)) { $logText = trim($logEntry); - if (!empty($logText)) { - $this->firewallLogs[] = ['raw' => $logText]; - } + } elseif (is_array($logEntry)) { + $logText = trim( + $logEntry['raw'] + ?? $logEntry['n'] + ?? $logEntry['t'] + ?? $logEntry['text'] + ?? $logEntry['log'] + ?? $logEntry['message'] + ?? $logEntry['line'] + ?? json_encode($logEntry) + ); } + + if ($logText === '') { + continue; + } + + $this->firewallLogs[] = $this->parseFirewallLog($logText); } - - if (empty($this->firewallLogs) && !empty($rawLogs)) { + + if (empty($this->firewallLogs) && ! empty($rawLogs)) { $this->logError = 'Logs received but could not be processed.'; } elseif (empty($this->firewallLogs)) { $this->logError = 'No firewall logs available. Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options).'; @@ -877,80 +893,112 @@ class Firewall extends Component 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'server_id' => $this->server->id, - 'vm_id' => $this->server->vm_id + 'vm_id' => $this->server->vm_id, ]); $this->firewallLogs = []; } } - protected function parseFirewallLog(string $logText): ?array + protected function parseFirewallLog(string $logText): array { - // Example log: 202500009 2 tap202500009i0-IN 01/Dec/2025:09:00:39 -1000 REJECT: IN=fwbr202500009i0 OUT=fwbr202500009i0 PHYSIN=fwln202500009i0 PHYSOUT=tap202500009i0 MAC=8e:ef:c1:86:d4:20:58:9c:fc:10:ff:d3:08:00 SRC=24.210.66.222 DST=45.154.99.21 LEN=52 TOS=0x00 PREC=0x00 TTL=115 ID=58510 DF PROTO=TCP SPT=50692 DPT=22 SEQ=2584199599 ACK=0 WINDOW=65535 SYN - - // Extract SRC, DST, TTL, PROTO, SPT, DPT, ACK $result = [ - 'source' => null, - 'destination' => null, - 'ttl' => null, - 'proto' => null, - 'spt' => null, - 'dpt' => null, - 'ack' => null, + 'raw' => $logText, + 'timestamp_display' => null, + 'timestamp_iso' => null, 'action' => null, - 'timestamp' => null, + 'action_badge_class' => 'bg-base/20 text-base/70', + 'level_numeric' => null, + 'level_label' => null, + 'level_badge_class' => 'bg-base/20 text-base/70', + 'protocol' => null, + 'source' => null, + 'source_port' => null, + 'destination' => null, + 'destination_port' => null, + 'iface' => null, ]; - - // Extract action (REJECT, ACCEPT, DROP) - usually before the colon - if (preg_match('/(REJECT|ACCEPT|DROP):/i', $logText, $matches)) { - $result['action'] = strtoupper($matches[1]); + + // Extract level and interface (first tokens) + if (preg_match('/^\S+\s+(\d+)\s+([^\s]+)/', $logText, $matches)) { + $levelNumeric = (int) $matches[1]; + $iface = $matches[2]; + $priMap = [ + 0 => 'emerg', + 1 => 'alert', + 2 => 'crit', + 3 => 'err', + 4 => 'warning', + 5 => 'notice', + 6 => 'info', + 7 => 'debug', + ]; + $levelLabel = $priMap[$levelNumeric] ?? 'info'; + $levelBadgeMap = [ + 'emerg' => 'bg-error text-white', + 'alert' => 'bg-error/80 text-white', + 'crit' => 'bg-error/60 text-white', + 'err' => 'bg-error/20 text-error', + 'warning' => 'bg-warning/20 text-warning', + 'notice' => 'bg-primary/20 text-primary', + 'info' => 'bg-primary/20 text-primary', + 'debug' => 'bg-base/20 text-base/70', + ]; + + $result['level_numeric'] = $levelNumeric; + $result['level_label'] = strtoupper($levelLabel); + $result['level_badge_class'] = $levelBadgeMap[$levelLabel] ?? 'bg-base/20 text-base/70'; + $result['iface'] = $iface; } - - // Extract timestamp - look for date pattern like "01/Dec/2025:09:00:39" - if (preg_match('/(\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2})/', $logText, $matches)) { - $result['timestamp'] = $matches[1]; + + // Extract timestamp and timezone + if (preg_match('/(\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2})\s+([+-]\d{4})/', $logText, $matches)) { + $timestamp = $matches[1]; + $tz = $matches[2]; + $tzNormalized = substr($tz, 0, 3) . ':' . substr($tz, 3); + try { + $carbon = Carbon::createFromFormat('d/M/Y:H:i:s P', $timestamp . ' ' . $tzNormalized); + $result['timestamp_iso'] = $carbon->toIso8601String(); + $result['timestamp_display'] = $carbon->format('Y-m-d H:i:s T'); + } catch (\Exception $e) { + $result['timestamp_display'] = str_replace('/', '-', $timestamp) . ' ' . $tzNormalized; + } } - - // Extract SRC + + // Extract action (REJECT, ACCEPT, DROP) + if (preg_match('/\b(REJECT|ACCEPT|DROP):/i', $logText, $matches)) { + $action = strtoupper($matches[1]); + $actionBadgeMap = [ + 'ACCEPT' => 'bg-success/20 text-success', + 'DROP' => 'bg-error/20 text-error', + 'REJECT' => 'bg-warning/20 text-warning', + ]; + $result['action'] = $action; + $result['action_badge_class'] = $actionBadgeMap[$action] ?? 'bg-base/20 text-base/70'; + } + + // Extract protocol + if (preg_match('/PROTO=([^\s]+)/', $logText, $matches)) { + $result['protocol'] = strtoupper($matches[1]); + } + + // Extract source/destination IPs and ports if (preg_match('/SRC=([^\s]+)/', $logText, $matches)) { $result['source'] = $matches[1]; } - - // Extract DST + if (preg_match('/DST=([^\s]+)/', $logText, $matches)) { $result['destination'] = $matches[1]; } - - // Extract TTL - if (preg_match('/TTL=(\d+)/', $logText, $matches)) { - $result['ttl'] = (int) $matches[1]; - } - - // Extract PROTO - if (preg_match('/PROTO=(\w+)/', $logText, $matches)) { - $result['proto'] = strtoupper($matches[1]); - } - - // Extract SPT (Source Port) + if (preg_match('/SPT=(\d+)/', $logText, $matches)) { - $result['spt'] = (int) $matches[1]; + $result['source_port'] = $matches[1]; } - - // Extract DPT (Destination Port) + if (preg_match('/DPT=(\d+)/', $logText, $matches)) { - $result['dpt'] = (int) $matches[1]; + $result['destination_port'] = $matches[1]; } - - // Extract ACK - if (preg_match('/ACK=(\d+)/', $logText, $matches)) { - $result['ack'] = (int) $matches[1]; - } - - // Only return if we have at least some data - if ($result['source'] || $result['destination'] || $result['proto']) { - return $result; - } - - return null; + + return $result; } public function render() diff --git a/extensions/Servers/Proxmox/views/components/layout.blade.php b/extensions/Servers/Proxmox/views/components/layout.blade.php index cb1c1ff4c..335d3b4e3 100644 --- a/extensions/Servers/Proxmox/views/components/layout.blade.php +++ b/extensions/Servers/Proxmox/views/components/layout.blade.php @@ -225,6 +225,17 @@ } @endforeach diff --git a/extensions/Servers/Proxmox/views/firewall.blade.php b/extensions/Servers/Proxmox/views/firewall.blade.php index 9badcd5d8..b3a7bda11 100644 --- a/extensions/Servers/Proxmox/views/firewall.blade.php +++ b/extensions/Servers/Proxmox/views/firewall.blade.php @@ -319,13 +319,158 @@ @endif @if(count($firewallLogs) > 0) -
-
- @foreach($firewallLogs as $log) -
- {{ $log['raw'] ?? '' }} -
- @endforeach +
+
+
+ + +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+
+ +
+ +

No logs matched your search.

@elseif(empty($logError))