%PDF- %PDF-
Direktori : /proc/2/cwd/opt/imunify360/venv/lib64/python3.8/site-packages/defence360agent/contracts/ |
Current File : //proc/2/cwd/opt/imunify360/venv/lib64/python3.8/site-packages/defence360agent/contracts/plugins.py |
""" This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. Copyright © 2019 Cloud Linux Software Inc. This software is also available under ImunifyAV commercial license, see <https://www.imunify360.com/legal/eula> """ import asyncio import inspect import logging import subprocess from abc import ABC, ABCMeta, abstractmethod from contextlib import suppress from functools import lru_cache, wraps from defence360agent.contracts.messages import Message, MessageType from defence360agent.utils import Scope logger = logging.getLogger(__name__) class BasePlugin(object): SCOPE = Scope.AV_IM360 _subclasses = [] def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) cls._subclasses.append(cls) @classmethod def get_active_plugins(cls): # consider all non-abstract subclasses are active return [ plugin for plugin in cls._subclasses if not inspect.isabstract(plugin) ] async def shutdown(self): """Shutdown plugin's subsystems, cancel running tasks, clean iptables (if plugin is protector). It should be safe to assume that it is called after corresponding create_source if applicable. It is called only from the shutdown task that runs at most once, meaning shutdown() is never called twice. """ pass def __repr__(self): return "%s.%s" % (self.__class__.__module__, self.__class__.__name__) class MessageSource(BasePlugin, ABC): @abstractmethod def create_source(self, loop, sink): """This method is a coroutine.""" class Sensor(MessageSource, ABC): """ Sensor is alias to MessageSource. """ def create_source(self, loop, sink): """This method is a coroutine.""" return self.create_sensor(loop, sink) @abstractmethod def create_sensor(self, loop, sink): """This method is a coroutine.""" class LogStreamReader(Sensor, metaclass=ABCMeta): source_file = None # Limit of bytes consumed from stream # while trying to read one line (128 kB) _LIMIT = 2 ** 17 _cmd = None async def create_sensor(self, loop, sink): self._loop = loop self._sink = sink self._cmd = None if not self.source_file: return self._cmd = ('/usr/bin/tail', # follow beyond the end of the file '--follow=name', '-n0', # keep trying to open a file if it is inaccessible '--retry', self.source_file) self._child_process = await asyncio.create_subprocess_exec( *self._cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=0, limit=self._LIMIT ) loop.create_task(self._infinite_read_and_proceed( self._child_process.stdout)) async def shutdown(self): if self._cmd is not None: cmd, self._cmd = self._cmd, None logger.debug("Terminating child process [%s]", cmd) # child process dies from the same signal when agent # is run from console (not as --daemon) with suppress(ProcessLookupError): self._child_process.kill() rc = await self._child_process.wait() logger.debug("Terminated child process [%s] with code [%d]", cmd, rc) @abstractmethod async def _infinite_read_and_proceed(self, stream_reader): raise NotImplementedError class BaseMessageProcessor: @lru_cache(maxsize=1) def _message_processors(self): rv = [] for attr_str in dir(self): if attr_str.startswith("_"): continue # skip non-public attributes func = getattr(self, attr_str) if callable(func) and hasattr(func, '_decorated_for_process_message'): rv.append(func) return rv async def process_message(self, message): logger.debug("Dispatching %r through %r...", message, self) for coro in self._message_processors(): result = await coro(message) if isinstance(result, Message): return result class MessageSink(BasePlugin, BaseMessageProcessor, ABC): class ProcessingOrder: # e.g. check is valid ipv4 PRE_PROCESS_MESSAGE = 10 # lfd plugin should process lfd alerts before other ignore plugins LFD = 18 # e.g. for ignore_alert_with_whitelisted_ip IGNORE_MESSAGE = 20 # Should be before check ip in graylist UNBLOCK_FROM_SUBNET = 30 # Check ip in the graylist already CHECK_IP_IN_GRAYLIST = 40 # Append ttl to alert GRAYLIST_TIMEOUT = 50 # Store graylist to db, fixup ServerPush:method=BLOCK graylist # expiration if it is lower then doslist expiration GRAYLIST_DB_FIXUP = 55 # this should run before IPSET_PROTECTOR IMPORT_EXPORT_WBLIST = 60 # make ml prediction before lazy_init ML_PREDICTION = 70 # the default DEFAULT = 80 IPSET_PROTECTOR = DEFAULT WEBSHIELD_PROTECTOR = 81 # should be run after ManageGrayList(DEFAULT) WHITELIST_UNBLOCKED = 90 # Synclist timestamp update SYNCLIST_UPDATE = 100 # post action POST_ACTION = 120 # event hook processing EVENT_HOOK = 150 # iContact ICONTACT_SENT = 200 # e.g. Accumulate POST_PROCESS_MESSAGE = 999 # alias for DEFAULT PROCESSING_ORDER = ProcessingOrder.DEFAULT @abstractmethod async def create_sink(self, loop): pass def expect(*message_type, async_lock=None, **expect_fields): """ @expect decorator for MessageSink.dosmth(message) async methods. MessageSink method will be called by MessageSink.process_message() if message_type and expect_fields match the message ones. @expect's can be stacked together and decision whether to call decorated coro is made by evaluating stacked @expect's with logical OR: @expect(SensorAlert) # -- OR -- @expect(ServerPush, method='BLOCK') def protect(message): ... """ def decorate(coro): if getattr(coro, '__name__', '').startswith('_'): raise TypeError('{coro} is not public'.format(coro=coro)) @wraps(coro) async def decorated(self, message): def match(): return (isinstance(message, message_type) and all(message.get(k) == v for k, v in expect_fields.items())) def is_stacked(coro): return hasattr(coro, '_decorated_for_process_message') def terminal(coro): if is_stacked(coro): return terminal(coro._decorated_for_process_message) return coro # process stacked decorators with logical OR if match(): if async_lock is True: await message.acquire() try: result = await terminal(coro)(self, message) except Exception as exc: if isinstance( message, MessageType.Lockable ) and message.locked(): message.release() raise exc else: if ( async_lock is False and isinstance(message, MessageType.Lockable) and message.locked() ): message.release() return result if is_stacked(coro): # Give next decorator a chance: logical OR return await coro(self, message) return None decorated._decorated_for_process_message = coro return decorated return decorate _plugin_registry = set() def thisguy(plugincls): """Register class as a plugin. >>> @thisguy >>> class ConcreteSink (MessageSink): >>> ... """ _plugin_registry.add(plugincls) return plugincls def theseguys(): """Enumerate classobj for registered plugins.""" return _plugin_registry