Merge pull request #1508 from master3395/v2.5.5-dev

V2.5.5 dev
This commit is contained in:
Usman Nasir
2025-09-19 11:20:08 +05:00
committed by GitHub
71 changed files with 7072 additions and 1608 deletions

View File

@@ -980,6 +980,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$scope.showAddonRequired = false;
$scope.addonInfo = {};
// IP Blocking functionality
$scope.blockingIP = null;
$scope.blockedIPs = {};
$scope.analyzeSSHSecurity = function() {
$scope.loadingSecurityAnalysis = true;
$scope.showAddonRequired = false;
@@ -999,6 +1003,64 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$scope.loadingSecurityAnalysis = false;
});
};
$scope.blockIPAddress = function(ipAddress) {
if (!$scope.blockingIP) {
$scope.blockingIP = ipAddress;
var data = {
ip_address: ipAddress
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post('/base/blockIPAddress', data, config).then(function (response) {
$scope.blockingIP = null;
if (response.data && response.data.status === 1) {
// Mark IP as blocked
$scope.blockedIPs[ipAddress] = true;
// Show success notification
new PNotify({
title: 'Success',
text: `IP address ${ipAddress} has been blocked successfully using ${response.data.firewall.toUpperCase()}`,
type: 'success',
delay: 5000
});
// Refresh security analysis to update alerts
$scope.analyzeSSHSecurity();
} else {
// Show error notification
new PNotify({
title: 'Error',
text: response.data && response.data.error ? response.data.error : 'Failed to block IP address',
type: 'error',
delay: 5000
});
}
}, function (err) {
$scope.blockingIP = null;
var errorMessage = 'Failed to block IP address';
if (err.data && err.data.error) {
errorMessage = err.data.error;
} else if (err.data && err.data.message) {
errorMessage = err.data.message;
}
new PNotify({
title: 'Error',
text: errorMessage,
type: 'error',
delay: 5000
});
});
}
};
// Initial fetch
$scope.refreshTopProcesses();

View File

@@ -663,6 +663,23 @@
<strong style="font-size: 12px; color: #1e293b;">Recommendation:</strong>
<p style="margin: 4px 0 0 0; font-size: 12px; color: #475569; white-space: pre-line;">{$ alert.recommendation $}</p>
</div>
<!-- Add to Firewall Button for Brute Force Attacks -->
<div ng-if="alert.title === 'Brute Force Attack Detected' && alert.details && alert.details['IP Address']" style="margin-top: 12px;">
<button ng-click="blockIPAddress(alert.details['IP Address'])"
ng-disabled="blockingIP === alert.details['IP Address']"
style="background: #dc2626; color: white; border: none; padding: 8px 16px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; gap: 6px;"
onmouseover="this.style.background='#b91c1c'"
onmouseout="this.style.background='#dc2626'">
<i class="fas fa-ban" ng-if="blockingIP !== alert.details['IP Address']"></i>
<i class="fas fa-spinner fa-spin" ng-if="blockingIP === alert.details['IP Address']"></i>
<span ng-if="blockingIP !== alert.details['IP Address']">Block IP</span>
<span ng-if="blockingIP === alert.details['IP Address']">Blocking...</span>
</button>
<span ng-if="blockedIPs && blockedIPs[alert.details['IP Address']]"
style="margin-left: 10px; color: #10b981; font-size: 12px; font-weight: 600;">
<i class="fas fa-check-circle"></i> Blocked
</span>
</div>
</div>
<span style="background: {$ alert.severity === 'high' ? '#fee2e2' : (alert.severity === 'medium' ? '#fef3c7' : (alert.severity === 'low' ? '#dbeafe' : '#d1fae5')) $};
color: {$ alert.severity === 'high' ? '#dc2626' : (alert.severity === 'medium' ? '#f59e0b' : (alert.severity === 'low' ? '#3b82f6' : '#10b981')) $};

View File

@@ -20,6 +20,9 @@
{{ cosmetic.MainDashboardCSS | safe }}
</style>
<!-- Mobile Responsive CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/mobile-responsive.css' %}?v={{ CP_VERSION }}">
<!-- Core Scripts -->
<script src="{% static 'baseTemplate/angularjs.1.6.5.js' %}?v={{ CP_VERSION }}"></script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
@@ -955,7 +958,7 @@
<body>
<!-- Header -->
<div id="header">
<button id="mobile-menu-toggle" onclick="toggleSidebar()">
<button id="mobile-menu-toggle" class="mobile-menu-toggle" onclick="toggleSidebar()" style="display: none;">
<i class="fas fa-bars"></i>
</button>
<div class="logo">
@@ -1177,6 +1180,9 @@
<a href="{% url 'listChildDomains' %}" class="menu-item">
<span>List Sub/Addon Domains</span>
</a>
<a href="{% url 'fixSubdomainLogs' %}" class="menu-item">
<span>Fix Subdomain Logs</span>
</a>
{% if admin or modifyWebsite %}
<a href="{% url 'modifyWebsite' %}" class="menu-item">
<span>Modify Website</span>
@@ -1457,6 +1463,10 @@
<a href="{% url 'manageSSL' %}" class="menu-item">
<span>Manage SSL</span>
</a>
<a href="{% url 'sslReconcile' %}" class="menu-item">
<span>SSL Reconciliation</span>
<span class="badge">NEW</span>
</a>
{% endif %}
{% if admin or hostnameSSL %}
<a href="{% url 'sslForHostName' %}" class="menu-item">
@@ -1786,9 +1796,51 @@
<!-- Scripts -->
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('show');
const sidebar = document.getElementById('sidebar');
const mainContent = document.getElementById('main-content');
const mobileToggle = document.getElementById('mobile-menu-toggle');
sidebar.classList.toggle('show');
// Add/remove sidebar-open class to main content for mobile
if (window.innerWidth <= 768) {
if (mainContent) {
mainContent.classList.toggle('sidebar-open');
}
}
}
// Show/hide mobile menu toggle based on screen size
function handleResize() {
const mobileToggle = document.getElementById('mobile-menu-toggle');
const sidebar = document.getElementById('sidebar');
const mainContent = document.getElementById('main-content');
if (window.innerWidth <= 768) {
mobileToggle.style.display = 'block';
// Close sidebar by default on mobile
sidebar.classList.remove('show');
if (mainContent) {
mainContent.classList.remove('sidebar-open');
}
} else {
mobileToggle.style.display = 'none';
// Show sidebar on desktop
sidebar.classList.remove('show');
if (mainContent) {
mainContent.classList.remove('sidebar-open');
}
}
}
// Add event listener for window resize
window.addEventListener('resize', handleResize);
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
handleResize();
});
function toggleSubmenu(id, element) {
const submenu = document.getElementById(id);
const allSubmenus = document.querySelectorAll('.submenu');

View File

@@ -24,6 +24,7 @@ urlpatterns = [
re_path(r'^getSSHUserActivity$', views.getSSHUserActivity, name='getSSHUserActivity'),
re_path(r'^getTopProcesses$', views.getTopProcesses, name='getTopProcesses'),
re_path(r'^analyzeSSHSecurity$', views.analyzeSSHSecurity, name='analyzeSSHSecurity'),
re_path(r'^blockIPAddress$', views.blockIPAddress, name='blockIPAddress'),
re_path(r'^dismiss_backup_notification$', views.dismiss_backup_notification, name='dismiss_backup_notification'),
re_path(r'^dismiss_ai_scanner_notification$', views.dismiss_ai_scanner_notification, name='dismiss_ai_scanner_notification'),
re_path(r'^get_notification_preferences$', views.get_notification_preferences, name='get_notification_preferences'),

View File

@@ -820,25 +820,18 @@ def analyzeSSHSecurity(request):
alerts = []
# Detect which firewall is in use
firewall_cmd = ''
# Use firewalld (CSF has been discontinued)
firewall_cmd = 'firewalld'
try:
# Check for CSF
csf_check = ProcessUtilities.outputExecutioner('which csf')
if csf_check and '/csf' in csf_check:
firewall_cmd = 'csf'
# Verify firewalld is active
firewalld_check = ProcessUtilities.outputExecutioner('systemctl is-active firewalld')
if not (firewalld_check and 'active' in firewalld_check):
# Firewalld not active, but continue analysis with firewalld commands
pass
except:
# Continue with firewalld as default
pass
if not firewall_cmd:
try:
# Check for firewalld
firewalld_check = ProcessUtilities.outputExecutioner('systemctl is-active firewalld')
if firewalld_check and 'active' in firewalld_check:
firewall_cmd = 'firewalld'
except:
firewall_cmd = 'firewalld' # Default to firewalld
# Determine log path
distro = ProcessUtilities.decideDistro()
if distro in [ProcessUtilities.ubuntu, ProcessUtilities.ubuntu20]:
@@ -941,10 +934,7 @@ def analyzeSSHSecurity(request):
# High severity: Brute force attacks
for ip, count in failed_passwords.items():
if count >= 10:
if firewall_cmd == 'csf':
recommendation = f'Block this IP immediately:\ncsf -d {ip} "Brute force attack - {count} failed attempts"'
else:
recommendation = f'Block this IP immediately:\nfirewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address={ip} drop" && firewall-cmd --reload'
recommendation = f'Block this IP immediately:\nfirewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address={ip} drop" && firewall-cmd --reload'
alerts.append({
'title': 'Brute Force Attack Detected',
@@ -1108,6 +1098,144 @@ def analyzeSSHSecurity(request):
except Exception as e:
return HttpResponse(json.dumps({'error': str(e)}), content_type='application/json', status=500)
@csrf_exempt
@require_POST
def blockIPAddress(request):
"""
Block an IP address using the appropriate firewall (CSF or firewalld)
"""
try:
user_id = request.session.get('userID')
if not user_id:
return HttpResponse(json.dumps({'error': 'Not logged in'}), content_type='application/json', status=403)
currentACL = ACLManager.loadedACL(user_id)
if not currentACL.get('admin', 0):
return HttpResponse(json.dumps({'error': 'Admin only'}), content_type='application/json', status=403)
# Check if user has CyberPanel addons
if not ACLManager.CheckForPremFeature('all'):
return HttpResponse(json.dumps({
'status': 0,
'error': 'Premium feature required'
}), content_type='application/json', status=403)
data = json.loads(request.body)
ip_address = data.get('ip_address', '').strip()
if not ip_address:
return HttpResponse(json.dumps({
'status': 0,
'error': 'IP address is required'
}), content_type='application/json', status=400)
# Validate IP address format and check for private/reserved ranges
import re
import ipaddress
ip_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
if not re.match(ip_pattern, ip_address):
return HttpResponse(json.dumps({
'status': 0,
'error': 'Invalid IP address format'
}), content_type='application/json', status=400)
# Check for private/reserved IP ranges to prevent self-blocking
try:
ip_obj = ipaddress.ip_address(ip_address)
if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local or ip_obj.is_reserved:
return HttpResponse(json.dumps({
'status': 0,
'error': 'Cannot block private, loopback, link-local, or reserved IP addresses'
}), content_type='application/json', status=400)
# Additional check for common problematic ranges
if (ip_address.startswith('127.') or # Loopback
ip_address.startswith('169.254.') or # Link-local
ip_address.startswith('224.') or # Multicast
ip_address.startswith('255.') or # Broadcast
ip_address in ['0.0.0.0', '::1']): # Invalid/loopback
return HttpResponse(json.dumps({
'status': 0,
'error': 'Cannot block system or reserved IP addresses'
}), content_type='application/json', status=400)
except ValueError:
return HttpResponse(json.dumps({
'status': 0,
'error': 'Invalid IP address'
}), content_type='application/json', status=400)
# Use firewalld (CSF has been discontinued)
firewall_cmd = 'firewalld'
try:
# Verify firewalld is active using subprocess for better security
import subprocess
firewalld_check = subprocess.run(['systemctl', 'is-active', 'firewalld'],
capture_output=True, text=True, timeout=10)
if not (firewalld_check.returncode == 0 and 'active' in firewalld_check.stdout):
return HttpResponse(json.dumps({
'status': 0,
'error': 'Firewalld is not active. Please enable firewalld service.'
}), content_type='application/json', status=500)
except subprocess.TimeoutExpired:
return HttpResponse(json.dumps({
'status': 0,
'error': 'Timeout checking firewalld status'
}), content_type='application/json', status=500)
except Exception as e:
return HttpResponse(json.dumps({
'status': 0,
'error': f'Cannot check firewalld status: {str(e)}'
}), content_type='application/json', status=500)
# Block the IP address using firewalld with subprocess for better security
success = False
error_message = ''
try:
# Use subprocess with explicit argument lists to prevent injection
rich_rule = f'rule family=ipv4 source address={ip_address} drop'
add_rule_cmd = ['firewall-cmd', '--permanent', '--add-rich-rule', rich_rule]
# Execute the add rule command
result = subprocess.run(add_rule_cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0:
# Reload firewall rules
reload_cmd = ['firewall-cmd', '--reload']
reload_result = subprocess.run(reload_cmd, capture_output=True, text=True, timeout=30)
if reload_result.returncode == 0:
success = True
else:
error_message = f'Failed to reload firewall rules: {reload_result.stderr}'
else:
error_message = f'Failed to add firewall rule: {result.stderr}'
except subprocess.TimeoutExpired:
error_message = 'Firewall command timed out'
except Exception as e:
error_message = f'Firewall command failed: {str(e)}'
if success:
# Log the action
import plogical.CyberCPLogFileWriter as logging
logging.CyberCPLogFileWriter.writeToFile(f'IP address {ip_address} blocked via CyberPanel dashboard by user {user_id}')
return HttpResponse(json.dumps({
'status': 1,
'message': f'Successfully blocked IP address {ip_address}',
'firewall': firewall_cmd
}), content_type='application/json')
else:
return HttpResponse(json.dumps({
'status': 0,
'error': error_message or 'Failed to block IP address'
}), content_type='application/json', status=500)
except Exception as e:
return HttpResponse(json.dumps({
'status': 0,
'error': f'Server error: {str(e)}'
}), content_type='application/json', status=500)
@csrf_exempt
@require_POST
def getSSHUserActivity(request):