AlkantarClanX12
Current Path : /opt/imunify360/venv/lib/python3.11/site-packages/imav/malwarelib/plugins/ |
Current File : //opt/imunify360/venv/lib/python3.11/site-packages/imav/malwarelib/plugins/detached_scan.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 shutil import time from logging import getLogger from typing import Dict, Optional, Union from defence360agent.contracts.hook_events import HookEvent from defence360agent.contracts.messages import MessageType from defence360agent.contracts.plugins import ( MessageSink, MessageSource, expect, ) from defence360agent.utils import Scope from imav.malwarelib.config import ( MalwareScanResourceType, MalwareScanType, ) from imav.malwarelib.model import MalwareScan as MalwareScanModel from imav.malwarelib.scan import ( ScanAlreadyCompleteError, ScanInfoError, ) from imav.malwarelib.scan.ai_bolit.detached import ( AiBolitDetachedScan, ) from imav.malwarelib.scan.mds.detached import MDSDetachedScan from imav.malwarelib.scan.queue_supervisor_sync import QueueSupervisorSync from imav.malwarelib.scan.scan_result import aggregate_result from imav.malwarelib.utils.user_list import fill_results_owner logger = getLogger(__name__) class DetachedScanPlugin(MessageSink, MessageSource): PROCESSING_ORDER = MessageSink.ProcessingOrder.PRE_PROCESS_MESSAGE SCOPE = Scope.AV loop, sink = None, None results_cache = {} # type: Dict[str, dict] async def create_source(self, loop, sink): self.loop = loop self.sink = sink async def create_sink(self, loop): pass @expect(MessageType.MalwareScan, async_lock=True) async def complete_scan(self, message): message_type = MalwareScanMessageInfo(message) if not message_type.is_detached: total_malicious = await self._count_total_malicious(message) message["summary"]["total_malicious"] = total_malicious return message elif message_type.is_summary: return await self._handle_summary(message) # message_type.is_result return await self._handle_results(message) async def _handle_summary(self, message): scan_id = message["summary"]["scanid"] # If summary arrives after results, results are read from cache if scan_id in self.results_cache: message["summary"]["completed"] = time.time() message["results"] = self.results_cache.pop(scan_id) total_malicious = await self._count_total_malicious(message) message["summary"]["total_malicious"] = total_malicious queued_scan = QueueSupervisorSync.queue.find( scanid=message["summary"]["scanid"] ) if queued_scan: QueueSupervisorSync.queue.remove(queued_scan) await self._call_scan_finished_hook( message["summary"], queued_scan.args if queued_scan else {} ) return message async def _handle_results(self, message): message = await self.aggregate_result(message) message_type = MalwareScanMessageInfo(message) summary = message["summary"] logger.info("Scan stopped") queued_scan = QueueSupervisorSync.queue.find(scanid=summary["scanid"]) if message_type.summary_from_db is None: if queued_scan: summary["file_patterns"] = queued_scan.args["file_patterns"] summary["exclude_patterns"] = queued_scan.args[ "exclude_patterns" ] QueueSupervisorSync.queue.remove(queued_scan) if summary.get("path") or summary.get("error"): # Scan failed summary["total_malicious"] = 0 await self._call_scan_finished_hook(summary, scan_args={}) return message # Summary is not in DB yet, save results to cache scan_id = message["summary"]["scanid"] self.results_cache[scan_id] = message["results"] # Report an error to Sentry if cache grows cache_size = len(self.results_cache) if cache_size > 1: logger.error("MalwareScan cache size is %d", cache_size) return scan = message_type.summary_from_db summary["scanid"] = scan.scanid summary["path"] = scan.path summary["started"] = scan.started summary["completed"] = time.time() if summary.get("total_files") is None: summary["total_files"] = scan.total_resources summary["type"] = scan.type summary["error"] = summary.get("error", None) message["summary"] = summary total_malicious = await self._count_total_malicious(message) message["summary"]["total_malicious"] = total_malicious if queued_scan: summary["file_patterns"] = queued_scan.args["file_patterns"] summary["exclude_patterns"] = queued_scan.args["exclude_patterns"] QueueSupervisorSync.queue.remove(queued_scan) await self._call_scan_finished_hook( summary, queued_scan.args if queued_scan else {} ) return message @staticmethod async def _count_total_malicious(message) -> int: return len( [ k for k, v in message["results"].items() if v["hits"][0]["suspicious"] is False ] ) async def _call_scan_finished_hook(self, summary, scan_args) -> None: scan_finished = HookEvent.MalwareScanningFinished( scan_id=summary["scanid"], scan_type=summary["type"], path=summary["path"], started=summary["started"], total_files=summary["total_files"], total_malicious=summary["total_malicious"], error=summary.get("error"), status="failed" if summary.get("error") else "ok", scan_params=scan_args, stats={ **{ key: value for key, value in summary.items() if key in ( # performance-related metrics "scan_time", "scan_time_hs", "scan_time_preg", "smart_time_hs", "smart_time_preg", "finder_time", "cas_time", "deobfuscate_time", "mem_peak", ) }, **{"total_files": summary["total_files"]}, }, ) await self.sink.process_message(scan_finished) await self._recheck_scan_queue() @staticmethod def _get_detached_scan( resource_type: Optional[Union[str, MalwareScanResourceType]], scan_id ): return AiBolitDetachedScan(scan_id) @expect(MessageType.MalwareScanComplete) async def complete_detached_scan(self, message): scan_id = message.get("scan_id") resource_type = message.get("resource_type") detached_scan = self._get_detached_scan(resource_type, scan_id) try: scan_message = await detached_scan.complete() except ScanAlreadyCompleteError as err: # This happens when AV is woken up by AiBolit. See DEF-11078. logger.warning( "Cannot complete scan %s, assuming it is already complete" ":\n%s", scan_id, err, ) return except ScanInfoError as err: logger.error( "Cannot complete %s scan %s, assuming it was not started:\n%s", detached_scan.RESOURCE_TYPE.value, scan_id, err, ) return finally: shutil.rmtree(str(detached_scan.detached_dir), ignore_errors=True) await self.sink.process_message(scan_message) @classmethod async def aggregate_result(cls, message): message["results"] = aggregate_result(message["results"]) await fill_results_owner(message["results"]) return message async def _recheck_scan_queue(self): await self.sink.process_message(MessageType.MalwareScanQueueRecheck()) class MalwareScanMessageInfo: """A helper class that allows to receive information about scan from MalwareScan message. """ def __init__(self, message): self.message = message self._summary_from_db = None self.scan_id = self.message["summary"]["scanid"] @property def is_detached(self): summary = self.message["summary"] return summary.get("type") in ( MalwareScanType.ON_DEMAND, MalwareScanType.BACKGROUND, MalwareScanType.USER, None, ) @property def is_summary(self): return self.message["results"] is None @property def summary_from_db(self): if not self._summary_from_db: summary_from_db = ( MalwareScanModel.select() .where(MalwareScanModel.scanid == self.scan_id) .limit(1) ) if summary_from_db: self._summary_from_db = summary_from_db[0] return self._summary_from_db class DetachedScanPluginIm360(DetachedScanPlugin): SCOPE = Scope.IM360 @staticmethod def _get_detached_scan( resource_type: Optional[Union[str, MalwareScanResourceType]], scan_id ): if resource_type is not None and ( MalwareScanResourceType(resource_type) is MalwareScanResourceType.DB ): return MDSDetachedScan(scan_id) return AiBolitDetachedScan(scan_id) @expect(MessageType.MalwareDatabaseScan) async def complete_scan_db(self, message): queued_scan = QueueSupervisorSync.queue.find(scanid=message["scan_id"]) if queued_scan: QueueSupervisorSync.queue.remove(queued_scan) scan_finished_event = HookEvent.MalwareScanningFinished( scan_id=message["scan_id"], scan_type=message["type"], path=message["path"], ) await self.sink.process_message(scan_finished_event) await self._recheck_scan_queue()