%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /opt/imunify360/venv/lib/python3.8/site-packages/defence360agent/contracts/
Upload File :
Create Path :
Current File : //opt/imunify360/venv/lib/python3.8/site-packages/defence360agent/contracts/messages.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
from enum import Enum
from typing import List, Optional, Set

from defence360agent.contracts.config import Core as CoreConfig
from defence360agent.malwarelib.config import MalwareScanType, QueuedScanState
from defence360agent.malwarelib.scan.mds.report import (
    MalwareDatabaseHitInfo,
    MalwareDatabaseScanReport,
)


class MessageNotFoundError(Exception):
    pass


class UnknownMessage:
    """
    Used as stub for MessageType
    """
    def __init__(self):
        raise MessageNotFoundError("Message class is not found.")

    def __getattr__(self, name):
        return "Unknown"  # pragma: no cover


class MessageT:
    _subclasses = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls._subclasses.append(cls)

    @classmethod
    def get_subclasses(cls):
        return tuple(cls._subclasses)


class _MessageType:
    """
    Used to get specific message class. For example,
    >>> _MessageType().ConfigUpdate
    <class 'defence360agent.contracts.messages.ConfigUpdate'>
    >>> _MessageType().NotExistMessage
    <class 'defence360agent.contracts.messages.UnknownMessage'>
    >>>
    """
    def __getattr__(self, name):
        for subcls in Message.get_subclasses():
            # is is supposed that all subclasses have different names
            if subcls.__name__ == name:
                return subcls
        return UnknownMessage


MessageType = _MessageType()


class ReportTarget(Enum):
    API = "api"
    PERSISTENT_CONNECTION = "conn"


class Reportable(MessageT):
    """
    Mixin class for messages that should be sent to the server
    """
    TARGET = ReportTarget.PERSISTENT_CONNECTION

    @classmethod
    def get_subclass_with_method(cls, method: str):
        """
        Return a subclass with the same DEFAULT_METHOD as *method*.
        It can be used to detect report target from message method.
        NOTE: it is not guaranteed that the class with the *method* is unique,
              in this case the first subclass found is returned, but
              it is tested that all such subclasses have the same TARGET.
        """
        for subclass in cls.__subclasses__():
            if method == getattr(subclass, "DEFAULT_METHOD"):
                return subclass
        return None  # pragma: no cover


class Received(MessageT):
    """
    Mixin class for messages received from the server.

    These messages are created in the client360 plugin when receiving a
    request from imunify360.cloudlinux.com.
    """

    @classmethod
    def get_subclass_with_action(cls, action: str):
        for subclass in cls.__subclasses__():
            received_actions = getattr(subclass, "RECEIVED_ACTIONS", []) or [
                getattr(subclass, "DEFAULT_METHOD")
            ]
            if action in received_actions:
                return subclass
        raise MessageNotFoundError(
            'Message class is not found for "{}" action'.format(action)
        )


class Lockable(MessageT):
    _lock = None

    @classmethod
    async def acquire(cls) -> None:
        if cls._lock is None:
            cls._lock = asyncio.Lock()
        await cls._lock.acquire()

    @classmethod
    def locked(cls) -> bool:
        return cls._lock is not None and cls._lock.locked()

    @classmethod
    def release(cls) -> None:
        if cls._lock is not None:
            cls._lock.release()


class Message(dict, MessageT):
    """
    Base class for messages to be passed as
    a parameter to plugins.MessageSink.process_message()
    """

    # Default method='...' to send to the Server
    DEFAULT_METHOD = ""
    PRIORITY = 10
    PROCESSING_TIME_THRESHOLD = 60  # 1 min
    #: fold collections' repr with more than the threshold number of items
    _FOLD_LIST_THRESHOLD = 100
    #: shorten strings longer than the threshold characters
    _SHORTEN_STR_THRESHOLD = 320

    def __init__(self, *args, **kwargs) -> None:
        if self.DEFAULT_METHOD:
            self["method"] = self.DEFAULT_METHOD
        super(Message, self).__init__(*args, **kwargs)

    @property
    def payload(self):
        return {k: v for k, v in self.items() if k != "method"}

    def __getattr__(self, name):
        """
        Called when an attribute lookup has not found the attribute
        in the usual places

        A shortcut to access an item from dict
        """
        try:
            return self[name]
        except KeyError as exc:
            raise AttributeError(name) from exc

    def __repr__(self):
        """
        Do not flood console.log with large sequences If there is a list
        of more than _FOLD_LIST_THRESHOLD items inside the message,
        then this list will be collapsed and the number of items in
        the list will be shown.  The message itself will not be
        collapsed.

        """
        items_to_fold = {
            k: _shorten_str(colxn, self._SHORTEN_STR_THRESHOLD)
            if isinstance(colxn, str)
            else f"<{len(colxn)} item(s)>"
            for k, colxn in self.items()
            if hasattr(colxn, "__len__")
            and len(colxn) > self._FOLD_LIST_THRESHOLD
        }

        folded_msg = self.copy()
        folded_msg.update(items_to_fold)
        return "{}({})".format(self.__class__.__qualname__, folded_msg)

    def __str__(self):
        return self.__repr__()


