Current File : //opt/imunify360/venv/lib64/python3.11/site-packages/defence360agent/subsys/panels/plesk/panel.py
import base64
import logging
import os
from binascii import Error as base64Error
from pathlib import Path
from typing import Dict, List, Sequence, Set, Tuple, Union
from xml.etree import ElementTree

from defence360agent.application.determine_hosting_panel import (
    is_plesk_installed,
)
from defence360agent.utils import OsReleaseInfo

from .. import base
from . import api
from .api import list_docroots
from .utils import PleskConfig

PLESK_KEY_REGISTRY = "/etc/sw/keys/"
PLESK_IMUNIFY360_PRODUCT_NAME = "ext-imunify360"
TCP_PORTS_PLESK = base.TCP_PORTS_COMMON + ["953", "990", "8443", "8447"]

logger = logging.getLogger(__name__)


def _safe_get_text(node, tag) -> str:
    """Avoid AttributeError if tag not found"""

    _node = node.find(tag)
    if _node is not None:
        return _node.text
    return ""


def _get_key_data(key) -> Tuple[str, str]:
    """Return product name and filename from key data"""

    filename = ""
    key_product_name = ""
    for data in key.findall("value/struct/member"):
        if _safe_get_text(data, "name") == "filename":
            filename = _safe_get_text(data, "value/string")
        if _safe_get_text(data, "name") == "key_product_name":
            key_product_name = _safe_get_text(data, "value/string")
    return key_product_name, filename


class PleskException(base.PanelException):
    pass


class Plesk(base.AbstractPanel):
    NAME = "Plesk"
    OPEN_PORTS = {
        "tcp": {
            "in": ["143", "465", "8880", "49152-65535"] + TCP_PORTS_PLESK,
            "out": ["113", "5224"] + TCP_PORTS_PLESK,
        },
        "udp": {
            "in": ["20", "21", "53", "443"],
            "out": ["20", "21", "53", "113", "123"],
        },
    }
    exception = PleskException

    @classmethod
    def is_installed(cls):
        return is_plesk_installed()

    @staticmethod
    async def version():
        with open("/usr/local/psa/version", "r") as f:
            return f.read().split()[0]

    @base.ensure_valid_panel()
    async def enable_imunify360_plugin(self, name=None):
        pass

    @base.ensure_valid_panel()
    async def disable_imunify360_plugin(self, plugin_name=None):
        pass

    async def get_users(self) -> List[str]:
        """Returns a list of Plesk system users"""
        try:
            return await api.get_users()
        except base.PanelException as e:
            logger.error("Failed to get users: %s", e)
            return []

    async def get_user_domains(self):
        """
        :return: list: domains hosted on server via plesk
        """
        return await api.get_domains()

    async def get_domain_to_owner(self):
        """
        :return: domain to list of users pairs
        """
        return await api.get_domain_to_user()

    async def get_user_to_email(self) -> Dict[str, str]:
        """
        Returns dict with user to email pairs
        """
        return await api.get_user_to_email()

    async def get_domains_per_user(self):
        """
        :return: user to list of domains pairs
        """
        return await api.get_user_to_domain()

    async def users_count(self) -> int:
        return await api.count_customers_with_subscriptions()

    @classmethod
    def get_modsec_config_path(cls):
        if OsReleaseInfo.id_like() & OsReleaseInfo.DEBIAN:
            return "/etc/apache2/mods-available/security2.conf"
        else:
            return "/etc/httpd/conf.d/security2.conf"

    def basedirs(self) -> Set[str]:
        basedir = PleskConfig("HTTPD_VHOSTS_D").get()
        return {basedir} if basedir else set()

    @classmethod
    def base_home_dir(cls, _) -> Path:
        # Local import to save memory on other panels
        from configparser import ConfigParser

        with open("/etc/psa/psa.conf") as c:
            text = "[dummy section]\n" + c.read()
        config = ConfigParser(delimiters=[" ", "\t"])
        config.read_string(text)
        base_dir = Path(
            config["dummy section"].get("HTTPD_VHOSTS_D", "/var/www/vhosts")
        )
        return base_dir

    @classmethod
    def _retrieve_key(cls) -> Union[str, None]:
        """Parse xml of registry and corresponding key file to retrive
        product key.

        return: str key or None if not found.
        """

        registry = ElementTree.parse(
            os.path.join(PLESK_KEY_REGISTRY, "registry.xml")
        )
        for member in registry.getroot().findall("struct/member"):
            if _safe_get_text(member, "name") == "active":
                for key in member.findall("value/struct/member"):
                    key_product_name, filename = _get_key_data(key)
                    if key_product_name == PLESK_IMUNIFY360_PRODUCT_NAME:
                        key_value = _safe_get_text(
                            ElementTree.parse(
                                os.path.join(
                                    PLESK_KEY_REGISTRY, "keys", filename
                                )
                            ),
                            "{http://parallels.com/schemas/keys/aps/3}"
                            "key-body",
                        )
                        return base64.b64decode(key_value.encode()).decode()
        return None

    @classmethod
    async def retrieve_key(cls) -> str:
        """Returns registration key from registered keys, if possible, raise
        PleskException if not successful."""

        try:
            result = cls._retrieve_key()
        except (ElementTree.ParseError, base64Error, FileNotFoundError) as e:
            raise PleskException("failed to retrieve key with error %s" % e)
        if result:
            logger.info("key retrieved %s", result)
            return result
        raise PleskException("The key not found")

    async def list_docroots(self):
        """
        :return: dict docroot to domain
        """
        docroot_domains_users = await api.list_docroots_domains_users()
        return {
            docroot: domain for docroot, domain, _ in docroot_domains_users
        }