Current File : //opt/imunify360/venv/lib64/python3.11/site-packages/imav/malwarelib/plugins/mrs_uploader.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 re
from asyncio import CancelledError, Queue
from contextlib import suppress
from logging import getLogger
from typing import List

from defence360agent.api import inactivity
from defence360agent.contracts.config import Malware as Config
from defence360agent.contracts.license import LicenseError
from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.plugins import (
    MessageSink,
    MessageSource,
    expect,
)
from defence360agent.utils import recurring_check
from imav.malwarelib.utils import malware_response

logger = getLogger(__name__)


class MRSUploader(MessageSink, MessageSource):
    ERR_MSG = "Failed to submit a file"
    SUSP_PATTERN = re.compile(r"(?:suspicious\..+|[CS]MW-SUS-.+|SMW-HEUR-ELF)")

    def __init__(self):
        self._upload_queue = Queue()

    async def create_source(self, loop, sink):
        self._sink = sink
        self._loop = loop
        self._upload_task = loop.create_task(self.upload())

    async def create_sink(self, loop):
        pass

    async def shutdown(self):
        self._upload_task.cancel()
        with suppress(CancelledError):
            await self._upload_task

    def _separate_hits_by_type(self, results) -> tuple:
        malicious = []
        suspicious = []
        extended_suspicious = []
        for file, data in results.items():
            is_extended_suspicious = False
            is_suspicious = False
            is_malicious = False
            for hit in data["hits"]:
                is_extended_suspicious |= hit.get("extended_suspicious", False)
                is_suspicious |= bool(
                    hit["suspicious"]
                    and self.SUSP_PATTERN.match(hit["matches"])
                )
                is_malicious |= not hit["suspicious"]
            hit_info = malware_response.HitInfo(file, data["hash"])
            if is_extended_suspicious:
                extended_suspicious.append(hit_info)
            elif is_suspicious:
                suspicious.append(hit_info)
            elif is_malicious:
                malicious.append(hit_info)
        return malicious, suspicious, extended_suspicious

    @expect(MessageType.MalwareScan)
    async def process_scan(self, message):
        results = message["results"]
        if results is None:
            return
        if not Config.SEND_FILES:
            logger.info("Uploading files to MRS is disabled")
            return
        (
            malicious_hits,
            suspicious_hits,
            extended_suspicious_hits,
        ) = self._separate_hits_by_type(results)
        if malicious_hits:
            await self._sink.process_message(
                MessageType.MalwareMRSUpload(
                    hits=malicious_hits, upload_reason="malicious"
                )
            )

        if suspicious_hits:
            # Move uploading to another task
            await self._sink.process_message(
                MessageType.MalwareMRSUpload(
                    hits=suspicious_hits, upload_reason="suspicious"
                )
            )
        if extended_suspicious_hits:  # pragma: no cover
            await self._sink.process_message(
                MessageType.MalwareMRSUpload(
                    hits=extended_suspicious_hits,
                    upload_reason="extended-suspicious",
                )
            )
        errors = message["summary"].get("errors")
        if errors:
            error_hits = [
                malware_response.HitInfo(hit["file"], hit["hash"])
                for hit in errors
            ]
            await self._sink.process_message(
                MessageType.MalwareMRSUpload(
                    hits=error_hits, upload_reason="scan_error"
                )
            )

    @recurring_check(0)
    async def upload(self):
        while True:
            files, upload_reason, message = await self._upload_queue.get()
            try:
                await self._upload_files(files, upload_reason, message)
            finally:
                self._upload_queue.task_done()

    async def _upload_files(self, files, upload_reason, message):
        with inactivity.track.task("mrs_upload"):
            for file in files:
                try:
                    await malware_response.upload_with_retries(
                        file, upload_reason=upload_reason
                    )
                except LicenseError as e:
                    logger.warning("Cannot process message %s: %s", message, e)
                    break
                except FileNotFoundError as e:
                    err = "{}. {}".format(self.ERR_MSG, e.strerror)
                    logger.warning("%s: %s", err, e.filename)

    @expect(MessageType.MalwareMRSUpload)
    async def process_hits(self, message):
        hits: List[malware_response.HitInfo] = message["hits"]
        upload_reason = message.get("upload_reason", "suspicious")

        unknown_hashes = await malware_response.check_known_hashes(
            self._loop, (hit.hash for hit in hits), upload_reason
        )
        if not unknown_hashes:
            logger.info("All files are known to MRS. Skipping uploading...")
            return

        files = (hit.file for hit in hits if hit.hash in unknown_hashes)
        self._upload_queue.put_nowait((files, upload_reason, message))