HEX
Server: LiteSpeed
System: Linux baran.dnsprotection.me 4.18.0-553.83.1.lve.el8.x86_64 #1 SMP Wed Nov 12 10:04:12 UTC 2025 x86_64
User: ddir12 (3618)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: //proc/self/root/opt/imunify360/venv/lib/python3.11/site-packages/im360/utils/__init__.py
import functools
import logging
import os
import pwd
import secrets
import shutil
import string
import subprocess
from pathlib import Path
from typing import Any, Callable, Type

from defence360agent.internals.feature_flags import is_enabled as is_ff_enabled

import yaml

from defence360agent.utils import LazyLock

logger = logging.getLogger(__name__)

MIN_TENANT_UID = 1000

# Reserved accounts that the password-reset RPCs must never target. Kept in
# sync with the proxy-side IsSystemUsername blocklist in src/proxy/auth/jwt.go.
SYSTEM_USERNAMES = frozenset(
    {
        "root",
        "daemon",
        "bin",
        "sys",
        "adm",
        "nobody",
        "_imunify",
        "mysql",
        "postgres",
        "redis",
        "memcached",
        "apache",
        "nginx",
        "httpd",
        "www-data",
        "www",
        "postfix",
        "mail",
        "mailman",
        "dovecot",
        "named",
        "bind",
        "dnsmasq",
        "nscd",
        "dbus",
        "polkitd",
        "chrony",
        "ntp",
        "sshd",
        "ftp",
        "vsftpd",
        "proftpd",
        "sync",
        "shutdown",
        "halt",
        "games",
        "operator",
        "lp",
        "news",
        "uucp",
        "man",
        "squid",
        "clamav",
        "clamupdate",
        "clamscan",
        "cpanel",
        "cpaneleximfilter",
        "cpses",
        "diradmin",
        "plesk",
        "psaadm",
        "psaftp",
        "varnish",
        "haproxy",
        "docker",
        "containerd",
        "systemd-network",
        "systemd-resolve",
        "systemd-timesync",
    }
)

_MERGED_CONFIG_PATH = Path(
    "/etc/sysconfig/imunify360/imunify360-merged.config"
)


class RulesLock(LazyLock):
    pass


class ModSecLock(LazyLock):
    pass


def return_value_on_error(error: Type[Exception], value: Any) -> Callable:
    """
    Decorator that causes coroutine to return *value* if it raises *error*.

    For example:

       return await return_value_on_error(FileNotFoundError, X)(coro)(*args)

    is equivalent to:

       try:
           return await coro(*args)
       except FileNotFoundError:
           return X
    """

    def decorator(coro):
        @functools.wraps(coro)
        async def wrapper(*args, **kwargs):
            try:
                return await coro(*args, **kwargs)
            except error as e:
                logger.info("Replacing %s from %s with %s", e, coro, value)
                return value

        return wrapper

    return decorator


class PasswordChangeError(Exception):
    def __init__(self, stderr, returncode):
        super().__init__(
            f"Password change process exited with code {returncode}: {stderr}"
        )


def generate_strong_password(
    length=20,
    required_char_groups=(
        string.ascii_lowercase,
        string.ascii_uppercase,
        string.digits,
        string.punctuation,
    ),
):
    """
    Return password with *length* char.
    It is guaranteed that password have at least one character from
    each given *required_char_groups*
    """
    if length < len(required_char_groups):
        raise ValueError(
            f"Given {length=} is too short. "
            "Can't get chars from all required groups "
            f"{len(required_char_groups)=}"
        )
    assert all(
        map(
            len,
            required_char_groups,
        )
    ), "got empty char group"

    alphabet = "".join(required_char_groups)
    while True:
        # generate password with given *length*
        # take characters from *alphabet* randomly
        # (uniformly/with equal probability)
        password = "".join([secrets.choice(alphabet) for _ in range(length)])
        # retry if the password is missing some required char groups
        if all(
            any(c in group for c in password) for group in required_char_groups
        ):
            return password