class MessageList(Message):
    def __init__(self, msg_list):
        super().__init__(list=msg_list)

    @property
    def payload(self):
        return self.list

    def __repr__(self):
        """
        Do not flood console.log with full MessageList
        """
        return "{}({})".format(
            self.__class__.__qualname__,
            "<{} item(s)>".format(len(self.get("list", []))),
        )


class ShortenReprListMixin:
    """
    Do not flood console.log with large sequences
    The method collapses messages that are a list.
    Instead of showing all the elements of the message,
    their number will be displayed.
    """

    def __repr__(self: dict):  # type: ignore
        return "{}({})".format(
            self.__class__.__qualname__,
            "<{} item(s)>".format(len(self.get("items", []))),
        )


class Accumulatable(Message):
    """Messages of this class will be grouped into a list of LIST_CLASS
    message instance by Accumulate plugin.  Messages whose do_accumulate()
    call returns False will not be added to list."""

    LIST_CLASS = MessageList

    def do_accumulate(self) -> bool:
        """Return True if this message is worth collecting, False otherwise."""
        return True


class ServerConnected(Message):
    pass


# alias (for better client code readability)
class ServerReconnected(ServerConnected):
    pass


class Ping(Message, Reportable):
    """
    Will send this message on connected, reconnected events
    to provide central server with agent version
    """

    DEFAULT_METHOD = "PING"
    PRIORITY = 0

    def __init__(self):
        super().__init__()
        self["version"] = CoreConfig.VERSION


class Ack(Message, Reportable):
    """
    Notify Server that a persistent message with *seq_number* has been
    received by Agent.

    """

    DEFAULT_METHOD = "ACK"

    def __init__(self, seq_number, **kwargs):
        super().__init__(**kwargs)
        self["_meta"] = dict(per_seq=seq_number)


class Noop(Message):
    """
    Sending NOOP to the agent to track the message in agent logs.
    """

    pass


class MalwareScanTask(Message):
    """
    Creates task to scan file for malware
    """

    DEFAULT_METHOD = "MALWARE_SCAN_TASK"


class MalwareScanComplete(Message):
    DEFAULT_METHOD = "MALWARE_SCAN_COMPLETE"


class MalwareCleanComplete(Message):
    DEFAULT_METHOD = "MALWARE_CLEAN_COMPLETE"


class MalwareRestoreComplete(Message):
    DEFAULT_METHOD = "MALWARE_RESTORE_COMPLETE"


class MalwareScanList(ShortenReprListMixin, Message, Reportable):
    DEFAULT_METHOD = "MALWARE_SCAN_LIST"


class MalwareScan(Accumulatable, Lockable):
    """
    Represents results of single scan
    """

    DEFAULT_METHOD = "MALWARE_SCAN"
    LIST_CLASS = MalwareScanList

    def do_accumulate(self) -> bool:
        results = self.get("results")
        if self.get("summary", {}).get("type") == MalwareScanType.BACKGROUND:
            return results is not None
        return bool(results)


class MalwareMRSUpload(Message):
    """
    Used to isolate a possibly long running uploading to MRS from the
    other MalwareScan handlers.
    """

    DEFAULT_METHOD = "MALWARE_MRS_UPLOAD"


class MalwareScanQueue(Message):
    DEFAULT_METHOD = "MALWARE_SCAN_QUEUE"


