feat: integrate network analysis feature in firewall view with detailed connection data display
This commit is contained in:
@@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Http;
|
|||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Paymenter\Extensions\Servers\Proxmox\Models\Server;
|
use Paymenter\Extensions\Servers\Proxmox\Models\Server;
|
||||||
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
|
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
|
||||||
|
use Paymenter\Extensions\Servers\Proxmox\Services\CoreRouterClient;
|
||||||
|
|
||||||
class Firewall extends Component
|
class Firewall extends Component
|
||||||
{
|
{
|
||||||
@@ -1566,6 +1567,165 @@ class Firewall extends Component
|
|||||||
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
|
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== NETWORK ANALYSIS INTEGRATION ====================
|
||||||
|
|
||||||
|
public function getNetworkAnalysisForLog(
|
||||||
|
string $sourceIP,
|
||||||
|
string $destIP,
|
||||||
|
string $sourcePort,
|
||||||
|
string $destPort,
|
||||||
|
string $protocol
|
||||||
|
): array {
|
||||||
|
try {
|
||||||
|
// Check if Core Router is configured
|
||||||
|
$coreRouterEnabled = \Illuminate\Support\Facades\Config::get('corerouter.enabled', false);
|
||||||
|
if (!$coreRouterEnabled) {
|
||||||
|
return [
|
||||||
|
'connections' => [],
|
||||||
|
'error' => 'Core Router is not configured'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get network analysis from Statistics component logic
|
||||||
|
$client = \Illuminate\Support\Facades\App::make(CoreRouterClient::class);
|
||||||
|
|
||||||
|
// Fetch all firewall states
|
||||||
|
$allStates = $client->listStates();
|
||||||
|
|
||||||
|
// Filter states matching the log entry
|
||||||
|
$matchingConnections = [];
|
||||||
|
|
||||||
|
foreach ($allStates as $state) {
|
||||||
|
$rawSource = (string)($state['source'] ?? '');
|
||||||
|
$rawDestination = (string)($state['destination'] ?? '');
|
||||||
|
|
||||||
|
// Extract IPs from the state
|
||||||
|
$stateSourceIP = $this->extractIpFromField($rawSource);
|
||||||
|
$stateDestIP = $this->extractIpFromField($rawDestination);
|
||||||
|
|
||||||
|
// Extract ports
|
||||||
|
$stateSourcePort = null;
|
||||||
|
$stateDestPort = null;
|
||||||
|
|
||||||
|
if (preg_match('/\[(\d+)\]$/', $rawSource, $matches)) {
|
||||||
|
$stateSourcePort = (int)$matches[1];
|
||||||
|
} elseif (preg_match('/:(\d+)$/', $rawSource, $matches) && filter_var(explode(':', $rawSource)[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
$stateSourcePort = (int)$matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/\[(\d+)\]$/', $rawDestination, $matches)) {
|
||||||
|
$stateDestPort = (int)$matches[1];
|
||||||
|
} elseif (preg_match('/:(\d+)$/', $rawDestination, $matches) && filter_var(explode(':', $rawDestination)[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
$stateDestPort = (int)$matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stateProtocol = strtoupper($state['protocol'] ?? '');
|
||||||
|
|
||||||
|
// Check if this state matches the log entry
|
||||||
|
// Match can be source->dest or dest->source (bidirectional)
|
||||||
|
$sourceMatch = (
|
||||||
|
strtolower(trim($stateSourceIP)) === strtolower(trim($sourceIP)) &&
|
||||||
|
strtolower(trim($stateDestIP)) === strtolower(trim($destIP))
|
||||||
|
);
|
||||||
|
|
||||||
|
$reverseMatch = (
|
||||||
|
strtolower(trim($stateSourceIP)) === strtolower(trim($destIP)) &&
|
||||||
|
strtolower(trim($stateDestIP)) === strtolower(trim($sourceIP))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check protocol match
|
||||||
|
$protocolMatch = empty($protocol) ||
|
||||||
|
strtoupper(trim($protocol)) === $stateProtocol;
|
||||||
|
|
||||||
|
// Check port match (if specified in log)
|
||||||
|
$portMatch = true;
|
||||||
|
if (!empty($destPort) && $destPort !== '-') {
|
||||||
|
$portMatch = ($stateDestPort == $destPort) || ($stateSourcePort == $destPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($sourceMatch || $reverseMatch) && $protocolMatch && $portMatch) {
|
||||||
|
$bytes = (int)($state['bytes_total'] ?? $state['bytes'] ?? 0);
|
||||||
|
$packets = (int)($state['packets_total'] ?? $state['packets'] ?? 0);
|
||||||
|
$stateName = strtoupper($state['state'] ?? 'UNKNOWN');
|
||||||
|
$interface = strtoupper($state['interface'] ?? 'N/A');
|
||||||
|
$direction = strtolower($state['direction'] ?? '') === 'in' ? 'Inbound' : 'Outbound';
|
||||||
|
|
||||||
|
$matchingConnections[] = [
|
||||||
|
'protocol' => $stateProtocol,
|
||||||
|
'source' => $stateSourceIP,
|
||||||
|
'destination' => $stateDestIP,
|
||||||
|
'source_port' => $stateSourcePort,
|
||||||
|
'destination_port' => $stateDestPort,
|
||||||
|
'raw_source' => $rawSource,
|
||||||
|
'raw_destination' => $rawDestination,
|
||||||
|
'bytes' => $bytes,
|
||||||
|
'bytes_human' => $this->formatBytes($bytes),
|
||||||
|
'packets' => $packets,
|
||||||
|
'state' => $stateName,
|
||||||
|
'interface' => $interface,
|
||||||
|
'direction' => $direction,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by bytes (highest first)
|
||||||
|
usort($matchingConnections, fn($a, $b) => $b['bytes'] <=> $a['bytes']);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'connections' => $matchingConnections,
|
||||||
|
'error' => null
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Failed to fetch network analysis for log entry', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'source' => $sourceIP,
|
||||||
|
'dest' => $destIP
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'connections' => [],
|
||||||
|
'error' => 'Failed to fetch network data: ' . $e->getMessage()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function extractIpFromField(string $field): string
|
||||||
|
{
|
||||||
|
if (empty($field)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$field = trim($field);
|
||||||
|
|
||||||
|
// Handle IPv4 "IP:port" format FIRST
|
||||||
|
if (preg_match('/^(\d+\.\d+\.\d+\.\d+):(\d+)$/', $field, $matches)) {
|
||||||
|
return $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle "IP[port]" format
|
||||||
|
if (preg_match('/^([^\[\]]+)(?:\[[^\]]+\])?$/', $field, $matches)) {
|
||||||
|
$ip = trim($matches[1]);
|
||||||
|
$ip = rtrim($ip, ':[]');
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formatBytes(int $bytes): string
|
||||||
|
{
|
||||||
|
if ($bytes === 0) {
|
||||||
|
return '0 B';
|
||||||
|
}
|
||||||
|
|
||||||
|
$units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||||
|
$power = floor(log($bytes, 1024));
|
||||||
|
$power = min($power, count($units) - 1);
|
||||||
|
|
||||||
|
return round($bytes / pow(1024, $power), 2) . ' ' . $units[$power];
|
||||||
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('proxmox::firewall')->layoutData([
|
return view('proxmox::firewall')->layoutData([
|
||||||
|
|||||||
@@ -844,6 +844,7 @@
|
|||||||
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Destination</th>
|
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Destination</th>
|
||||||
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Protocol</th>
|
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Protocol</th>
|
||||||
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Port</th>
|
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Port</th>
|
||||||
|
<th class="text-center py-3 px-4 text-xs font-semibold text-base/70 uppercase">Analysis</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -1017,6 +1018,211 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="py-3 px-4 text-sm">{{ $log['protocol'] ?? '-' }}</td>
|
<td class="py-3 px-4 text-sm">{{ $log['protocol'] ?? '-' }}</td>
|
||||||
<td class="py-3 px-4 text-xs font-mono">{{ $log['destination_port'] ?? '-' }}</td>
|
<td class="py-3 px-4 text-xs font-mono">{{ $log['destination_port'] ?? '-' }}</td>
|
||||||
|
<td class="py-3 px-4 text-center">
|
||||||
|
@php
|
||||||
|
$sourceIP = $log['source'] ?? '';
|
||||||
|
$destIP = $log['destination'] ?? '';
|
||||||
|
$sourcePort = $log['source_port'] ?? '';
|
||||||
|
$destPort = $log['destination_port'] ?? '';
|
||||||
|
$protocol = $log['protocol'] ?? '';
|
||||||
|
@endphp
|
||||||
|
<div class="relative"
|
||||||
|
x-data="{
|
||||||
|
open: false,
|
||||||
|
loading: false,
|
||||||
|
connectionData: null,
|
||||||
|
error: null,
|
||||||
|
loadConnectionData() {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
$wire.getNetworkAnalysisForLog('{{ $sourceIP }}', '{{ $destIP }}', '{{ $sourcePort }}', '{{ $destPort }}', '{{ $protocol }}')
|
||||||
|
.then(data => {
|
||||||
|
this.connectionData = data;
|
||||||
|
this.loading = false;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.error = 'Failed to load connection data';
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
x-cloak>
|
||||||
|
<button type="button"
|
||||||
|
@click="open = true; if (!connectionData && !loading) { loadConnectionData(); }"
|
||||||
|
class="text-primary hover:text-primary/80 transition-colors p-1 rounded hover:bg-primary/10"
|
||||||
|
title="View Network Analysis">
|
||||||
|
<x-ri-line-chart-line class="size-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{-- Network Analysis Modal --}}
|
||||||
|
<div x-show="open"
|
||||||
|
x-transition:enter="transition ease-out duration-200"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="transition ease-in duration-150"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
@click="open = false"
|
||||||
|
@keydown.escape.window="open = false"
|
||||||
|
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
|
||||||
|
style="display: none;">
|
||||||
|
<div @click.stop
|
||||||
|
class="relative w-full max-w-5xl bg-gradient-to-br from-background via-background-secondary to-background rounded-2xl border border-primary/20 shadow-2xl max-h-[90vh] overflow-hidden">
|
||||||
|
<button @click="open = false"
|
||||||
|
class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors z-10 bg-background/80 backdrop-blur-sm rounded-full p-2 hover:bg-background">
|
||||||
|
<x-ri-close-line class="size-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{-- Modal Header --}}
|
||||||
|
<div class="bg-gradient-to-r from-primary/10 to-secondary/10 border-b border-primary/20 p-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-12 h-12 rounded-xl bg-primary/20 flex items-center justify-center">
|
||||||
|
<x-ri-line-chart-line class="size-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-base">Network Connection Analysis</h3>
|
||||||
|
<p class="text-sm text-base/60 mt-1">Detailed connection information from firewall state</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Modal Content --}}
|
||||||
|
<div class="p-6 overflow-y-auto" style="max-height: calc(90vh - 180px);">
|
||||||
|
{{-- Loading State --}}
|
||||||
|
<div x-show="loading && !connectionData" class="text-center py-12">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-4">
|
||||||
|
<svg class="animate-spin size-8 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-base/60">Fetching connection data...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Error State --}}
|
||||||
|
<div x-show="error && !loading && !connectionData" class="text-center py-12">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-warning/10 mb-4">
|
||||||
|
<x-ri-error-warning-line class="size-8 text-warning" />
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-warning" x-text="error"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Connection Data Display --}}
|
||||||
|
<div x-show="connectionData && !loading">
|
||||||
|
<template x-if="connectionData && connectionData.connections && connectionData.connections.length > 0">
|
||||||
|
<div>
|
||||||
|
<template x-for="(conn, index) in connectionData.connections" :key="index">
|
||||||
|
<div class="mb-6 last:mb-0 bg-background/50 border border-neutral/40 rounded-xl p-5 hover:border-primary/30 transition-colors">
|
||||||
|
{{-- Connection Header --}}
|
||||||
|
<div class="flex items-center justify-between mb-4 pb-3 border-b border-neutral/20">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="px-3 py-1 rounded-lg text-xs font-semibold bg-primary/20 text-primary border border-primary/30" x-text="'Connection #' + (index + 1)"></span>
|
||||||
|
<span class="px-2 py-1 rounded text-xs font-medium bg-secondary/20 text-secondary" x-text="conn.protocol || 'N/A'"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span :class="{
|
||||||
|
'bg-success/20 text-success border-success/30': conn.direction === 'Inbound',
|
||||||
|
'bg-info/20 text-info border-info/30': conn.direction === 'Outbound'
|
||||||
|
}" class="px-2 py-1 rounded text-xs font-medium border" x-text="conn.direction || 'N/A'"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Connection Details Grid --}}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{{-- Source Information --}}
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h4 class="text-sm font-semibold text-success flex items-center gap-2">
|
||||||
|
<x-ri-arrow-right-line class="size-4" />
|
||||||
|
Source
|
||||||
|
</h4>
|
||||||
|
<div class="pl-6 space-y-2">
|
||||||
|
<div class="flex items-center justify-between py-1.5 px-3 bg-background rounded-lg">
|
||||||
|
<span class="text-xs text-base/60">IP Address</span>
|
||||||
|
<span class="text-sm font-mono font-semibold" x-text="conn.source || 'N/A'"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between py-1.5 px-3 bg-background rounded-lg">
|
||||||
|
<span class="text-xs text-base/60">Port</span>
|
||||||
|
<span class="text-sm font-mono font-semibold" x-text="conn.source_port || 'N/A'"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Destination Information --}}
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h4 class="text-sm font-semibold text-error flex items-center gap-2">
|
||||||
|
<x-ri-arrow-left-line class="size-4" />
|
||||||
|
Destination
|
||||||
|
</h4>
|
||||||
|
<div class="pl-6 space-y-2">
|
||||||
|
<div class="flex items-center justify-between py-1.5 px-3 bg-background rounded-lg">
|
||||||
|
<span class="text-xs text-base/60">IP Address</span>
|
||||||
|
<span class="text-sm font-mono font-semibold" x-text="conn.destination || 'N/A'"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between py-1.5 px-3 bg-background rounded-lg">
|
||||||
|
<span class="text-xs text-base/60">Port</span>
|
||||||
|
<span class="text-sm font-mono font-semibold" x-text="conn.destination_port || 'N/A'"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Connection Statistics --}}
|
||||||
|
<div class="mt-4 pt-4 border-t border-neutral/20">
|
||||||
|
<h4 class="text-sm font-semibold mb-3 flex items-center gap-2">
|
||||||
|
<x-ri-bar-chart-line class="size-4 text-primary" />
|
||||||
|
Traffic Statistics
|
||||||
|
</h4>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
<div class="bg-gradient-to-br from-primary/10 to-primary/5 border border-primary/20 rounded-lg p-3">
|
||||||
|
<p class="text-xs text-base/60 mb-1">Data Transferred</p>
|
||||||
|
<p class="text-lg font-bold text-primary" x-text="conn.bytes_human || '0 B'"></p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gradient-to-br from-secondary/10 to-secondary/5 border border-secondary/20 rounded-lg p-3">
|
||||||
|
<p class="text-xs text-base/60 mb-1">Packets</p>
|
||||||
|
<p class="text-lg font-bold text-secondary" x-text="conn.packets || '0'"></p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gradient-to-br from-info/10 to-info/5 border border-info/20 rounded-lg p-3">
|
||||||
|
<p class="text-xs text-base/60 mb-1">State</p>
|
||||||
|
<p class="text-sm font-bold text-info" x-text="conn.state || 'UNKNOWN'"></p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gradient-to-br from-success/10 to-success/5 border border-success/20 rounded-lg p-3">
|
||||||
|
<p class="text-xs text-base/60 mb-1">Interface</p>
|
||||||
|
<p class="text-sm font-bold text-success" x-text="conn.interface || 'N/A'"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
{{-- No Connections Found --}}
|
||||||
|
<template x-if="connectionData && (!connectionData.connections || connectionData.connections.length === 0)">
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-base/10 mb-4">
|
||||||
|
<x-ri-information-line class="size-8 text-base/60" />
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-base/60">No matching network analysis data found for this log entry.</p>
|
||||||
|
<p class="text-xs text-base/50 mt-2">The connection may have already closed or network analysis data may not be available.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Modal Footer --}}
|
||||||
|
<div class="border-t border-neutral/20 bg-background/50 p-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p class="text-xs text-base/50">Data from Core Router network state tracking</p>
|
||||||
|
<button @click="open = false"
|
||||||
|
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Reference in New Issue
Block a user