From 694cb03c805f76ac0ba1364bb47278f0dc56a072 Mon Sep 17 00:00:00 2001 From: Master3395 Date: Wed, 17 Sep 2025 00:32:07 +0200 Subject: [PATCH] Add firewall rule management features and enhance repository setup - Implemented functionality to edit existing firewall rules, including validation and error handling. - Added endpoints for exporting and importing firewall rules in JSON format, allowing users to manage rules more efficiently. - Enhanced the user interface with modals for editing rules and buttons for exporting/importing rules. - Updated the `cyberpanel.sh` script to support AlmaLinux 10 and improved LiteSpeed GPG key import with fallback options. - Refactored repository setup to accommodate different OS versions, ensuring compatibility with CentOS and AlmaLinux. --- cyberpanel.sh | 43 ++- firewall/EXPORT_IMPORT_FIREWALL_RULES.md | 121 +++++++++ firewall/firewallManager.py | 231 ++++++++++++++++ firewall/static/firewall/firewall.js | 247 +++++++++++++++++ firewall/templates/firewall/firewall.html | 308 +++++++++++++++++++++- firewall/urls.py | 7 + firewall/views.py | 33 +++ key.pem | 20 -- 8 files changed, 977 insertions(+), 33 deletions(-) create mode 100644 firewall/EXPORT_IMPORT_FIREWALL_RULES.md delete mode 100644 key.pem diff --git a/cyberpanel.sh b/cyberpanel.sh index b3c988a92..c1e4d195e 100644 --- a/cyberpanel.sh +++ b/cyberpanel.sh @@ -270,6 +270,11 @@ setup_epel_repo() { yum install -y https://cyberpanel.sh/dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm Check_Return "yum repo" "no_exit" ;; + "10") + # AlmaLinux 10 EPEL support + yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm + Check_Return "yum repo" "no_exit" + ;; esac } @@ -309,11 +314,12 @@ gpgcheck=1 EOF elif [[ "$Server_OS_Version" = "10" ]] && uname -m | grep -q 'x86_64'; then cat </etc/yum.repos.d/MariaDB.repo -# MariaDB 10.11 CentOS repository list - created 2021-08-06 02:01 UTC +# MariaDB 10.11 RHEL10 repository list - AlmaLinux 10 compatible # http://downloads.mariadb.org/mariadb/repositories/ [mariadb] name = MariaDB -baseurl = http://yum.mariadb.org/10.11/rhel9-amd64/ +baseurl = http://yum.mariadb.org/10.11/rhel10-amd64/ +module_hotfixes=1 gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB enabled=1 gpgcheck=1 @@ -1117,8 +1123,14 @@ log_function_start "Pre_Install_Setup_Repository" log_info "Setting up package repositories for $Server_OS $Server_OS_Version" if [[ $Server_OS = "CentOS" ]] ; then log_debug "Importing LiteSpeed GPG key" - rpm --import https://cyberpanel.sh/rpms.litespeedtech.com/centos/RPM-GPG-KEY-litespeed - #import the LiteSpeed GPG key + # Import LiteSpeed GPG key with fallback + rpm --import https://cyberpanel.sh/rpms.litespeedtech.com/centos/RPM-GPG-KEY-litespeed || { + warning "Primary GPG key import failed, trying alternative source" + rpm --import https://rpms.litespeedtech.com/centos/RPM-GPG-KEY-litespeed || { + error "Failed to import LiteSpeed GPG key from all sources" + return 1 + } + } yum clean all yum autoremove -y epel-release @@ -1151,7 +1163,12 @@ if [[ $Server_OS = "CentOS" ]] ; then dnf config-manager --set-enabled crb fi - yum install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm + # Install appropriate remi-release based on version + if [[ "$Server_OS_Version" = "9" ]]; then + yum install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm + elif [[ "$Server_OS_Version" = "10" ]]; then + yum install -y https://rpms.remirepo.net/enterprise/remi-release-10.rpm + fi Check_Return "yum repo" "no_exit" fi @@ -1341,8 +1358,22 @@ if [[ "$Server_OS" = "CentOS" ]] || [[ "$Server_OS" = "openEuler" ]] ; then #!/bin/bash - dnf install -y libnsl zip wget strace net-tools curl which bc telnet htop libevent-devel gcc libattr-devel xz-devel MariaDB-server MariaDB-client MariaDB-devel curl-devel git platform-python-devel tar socat python3 zip unzip bind-utils gpgme-devel openssl-devel + dnf install -y libnsl zip wget strace net-tools curl which bc telnet htop libevent-devel gcc libattr-devel xz-devel MariaDB-server MariaDB-client MariaDB-devel curl-devel git platform-python-devel tar socat python3 zip unzip bind-utils gpgme-devel openssl-devel boost-devel boost-program-options Check_Return + + # Fix boost library compatibility for galera-4 on AlmaLinux 10 + if [[ "$Server_OS_Version" = "10" ]]; then + # Create symlink for boost libraries if needed + if [ ! -f /usr/lib64/libboost_program_options.so.1.75.0 ]; then + BOOST_VERSION=$(find /usr/lib64 -name "libboost_program_options.so.*" | head -1 | sed 's/.*libboost_program_options\.so\.//') + if [ -n "$BOOST_VERSION" ]; then + ln -sf /usr/lib64/libboost_program_options.so.$BOOST_VERSION /usr/lib64/libboost_program_options.so.1.75.0 + log_info "Created boost library symlink for galera-4 compatibility: $BOOST_VERSION -> 1.75.0" + else + warning "Could not find boost libraries, galera-4 may not work properly" + fi + fi + fi elif [[ "$Server_OS_Version" = "20" ]] || [[ "$Server_OS_Version" = "22" ]] || [[ "$Server_OS_Version" = "24" ]] ; then dnf install -y libnsl zip wget strace net-tools curl which bc telnet htop libevent-devel gcc libattr-devel xz-devel mariadb-devel curl-devel git python3-devel tar socat python3 zip unzip bind-utils gpgme-devel Check_Return diff --git a/firewall/EXPORT_IMPORT_FIREWALL_RULES.md b/firewall/EXPORT_IMPORT_FIREWALL_RULES.md new file mode 100644 index 000000000..9fb19b983 --- /dev/null +++ b/firewall/EXPORT_IMPORT_FIREWALL_RULES.md @@ -0,0 +1,121 @@ +# Firewall Rules Export/Import Feature + +## Overview + +This feature allows CyberPanel administrators to export and import firewall rules between servers, making it easy to replicate security configurations across multiple servers. + +## Features + +### Export Functionality +- Exports all custom firewall rules to a JSON file +- Excludes default CyberPanel rules (CyberPanel Admin, SSHCustom) to prevent conflicts +- Includes metadata such as export timestamp and rule count +- Downloads file directly to the user's browser + +### Import Functionality +- Imports firewall rules from a previously exported JSON file +- Validates file format before processing +- Skips duplicate rules (same name, protocol, port, and IP address) +- Excludes default CyberPanel rules from import +- Provides detailed import summary (imported, skipped, error counts) +- Shows specific error messages for failed imports + +## Usage + +### Exporting Rules +1. Navigate to the Firewall section in CyberPanel +2. Click the "Export Rules" button in the Firewall Rules panel header +3. The system will generate and download a JSON file containing your custom rules + +### Importing Rules +1. Navigate to the Firewall section in CyberPanel +2. Click the "Import Rules" button in the Firewall Rules panel header +3. Select a previously exported JSON file +4. The system will process the import and show a summary of results + +## File Format + +The exported JSON file has the following structure: + +```json +{ + "version": "1.0", + "exported_at": "2024-01-15 14:30:25", + "total_rules": 5, + "rules": [ + { + "name": "Custom Web Server", + "proto": "tcp", + "port": "8080", + "ipAddress": "0.0.0.0/0" + }, + { + "name": "Database Access", + "proto": "tcp", + "port": "3306", + "ipAddress": "192.168.1.0/24" + } + ] +} +``` + +## Security Considerations + +- Only administrators can export/import firewall rules +- Default CyberPanel rules are excluded to prevent system conflicts +- Import process validates file format and rule data +- Failed imports are logged for troubleshooting +- Duplicate rules are automatically skipped + +## Error Handling + +The system provides comprehensive error handling: +- Invalid file format detection +- Missing required fields validation +- Individual rule import error tracking +- Detailed error messages for troubleshooting +- Import summary with counts of successful, skipped, and failed imports + +## Technical Implementation + +### Backend Components +- `exportFirewallRules()` method in `FirewallManager` +- `importFirewallRules()` method in `FirewallManager` +- New URL patterns for export/import endpoints +- File upload handling for import functionality + +### Frontend Components +- Export/Import buttons in firewall UI +- File download handling for exports +- File upload dialog for imports +- Progress indicators and error messaging +- Import summary display + +### Database Integration +- Uses existing `FirewallRules` model +- Maintains referential integrity +- Preserves rule relationships and constraints + +## Benefits + +1. **Time Efficiency**: Significantly reduces time to replicate firewall rules across servers +2. **Error Reduction**: Minimizes human error in manual rule creation +3. **Consistency**: Ensures identical security policies across multiple servers +4. **Backup**: Provides a way to backup and restore firewall configurations +5. **Migration**: Simplifies server migration and setup processes + +## Compatibility + +- Compatible with CyberPanel's existing firewall system +- Works with both TCP and UDP protocols +- Supports all IP address formats (single IPs, CIDR ranges) +- Maintains compatibility with existing firewall utilities + +## Future Enhancements + +Potential future improvements could include: +- Rule conflict detection and resolution +- Selective rule import (choose specific rules) +- Rule templates and presets +- Bulk rule management +- Integration with configuration management tools diff --git a/firewall/firewallManager.py b/firewall/firewallManager.py index 5375343f8..cec9ef97d 100644 --- a/firewall/firewallManager.py +++ b/firewall/firewallManager.py @@ -168,6 +168,79 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) + def editRule(self, userID = None, data = None): + """ + Edit an existing firewall rule + """ + try: + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] == 1: + pass + else: + return ACLManager.loadErrorJson('edit_status', 0) + + ruleID = data['id'] + newRuleName = data['ruleName'] + newRuleProtocol = data['ruleProtocol'] + newRulePort = data['rulePort'] + newRuleIP = data['ruleIP'] + + # Get the existing rule + try: + existingRule = FirewallRules.objects.get(id=ruleID) + except FirewallRules.DoesNotExist: + final_dic = {'status': 0, 'edit_status': 0, 'error_message': 'Rule not found'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Store old values for system firewall update + oldProtocol = existingRule.proto + oldPort = existingRule.port + oldIP = existingRule.ipAddress + + # Check if any values actually changed + if (existingRule.name == newRuleName and + existingRule.proto == newRuleProtocol and + existingRule.port == newRulePort and + existingRule.ipAddress == newRuleIP): + final_dic = {'status': 1, 'edit_status': 1, 'error_message': "No changes detected"} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Check if another rule with the same name already exists (excluding current rule) + if existingRule.name != newRuleName: + duplicateRule = FirewallRules.objects.filter(name=newRuleName).exclude(id=ruleID).first() + if duplicateRule: + final_dic = {'status': 0, 'edit_status': 0, 'error_message': f'A rule with name "{newRuleName}" already exists'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Update the rule in the system firewall + # First remove the old rule + FirewallUtilities.deleteRule(oldProtocol, oldPort, oldIP) + + # Then add the new rule + FirewallUtilities.addRule(newRuleProtocol, newRulePort, newRuleIP) + + # Update the database record + existingRule.name = newRuleName + existingRule.proto = newRuleProtocol + existingRule.port = newRulePort + existingRule.ipAddress = newRuleIP + existingRule.save() + + logging.CyberCPLogFileWriter.writeToFile(f"Firewall rule edited successfully. ID: {ruleID}, Name: {newRuleName}") + + final_dic = {'status': 1, 'edit_status': 1, 'error_message': "None"} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + except BaseException as msg: + final_dic = {'status': 0, 'edit_status': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + def reloadFirewall(self, userID = None, data = None): try: @@ -1755,6 +1828,164 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) + def exportFirewallRules(self, userID = None): + """ + Export all custom firewall rules to a JSON file, excluding default CyberPanel rules + """ + try: + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] == 1: + pass + else: + return ACLManager.loadErrorJson('exportStatus', 0) + + # Get all firewall rules + rules = FirewallRules.objects.all() + + # Default CyberPanel rules to exclude + default_rules = ['CyberPanel Admin', 'SSHCustom'] + + # Filter out default rules + custom_rules = [] + for rule in rules: + if rule.name not in default_rules: + custom_rules.append({ + 'name': rule.name, + 'proto': rule.proto, + 'port': rule.port, + 'ipAddress': rule.ipAddress + }) + + # Create export data with metadata + export_data = { + 'version': '1.0', + 'exported_at': time.strftime('%Y-%m-%d %H:%M:%S'), + 'total_rules': len(custom_rules), + 'rules': custom_rules + } + + # Create JSON response with file download + json_content = json.dumps(export_data, indent=2) + + logging.CyberCPLogFileWriter.writeToFile(f"Firewall rules exported successfully. Total rules: {len(custom_rules)}") + + # Return file as download + response = HttpResponse(json_content, content_type='application/json') + response['Content-Disposition'] = f'attachment; filename="firewall_rules_export_{int(time.time())}.json"' + + return response + + except BaseException as msg: + final_dic = {'exportStatus': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + def importFirewallRules(self, userID = None, data = None): + """ + Import firewall rules from a JSON file + """ + try: + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] == 1: + pass + else: + return ACLManager.loadErrorJson('importStatus', 0) + + # Handle file upload + if hasattr(self.request, 'FILES') and 'import_file' in self.request.FILES: + import_file = self.request.FILES['import_file'] + + # Read file content + import_data = json.loads(import_file.read().decode('utf-8')) + else: + # Fallback to file path method + import_file_path = data.get('import_file_path', '') + + if not import_file_path or not os.path.exists(import_file_path): + final_dic = {'importStatus': 0, 'error_message': 'Import file not found or invalid path'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Read and parse the import file + with open(import_file_path, 'r') as f: + import_data = json.load(f) + + # Validate the import data structure + if 'rules' not in import_data: + final_dic = {'importStatus': 0, 'error_message': 'Invalid import file format. Missing rules array.'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + imported_count = 0 + skipped_count = 0 + error_count = 0 + errors = [] + + # Default CyberPanel rules to exclude from import + default_rules = ['CyberPanel Admin', 'SSHCustom'] + + for rule_data in import_data['rules']: + try: + # Skip default rules + if rule_data.get('name', '') in default_rules: + skipped_count += 1 + continue + + # Check if rule already exists + existing_rule = FirewallRules.objects.filter( + name=rule_data['name'], + proto=rule_data['proto'], + port=rule_data['port'], + ipAddress=rule_data['ipAddress'] + ).first() + + if existing_rule: + skipped_count += 1 + continue + + # Add the rule to the system firewall + FirewallUtilities.addRule( + rule_data['proto'], + rule_data['port'], + rule_data['ipAddress'] + ) + + # Add the rule to the database + new_rule = FirewallRules( + name=rule_data['name'], + proto=rule_data['proto'], + port=rule_data['port'], + ipAddress=rule_data['ipAddress'] + ) + new_rule.save() + + imported_count += 1 + + except Exception as e: + error_count += 1 + errors.append(f"Rule '{rule_data.get('name', 'Unknown')}': {str(e)}") + logging.CyberCPLogFileWriter.writeToFile(f"Error importing rule {rule_data.get('name', 'Unknown')}: {str(e)}") + + logging.CyberCPLogFileWriter.writeToFile(f"Firewall rules import completed. Imported: {imported_count}, Skipped: {skipped_count}, Errors: {error_count}") + + final_dic = { + 'importStatus': 1, + 'error_message': "None", + 'imported_count': imported_count, + 'skipped_count': skipped_count, + 'error_count': error_count, + 'errors': errors + } + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + except BaseException as msg: + final_dic = {'importStatus': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + diff --git a/firewall/static/firewall/firewall.js b/firewall/static/firewall/firewall.js index 1d666ed78..e70ca46e8 100644 --- a/firewall/static/firewall/firewall.js +++ b/firewall/static/firewall/firewall.js @@ -16,6 +16,10 @@ app.controller('firewallController', function ($scope, $http) { $scope.couldNotConnect = true; $scope.rulesDetails = false; + // Edit modal variables + $scope.showEditModal = false; + $scope.editingRule = {}; + firewallStatus(); populateCurrentRecords(); @@ -503,6 +507,249 @@ app.controller('firewallController', function ($scope, $http) { }; + // Export/Import Functions + $scope.exportRules = function () { + $scope.rulesLoading = false; + $scope.actionFailed = true; + $scope.actionSuccess = true; + + url = "/firewall/exportFirewallRules"; + + var data = {}; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(exportSuccess, exportError); + + function exportSuccess(response) { + $scope.rulesLoading = true; + + // Check if response is JSON (error) or file download + if (typeof response.data === 'string' && response.data.includes('{')) { + try { + var errorData = JSON.parse(response.data); + if (errorData.exportStatus === 0) { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = errorData.error_message; + return; + } + } catch (e) { + // If not JSON, assume it's the file content + } + } + + // If we get here, it's a successful file download + $scope.actionFailed = true; + $scope.actionSuccess = false; + } + + function exportError(response) { + $scope.rulesLoading = true; + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = "Could not connect to server. Please refresh this page."; + } + }; + + $scope.importRules = function () { + // Create file input element + var input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.style.display = 'none'; + + input.onchange = function(event) { + var file = event.target.files[0]; + if (file) { + var reader = new FileReader(); + reader.onload = function(e) { + try { + var importData = JSON.parse(e.target.result); + + // Validate file format + if (!importData.rules || !Array.isArray(importData.rules)) { + $scope.$apply(function() { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = "Invalid import file format. Please select a valid firewall rules export file."; + }); + return; + } + + // Upload file to server + uploadImportFile(file); + } catch (error) { + $scope.$apply(function() { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = "Invalid JSON file. Please select a valid firewall rules export file."; + }); + } + }; + reader.readAsText(file); + } + }; + + document.body.appendChild(input); + input.click(); + document.body.removeChild(input); + }; + + function uploadImportFile(file) { + $scope.rulesLoading = false; + $scope.actionFailed = true; + $scope.actionSuccess = true; + + var formData = new FormData(); + formData.append('import_file', file); + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken'), + 'Content-Type': undefined + }, + transformRequest: angular.identity + }; + + $http.post("/firewall/importFirewallRules", formData, config).then(importSuccess, importError); + + function importSuccess(response) { + $scope.rulesLoading = true; + + if (response.data.importStatus === 1) { + $scope.actionFailed = true; + $scope.actionSuccess = false; + + // Refresh rules list + populateCurrentRecords(); + + // Show import summary + var summary = `Import completed successfully!\n` + + `Imported: ${response.data.imported_count} rules\n` + + `Skipped: ${response.data.skipped_count} rules\n` + + `Errors: ${response.data.error_count} rules`; + + if (response.data.errors && response.data.errors.length > 0) { + summary += `\n\nErrors:\n${response.data.errors.join('\n')}`; + } + + alert(summary); + } else { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = response.data.error_message; + } + } + + function importError(response) { + $scope.rulesLoading = true; + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = "Could not connect to server. Please refresh this page."; + } + } + + // Edit Rule Functions + $scope.editRule = function(rule) { + $scope.editingRule = { + id: rule.id, + name: rule.name, + proto: rule.proto, + port: rule.port, + ipAddress: rule.ipAddress + }; + $scope.showEditModal = true; + }; + + $scope.closeEditModal = function() { + $scope.showEditModal = false; + $scope.editingRule = {}; + }; + + $scope.saveEditedRule = function() { + // Basic validation + if (!$scope.editingRule.name || !$scope.editingRule.port || !$scope.editingRule.ipAddress) { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = "Please fill in all required fields."; + return; + } + + $scope.rulesLoading = false; + $scope.actionFailed = true; + $scope.actionSuccess = true; + + url = "/firewall/editRule"; + + var data = { + id: $scope.editingRule.id, + ruleName: $scope.editingRule.name, + ruleProtocol: $scope.editingRule.proto, + rulePort: $scope.editingRule.port, + ruleIP: $scope.editingRule.ipAddress + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(editSuccess, editError); + + function editSuccess(response) { + $scope.rulesLoading = true; + + if (response.data.edit_status === 1) { + // Close modal and refresh rules + $scope.closeEditModal(); + populateCurrentRecords(); + + $scope.actionFailed = true; + $scope.actionSuccess = false; + } else { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = response.data.error_message; + } + } + + function editError(response) { + $scope.rulesLoading = true; + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = "Could not connect to server. Please refresh this page."; + } + }; + + // Close modal when clicking outside or pressing Escape + $scope.$on('$locationChangeStart', function() { + $scope.closeEditModal(); + }); + + // Keyboard support for modal + $(document).on('keydown', function(e) { + if ($scope.showEditModal && e.keyCode === 27) { // Escape key + $scope.$apply(function() { + $scope.closeEditModal(); + }); + } + }); + + // Focus management for modal + $scope.$watch('showEditModal', function(newVal) { + if (newVal) { + setTimeout(function() { + $('#editRuleModal input[name="ruleName"]').focus(); + }, 100); + } + }); + }); diff --git a/firewall/templates/firewall/firewall.html b/firewall/templates/firewall/firewall.html index dace7774b..da14c3f41 100644 --- a/firewall/templates/firewall/firewall.html +++ b/firewall/templates/firewall/firewall.html @@ -421,6 +421,205 @@ transform: scale(1.1); } + .btn-edit { + background: var(--bg-info-light, #dbeafe); + color: var(--info-color, #3b82f6); + width: 32px; + height: 32px; + border-radius: 6px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + font-weight: 700; + border: none; + } + + .btn-edit:hover { + background: var(--info-color, #3b82f6); + color: var(--text-light, white); + transform: scale(1.1); + } + + /* Export/Import Buttons */ + .export-import-buttons { + display: flex; + gap: 0.75rem; + align-items: center; + } + + .btn-export, .btn-import { + padding: 0.5rem 1rem; + border-radius: 8px; + font-weight: 500; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.3s ease; + border: none; + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: rgba(255, 255, 255, 0.2); + color: var(--text-light, white); + backdrop-filter: blur(10px); + } + + .btn-export:hover, .btn-import:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + } + + .btn-export:disabled, .btn-import:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; + } + + /* Edit Modal Styles */ + .modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.3s ease-out; + } + + @keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + + .modal-content { + background: var(--bg-secondary, white); + border-radius: 16px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + max-width: 500px; + width: 90%; + max-height: 90vh; + overflow-y: auto; + animation: slideInUp 0.3s ease-out; + } + + @keyframes slideInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .modal-header { + padding: 1.5rem 2rem; + border-bottom: 1px solid var(--border-color, #e8e9ff); + display: flex; + align-items: center; + justify-content: space-between; + background: linear-gradient(135deg, var(--firewall-gradient-start, #ef4444) 0%, var(--firewall-gradient-end, #dc2626) 100%); + color: var(--text-light, white); + border-radius: 16px 16px 0 0; + } + + .modal-title { + font-size: 1.25rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.75rem; + margin: 0; + } + + .modal-close { + background: rgba(255, 255, 255, 0.2); + border: none; + color: var(--text-light, white); + width: 32px; + height: 32px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + } + + .modal-close:hover { + background: rgba(255, 255, 255, 0.3); + transform: scale(1.1); + } + + .modal-body { + padding: 2rem; + } + + .edit-rule-form { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .modal-footer { + padding: 1.5rem 2rem; + border-top: 1px solid var(--border-color, #e8e9ff); + display: flex; + gap: 1rem; + justify-content: flex-end; + background: var(--bg-tertiary, #f8f9ff); + border-radius: 0 0 16px 16px; + } + + .btn-cancel, .btn-save { + padding: 0.75rem 1.5rem; + border-radius: 8px; + font-weight: 500; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.3s ease; + border: none; + display: inline-flex; + align-items: center; + gap: 0.5rem; + min-width: 120px; + justify-content: center; + } + + .btn-cancel { + background: var(--bg-muted, #f1f5f9); + color: var(--text-secondary, #475569); + } + + .btn-cancel:hover { + background: var(--bg-muted-hover, #e2e8f0); + transform: translateY(-2px); + } + + .btn-save { + background: var(--firewall-accent, #ef4444); + color: var(--text-light, white); + } + + .btn-save:hover { + background: var(--firewall-accent-dark, #dc2626); + transform: translateY(-2px); + box-shadow: 0 4px 12px var(--firewall-shadow, rgba(239, 68, 68, 0.3)); + } + + .btn-save:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; + } + .empty-state { text-align: center; padding: 4rem 2rem; @@ -600,7 +799,25 @@ {% trans "Firewall Rules" %} -
+
+
+
+ + +
+
@@ -680,12 +897,20 @@ {$ rule.port $} - +
+ + +
@@ -717,6 +942,75 @@ + + + {% endblock %} \ No newline at end of file diff --git a/firewall/urls.py b/firewall/urls.py index 56e9fc187..8cf877b4c 100644 --- a/firewall/urls.py +++ b/firewall/urls.py @@ -32,6 +32,13 @@ urlpatterns = [ path('modSecRulesPacks', views.modSecRulesPacks, name='modSecRulesPacks'), path('getOWASPAndComodoStatus', views.getOWASPAndComodoStatus, name='getOWASPAndComodoStatus'), path('installModSecRulesPack', views.installModSecRulesPack, name='installModSecRulesPack'), + + # Firewall Export/Import + path('exportFirewallRules', views.exportFirewallRules, name='exportFirewallRules'), + path('importFirewallRules', views.importFirewallRules, name='importFirewallRules'), + + # Firewall Rule Edit + path('editRule', views.editRule, name='editRule'), path('getRulesFiles', views.getRulesFiles, name='getRulesFiles'), path('enableDisableRuleFile', views.enableDisableRuleFile, name='enableDisableRuleFile'), diff --git a/firewall/views.py b/firewall/views.py index d7f91dd93..369f94602 100644 --- a/firewall/views.py +++ b/firewall/views.py @@ -648,3 +648,36 @@ def saveLitespeed_conf(request): return fm.saveLitespeed_conf(userID, json.loads(request.body)) except KeyError: return redirect(loadLoginPage) + + +def exportFirewallRules(request): + try: + userID = request.session['userID'] + fm = FirewallManager() + return fm.exportFirewallRules(userID) + except KeyError: + return redirect(loadLoginPage) + + +def importFirewallRules(request): + try: + userID = request.session['userID'] + fm = FirewallManager(request) + + # Handle file upload + if request.method == 'POST' and 'import_file' in request.FILES: + return fm.importFirewallRules(userID, None) + else: + # Handle JSON data + return fm.importFirewallRules(userID, json.loads(request.body)) + except KeyError: + return redirect(loadLoginPage) + + +def editRule(request): + try: + userID = request.session['userID'] + fm = FirewallManager() + return fm.editRule(userID, json.loads(request.body)) + except KeyError: + return redirect(loadLoginPage) \ No newline at end of file diff --git a/key.pem b/key.pem deleted file mode 100644 index b83a0a6bf..000000000 --- a/key.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDNDCCAhwCCQDEgz2Vkmv5NDANBgkqhkiG9w0BAQsFADBcMQswCQYDVQQGEwJV -UzEPMA0GA1UECAwGRGVuaWFsMRQwEgYDVQQHDAtTcHJpbmdmaWVsZDEMMAoGA1UE -CgwDRGlzMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wHhcNMjIwODI2MTEwNzEy -WhcNMzIwODIzMTEwNzEyWjBcMQswCQYDVQQGEwJVUzEPMA0GA1UECAwGRGVuaWFs -MRQwEgYDVQQHDAtTcHJpbmdmaWVsZDEMMAoGA1UECgwDRGlzMRgwFgYDVQQDDA93 -d3cuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq -BesL5DNNansmBYyn0DSqJyBOwGEIH/Ur4KFbKICrhy376gec2UyGG5lurgQa8Nz6 -Gt7Z1B9LbLVE3bXS1f82bJHyPFUP8WmQuC+/3ZMRPFiG7/4n//0QwMptkDvGb5E1 -0NXfJjuGHNfpVaHAs83v9mvUKc7oOjxwa+lbkhobD8HKByzAi/fpD90WQS9JRUJX -8lGquw+k5flL31AkqCyZOJw22VEoIpoF7RSh0xZvpsLze6G1thY5R27YWChcOR0E -m9/TUZ9/oCaP3WBvCldjr5OT6eZxJJzlt1jYndQKUyO5OtaJuNd4MkCNz9u9wwjE -CfZF7VQj8FABR9tqcVEfAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGCNMmGXem6k -iqvJG2eFvuSdIh+x7x8EnXpRi2lW0ZqLYZfZuiMQL1fedhVoix+rzo9/Qhj6BTnt -5cC5oexhj/s76gqehNatMC0HAbIcK+MpvmwNoA/7U/bQAlbNxR3aLKSV0/B5YlTP -9yUoMtBqGEiesqAVAD26jOG5Ch1fHHElPtp3rE6qxGsAL0Eu+2Gezq3OqW2ejlvY -hZFpB/ZEynmYDUjT02+2J+3bAhfGaeUXC75YsbyfRAdc0OHa9/r7RhK+tgzmUhJt -sKUDW2OIwOTumUOSDgh1ayeTBddRAcMyIoGFeHF9degfJFiSo4vQrmaqFr9/bUFp -pn+y1/A3gNY= ------END CERTIFICATE-----