Linux evo.fastest-server.com 5.14.0-284.1101.el9.tuxcare.11.els11.x86_64 #1 SMP PREEMPT_DYNAMIC Fri Aug 14 13:30:35 UTC 2026 x86_64
LiteSpeed
Server IP : 103.249.112.113 & Your IP : 216.73.217.135
Domains : 988 Domain
User : tanishks
Terminal
Auto Root
Create File
Create Folder
Localroot Suggester
Backdoor Destroyer
Readme
/
usr /
share /
lve /
dbgovernor /
scripts /
Delete
Unzip
Name
Size
Permission
Date
Action
__pycache__
[ DIR ]
drwxr-xr-x
2026-09-04 11:34
chek_mysql_rpms_local
1.79
KB
-rwxr-xr-x
2026-08-06 17:01
cpanel-common-lve
2.16
KB
-rwxr-xr-x
2026-08-06 17:01
cpanel-delete-hooks
1.98
KB
-rwxr-xr-x
2026-08-06 17:01
cpanel-install-hooks
3.48
KB
-rwxr-xr-x
2026-08-06 17:01
cpanel-mysql-url-detect.pm
1.6
KB
-rwxr-xr-x
2026-08-06 17:01
cpanel_map_rebuilder
1.5
KB
-rwxr-xr-x
2026-08-06 17:01
dbgovernor_map
4.15
KB
-rwxr-xr-x
2026-08-06 17:01
dbgovernor_map.py
3.16
KB
-rwxr-xr-x
2026-08-06 17:01
dbgovernor_map_plesk.py
4.21
KB
-rwxr-xr-x
2026-08-06 17:01
dbgovernor_version.py
28
B
-rw-r--r--
2026-08-06 17:01
dbgovernor_watchdog.py
7.03
KB
-rwxr-xr-x
2026-08-06 17:01
detect-cpanel-mysql-version.pm
4.83
KB
-rwxr-xr-x
2026-08-06 17:01
map_hook
664
B
-rwxr-xr-x
2026-08-06 17:01
merge_logs.py
984
B
-rwxr-xr-x
2026-08-06 17:01
mysql_backup.sh
24.38
KB
-rwxr-xr-x
2026-08-06 17:01
mysql_hook
42
B
-rwxr-xr-x
2026-08-06 17:01
sentry_cleaner.sh
1.21
KB
-rwxr-xr-x
2026-08-06 17:01
sentry_daemon.py
19.72
KB
-rwxr-xr-x
2026-08-06 17:01
sentry_sdk_wrapper.py
7.14
KB
-rwxr-xr-x
2026-08-06 17:01
set_cpanel_mysql_version.pm
381
B
-rwxr-xr-x
2026-08-06 17:01
sync_hook
501
B
-rwxr-xr-x
2026-08-06 17:01
Save
Rename
#!/opt/cloudlinux/venv/bin/python3 # coding:utf-8 # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2024 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENSE.TXT # import os import sys import stat import time import signal import sentry_sdk import sentry_sdk_wrapper import logging import re import itertools DAEMON_INTERVAL = 10 # Cap how many depot entries are pulled per scan cycle. The depot is writable by the # less-privileged mysql user (see symlink-safety note below), so an attacker can plant an # unbounded number of *.txt files. scandir() + this cap keep the root daemon's per-cycle # memory bounded; the leftover entries are not dropped - they are picked up on the next # cycle, since each cycle re-scans the depot. MAX_DEPOT_ENTRIES_PER_CYCLE = 1000 SENTRY_DEPOT_ROOT = "/var/lve/dbgovernor/logging/sentry-depot" SENTRY_DEPOT_DB_GOVERNOR = SENTRY_DEPOT_ROOT + "/db_governor" SENTRY_DEPOT_MYSQLD = SENTRY_DEPOT_ROOT + "/mysqld" SENTRY_DEPOT_EXT = ".txt" DB_GOVERNOR_LOGS_WILDCARD = SENTRY_DEPOT_DB_GOVERNOR + "/*" + SENTRY_DEPOT_EXT MYSQLD_LOGS_WILDCARD = SENTRY_DEPOT_MYSQLD + "/*" + SENTRY_DEPOT_EXT class SentryDaemon: """ A daemon process to forward 'db_governor' and extended 'mysqld' logs to Sentry. """ def __init__(self, db_governor_logs_wildcard, mysqld_logs_wildcard): """ Initializes SentryDaemon with given log path wildcards. Args: db_governor_logs_wildcard (str): wildcard path for Sentry log files from db_governor. mysqld_logs_wildcard (str): wildcard path for Sentry log files from mysqld. """ self.db_governor_logs_wildcard = db_governor_logs_wildcard self.mysqld_logs_wildcard = mysqld_logs_wildcard sentry_sdk_wrapper.init() self.internal_logger = logging.getLogger("sentry_daemon") # for internal, non-forwarded events self.preface_sent = False class TerminateException(Exception): def __init__(self): super().__init__() @staticmethod def handle_sigterm(signum, frame): """ On SIGTERM signal, throw an exception caught in the outermost code and triggering daemon shutdown. Make sure this type of exception is re-thrown in all of inner try/except's. Args: signum (int): signal number. frame (frame): current stack frame. """ raise SentryDaemon.TerminateException() @staticmethod def print(s): print(s, file=sys.stderr) @staticmethod def print_sentry_transport_status(prompt): # use it for debugging, if you want to reconsider our interaction with Sentry transport SentryDaemon.print(f"{prompt}: queuse size={sentry_sdk.Hub.current.client.transport._worker._queue.qsize()}, healthy={sentry_sdk_wrapper.is_healthy()}") @staticmethod def _scan_depot(wildcard): """ Lazily yield depot file paths matching `wildcard` (a "<dir>/<pat>" glob). Unlike glob.iglob(), this does NOT materialize the whole directory: os.scandir() streams DirEntry objects from the kernel in batches, so a caller that stops early (via itertools.islice) leaves the rest of an attacker-sized backlog untouched and keeps peak memory bounded. The directory handle is closed by the `with` block. Symlinks are not yielded - the depot is writable by the less-privileged mysql user, and following links as root would leak arbitrary file contents (see F-23 note in the processing loop). is_symlink() uses scandir's cached lstat, so it does not follow. """ depot_dir = os.path.dirname(wildcard) pattern = os.path.basename(wildcard) # e.g. "*.txt" prefix, _, suffix = pattern.partition("*") # split the single wildcard star try: it_ctx = os.scandir(depot_dir) except FileNotFoundError: # depot not created yet -> empty, like glob.glob's silent [] return with it_ctx as it: for entry in it: name = entry.name if not (name.startswith(prefix) and name.endswith(suffix)): continue if entry.is_symlink(): # never enumerate (let alone follow) attacker-planted links continue yield entry.path def run(self): """ Starts the daemon to read log files and send logs to Sentry. """ self.print(f"Started reading log files in {self.db_governor_logs_wildcard} and {self.mysqld_logs_wildcard}") events_ever_lost, loss_report_sent, loss_reporting_complete = False, False, False while True: send_logs = True # by default, all found logs will be sent to Sentry # Handle loss reporting if events_ever_lost and not loss_reporting_complete: # Since the detection of event loss, we're struggling for reporting it. This reporting takes place only once per daemon session. send_logs = False # For this period, we suspend transmission of regular log files - otherwise they can choke us again and leave us no chance to render the loss on Sentry server. healthy = sentry_sdk_wrapper.is_healthy() # It's vital to make no movements while this is False. 'sentry_sdk' is fragile enough under high load, and we need to report the loss reliably. self.print(f"Event loss reporting phase, regular log files are being skipped; transport healthy: {healthy}") if healthy: # No state change while unhealthy. We wait for health _before_ sending the loss report, and once again _after_ sending it. if loss_report_sent: loss_reporting_complete = True # We're healthy after the loss report transmission. Loss reporting is over. self.print("Event loss reporting complete") # On the next iteration we shall return to normal log file processing. else: self.internal_logger.warning("Errors possibly lost") # We're healthy, but haven't yet sent the loss report. Send it now. loss_report_sent = True # We send it only once per daemon session. self.print("Event loss report sent") # Scan for log files report = "" report_nonzero = False for wildcard, logger in [ (self.db_governor_logs_wildcard, "db_governor"), (self.mysqld_logs_wildcard, "mysqld")]: logs = [] try: # scandir() is a *true* lazy iterator: it pulls directory entries from the # kernel in fixed-size batches, so islice() over it stops after the cap and # the remaining entries are never materialized. (glob.iglob() would defeat # this - for a non-recursive "dir/*" pattern CPython internally does # list(os.scandir(dir)) + fnmatch.filter(), draining the whole depot before # yielding anything, leaving peak memory O(N) in the attacker-planted file # count.) Remaining entries are handled next cycle, since each cycle re-scans. logs = list(itertools.islice(self._scan_depot(wildcard), MAX_DEPOT_ENTRIES_PER_CYCLE)) except SentryDaemon.TerminateException: # handling it separately spares us of knowing possible exception types from the scan raise except Exception as e: self.internal_logger.error(f"Failed to scan '{wildcard}': {e}") # bug, can't normally happen -> print locally + report to Sentry (loggers are intercepted by 'sentry_sdk') n_sent, n_deleted = 0, 0 for log in logs: # Symlink-safety: sentry_daemon runs as root and processes files under a # depot that is writable by the mysql user (libgovernor.so inside mysqld # writes there). A plain open()/os.remove() would follow attacker-planted # symlinks and leak arbitrary root-readable file contents to Sentry. Skip # symlinks here and pair it with O_NOFOLLOW on the read below so even a # race-replacement between this check and the open cannot bypass the # protection. Importantly, we do *not* unlink() the symlink: that would # delete operator-placed shipping symlinks, regressing legitimate setups. # Dangling symlinks become low-rate log noise (one line per cycle), not a # security hole. if os.path.islink(log): self.print(f"Skipping symlink '{log}'") # operator-set or attacker-planted -> never follow as root continue if not os.path.exists(log): self.print(f"Disappeared file '{log}'") # can be caused by races with sentry_cleaner.sh -> print only locally continue # MySQL version can be empty - it's not always available inside 'db_governor', # and never available in 'mysqld'. # The latter sounds so ridiculous, we surely have to fix it soon. match = re.match(r"(.*)-mysql\.", os.path.basename(log)) # basename() must not throw - 'log' is a valid path, proven above if not match: self.internal_logger.error(f"Invalid file name '{log}'") # bug -> print + Sentry else: ver_mysql = match.group(1) if send_logs: message = None try: # O_NOFOLLOW closes the TOCTOU window between islink() above # and this open(): if `log` has been swapped for a symlink in # between, open() fails with ELOOP instead of dereferencing. # O_NONBLOCK guarantees the open itself never blocks if `log` is # a FIFO/special the mysql user planted (no writer => infinite # hang of this single-threaded daemon otherwise). fd = os.open(log, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) try: f = os.fdopen(fd, 'r') # takes ownership of fd on success; on failure fd remains ours to close except BaseException: os.close(fd) raise with f: # fstat the opened fd (TOCTOU-safe): only ship regular files, # skipping FIFOs/sockets/devices a less-privileged writer planted. st = os.fstat(f.fileno()) if not stat.S_ISREG(st.st_mode): self.print(f"Skipping non-regular file '{log}'") # attacker-planted special -> never read as root message = None elif st.st_nlink > 1: # Hard-link safety: islink() and O_NOFOLLOW only ever catch a # symlink in the final path component. A hard link is an # ordinary directory entry sharing an inode with a file that # may live outside the depot (e.g. a root-readable file # elsewhere on the same filesystem), and it is S_ISREG just # like a real log file, so it passes every check above # unnoticed. A log file freshly written by mysqld has # st_nlink == 1; reject anything higher rather than read it as # root. As with the symlink check, we do not unlink() it here. self.print(f"Skipping hard-linked file '{log}'") # attacker-planted hard link -> never read as root message = None else: try: message = f.read() except SentryDaemon.TerminateException: raise except Exception as e: self.print(f"Failed to read {log}: {e}") # races -> print only except SentryDaemon.TerminateException: raise except Exception as e: self.print(f"Failed to open {log}: {e}") # races -> print only if message is not None: self.process_message(logger, message.strip(), ver_mysql) n_sent += 1 try: os.remove(log) n_deleted += 1 except SentryDaemon.TerminateException: raise except Exception as e: self.print(f"Failed to delete {log}: {e}") # races -> print only if len(report): report += "; " report += f"{logger}: {n_sent} sent, {n_deleted} deleted" if n_sent or n_deleted: report_nonzero = True if report_nonzero: # CLOS-2885, don't bloat log under zero Sentry activity self.print(report) # how many files were sent and deleted # Detect event loss. Telemetry-disabled (no DSN) is quiescent, not a failure: # there is nothing to send and no transport to lose events on, so we must not # flip events_ever_lost (which would suspend send_logs forever in a no-op spin). if (not events_ever_lost and not sentry_sdk_wrapper.telemetry_disabled() and not sentry_sdk_wrapper.is_healthy()): events_ever_lost = True # this could trigger due to Rate Limiting response from Sentry server, or due to local queue overflow, or other internal problem self.print("Event loss first detected") # Sleep for a bit before checking the log files again time.sleep(DAEMON_INTERVAL) def process_message(self, logger, message, ver_mysql): """ Processes a single message received from the client. Args: message (str): log message received. logger (str): logger to use for sending the message to Sentry. """ # The purpose of this preface Sentry event is to guarantee that we see the complete Python attributes, like module list, at least once - # because we strip them from the following forwarded events. if not self.preface_sent: self.internal_logger.warning("Hello, bad news, errors follow...") self.preface_sent = True self.print("Preface sent") norsqr = r"([^]]+)" # anything without right square bracket insqr = rf"\[{norsqr}\]" # anything in square brackets message_format = rf"\s*{insqr}\s*\[(\d+):(\d+)\][\s!]*\[{norsqr}:(\d+):{norsqr}\]\s*{insqr}\s*(.*)$" match = re.match(message_format, message) if match: timestamp, process, thread, src_file, src_line, src_func, tags, text = match.groups() try: process, thread, src_line = int(process), int(thread), int(src_line) except ValueError: match = None # use 'match' as a generic validity marker tags = tags.split(":") if not all(tags): # empty tags not permitted match = None tags = [t for t in tags if t != "ERRSENTRY"] # omit this one - it's always present in Sentry-reported log messages (unless we use some cryptic internal-use-only file flags) src_func += "()" if not match: self.internal_logger.error(f"Invalid message format in '{logger}' log: '{message}'") # sends to Sentry and prints locally return with sentry_sdk.push_scope() as scope: # set message-specific tags scope.set_tag("mysql.version", sentry_sdk_wrapper.VIS(ver_mysql)) scope.set_tag("actual_time", timestamp) scope.set_tag("process", process) scope.set_tag("thread", thread) for tag in tags: scope.set_tag(tag, True) # Extract every "key=val", replace with "key=<...>", and add "val" as a Sentry tag. def substitute_one_match(match): key, val = match.groups() # "specific." prefix - to easily distinguish them visually and to avoid clashes with common tags scope.set_tag("specific." + key, val.strip("'")) return f"{key}=<...>" text = re.sub(r"\b([a-zA-Z]\w*)=('.+?'|\w+\b)", substitute_one_match, text) # Sentry server overrides the transmitted value of 'event.type' and sets it to 'error' only if it finds the actual error cause - the exception. # 'sentry_sdk' is designed to catch and report exceptions in its native language environment - Python in our case. # To emulate an error event with the appropriate type, logger name and source code attributes, I found no easier way than building it manually: event = { "level": "error", "logger": logger, "exception": { # We need to trigger an error somehow. Alternatively, we could use 'threads'->'stacktrace', but it has its downsides. "values": [ { "type": text, # SIC! This is shown as an event title in case of exceptions. "value": "", "thread_id": thread, "stacktrace": { "frames": [ { "function": src_func, "lineno": src_line, "filename": src_file } ] } } ] } } sentry_sdk_wrapper.strip_event_pythonicity = True # tell before_send() to remove event attributes that are irrelevant for an event forwarded from C code sentry_sdk.capture_event(event) sentry_sdk_wrapper.strip_event_pythonicity = False def cleanup(self): """ Cleans up the resources used by the daemon. Does not clean the log files, so that they could be transmitted to Sentry on the next daemon run. """ pass if __name__ == "__main__": daemon = SentryDaemon(DB_GOVERNOR_LOGS_WILDCARD, MYSQLD_LOGS_WILDCARD) signal.signal(signal.SIGTERM, SentryDaemon.handle_sigterm) try: daemon.run() except SentryDaemon.TerminateException: # Unfortunately, we shouldn't print() here, because it often leads to errors - e.g., BrokenPipe. # stdout and stderr seem to be in complicated state during SIGTERM handling. daemon.cleanup() except KeyboardInterrupt: daemon.cleanup() finally: daemon.cleanup()