Skip to content

Commit 85da2a1

Browse files
authored
Merge pull request #1606 from master3395/v2.5.5-dev
Implement ImunifyAV asset management and routing
2 parents 34f10ce + 0aca2a5 commit 85da2a1

File tree

8 files changed

+352
-13
lines changed

8 files changed

+352
-13
lines changed

CLManager/CageFS.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,30 @@ def submitinstallImunify(key):
249249
except BaseException as msg:
250250
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, str(msg) + ' [404].', 1)
251251

252+
@staticmethod
253+
def _ensure_imunifyav_assets(statusFile):
254+
try:
255+
commands = [
256+
'mkdir -p /etc/sysconfig/imunify360/generic',
257+
'mkdir -p /usr/local/CyberCP/public/imunifyav',
258+
'chown -R lscpd:lscpd /usr/local/CyberCP/public/imunifyav 2>/dev/null || true',
259+
'chmod 755 /usr/local/CyberCP/public/imunifyav 2>/dev/null || true',
260+
'chown -R lscpd:lscpd /etc/sysconfig/imunify360 2>/dev/null || true'
261+
]
262+
263+
for command in commands:
264+
ServerStatusUtil.executioner(command, statusFile)
265+
266+
if os.path.exists('/etc/redhat-release'):
267+
pkg_cmd = 'yum install -y imunify-ui-generic imunify-antivirus || yum reinstall -y imunify-ui-generic'
268+
else:
269+
pkg_cmd = 'apt-get update -y >/dev/null 2>&1 && apt-get install -y imunify-antivirus || true'
270+
271+
ServerStatusUtil.executioner(pkg_cmd, statusFile)
272+
except BaseException as msg:
273+
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
274+
f"ImunifyAV asset verification warning: {str(msg)}\n", 1)
275+
252276
@staticmethod
253277
def submitinstallImunifyAV():
254278
try:
@@ -327,6 +351,8 @@ def submitinstallImunifyAV():
327351
command = 'bash imav-deploy.sh --yes'
328352
ServerStatusUtil.executioner(command, statusFile)
329353

354+
CageFS._ensure_imunifyav_assets(statusFile)
355+
330356
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
331357
"ImunifyAV reinstalled..\n", 1)
332358

