ok

Mini Shell

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

from asyncio import AbstractEventLoop, Event
from collections.abc import Callable
from logging import getLogger
from pathlib import Path

from defence360agent.utils import recurring_check
from imav.malwarelib.subsys.ainotify import Event as IEvent, Inotify, Watcher

log = getLogger(__name__)

_PLUGIN_NAMES = (
    "cphulk",
    "lfd",
    "modsec",
    "ossec",
)
_DEFAULT_PATH = Path("/etc/imunify360/rules/disabled-rules")
_WAIT_DIR_TIMEOUT = 10


class _RuleParsingError(Exception):
    pass


def _parse_rule(line: str) -> tuple[str, int]:
    if ":" not in line:
        raise _RuleParsingError("Delimiter ':' is not found in rule:")
    fields = line.split(":", maxsplit=2)
    if len(fields) != 3:
        raise _RuleParsingError(
            f"Wrong amount of fields, 3 expected but {len(fields)} found:"
        )
    plugin_id = fields[0].strip().lower()
    if plugin_id not in _PLUGIN_NAMES:
        raise _RuleParsingError(f"Unknown plugin ID value '{plugin_id!s}':")
    rule_value = fields[1]
    try:
        rule_id = int(rule_value)
    except ValueError as error:
        raise _RuleParsingError(
            f"Invalid rule ID value '{rule_value!s}':"
        ) from error
    return plugin_id, rule_id


def _load_rules(path: Path) -> dict[str, set[int]]:
    if not path.is_file():
        log.debug(
            "Config '%s' with shared disabled rules is not found.",
            path,
        )
        return {}
    result = {}
    with path.open(mode="rt") as rules_file:
        for line_no, raw_line in enumerate(rules_file, start=1):
            if not (line := raw_line.strip()):
                continue
            try:
                plugin_id, rule_id = _parse_rule(line)
            except _RuleParsingError as error:
                log.warning(
                    "%s:%d: %s.",
                    path,
                    line_no,
                    str(error),
                )
            except Exception:
                log.exception("%s:%d", path, line_no)
            else:
                result.setdefault(plugin_id, set()).add(rule_id)
    return result


def get_shared_disabled_modsec_rules_ids(*, path: Path = None) -> set[int]:
    return _load_rules(path or _DEFAULT_PATH).get("modsec", set())


class DisabledRulesWatcher:
    def __init__(
        self,
        loop: AbstractEventLoop,
        *,
        path: Path = None,
        on_change_cb: Callable[..., None] = None,
    ):
        self.__cb = on_change_cb
        self.__event = Event()
        self.__path = path or _DEFAULT_PATH
        self.__name = self.__path.name.encode("ascii")
        self.__rules = {}
        self.__watcher = None
        self.__task = None
        self.__start(loop)

    def __start(self, loop: AbstractEventLoop):
        if not (dir_path := self.__path.parent).is_dir():
            log.error(
                "Shared disabled rules directory '%s' is not exist.", dir_path
            )
            return
        self.__rules = _load_rules(self.__path)
        self.__watcher = Watcher(loop, coro_callback=self.__on_io_notify)
        self.__watcher.watch(
            str(dir_path).encode("ascii"),
            Inotify.CLOSE_WRITE | Inotify.MOVED_TO | Inotify.DELETE,
        )
        self.__task = loop.create_task(self.__process_events())

    async def __on_io_notify(self, io_event: IEvent):
        # Squash many inotify events into one asyncio event.
        # It allows to prevent too fast rules reloading.
        if io_event.name == self.__name:
            self.__event.set()

    @recurring_check(0)
    async def __process_events(self):
        try:
            await self.__event.wait()
        finally:
            self.__event.clear()
        self.__rules = _load_rules(self.__path)
        if self.__cb is not None:
            self.__cb()

    def close(self):
        if self.__task is not None:
            self.__task.cancel()
        if self.__watcher is not None:
            self.__watcher.close()

    def match(self, plugin_id: str, rule_id: int) -> bool:
        return rule_id in self.__rules.get(plugin_id, set())

    def count(self) -> int:
        return sum(map(len, self.__rules.values()))

Zerion Mini Shell 1.0