ok
Direktori : /proc/self/root/opt/imunify360/venv/lib/python3.11/site-packages/im360/plugins/ |
Current File : //proc/self/root/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()