class MalwareScanQueuePut(MalwareScanQueue):
    def __init__(self, *, paths, scan_args):
        super().__init__(paths=paths, scan_args=scan_args)


class MalwareScanQueueRemove(MalwareScanQueue):
    def __init__(self, *, scan_ids):
        super().__init__(scan_ids=scan_ids)


class MalwareScanQueueRecheck(MalwareScanQueue):
    def __init__(self):
        super().__init__()


class MalwareScanQueueStopBackground(MalwareScanQueue):
    def __init__(self):
        super().__init__()


class MalwareScanQueueUpdateStatus(MalwareScanQueue):
    def __init__(
        self, *, scan_ids: List[str] = None, status: QueuedScanState = None
    ):
        super().__init__(scan_ids=scan_ids, status=status)


class MalwareResponse(Message, Received):
    TYPES = BLACK, WHITE, KNOWN, UNKNOWN = "BLACK", "WHITE", "KNOWN", "UNKNOWN"
    DEFAULT_METHOD = "MALWARE_RESPONSE"


class MalwareSendFiles(Message, Received):
    DEFAULT_METHOD = "MALWARE_SEND_FILES"


class MalwareRescanFiles(Message, Received):
    DEFAULT_METHOD = "MALWARE_RESCAN_FILES"


class ServerConfig(Message, Reportable):
    """
    Information about server environment
    """

    DEFAULT_METHOD = "SERVER_CONFIG"
    TARGET = ReportTarget.API

    def __repr__(self):
        return "{}()".format(self.__class__.__qualname__)


class FilesUpdated(Message):
    """
    To consume products of files.update()
    """

    def __init__(self, files_type, files_index):
        """
        :param files_type: files.Type
        :param files_index: files.LocalIndex
        """
        # explicit is better than implicit
        self["files_type"] = files_type
        self["files_index"] = files_index

    def __repr__(self):
        """
        Do not flood console.log with large sequences
        """
        return "{}({{'files_type':'{}', 'files_index':{}}})".format(
            self.__class__.__qualname__,
            self["files_type"],
            self["files_index"],
        )


class UpdateFiles(Message, Received):
    """
    Update files by getting message from the server
    """

    DEFAULT_METHOD = "UPDATE"


class ConfigUpdate(Message):
    DEFAULT_METHOD = "CONFIG_UPDATE"


class Reject(Exception):
    """
    Kinda message filtering facility.
    Raised in order to stop message processing through plugins.
    Takes reason of reject as argument.
    """

    pass


class MalwareCleanupTask(Message):
    """
    Creates task to cleanup files
    """

    DEFAULT_METHOD = "MALWARE_CLEANUP_TASK"

    def __init__(
        self,
        *,
        hits,
        standard_only=None,
        cause=None,
        initiator=None,
        scan_id=None,
        post_action=None,
    ):
        super().__init__(
            hits=hits,
            standard_only=standard_only,
            cause=cause,
            initiator=initiator,
            scan_id=scan_id,
            post_action=post_action,
        )


class MalwareCleanupList(ShortenReprListMixin, Message, Reportable):
    DEFAULT_METHOD = "MALWARE_CLEANUP_LIST"


class MalwareCleanup(Accumulatable):
    """
    Represents results of single cleanup
    """

    DEFAULT_METHOD = "MALWARE_CLEANUP"
    LIST_CLASS = MalwareCleanupList


class CommandInvoke(Message, Reportable):
    DEFAULT_METHOD = "COMMAND_INVOKE"


class ScanFailed(Message, Reportable):
    DEFAULT_METHOD = "SCAN_FAILED"


class CleanupFailed(Message, Reportable):
    DEFAULT_METHOD = "CLEANUP_FAILED"


class RestoreFromBackupTask(Message):
    """
    Creates a task to restore files from backup
    """

    DEFAULT_METHOD = "MALWARE_RESTORE_FROM_BACKUP"


