Private
Public Access
1
0

feat(proxmox): add smart firewall workflow and UI refresh

Introduce starter firewall rule templates, one-click rule creation from log entries, and quick log filter presets to speed up incident response. Add a shared quick actions hub plus refreshed help and overview surfaces to improve discoverability and make key VPS tasks faster.

Made-with: Cursor
This commit is contained in:
2026-03-28 16:24:12 -04:00
parent 3246a86f91
commit b8677f5b7d
5 changed files with 320 additions and 11 deletions

View File

@@ -14,6 +14,53 @@ use Paymenter\Extensions\Servers\Proxmox\Services\CoreRouterClient;
class Firewall extends Component
{
protected const RULE_TEMPLATES = [
'web-server' => [
'label' => 'Web Server Baseline',
'description' => 'Allow HTTP/HTTPS traffic',
'action' => 'ACCEPT',
'type' => 'in',
'proto' => 'tcp',
'dport' => '80,443',
'comment' => 'Web traffic baseline',
],
'ssh-only' => [
'label' => 'SSH Access',
'description' => 'Allow SSH access on port 22',
'action' => 'ACCEPT',
'type' => 'in',
'proto' => 'tcp',
'dport' => '22',
'comment' => 'SSH access',
],
'mail-server' => [
'label' => 'Mail Server',
'description' => 'Allow SMTP/Submission/IMAP/POP3',
'action' => 'ACCEPT',
'type' => 'in',
'proto' => 'tcp',
'dport' => '25,465,587,110,995,143,993',
'comment' => 'Mail server ports',
],
'dns-server' => [
'label' => 'DNS Server',
'description' => 'Allow DNS queries on UDP/TCP 53',
'action' => 'ACCEPT',
'type' => 'in',
'proto' => 'udp',
'dport' => '53',
'comment' => 'DNS queries',
],
];
protected const LOG_PRESETS = [
'all' => 'All Events',
'blocked' => 'Blocked Events',
'ssh-drops' => 'SSH Drops',
'web-traffic' => 'Web Traffic',
'ipv6' => 'IPv6 Traffic',
];
// Server & Core State
public bool $firewallEnabled = false;
public bool $loading = false;
@@ -258,6 +305,7 @@ class Firewall extends Component
public int $logLimit = 500; // Reduced to prevent 413 errors
public string $logError = '';
public bool $logsLoaded = false; // Track if logs have been loaded
public string $activeLogPreset = 'all';
// Pagination for logs
public int $currentPage = 1;
@@ -614,6 +662,16 @@ class Firewall extends Component
return self::MACRO_DEFINITIONS;
}
public function getRuleTemplatesProperty(): array
{
return self::RULE_TEMPLATES;
}
public function getLogPresetsProperty(): array
{
return self::LOG_PRESETS;
}
/**
* Get details for a specific macro
*/
@@ -655,6 +713,60 @@ class Firewall extends Component
$this->showRuleForm = true;
}
public function applyRuleTemplate(string $template): void
{
$selectedTemplate = self::RULE_TEMPLATES[$template] ?? null;
if (!$selectedTemplate) {
$this->error = 'Invalid rule template selected';
$this->showErrorModal = true;
return;
}
$this->startCreateRule();
$this->ruleAction = $selectedTemplate['action'] ?? 'ACCEPT';
$this->ruleType = $selectedTemplate['type'] ?? 'in';
$this->ruleProto = $selectedTemplate['proto'] ?? '';
$this->ruleDport = $selectedTemplate['dport'] ?? '';
$this->ruleComment = $selectedTemplate['comment'] ?? '';
$this->ruleEnable = true;
$this->ruleLog = 'nolog';
}
public function startCreateRuleFromLog(int $filteredLogIndex): void
{
$logs = $this->getFilteredLogsProperty();
if (!isset($logs[$filteredLogIndex])) {
$this->error = 'Could not find the selected log entry';
$this->showErrorModal = true;
return;
}
$log = $logs[$filteredLogIndex];
$this->startCreateRule();
$action = strtoupper((string) ($log['action'] ?? 'DROP'));
$protocol = strtolower((string) ($log['protocol'] ?? ''));
$allowedProtocols = ['tcp', 'udp', 'icmp', 'igmp', 'ah', 'esp', 'ospf'];
$source = $this->normalizeLogField($log['source'] ?? '');
$destination = $this->normalizeLogField($log['destination'] ?? '');
$sourcePort = $this->normalizeLogField($log['source_port'] ?? '');
$destinationPort = $this->normalizeLogField($log['destination_port'] ?? '');
$interface = $this->normalizeLogField($log['interface'] ?? '');
$this->ruleAction = in_array($action, ['ACCEPT', 'REJECT', 'DROP'], true) ? $action : 'DROP';
$this->ruleType = 'in';
$this->ruleSource = $source;
$this->ruleDest = $destination;
$this->ruleProto = in_array($protocol, $allowedProtocols, true) ? $protocol : '';
$this->ruleSport = $sourcePort;
$this->ruleDport = $destinationPort;
$this->ruleIface = $interface;
$this->ruleComment = 'Created from firewall log (' . ($log['timestamp_display'] ?? ($log['timestamp'] ?? now()->toDateTimeString())) . ')';
$this->ruleEnable = true;
$this->ruleLog = 'nolog';
}
public function startEditRule(int $index): void
{
if (!isset($this->rules[$index])) {
@@ -1481,21 +1593,23 @@ class Firewall extends Component
public function getPaginatedLogsProperty(): array
{
if (!$this->logsLoaded || empty($this->firewallLogs)) {
$filteredLogs = $this->getFilteredLogsProperty();
if (!$this->logsLoaded || empty($filteredLogs)) {
return [];
}
// Logs are already sorted by timestamp (most recent first) in parseFirewallLogs
// Logs are already sorted by timestamp (most recent first) in parseFirewallLogs.
$offset = ($this->currentPage - 1) * $this->perPage;
return array_slice($this->firewallLogs, $offset, $this->perPage);
return array_slice($filteredLogs, $offset, $this->perPage);
}
public function getTotalPagesProperty(): int
{
if (!$this->logsLoaded || empty($this->firewallLogs)) {
$filteredLogs = $this->getFilteredLogsProperty();
if (!$this->logsLoaded || empty($filteredLogs)) {
return 1;
}
return (int)ceil(count($this->firewallLogs) / $this->perPage);
return (int)ceil(count($filteredLogs) / $this->perPage);
}
public function goToPage(int $page): void
@@ -1534,6 +1648,43 @@ class Firewall extends Component
$this->currentPage = 1;
}
public function setLogPreset(string $preset): void
{
if (!array_key_exists($preset, self::LOG_PRESETS)) {
return;
}
$this->activeLogPreset = $preset;
$this->currentPage = 1;
}
public function getFilteredLogsProperty(): array
{
if (!$this->logsLoaded || empty($this->firewallLogs)) {
return [];
}
$preset = $this->activeLogPreset ?: 'all';
if ($preset === 'all') {
return $this->firewallLogs;
}
return array_values(array_filter($this->firewallLogs, function (array $log) use ($preset) {
$action = strtoupper((string) ($log['action'] ?? ''));
$source = strtolower((string) ($log['source'] ?? ''));
$destination = strtolower((string) ($log['destination'] ?? ''));
$destinationPort = (string) ($log['destination_port'] ?? '');
return match ($preset) {
'blocked' => in_array($action, ['DROP', 'REJECT'], true),
'ssh-drops' => in_array($action, ['DROP', 'REJECT'], true) && $destinationPort === '22',
'web-traffic' => in_array($destinationPort, ['80', '443'], true),
'ipv6' => str_contains($source, ':') || str_contains($destination, ':'),
default => true,
};
}));
}
// ==================== LOG PARSING ====================
protected function parseFirewallLogs(array $rawLogs): array
@@ -1623,6 +1774,20 @@ class Firewall extends Component
return $parsed;
}
protected function normalizeLogField(string $value): string
{
$trimmed = trim($value);
if ($trimmed === '' || $trimmed === '-') {
return '';
}
if (str_contains($trimmed, ',')) {
$trimmed = trim((string) explode(',', $trimmed)[0]);
}
return $trimmed;
}
protected function convertTimestamp(string $timestamp): string
{
try {