diff --git a/baseTemplate/static/baseTemplate/custom-js/system-status.js b/baseTemplate/static/baseTemplate/custom-js/system-status.js index 3d5c7bcd6..8b1d54cd9 100644 --- a/baseTemplate/static/baseTemplate/custom-js/system-status.js +++ b/baseTemplate/static/baseTemplate/custom-js/system-status.js @@ -1008,8 +1008,11 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { if (!$scope.blockingIP) { $scope.blockingIP = ipAddress; + // Use the new Banned IPs system instead of the old blockIPAddress var data = { - ip_address: ipAddress + ip: ipAddress, + reason: 'Brute force attack detected from SSH Security Analysis', + duration: 'permanent' }; var config = { @@ -1018,7 +1021,7 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } }; - $http.post('/base/blockIPAddress', data, config).then(function (response) { + $http.post('/firewall/addBannedIP', data, config).then(function (response) { $scope.blockingIP = null; if (response.data && response.data.status === 1) { // Mark IP as blocked @@ -1026,8 +1029,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { // Show success notification new PNotify({ - title: 'Success', - text: `IP address ${ipAddress} has been blocked successfully using ${response.data.firewall.toUpperCase()}`, + title: 'IP Address Banned', + text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`, type: 'success', delay: 5000 }); diff --git a/baseTemplate/templates/baseTemplate/homePage.html b/baseTemplate/templates/baseTemplate/homePage.html index 5523d03d3..53d11204c 100644 --- a/baseTemplate/templates/baseTemplate/homePage.html +++ b/baseTemplate/templates/baseTemplate/homePage.html @@ -234,6 +234,7 @@ width: 100%; border-collapse: collapse; margin-top: 15px; + table-layout: fixed; } .activity-table th { @@ -535,12 +536,12 @@ - - - - - - + + + + + + @@ -551,7 +552,15 @@ @@ -561,12 +570,12 @@
USERIPCOUNTRYDATESESSIONACTIVITYUSERIPCOUNTRYDATESESSIONACTIVITY
{$ login.date $} {$ login.session $} - +
+ + +
- - - - - - + + + + + + @@ -577,7 +586,12 @@ @@ -672,9 +686,12 @@ onmouseout="this.style.background='#dc2626'"> - Block IP - Blocking... + Ban IP Permanently + Banning... + + Manage in Firewall + Blocked diff --git a/dockerManager/container.py b/dockerManager/container.py index 6d98882c8..275891abf 100644 --- a/dockerManager/container.py +++ b/dockerManager/container.py @@ -2095,4 +2095,211 @@ class ContainerManager(multi.Thread): logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.deleteContainerKeepData]') data_ret = {'deleteContainerKeepDataStatus': 0, 'error_message': str(msg)} json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + def updateContainer(self, userID=None, data=None): + """ + Update container with new image while preserving data using Docker volumes + This function handles the complete container update process: + 1. Stops the current container + 2. Creates a backup of the container configuration + 3. Removes the old container + 4. Pulls the new image + 5. Creates a new container with the same configuration but new image + 6. Preserves all volumes and data + """ + try: + admin = Administrator.objects.get(pk=userID) + if admin.acl.adminStatus != 1: + return ACLManager.loadError() + + client = docker.from_env() + dockerAPI = docker.APIClient() + + containerName = data['containerName'] + newImage = data['newImage'] + newTag = data.get('newTag', 'latest') + + # Get the current container + try: + currentContainer = client.containers.get(containerName) + except docker.errors.NotFound: + data_ret = {'updateContainerStatus': 0, 'error_message': f'Container {containerName} not found'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + except Exception as e: + data_ret = {'updateContainerStatus': 0, 'error_message': f'Error accessing container: {str(e)}'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + # Get container configuration for recreation + containerConfig = currentContainer.attrs['Config'] + hostConfig = currentContainer.attrs['HostConfig'] + + # Extract volumes for data preservation + volumes = {} + if 'Binds' in hostConfig and hostConfig['Binds']: + for bind in hostConfig['Binds']: + if ':' in bind: + parts = bind.split(':') + if len(parts) >= 2: + host_path = parts[0] + container_path = parts[1] + mode = parts[2] if len(parts) > 2 else 'rw' + volumes[host_path] = {'bind': container_path, 'mode': mode} + + # Extract environment variables + environment = containerConfig.get('Env', []) + envDict = {} + for env in environment: + if '=' in env: + key, value = env.split('=', 1) + envDict[key] = value + + # Extract port mappings + portConfig = {} + if 'PortBindings' in hostConfig and hostConfig['PortBindings']: + for container_port, host_bindings in hostConfig['PortBindings'].items(): + if host_bindings and len(host_bindings) > 0: + host_port = host_bindings[0]['HostPort'] + portConfig[container_port] = host_port + + # Extract memory limit + memory_limit = hostConfig.get('Memory', 0) + if memory_limit > 0: + memory_limit = memory_limit // 1048576 # Convert bytes to MB + + # Stop the current container + try: + if currentContainer.status == 'running': + currentContainer.stop(timeout=30) + logging.CyberCPLogFileWriter.writeToFile(f'Stopped container {containerName} for update') + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Error stopping container {containerName}: {str(e)}') + data_ret = {'updateContainerStatus': 0, 'error_message': f'Error stopping container: {str(e)}'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + # Remove the old container + try: + currentContainer.remove(force=True) + logging.CyberCPLogFileWriter.writeToFile(f'Removed old container {containerName}') + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Error removing old container {containerName}: {str(e)}') + data_ret = {'updateContainerStatus': 0, 'error_message': f'Error removing old container: {str(e)}'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + # Pull the new image + try: + image_name = f"{newImage}:{newTag}" + logging.CyberCPLogFileWriter.writeToFile(f'Pulling new image {image_name}') + client.images.pull(newImage, tag=newTag) + logging.CyberCPLogFileWriter.writeToFile(f'Successfully pulled image {image_name}') + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Error pulling image {newImage}:{newTag}: {str(e)}') + data_ret = {'updateContainerStatus': 0, 'error_message': f'Error pulling new image: {str(e)}'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + # Create new container with same configuration but new image + try: + containerArgs = { + 'image': image_name, + 'detach': True, + 'name': containerName, + 'ports': portConfig, + 'publish_all_ports': True, + 'environment': envDict, + 'volumes': volumes + } + + if memory_limit > 0: + containerArgs['mem_limit'] = memory_limit * 1048576 + + newContainer = client.containers.create(**containerArgs) + logging.CyberCPLogFileWriter.writeToFile(f'Created new container {containerName} with image {image_name}') + + # Start the new container + newContainer.start() + logging.CyberCPLogFileWriter.writeToFile(f'Started updated container {containerName}') + + except docker.errors.APIError as err: + error_message = str(err) + if "port is already allocated" in error_message: + try: + newContainer.remove(force=True) + except: + pass + data_ret = {'updateContainerStatus': 0, 'error_message': f'Docker API error creating container: {error_message}'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + except docker.errors.ImageNotFound as err: + error_message = str(err) + data_ret = {'updateContainerStatus': 0, 'error_message': f'New image not found: {error_message}'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Error creating new container {containerName}: {str(e)}') + data_ret = {'updateContainerStatus': 0, 'error_message': f'Error creating new container: {str(e)}'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + # Log successful update + logging.CyberCPLogFileWriter.writeToFile(f'Successfully updated container {containerName} to image {image_name}') + + data_ret = { + 'updateContainerStatus': 1, + 'error_message': 'None', + 'message': f'Container {containerName} successfully updated to {image_name}' + } + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + except Exception as msg: + logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.updateContainer]') + data_ret = {'updateContainerStatus': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + def listContainers(self, userID=None): + """ + Get list of all Docker containers + """ + try: + admin = Administrator.objects.get(pk=userID) + if admin.acl.adminStatus != 1: + return ACLManager.loadError() + + client = docker.from_env() + + # Get all containers (including stopped ones) + containers = client.containers.list(all=True) + + container_list = [] + for container in containers: + container_info = { + 'name': container.name, + 'image': container.image.tags[0] if container.image.tags else container.image.short_id, + 'status': container.status, + 'state': container.attrs['State']['Status'], + 'created': container.attrs['Created'], + 'ports': container.attrs['NetworkSettings']['Ports'], + 'mounts': container.attrs['Mounts'], + 'id': container.short_id + } + container_list.append(container_info) + + data_ret = { + 'status': 1, + 'error_message': 'None', + 'containers': container_list + } + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + except Exception as msg: + logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.listContainers]') + data_ret = {'status': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) return HttpResponse(json_data) \ No newline at end of file diff --git a/dockerManager/static/dockerManager/dockerManager.js b/dockerManager/static/dockerManager/dockerManager.js index e049906dd..d2ab7a9e3 100644 --- a/dockerManager/static/dockerManager/dockerManager.js +++ b/dockerManager/static/dockerManager/dockerManager.js @@ -2495,4 +2495,259 @@ app.controller('manageImages', function ($scope, $http) { }) } +}); + +// Container List Controller +app.controller('listContainers', function ($scope, $http, $timeout, $window) { + $scope.containers = []; + $scope.loading = false; + $scope.updateContainerName = ''; + $scope.currentImage = ''; + $scope.newImage = ''; + $scope.newTag = 'latest'; + + // Load containers list + $scope.loadContainers = function() { + $scope.loading = true; + var url = '/docker/listContainers'; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, {}, config).then(function(response) { + $scope.loading = false; + if (response.data.status === 1) { + $scope.containers = response.data.containers || []; + } else { + new PNotify({ + title: 'Error Loading Containers', + text: response.data.error_message || 'Failed to load containers', + type: 'error' + }); + } + }, function(error) { + $scope.loading = false; + new PNotify({ + title: 'Connection Error', + text: 'Could not connect to server', + type: 'error' + }); + }); + }; + + // Initialize containers on page load + $scope.loadContainers(); + + // Open update container modal + $scope.openUpdateModal = function(container) { + $scope.updateContainerName = container.name; + $scope.currentImage = container.image; + $scope.newImage = ''; + $scope.newTag = 'latest'; + $('#updateContainer').modal('show'); + }; + + // Perform container update + $scope.performUpdate = function() { + if (!$scope.newImage && !$scope.newTag) { + new PNotify({ + title: 'Missing Information', + text: 'Please enter a new image name or tag', + type: 'error' + }); + return; + } + + // If no new image specified, use current image with new tag + var imageToUse = $scope.newImage || $scope.currentImage.split(':')[0]; + var tagToUse = $scope.newTag || 'latest'; + + var data = { + containerName: $scope.updateContainerName, + newImage: imageToUse, + newTag: tagToUse + }; + + var url = '/docker/updateContainer'; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + // Show loading + $('#updateContainer').modal('hide'); + new PNotify({ + title: 'Updating Container', + text: 'Please wait while the container is being updated...', + type: 'info', + hide: false + }); + + $http.post(url, data, config).then(function(response) { + if (response.data.updateContainerStatus === 1) { + new PNotify({ + title: 'Container Updated Successfully', + text: response.data.message || 'Container has been updated successfully', + type: 'success' + }); + // Reload containers list + $scope.loadContainers(); + } else { + new PNotify({ + title: 'Update Failed', + text: response.data.error_message || 'Failed to update container', + type: 'error' + }); + } + }, function(error) { + new PNotify({ + title: 'Update Failed', + text: 'Could not connect to server', + type: 'error' + }); + }); + }; + + // Container actions + $scope.startContainer = function(containerName) { + var data = { containerName: containerName }; + var url = '/docker/startContainer'; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + if (response.data.startContainerStatus === 1) { + new PNotify({ + title: 'Container Started', + text: 'Container has been started successfully', + type: 'success' + }); + $scope.loadContainers(); + } else { + new PNotify({ + title: 'Start Failed', + text: response.data.error_message || 'Failed to start container', + type: 'error' + }); + } + }); + }; + + $scope.stopContainer = function(containerName) { + var data = { containerName: containerName }; + var url = '/docker/stopContainer'; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + if (response.data.stopContainerStatus === 1) { + new PNotify({ + title: 'Container Stopped', + text: 'Container has been stopped successfully', + type: 'success' + }); + $scope.loadContainers(); + } else { + new PNotify({ + title: 'Stop Failed', + text: response.data.error_message || 'Failed to stop container', + type: 'error' + }); + } + }); + }; + + $scope.restartContainer = function(containerName) { + var data = { containerName: containerName }; + var url = '/docker/restartContainer'; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + if (response.data.restartContainerStatus === 1) { + new PNotify({ + title: 'Container Restarted', + text: 'Container has been restarted successfully', + type: 'success' + }); + $scope.loadContainers(); + } else { + new PNotify({ + title: 'Restart Failed', + text: response.data.error_message || 'Failed to restart container', + type: 'error' + }); + } + }); + }; + + $scope.deleteContainerWithData = function(containerName) { + if (confirm('Are you sure you want to delete this container and all its data? This action cannot be undone.')) { + var data = { containerName: containerName }; + var url = '/docker/deleteContainerWithData'; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + if (response.data.deleteContainerWithDataStatus === 1) { + new PNotify({ + title: 'Container Deleted', + text: 'Container and all data have been deleted successfully', + type: 'success' + }); + $scope.loadContainers(); + } else { + new PNotify({ + title: 'Delete Failed', + text: response.data.error_message || 'Failed to delete container', + type: 'error' + }); + } + }); + } + }; + + $scope.deleteContainerKeepData = function(containerName) { + if (confirm('Are you sure you want to delete this container but keep the data? The container will be removed but volumes will be preserved.')) { + var data = { containerName: containerName }; + var url = '/docker/deleteContainerKeepData'; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + if (response.data.deleteContainerKeepDataStatus === 1) { + new PNotify({ + title: 'Container Deleted', + text: 'Container has been deleted but data has been preserved', + type: 'success' + }); + $scope.loadContainers(); + } else { + new PNotify({ + title: 'Delete Failed', + text: response.data.error_message || 'Failed to delete container', + type: 'error' + }); + } + }); + } + }; }); \ No newline at end of file diff --git a/dockerManager/urls.py b/dockerManager/urls.py index f4746f342..52e79295c 100644 --- a/dockerManager/urls.py +++ b/dockerManager/urls.py @@ -28,6 +28,7 @@ urlpatterns = [ re_path(r'^removeImage$', views.removeImage, name='removeImage'), re_path(r'^pullImage$', views.pullImage, name='pullImage'), re_path(r'^updateContainer$', views.updateContainer, name='updateContainer'), + re_path(r'^listContainers$', views.listContainers, name='listContainers'), re_path(r'^deleteContainerWithData$', views.deleteContainerWithData, name='deleteContainerWithData'), re_path(r'^deleteContainerKeepData$', views.deleteContainerKeepData, name='deleteContainerKeepData'), re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'), diff --git a/dockerManager/views.py b/dockerManager/views.py index 28069bf0b..3324485bf 100644 --- a/dockerManager/views.py +++ b/dockerManager/views.py @@ -743,5 +743,26 @@ def getContainerEnv(request): 'success': 0, 'message': str(e) }), content_type='application/json') + except KeyError: + return redirect(loadLoginPage) + +@preDockerRun +def listContainers(request): + """ + Get list of all Docker containers + """ + try: + userID = request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] == 1: + pass + else: + return ACLManager.loadErrorJson() + + cm = ContainerManager() + coreResult = cm.listContainers(userID) + + return coreResult except KeyError: return redirect(loadLoginPage) \ No newline at end of file diff --git a/firewall/firewallManager.py b/firewall/firewallManager.py index 5375343f8..e0d93c922 100644 --- a/firewall/firewallManager.py +++ b/firewall/firewallManager.py @@ -1755,6 +1755,267 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) + def getBannedIPs(self, userID=None): + """ + Get list of banned IP addresses + """ + try: + admin = Administrator.objects.get(pk=userID) + if admin.acl.adminStatus != 1: + return ACLManager.loadError() + + # For now, we'll use a simple file-based storage + # In production, you might want to use a database + banned_ips_file = '/etc/cyberpanel/banned_ips.json' + + banned_ips = [] + if os.path.exists(banned_ips_file): + try: + with open(banned_ips_file, 'r') as f: + banned_ips = json.load(f) + except: + banned_ips = [] + + # Filter out expired bans + current_time = time.time() + active_banned_ips = [] + + for banned_ip in banned_ips: + if banned_ip.get('expires') == 'Never' or banned_ip.get('expires', 0) > current_time: + banned_ip['active'] = True + if banned_ip.get('expires') != 'Never': + banned_ip['expires'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip['expires'])) + else: + banned_ip['expires'] = 'Never' + banned_ip['banned_on'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('banned_on', current_time))) + else: + banned_ip['active'] = False + banned_ip['expires'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('expires', current_time))) + banned_ip['banned_on'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('banned_on', current_time))) + + active_banned_ips.append(banned_ip) + + final_dic = {'status': 1, 'bannedIPs': active_banned_ips} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + except BaseException as msg: + final_dic = {'status': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + def addBannedIP(self, userID=None, data=None): + """ + Add a banned IP address + """ + try: + admin = Administrator.objects.get(pk=userID) + if admin.acl.adminStatus != 1: + return ACLManager.loadError() + + ip = data.get('ip', '').strip() + reason = data.get('reason', '').strip() + duration = data.get('duration', '24h') + + if not ip or not reason: + final_dic = {'status': 0, 'error_message': 'IP address and reason are required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Validate IP address format + import ipaddress + try: + ipaddress.ip_address(ip.split('/')[0]) # Handle CIDR notation + except ValueError: + final_dic = {'status': 0, 'error_message': 'Invalid IP address format'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Calculate expiration time + current_time = time.time() + if duration == 'permanent': + expires = 'Never' + else: + duration_map = { + '1h': 3600, + '24h': 86400, + '7d': 604800, + '30d': 2592000 + } + duration_seconds = duration_map.get(duration, 86400) + expires = current_time + duration_seconds + + # Load existing banned IPs + banned_ips_file = '/etc/cyberpanel/banned_ips.json' + banned_ips = [] + if os.path.exists(banned_ips_file): + try: + with open(banned_ips_file, 'r') as f: + banned_ips = json.load(f) + except: + banned_ips = [] + + # Check if IP is already banned + for banned_ip in banned_ips: + if banned_ip.get('ip') == ip and banned_ip.get('active', True): + final_dic = {'status': 0, 'error_message': f'IP address {ip} is already banned'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Add new banned IP + new_banned_ip = { + 'id': int(time.time()), + 'ip': ip, + 'reason': reason, + 'duration': duration, + 'banned_on': current_time, + 'expires': expires, + 'active': True + } + banned_ips.append(new_banned_ip) + + # Ensure directory exists + os.makedirs(os.path.dirname(banned_ips_file), exist_ok=True) + + # Save to file + with open(banned_ips_file, 'w') as f: + json.dump(banned_ips, f, indent=2) + + # Apply firewall rule to block the IP + try: + # Add iptables rule to block the IP + if '/' in ip: + # CIDR notation + subprocess.run(['iptables', '-A', 'INPUT', '-s', ip, '-j', 'DROP'], check=True) + else: + # Single IP + subprocess.run(['iptables', '-A', 'INPUT', '-s', ip, '-j', 'DROP'], check=True) + + logging.CyberCPLogFileWriter.writeToFile(f'Banned IP {ip} with reason: {reason}') + except subprocess.CalledProcessError as e: + logging.CyberCPLogFileWriter.writeToFile(f'Failed to add iptables rule for {ip}: {str(e)}') + + final_dic = {'status': 1, 'message': f'IP address {ip} has been banned successfully'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + except BaseException as msg: + final_dic = {'status': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + def removeBannedIP(self, userID=None, data=None): + """ + Remove/unban an IP address + """ + try: + admin = Administrator.objects.get(pk=userID) + if admin.acl.adminStatus != 1: + return ACLManager.loadError() + + banned_ip_id = data.get('id') + + # Load existing banned IPs + banned_ips_file = '/etc/cyberpanel/banned_ips.json' + banned_ips = [] + if os.path.exists(banned_ips_file): + try: + with open(banned_ips_file, 'r') as f: + banned_ips = json.load(f) + except: + banned_ips = [] + + # Find and update the banned IP + ip_to_unban = None + for banned_ip in banned_ips: + if banned_ip.get('id') == banned_ip_id: + banned_ip['active'] = False + banned_ip['unbanned_on'] = time.time() + ip_to_unban = banned_ip['ip'] + break + + if not ip_to_unban: + final_dic = {'status': 0, 'error_message': 'Banned IP not found'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Save updated banned IPs + with open(banned_ips_file, 'w') as f: + json.dump(banned_ips, f, indent=2) + + # Remove iptables rule + try: + # Remove iptables rule to unblock the IP + if '/' in ip_to_unban: + # CIDR notation + subprocess.run(['iptables', '-D', 'INPUT', '-s', ip_to_unban, '-j', 'DROP'], check=False) + else: + # Single IP + subprocess.run(['iptables', '-D', 'INPUT', '-s', ip_to_unban, '-j', 'DROP'], check=False) + + logging.CyberCPLogFileWriter.writeToFile(f'Unbanned IP {ip_to_unban}') + except subprocess.CalledProcessError as e: + logging.CyberCPLogFileWriter.writeToFile(f'Failed to remove iptables rule for {ip_to_unban}: {str(e)}') + + final_dic = {'status': 1, 'message': f'IP address {ip_to_unban} has been unbanned successfully'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + except BaseException as msg: + final_dic = {'status': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + def deleteBannedIP(self, userID=None, data=None): + """ + Permanently delete a banned IP record + """ + try: + admin = Administrator.objects.get(pk=userID) + if admin.acl.adminStatus != 1: + return ACLManager.loadError() + + banned_ip_id = data.get('id') + + # Load existing banned IPs + banned_ips_file = '/etc/cyberpanel/banned_ips.json' + banned_ips = [] + if os.path.exists(banned_ips_file): + try: + with open(banned_ips_file, 'r') as f: + banned_ips = json.load(f) + except: + banned_ips = [] + + # Find and remove the banned IP + ip_to_delete = None + updated_banned_ips = [] + for banned_ip in banned_ips: + if banned_ip.get('id') == banned_ip_id: + ip_to_delete = banned_ip['ip'] + else: + updated_banned_ips.append(banned_ip) + + if not ip_to_delete: + final_dic = {'status': 0, 'error_message': 'Banned IP record not found'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Save updated banned IPs + with open(banned_ips_file, 'w') as f: + json.dump(updated_banned_ips, f, indent=2) + + logging.CyberCPLogFileWriter.writeToFile(f'Deleted banned IP record for {ip_to_delete}') + + final_dic = {'status': 1, 'message': f'Banned IP record for {ip_to_delete} has been deleted successfully'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + except BaseException as msg: + final_dic = {'status': 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..117f057f2 100644 --- a/firewall/static/firewall/firewall.js +++ b/firewall/static/firewall/firewall.js @@ -16,9 +16,21 @@ app.controller('firewallController', function ($scope, $http) { $scope.couldNotConnect = true; $scope.rulesDetails = false; + // Banned IPs variables + $scope.activeTab = 'rules'; + $scope.bannedIPs = []; + $scope.bannedIPsLoading = false; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + $scope.banIP = ''; + $scope.banReason = ''; + $scope.banDuration = '24h'; + firewallStatus(); populateCurrentRecords(); + populateBannedIPs(); $scope.addRule = function () { @@ -2393,4 +2405,140 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) } } + // Banned IPs Functions + function populateBannedIPs() { + $scope.bannedIPsLoading = true; + var url = "/firewall/getBannedIPs"; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, {}, config).then(function(response) { + $scope.bannedIPsLoading = false; + if (response.data.status === 1) { + $scope.bannedIPs = response.data.bannedIPs || []; + } else { + $scope.bannedIPs = []; + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = response.data.error_message; + } + }, function(error) { + $scope.bannedIPsLoading = false; + $scope.bannedIPCouldNotConnect = false; + }); + } + + $scope.addBannedIP = function() { + if (!$scope.banIP || !$scope.banReason) { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = "Please fill in all required fields"; + return; + } + + $scope.bannedIPsLoading = true; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + + var data = { + ip: $scope.banIP, + reason: $scope.banReason, + duration: $scope.banDuration + }; + + var url = "/firewall/addBannedIP"; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + $scope.bannedIPsLoading = false; + if (response.data.status === 1) { + $scope.bannedIPActionSuccess = false; + $scope.banIP = ''; + $scope.banReason = ''; + $scope.banDuration = '24h'; + populateBannedIPs(); // Refresh the list + } else { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = response.data.error_message; + } + }, function(error) { + $scope.bannedIPsLoading = false; + $scope.bannedIPCouldNotConnect = false; + }); + }; + + $scope.removeBannedIP = function(id, ip) { + if (!confirm('Are you sure you want to unban IP address ' + ip + '?')) { + return; + } + + $scope.bannedIPsLoading = true; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + + var data = { id: id }; + + var url = "/firewall/removeBannedIP"; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + $scope.bannedIPsLoading = false; + if (response.data.status === 1) { + $scope.bannedIPActionSuccess = false; + populateBannedIPs(); // Refresh the list + } else { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = response.data.error_message; + } + }, function(error) { + $scope.bannedIPsLoading = false; + $scope.bannedIPCouldNotConnect = false; + }); + }; + + $scope.deleteBannedIP = function(id, ip) { + if (!confirm('Are you sure you want to permanently delete the record for IP address ' + ip + '? This action cannot be undone.')) { + return; + } + + $scope.bannedIPsLoading = true; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + + var data = { id: id }; + + var url = "/firewall/deleteBannedIP"; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + $scope.bannedIPsLoading = false; + if (response.data.status === 1) { + $scope.bannedIPActionSuccess = false; + populateBannedIPs(); // Refresh the list + } else { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = response.data.error_message; + } + }, function(error) { + $scope.bannedIPsLoading = false; + $scope.bannedIPCouldNotConnect = false; + }); + }; + }); \ No newline at end of file diff --git a/firewall/templates/firewall/firewall.html b/firewall/templates/firewall/firewall.html index dace7774b..2081c94ed 100644 --- a/firewall/templates/firewall/firewall.html +++ b/firewall/templates/firewall/firewall.html @@ -526,6 +526,265 @@ min-width: 600px; } } + + /* Tab Navigation Styles */ + .tab-navigation { + display: flex; + background: var(--bg-secondary, white); + border-radius: 12px; + padding: 0.5rem; + margin-bottom: 2rem; + box-shadow: 0 2px 8px var(--shadow-light, rgba(0,0,0,0.1)); + border: 1px solid var(--border-color, #e8e9ff); + } + + .tab-button { + flex: 1; + padding: 1rem 1.5rem; + background: transparent; + border: none; + border-radius: 8px; + color: var(--text-secondary, #64748b); + font-weight: 500; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + } + + .tab-button:hover { + background: var(--bg-hover, #f8f9ff); + color: var(--accent-color, #5b5fcf); + } + + .tab-button.tab-active { + background: var(--accent-color, #5b5fcf); + color: var(--bg-secondary, white); + box-shadow: 0 2px 8px rgba(91, 95, 207, 0.3); + } + + .tab-button i { + font-size: 1rem; + } + + /* Banned IPs Panel Styles */ + .banned-ips-panel { + background: var(--bg-secondary, white); + border-radius: 16px; + box-shadow: 0 4px 12px var(--shadow-medium, rgba(0,0,0,0.15)); + border: 1px solid var(--border-color, #e8e9ff); + overflow: hidden; + } + + .add-banned-section { + padding: 2rem; + border-bottom: 1px solid var(--border-light, #f3f4f6); + background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%); + } + + .banned-form { + display: grid; + grid-template-columns: 1fr 1fr 1fr auto; + gap: 1rem; + align-items: end; + } + + .btn-ban { + background: var(--danger-color, #ef4444); + color: var(--bg-secondary, white); + border: none; + padding: 0.75rem 1.5rem; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 0.5rem; + height: fit-content; + } + + .btn-ban:hover { + background: var(--danger-hover, #dc2626); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4); + } + + .banned-list-section { + padding: 2rem; + } + + .banned-table { + width: 100%; + border-collapse: collapse; + background: var(--bg-secondary, white); + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 8px var(--shadow-light, rgba(0,0,0,0.1)); + } + + .banned-table th { + background: linear-gradient(135deg, var(--bg-hover, #f8f9ff) 0%, var(--bg-gradient, #f0f1ff) 100%); + padding: 1rem; + text-align: left; + font-weight: 600; + color: var(--text-primary, #1e293b); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 2px solid var(--border-color, #e8e9ff); + } + + .banned-table td { + padding: 1rem; + color: var(--text-secondary, #64748b); + font-size: 0.875rem; + border-bottom: 1px solid var(--border-light, #f3f4f6); + vertical-align: middle; + } + + .banned-table tbody tr { + transition: all 0.2s ease; + } + + .banned-table tbody tr:hover { + background: var(--bg-hover, #f8f9ff); + } + + .ip-address { + font-weight: 600; + color: var(--text-primary, #1e293b); + font-family: 'Courier New', monospace; + } + + .ip-address i { + color: var(--accent-color, #5b5fcf); + margin-right: 0.5rem; + } + + .reason-text { + background: var(--bg-light, #f8f9fa); + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + color: var(--text-secondary, #64748b); + } + + .banned-date, .expires-date { + color: var(--text-secondary, #64748b); + font-size: 0.8rem; + } + + .banned-date i, .expires-date i { + color: var(--accent-color, #5b5fcf); + margin-right: 0.25rem; + } + + .status-badge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .status-badge.status-active { + background: rgba(239, 68, 68, 0.1); + color: var(--danger-color, #ef4444); + } + + .status-badge.status-expired { + background: rgba(107, 114, 128, 0.1); + color: #6b7280; + } + + .status-badge i { + font-size: 0.5rem; + } + + .actions { + display: flex; + gap: 0.5rem; + } + + .btn-unban { + background: var(--success-color, #10b981); + color: var(--bg-secondary, white); + border: none; + padding: 0.5rem 1rem; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.8rem; + } + + .btn-unban:hover { + background: var(--success-hover, #059669); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3); + } + + .btn-delete { + background: var(--danger-color, #ef4444); + color: var(--bg-secondary, white); + border: none; + padding: 0.5rem; + border-radius: 6px; + cursor: pointer; + transition: all 0.3s ease; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + } + + .btn-delete:hover { + background: var(--danger-hover, #dc2626); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3); + } + + /* Responsive Design for Banned IPs */ + @media (max-width: 768px) { + .banned-form { + grid-template-columns: 1fr; + gap: 1rem; + } + + .btn-ban { + width: 100%; + justify-content: center; + } + + .banned-list-section { + padding: 1rem; + overflow-x: auto; + } + + .banned-table { + min-width: 800px; + } + + .tab-navigation { + flex-direction: column; + } + + .tab-button { + width: 100%; + } + } {% endblock %} @@ -591,8 +850,26 @@ + +
+ + +
+ -
+
@@ -717,6 +994,144 @@
+ + +
+
+
+
+ +
+ {% trans "Banned IP Addresses" %} +
+
+
+ + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + +
+
USERIPCOUNTRYDATESESSIONACTIVITYUSERIPCOUNTRYDATESESSIONACTIVITY
Wed Jun 4 20:59 Still Logged In - +
+ + +
+ + + + + + + + + + + + + + + + + + + + +
{% trans "IP Address" %}{% trans "Reason" %}{% trans "Banned On" %}{% trans "Expires" %}{% trans "Status" %}{% trans "Actions" %}
+ + {$ bannedIP.ip $} + + {$ bannedIP.reason $} + + + {$ bannedIP.banned_on | date:'MMM dd, yyyy HH:mm' $} + + + {% trans "Never" %} + {$ bannedIP.expires | date:'MMM dd, yyyy HH:mm' $} + + + + {$ bannedIP.active ? 'Active' : 'Expired' $} + + + + +
+ + +
+ +

{% trans "No Banned IPs" %}

+

{% trans "All IP addresses are currently allowed. Add banned IPs to block suspicious or malicious traffic." %}

+
+ + + +
+
+ + {% trans "Action failed. Error message:" %} {$ bannedIPErrorMessage $} +
+ +
+ + {% trans "Action completed successfully." %} +
+ +
+ + {% trans "Could not connect to server. Please refresh this page." %} +
+
+ {% endblock %} \ No newline at end of file diff --git a/firewall/urls.py b/firewall/urls.py index 56e9fc187..b866bbbe7 100644 --- a/firewall/urls.py +++ b/firewall/urls.py @@ -32,6 +32,12 @@ urlpatterns = [ path('modSecRulesPacks', views.modSecRulesPacks, name='modSecRulesPacks'), path('getOWASPAndComodoStatus', views.getOWASPAndComodoStatus, name='getOWASPAndComodoStatus'), path('installModSecRulesPack', views.installModSecRulesPack, name='installModSecRulesPack'), + + # Banned IPs + path('getBannedIPs', views.getBannedIPs, name='getBannedIPs'), + path('addBannedIP', views.addBannedIP, name='addBannedIP'), + path('removeBannedIP', views.removeBannedIP, name='removeBannedIP'), + path('deleteBannedIP', views.deleteBannedIP, name='deleteBannedIP'), path('getRulesFiles', views.getRulesFiles, name='getRulesFiles'), path('enableDisableRuleFile', views.enableDisableRuleFile, name='enableDisableRuleFile'), diff --git a/firewall/views.py b/firewall/views.py index d7f91dd93..609af0adf 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) + +# Banned IPs Views +def getBannedIPs(request): + try: + userID = request.session['userID'] + fm = FirewallManager() + return fm.getBannedIPs(userID) + except KeyError: + return redirect(loadLoginPage) + +def addBannedIP(request): + try: + userID = request.session['userID'] + fm = FirewallManager() + return fm.addBannedIP(userID, json.loads(request.body)) + except KeyError: + return redirect(loadLoginPage) + +def removeBannedIP(request): + try: + userID = request.session['userID'] + fm = FirewallManager() + return fm.removeBannedIP(userID, json.loads(request.body)) + except KeyError: + return redirect(loadLoginPage) + +def deleteBannedIP(request): + try: + userID = request.session['userID'] + fm = FirewallManager() + return fm.deleteBannedIP(userID, json.loads(request.body)) + except KeyError: + return redirect(loadLoginPage) \ No newline at end of file