class cPanelEvent(Message):
    DEFAULT_METHOD = "PANEL_EVENT"
    ALLOWED_FIELDS = {
        "new_pkg",
        "plan",
        "exclude",
        "imunify360_proactive",
        "imunify360_av",
        "imunify360_secure_site",
    }

    @classmethod
    def from_hook_event(
        cls, username: str, hook: str, ts: float, fields: dict
    ):
        data = {
            k.lower(): v
            for k, v in fields.items()
            if k.lower() in cls.ALLOWED_FIELDS
        }
        # Check for user rename
        if (
            hook == "Modify"
            and "user" in fields
            and "newuser" in fields
            and fields["user"] != fields["newuser"]
        ):
            data["old_username"] = fields["user"]
        return cls(
            {
                "username": username,
                "hook": hook,
                "data": data,
                "timestamp": ts,
            }
        )


MSGS_WITHOUT_IP = [
    msg.DEFAULT_METHOD
    for msg in (
        Ack,
        MalwareScan,
        MalwareScanList,
        MalwareScanComplete,
        MalwareCleanComplete,
        MalwareRestoreComplete,
        Noop,
    )
]


class MalwareIgnorePathUpdated(Message):
    """Signal through a message bus that MalwareIgnorePath has been updated."""

    pass


class IContactSent(Message, Reportable):
    DEFAULT_METHOD = "ICONTACT_SENT"


def _shorten_str(s: str, limit: int) -> str:
    """Shorten *s* string if its length exceeds *limit*."""
    assert limit > 4
    return f"{s[:limit//2-1]}...{s[-limit//2+2:]}" if len(s) > limit else s


class BackupInfo(Message, Reportable):
    """Information about enabled backup backend"""

    DEFAULT_METHOD = "BACKUP_INFO"


class MDSReportList(ShortenReprListMixin, Message, Reportable):
    DEFAULT_METHOD = "MDS_SCAN_LIST"


class MDSReport(Accumulatable):
    LIST_CLASS = MDSReportList


class MalwareDatabaseScan(Message):
    """
    Represents results of a single MDS scan
    """

    args: Optional[List[str]]
    path: str
    scan_id: str
    type: str
    error: Optional[str]
    started: int
    completed: int
    total_resources: int
    total_malicious: int
    hits: Set[MalwareDatabaseHitInfo]
    initiator: Optional[str]

    def __init__(
        self,
        *,
        args: Optional[List[str]],
        path: Optional[str],
        scan_id: str,
        type: str,
        started: int = 0,
        completed: int = 0,
        total_resources: int = 0,
        total_malicious: int = 0,
        hits: Set[MalwareDatabaseHitInfo] = None,
        error: Optional[str] = None,
        initiator: Optional[str] = None,
    ):
        super().__init__(
            args=args,
            path=path,
            scan_id=scan_id,
            type=type,
            started=started,
            completed=completed,
            total_resources=total_resources,
            total_malicious=total_malicious,
            hits=hits,
            error=error,
            initiator=initiator,
        )

    def update_with_report(self, report: MalwareDatabaseScanReport):
        self.update(
            started=report.started,
            completed=report.completed,
            total_resources=report.total_resources,
            total_malicious=report.total_malicious,
            hits=report.hits,
        )

    def update_with_error(self, message):
        return self.update(error=message)

    def __setitem__(self, key, value):
        raise NotImplementedError

    def __delitem__(self, key):
        raise NotImplementedError


class MalwareDatabaseRestore(Message):
    """
    Represents results of a single MDS restore
    """


class MalwareDatabaseRestoreTask(Message):
    """
    Represents a single MDS restore task
    """

    path: str
    app_name: str

    def __init__(self, path: str, app_name: str):
        super().__init__(path=path, app_name=app_name)


class MalwareDatabaseCleanup(Message):
    """
    Represents results of a single mds cleanup
    """

    succeeded: Set[MalwareDatabaseHitInfo]
    failed: Set[MalwareDatabaseHitInfo]

    def __init__(
        self,
        succeeded: Set[MalwareDatabaseHitInfo],
        failed: Set[MalwareDatabaseHitInfo],
    ):
        super().__init__(
            succeeded=succeeded,
            failed=failed,
        )


class MalwareDatabaseCleanupFailed(Message):
    """
    Signifies an MDS cleanup failure
    """

    error: str

    def __init__(self, *, error: str):
        super().__init__(error=error)


class MalwareDatabaseRestoreFailed(Message):
    """
    Signifies an MDS restore failure
    """

    error: str

    def __init__(self, *, error: str):
        super().__init__(error=error)

Zerion Mini Shell 1.0