CyberCP/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
"""
1616
from django.urls import path, re_path, include
1717
from django.contrib import admin
18+
from firewall import views as firewall_views
1819

1920
urlpatterns = [
2021
path('base/', include('baseTemplate.urls')),
2122
path('', include('loginSystem.urls')),
23+
path('imunifyav/', firewall_views.imunifyAV, name='imunifyav_root'),
24+
path('ImunifyAV/', firewall_views.imunifyAV, name='imunifyav_root_legacy'),
2225
path('packages/', include('packages.urls')),
2326
path('websites/', include('websiteFunctions.urls')),
2427
path('tuning/', include('tuning.urls')),
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import sys
2+
import types
3+
import unittest
4+
from unittest import mock
5+
6+
# Provide minimal stubs when running tests outside the target servers.
7+
if 'django' not in sys.modules:
8+
django_stub = types.ModuleType("django")
9+
django_stub.setup = lambda: None
10+
sys.modules['django'] = django_stub
11+
12+
if 'MySQLdb' not in sys.modules:
13+
mysql_stub = types.ModuleType("MySQLdb")
14+
mysql_stub.connect = lambda *args, **kwargs: None
15+
cursors_stub = types.SimpleNamespace(SSCursor=object)
16+
mysql_stub.cursors = cursors_stub
17+
sys.modules['MySQLdb'] = mysql_stub
18+
sys.modules['MySQLdb.cursors'] = types.ModuleType("MySQLdb.cursors")
19+
sys.modules['MySQLdb.cursors'].SSCursor = object
20+
21+
from plogical import mysqlUtilities
22+
23+
24+
class DummyConnection:
25+
def __init__(self, literal_side_effect=None):
26+
self.literal_calls = []
27+
self.literal_side_effect = literal_side_effect
28+
self.closed = False
29+
30+
def literal(self, value):
31+
if self.literal_side_effect:
32+
raise self.literal_side_effect
33+
self.literal_calls.append(value)
34+
return f"'{value}'"
35+
36+
def close(self):
37+
self.closed = True
38+
39+
40+
class DummyCursor:
41+
def __init__(self):
42+
self.executed = []
43+
self.fetchone_value = None
44+
45+
def execute(self, query, params=None):
46+
self.executed.append((query, params))
47+
48+
def fetchone(self):
49+
return self.fetchone_value
50+
51+
52+
class MysqlUtilitiesChangePasswordTests(unittest.TestCase):
53+
def test_numeric_password_is_coerced_and_sanitized(self):
54+
connection = DummyConnection()
55+
cursor = DummyCursor()
56+
57+
with mock.patch.object(mysqlUtilities.mysqlUtilities, 'setupConnection', return_value=(connection, cursor)), \
58+
mock.patch.object(mysqlUtilities.mysqlUtilities, 'resolve_mysql_username', return_value='demo_user'), \
59+
mock.patch('plogical.mysqlUtilities.os.path.exists', return_value=False):
60+
result = mysqlUtilities.mysqlUtilities.changePassword('demo_user', 987654321)
61+
62+
self.assertEqual(result, 1)
63+
self.assertIn('987654321', connection.literal_calls)
64+
self.assertEqual(len(cursor.executed), 2) # USE mysql + password update
65+
self.assertIn("PASSWORD('987654321')", cursor.executed[-1][0])
66+
67+
def test_logs_error_when_literal_fails(self):
68+
connection = DummyConnection(literal_side_effect=ValueError("boom"))
69+
cursor = DummyCursor()
70+
71+
with mock.patch.object(mysqlUtilities.mysqlUtilities, 'setupConnection', return_value=(connection, cursor)), \
72+
mock.patch.object(mysqlUtilities.mysqlUtilities, 'resolve_mysql_username', return_value='demo_user'), \
73+
mock.patch('plogical.mysqlUtilities.os.path.exists', return_value=False), \
74+
mock.patch('plogical.mysqlUtilities.logging.CyberCPLogFileWriter.writeToFile') as log_mock:
75+
result = mysqlUtilities.mysqlUtilities.changePassword('demo_user', 'secret')
76+
77+
self.assertEqual(result, 0)
78+
log_calls = [call.args[0] for call in log_mock.call_args_list]
79+
self.assertTrue(any('[mysqlUtilities.changePassword.literal]' in msg for msg in log_calls))
80+
81+
82+
class MysqlUtilitiesResolveUserTests(unittest.TestCase):
83+
def test_resolves_username_when_orm_missing(self):
84+
class _StubManager:
85+
def get(self, *args, **kwargs):
86+
raise Exception("not found")
87+
88+
dbusers_stub = types.SimpleNamespace(objects=_StubManager())
89+
databases_stub = types.SimpleNamespace(objects=_StubManager())
90+
91+
cursor = DummyCursor()
92+
cursor.fetchone_value = None
93+
94+
with mock.patch('plogical.mysqlUtilities.DBUsers', dbusers_stub, create=True), \
95+
mock.patch('plogical.mysqlUtilities.Databases', databases_stub, create=True):
96+
resolved = mysqlUtilities.mysqlUtilities.resolve_mysql_username('mystery_user', cursor)
97+
98+
self.assertEqual(resolved, 'mystery_user')
99+
100+
101+
if __name__ == "__main__":
102+
unittest.main()
103+
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Lightweight regression test for the /imunifyav route.
4+
5+
Authenticates by injecting a valid CyberPanel session (using the first Administrator record),
6+
requests both /imunifyav and /ImunifyAV, and records the HTTP status alongside the
7+
imunify-antivirus service state.
8+
"""
9+
import json
10+
import os
11+
import subprocess
12+
import sys
13+
from datetime import datetime
14+
from pathlib import Path
15+
16+
ROOT_DIR = Path(__file__).resolve().parents[2]
17+
sys.path.append(str(ROOT_DIR))
18+
19+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings")
20+
21+
import django # noqa: E402
22+
django.setup()
23+
24+
from django.test import Client # noqa: E402
25+
from loginSystem.models import Administrator # noqa: E402
26+
27+
LOG_PATH = ROOT_DIR / "test_logs" / "imunifyav_route.log"
28+
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
29+
30+
31+
def get_service_state():
32+
try:
33+
output = subprocess.check_output(
34+
["systemctl", "is-active", "imunify-antivirus"],
35+
stderr=subprocess.STDOUT,
36+
timeout=10,
37+
text=True,
38+
).strip()
39+
return output
40+
except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
41+
return "unavailable"
42+
43+
44+
def perform_request(client, path):
45+
try:
46+
response = client.get(path, follow=True)
47+
return response.status_code, None
48+
except Exception as exc: # pragma: no cover - diagnostic logging
49+
return None, str(exc)
50+
51+
52+
def build_client():
53+
admin = Administrator.objects.first()
54+
if not admin:
55+
return None, "No administrator records available to seed the session."
56+
57+
client = Client()
58+
session = client.session
59+
session["userID"] = admin.pk
60+
session.save()
61+
return client, None
62+
63+
64+
def log_result(entry):
65+
entry["timestamp"] = datetime.utcnow().isoformat() + "Z"
66+
with open(LOG_PATH, "a", encoding="utf-8") as handle:
67+
handle.write(json.dumps(entry, ensure_ascii=False) + "\n")
68+
69+
70+
def main():
71+
client, client_error = build_client()
72+
service_state = get_service_state()
73+
results = {
74+
"module": "imunifyav_route_check",
75+
"service_state": service_state,
76+
"retry": 0,
77+
"errors": [],
78+
"responses": {},
79+
}
80+
81+
if client is None:
82+
results["errors"].append(client_error or "Unable to initialize Django test client.")
83+
log_result(results)
84+
print(json.dumps(results, indent=2))
85+
sys.exit(1)
86+
87+
for path in ("/imunifyav/", "/ImunifyAV/"):
88+
status_code, error = perform_request(client, path)
89+
results["responses"][path] = status_code
90+
if error:
91+
results["errors"].append(f"{path}: {error}")
92+
93+
log_result(results)
94+
print(json.dumps(results, indent=2))
95+
96+
if results["errors"]:
97+
sys.exit(1)
98+
99+
100+
if __name__ == "__main__":
101+
main()
102+

