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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user