def _assert_non_system_user(username):
    from defence360agent.rpc_tools import ValidationError

    if not username or username.lower() in SYSTEM_USERNAMES:
        logger.warning(
            "Rejected password change for reserved account %r", username
        )
        raise ValidationError(
            "Refusing to change password for reserved system "
            f"account: {username!r}"
        )
    try:
        uid = pwd.getpwnam(username).pw_uid
    except KeyError:
        return
    if uid < MIN_TENANT_UID:
        logger.warning(
            "Rejected password change for non-tenant account %r (uid %d)",
            username,
            uid,
        )
        raise ValidationError(
            "Refusing to change password for non-tenant account "
            f"(uid {uid} < {MIN_TENANT_UID}): {username!r}"
        )


def change_system_password(username, new_password, *, passwd_cmd=None):
    _assert_non_system_user(username)
    if passwd_cmd is None:
        passwd_cmd = [shutil.which("passwd", path=os.defpath)]
    assert isinstance(passwd_cmd, list)
    try:
        subprocess.run(
            [*passwd_cmd, username],
            input=b"\n".join([new_password.encode()] * 2),
            capture_output=True,
            check=True,
        )
    except subprocess.CalledProcessError as e:
        raise PasswordChangeError(e.stderr, e.returncode)


def is_apache2nginx_enabled() -> bool:
    """Check if apache2nginx is enabled by reading /var/lib/apache2nginx/state."""
    state_file = Path("/var/lib/apache2nginx/state")
    if state_file.is_file():
        try:
            content = state_file.read_text().strip()
            return content == "on"
        except OSError:
            logger.exception("Failed to read apache2nginx state file")
    return False


def _use_feature_flags_from_yaml() -> bool:
    # Read WEBSHIELD.use_feature_flags directly from the merged config YAML
    # instead of going through the Webshield.USE_FEATURE_FLAGS FromConfig
    # descriptor. Reading the descriptor is what we'd normally do, but in
    # this code path it would cause unbounded recursion at agent startup:
    #
    #     use_coraza4cpanel()
    #       -> Webshield.USE_FEATURE_FLAGS              # FromConfig.__get__
    #         -> config_to_dict()
    #           -> cerberus Validator.normalized()      # normalizes the
    #              whole schema, including FIREWALL
    #             -> default_setter lambda for the      # firewall.py:70
    #                FIREWALL.TCP_IN_IPv4 etc. ports
    #               -> get_default_ports()              # firewall.py:28
    #                 -> HostingPanel().OPEN_PORTS
    #                   -> get_hosting_panel()
    #                     -> cPanel.__new__()           # cpanel/panel.py:100
    #                       -> use_coraza4cpanel()      # <-- back to start
    #
    # The cycle exists because HostingPanel ports defaults are resolved
    # lazily through HostingPanel() at normalization time, and cPanel's
    # __new__ branches on use_coraza4cpanel() to decide whether to return
    # the Coraza-flavored subclass. Adding a config dependency inside
    # use_coraza4cpanel closed the loop.
    #
    # Reading the YAML directly skips cerberus normalization and breaks the
    # cycle. The merged config is the file the agent writes after merging
    # defaults with user overrides, so it reflects the effective value the
    # descriptor would have returned. If the file is missing (fresh install,
    # or pre-feature-flags agent that hasn't rewritten it yet), we fall back
    # to True to match the schema default.
    try:
        with _MERGED_CONFIG_PATH.open() as fp:
            merged = yaml.safe_load(fp) or {}
    except FileNotFoundError:
        return True
    except (OSError, yaml.YAMLError) as error:
        logger.warning(
            "Failed to read %s: %s; assuming use_feature_flags=True",
            _MERGED_CONFIG_PATH,
            error,
        )
        return True
    return merged.get("WEBSHIELD", {}).get("use_feature_flags", True)


def _feature_flag_enabled(flag_name: str) -> bool:
    if not _use_feature_flags_from_yaml():
        return False
    return is_ff_enabled(flag_name, default=False)


def use_coraza4cpanel() -> bool:
    return _feature_flag_enabled("coraza_for_cpanel_apache")


def coraza_for_cloudways_hybrid_flag() -> bool:
    return _feature_flag_enabled("coraza_for_cloudways_hybrid")