ok

Mini Shell

Direktori : /opt/imunify360/venv/lib/python3.11/site-packages/im360/plugins/
Upload File :
Current File : //opt/imunify360/venv/lib/python3.11/site-packages/im360/plugins/pam_manager.py

"""PAM module management plugin.

Changes PAM module state (enabled/disabled) to match imunify360 config.
"""
import asyncio
import contextlib
import logging

from defence360agent.contracts import config
from defence360agent.contracts.config import SystemConfig
from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.plugins import MessageSink, expect
from defence360agent.utils import recurring_check
from im360.subsys import ossec, pam
from im360.simple_rpc.resident_socket import send_to_socket

logger = logging.getLogger(__name__)


class PAMManager(MessageSink):
    _CONFIG_PERIODIC_CHECK = 3600  # seconds
    _SSHD_ENABLED = config.FromConfig("PAM", "enable")
    _DOVECOT_PROTECTION_ENABLED = config.FromConfig(
        "PAM", "exim_dovecot_protection"
    )
    _DOVECOT_NATIVE_ENABLED = config.FromConfig("PAM", "exim_dovecot_native")
    _FTP_ENABLED = config.FromConfig("PAM", "ftp_protection")

    def __init__(self):
        self._tasks = []
        self._status_check_required = asyncio.Event()
        self._loop = None

    async def create_sink(self, loop) -> None:
        self._loop = loop
        self._tasks.append(loop.create_task(self._status_checker()))
        self._tasks.append(loop.create_task(self._initiate_status_check()))

    async def shutdown(self) -> None:
        for task in self._tasks:
            if task is not None:
                task.cancel()
                with contextlib.suppress(asyncio.CancelledError):
                    await task

    async def _ensure_status(self) -> None:
        status = await pam.get_status()

        dovecot_protection_enabled = await self._ensure_status_for_dovecot(
            desired_dovecot_status=pam.DovecotStatus.DISABLED
            if not self._DOVECOT_PROTECTION_ENABLED
            else pam.DovecotStatus.PAM
            if not self._DOVECOT_NATIVE_ENABLED
            else pam.DovecotStatus.NATIVE,
            pam_status=status,
        )

        ftp_enabled = await self._ensure_status_for_service(
            self._FTP_ENABLED, status, pam.PamService.FTP
        )

        sshd_enabled = await self._ensure_status_for_service(
            self._SSHD_ENABLED, status, pam.PamService.SSHD
        )
        something_has_been_enabled = any(
            [
                dovecot_protection_enabled,
                ftp_enabled,
                sshd_enabled,
            ]
        )
        # ensure OSSEC status
        status = dict(await pam.get_status())
        # . merge dovecot status for ossec
        status[ossec.DOVECOT] = (
            pam.PamServiceStatusValue.disabled
            if status[pam.PamService.DOVECOT_NATIVE]
            == status[pam.PamService.DOVECOT_PAM]
            == pam.PamServiceStatusValue.disabled
            else pam.PamServiceStatusValue.enabled
        )
        del status[pam.PamService.DOVECOT_NATIVE]
        del status[pam.PamService.DOVECOT_PAM]
        try:
            await ossec.configure_for_pam(status)
        except ossec.OssecRulesError as exc:
            logger.error("Failed to update OSSEC configuration: %s", exc)
        if something_has_been_enabled:
            await send_to_socket(
                msg={
                    "method": "UPDATE_CUSTOM_LISTS",
                },
                wait_for_response=False,
            )

    async def _ensure_status_for_dovecot(
        self, desired_dovecot_status: pam.DovecotStatus, pam_status: dict
    ) -> bool:
        """Ensure pam status corresponds to the desired dovecot status.

        Special handling for 3 states.

        Return whether pam/native modules were enabled.
        """
        if desired_dovecot_status is pam.DovecotStatus.DISABLED:
            if not (
                pam_status[pam.PamService.DOVECOT_NATIVE]
                == pam_status[pam.PamService.DOVECOT_PAM]
                == pam.PamServiceStatusValue.disabled
            ):  # something is enabled
                # disable dovecot
                # note: either pam/native will do here; both should be disabled
                await pam.disable(pam.PamService.DOVECOT_NATIVE)
                logger.info("PAM module has been disabled for dovecot")
        elif desired_dovecot_status is pam.DovecotStatus.PAM:
            if (
                pam_status[pam.PamService.DOVECOT_PAM]
                == pam.PamServiceStatusValue.enabled
            ):  # already enabled
                if (
                    pam_status[pam.PamService.DOVECOT_NATIVE]
                    == pam.PamServiceStatusValue.enabled
                ):  # pragma: no cover
                    # shouldn't happen, report to Sentry
                    logger.error(
                        "Unexpected PAM state: both pam/native are enabled."
                        " Status: %s",
                        pam_status,
                    )
            else:  # enable dovecot pam
                pam_service = pam.PamService.DOVECOT_PAM
                await pam.enable(pam_service)
                logger.info("PAM module has been enabled for %s", pam_service)
                return True
        elif desired_dovecot_status is pam.DovecotStatus.NATIVE:
            if (
                pam_status[pam.PamService.DOVECOT_NATIVE]
                == pam.PamServiceStatusValue.enabled
            ):  # already enabled
                if (
                    pam_status[pam.PamService.DOVECOT_PAM]
                    == pam.PamServiceStatusValue.enabled
                ):  # pragma: no cover
                    # shouldn't happen, report to Sentry
                    logger.error(
                        "Unexpected PAM state: both pam/native are enabled."
                        " Status: %s",
                        pam_status,
                    )
            else:  # enable dovecot native
                pam_service = pam.PamService.DOVECOT_NATIVE
                await pam.enable(pam_service)
                logger.info("PAM module has been enabled for %s", pam_service)
                return True
        else:  # pragma: no cover
            assert 0, "can't happen"
        return False  # nothing has been enabled

    async def _ensure_status_for_service(
        self, should_be_enabled, status, pam_service
    ):
        expected_service_status = (
            pam.PamServiceStatusValue.enabled
            if should_be_enabled
            else pam.PamServiceStatusValue.disabled
        )
        if expected_service_status != status[pam_service]:
            if should_be_enabled:
                await pam.enable(pam_service)
                logger.info("PAM module has been enabled for %s", pam_service)
                return True
            await pam.disable(pam_service)
            logger.info("PAM module has been disabled for %s", pam_service)
        return False

    @recurring_check(0)
    async def _status_checker(self):
        await self._status_check_required.wait()
        self._status_check_required.clear()
        await self._ensure_status()

    @recurring_check(_CONFIG_PERIODIC_CHECK)
    async def _initiate_status_check(self):
        self._status_check_required.set()

    @expect(MessageType.ConfigUpdate)
    async def on_config_update(self, message: MessageType.ConfigUpdate):
        if isinstance(message["conf"], SystemConfig):
            self._status_check_required.set()

Zerion Mini Shell 1.0