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.
This commit is contained in:
Master3395
2025-09-17 00:32:07 +02:00
parent 42428bfc6a
commit 694cb03c80
8 changed files with 977 additions and 33 deletions

View File

@@ -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 <<EOF >/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

View File

@@ -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

View File

@@ -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)

View File

@@ -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);
}
});
});

View File

@@ -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 @@
</div>
{% trans "Firewall Rules" %}
</div>
<div ng-show="rulesLoading" class="loading-spinner"></div>
<div style="display: flex; align-items: center; gap: 1rem;">
<div ng-show="rulesLoading" class="loading-spinner"></div>
<div class="export-import-buttons">
<button type="button"
ng-click="exportRules()"
class="btn-export"
ng-disabled="rulesLoading">
<i class="fas fa-download"></i>
{% trans "Export Rules" %}
</button>
<button type="button"
ng-click="importRules()"
class="btn-import"
ng-disabled="rulesLoading">
<i class="fas fa-upload"></i>
{% trans "Import Rules" %}
</button>
</div>
</div>
</div>
<!-- Add Rule Section -->
@@ -680,12 +897,20 @@
<span class="port-number">{$ rule.port $}</span>
</td>
<td>
<button type="button"
ng-click="deleteRule(rule.id, rule.proto, rule.port, rule.ipAddress)"
class="btn-delete"
title="{% trans 'Delete Rule' %}">
<i class="fas fa-times"></i>
</button>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<button type="button"
ng-click="editRule(rule)"
class="btn-edit"
title="{% trans 'Edit Rule' %}">
<i class="fas fa-edit"></i>
</button>
<button type="button"
ng-click="deleteRule(rule.id, rule.proto, rule.port, rule.ipAddress)"
class="btn-delete"
title="{% trans 'Delete Rule' %}">
<i class="fas fa-times"></i>
</button>
</div>
</td>
</tr>
</tbody>
@@ -717,6 +942,75 @@
</div>
</div>
</div>
<!-- Edit Rule Modal -->
<div id="editRuleModal" class="modal" ng-show="showEditModal" ng-click="closeEditModal()">
<div class="modal-content" ng-click="$event.stopPropagation()">
<div class="modal-header">
<h3 class="modal-title">
<i class="fas fa-edit"></i>
{% trans "Edit Firewall Rule" %}
</h3>
<button type="button" class="modal-close" ng-click="closeEditModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<form class="edit-rule-form">
<div class="form-group">
<label class="form-label">{% trans "Rule Name" %}</label>
<input type="text"
class="form-control"
ng-model="editingRule.name"
name="ruleName"
placeholder="{% trans 'e.g., Allow SSH' %}"
required>
</div>
<div class="form-group">
<label class="form-label">{% trans "Protocol" %}</label>
<select ng-model="editingRule.proto" class="form-control select-control">
<option value="tcp">TCP</option>
<option value="udp">UDP</option>
</select>
</div>
<div class="form-group">
<label class="form-label">{% trans "IP Address" %}</label>
<input type="text"
class="form-control"
ng-model="editingRule.ipAddress"
placeholder="{% trans '0.0.0.0/0 for all IPs' %}"
required>
</div>
<div class="form-group">
<label class="form-label">{% trans "Port" %}</label>
<input type="text"
class="form-control"
ng-model="editingRule.port"
placeholder="{% trans 'e.g., 80' %}"
required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button"
class="btn-cancel"
ng-click="closeEditModal()">
<i class="fas fa-times"></i>
{% trans "Cancel" %}
</button>
<button type="button"
class="btn-save"
ng-click="saveEditedRule()"
ng-disabled="rulesLoading">
<i class="fas fa-save"></i>
{% trans "Save Changes" %}
</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -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'),

View File

@@ -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)

20
key.pem
View File

@@ -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-----