databases/databaseManager.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings")
99
django.setup()
1010
from django.http import HttpResponse
11+
from django.db.models import Q
1112
import json
1213
from plogical.acl import ACLManager
1314
import plogical.CyberCPLogFileWriter as logging
@@ -227,19 +228,26 @@ def changePassword(self, userID = None, data = None):
227228
return ACLManager.loadErrorJson('changePasswordStatus', 0)
228229

229230
userName = data['dbUserName']
230-
dbPassword = data['dbPassword']
231+
dbPassword = str(data['dbPassword']) if data['dbPassword'] is not None else ''
231232

232-
db = Databases.objects.filter(dbUser=userName)
233+
db_queryset = Databases.objects.filter(Q(dbUser=userName) | Q(dbName=userName))
234+
if not db_queryset.exists():
235+
data_ret = {'status': 0, 'changePasswordStatus': 0,
236+
'error_message': "Database or database user could not be found."}
237+
json_data = json.dumps(data_ret)
238+
return HttpResponse(json_data)
239+
240+
database_obj = db_queryset.first()
233241

234242
admin = Administrator.objects.get(pk=userID)
235243

236-
if ACLManager.checkOwnership(db[0].website.domain, admin, currentACL) == 1:
244+
if ACLManager.checkOwnership(database_obj.website.domain, admin, currentACL) == 1:
237245
pass
238246
else:
239247
return ACLManager.loadErrorJson()
240248

241249
try:
242-
meta = DBMeta.objects.get(database=db[0], key=DatabaseManager.REMOTE_ACCESS)
250+
meta = DBMeta.objects.get(database=database_obj, key=DatabaseManager.REMOTE_ACCESS)
243251
host = json.loads(meta.value)['remoteIP']
244252
except:
245253
host = None

firewall/templates/firewall/imunifyAV.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ <h3 class="title-hero">
2727

2828
<p>{% trans "ImunifyAV is now integrated via their new API. You can manage Imunify by clicking below. You can use your server root credentials to access Imunify." %}</p>
2929
<br>
30-
<a target="_blank" href="/imunifyav">
30+
<a target="_blank" href="{% url 'imunifyav_root' %}">
3131
<button class="btn btn-primary">Access Now
3232
</button>
3333
</a>

modules/fixes/cyberpanel_fixes.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,34 @@ fix_missing_dependencies() {
216216
print_status "$GREEN" "✅ Missing dependencies fixed"
217217
}
218218

219+
ensure_imunify_av_assets() {
220+
local package_manager=$1
221+
222+
print_status "$BLUE" "🔐 Ensuring ImunifyAV UI assets are installed..."
223+
224+
mkdir -p /etc/sysconfig/imunify360/generic 2>/dev/null || true
225+
mkdir -p /usr/local/CyberCP/public/imunifyav 2>/dev/null || true
226+
227+
case $package_manager in
228+
"yum"|"dnf")
229+
if ! $package_manager install -y imunify-ui-generic imunify-antivirus >/dev/null 2>&1; then
230+
print_status "$YELLOW" "⚠️ Unable to install Imunify packages via $package_manager (will continue)."
231+
fi
232+
;;
233+
"apt")
234+
apt-get update -y >/dev/null 2>&1 || true
235+
if ! apt-get install -y imunify-antivirus >/dev/null 2>&1; then
236+
print_status "$YELLOW" "⚠️ imunify-antivirus package not available in APT repositories."
237+
fi
238+
;;
239+
esac
240+
241+
chown -R lscpd:lscpd /usr/local/CyberCP/public/imunifyav 2>/dev/null || true
242+
chown -R lscpd:lscpd /etc/sysconfig/imunify360 2>/dev/null || true
243+
244+
print_status "$GREEN" "✅ ImunifyAV assets verified"
245+
}
246+
219247
# Function to check service status
220248
check_service_status() {
221249
local service_name=$1
@@ -352,6 +380,9 @@ apply_cyberpanel_fixes() {
352380

353381
# Fix missing dependencies
354382
fix_missing_dependencies "$package_manager"
383+
384+
# Ensure ImunifyAV UI assets are always present
385+
ensure_imunify_av_assets "$package_manager"
355386

356387
print_status "$GREEN" "✅ All CyberPanel fixes applied successfully"
357388

0 commit comments

Comments
 (0)