diff --git a/src/analysis/PluginBase.py b/src/analysis/PluginBase.py
index 275d7cbf8..ca603ddfa 100644
--- a/src/analysis/PluginBase.py
+++ b/src/analysis/PluginBase.py
@@ -3,10 +3,10 @@
from queue import Empty
from time import time
-from helperFunctions.dependency import get_unmatched_dependencies, schedule_dependencies
from helperFunctions.parsing import bcolors
from helperFunctions.process import ExceptionSafeProcess, terminate_process_and_childs
from helperFunctions.tag import TagColor
+from objects.file import FileObject
from plugins.base import BasePlugin
@@ -16,12 +16,12 @@ class AnalysisBasePlugin(BasePlugin): # pylint: disable=too-many-instance-attri
recursive flag: If True (default) recursively analyze included files
'''
VERSION = 'not set'
+ SYSTEM_VERSION = None
timeout = None
def __init__(self, plugin_administrator, config=None, recursive=True, no_multithread=False, timeout=300, offline_testing=False, plugin_path=None): # pylint: disable=too-many-arguments
super().__init__(plugin_administrator, config=config, plugin_path=plugin_path)
- self.history = set()
self.check_config(no_multithread)
self.recursive = recursive
self.in_queue = Queue()
@@ -35,35 +35,22 @@ def __init__(self, plugin_administrator, config=None, recursive=True, no_multith
if not offline_testing:
self.start_worker()
- def add_job(self, fw_object):
- if self._job_is_already_done(fw_object):
- logging.debug('{} analysis already done -> skip: {}\n Analysis Dependencies: {}'.format(
- self.NAME, fw_object.get_uid(), fw_object.analysis_dependency))
- elif self._recursive_condition_is_set(fw_object):
- if self._dependencies_are_fulfilled(fw_object):
- self.history.add(fw_object.get_uid())
- self.in_queue.put(fw_object)
- return
- self._reschedule_job(fw_object)
+ def add_job(self, fw_object: FileObject):
+ if self._dependencies_are_unfulfilled(fw_object):
+ logging.error('{}: dependencies of plugin {} not fulfilled'.format(fw_object.get_uid(), self.NAME))
+ elif self._analysis_depth_not_reached_yet(fw_object):
+ self.in_queue.put(fw_object)
+ return
self.out_queue.put(fw_object)
- def _reschedule_job(self, fw_object):
- unmatched_dependencies = get_unmatched_dependencies([fw_object], self.DEPENDENCIES)
- logging.debug('{} rescheduled due to unmatched dependencies:\n {}'.format(fw_object.get_virtual_file_paths(), unmatched_dependencies))
- fw_object.scheduled_analysis = schedule_dependencies(fw_object.scheduled_analysis, unmatched_dependencies, self.NAME)
- fw_object.analysis_dependency = fw_object.analysis_dependency.union(set(unmatched_dependencies))
- logging.debug('new schedule for {}:\n {}\nAnalysis Dependencies: {}'.format(
- fw_object.get_virtual_file_paths(), fw_object.scheduled_analysis, fw_object.analysis_dependency))
+ def _dependencies_are_unfulfilled(self, fw_object: FileObject):
+ # FIXME plugins can be in processed_analysis and could still be skipped, etc. -> need a way to verify that
+ # FIXME the analysis ran successfully
+ return any(dep not in fw_object.processed_analysis for dep in self.DEPENDENCIES)
- def _job_is_already_done(self, fw_object):
- return (fw_object.get_uid() in self.history) and (self.NAME not in fw_object.analysis_dependency)
-
- def _recursive_condition_is_set(self, fo):
+ def _analysis_depth_not_reached_yet(self, fo):
return self.recursive or fo.depth == 0
- def _dependencies_are_fulfilled(self, fo):
- return get_unmatched_dependencies([fo], self.DEPENDENCIES) == []
-
def process_object(self, file_object): # pylint: disable=no-self-use
'''
This function must be implemented by the plugin
@@ -96,6 +83,8 @@ def shutdown(self):
self.in_queue.close()
self.out_queue.close()
+# ---- internal functions ----
+
def add_analysis_tag(self, file_object, tag_name, value, color=TagColor.LIGHT_BLUE, propagate=False):
new_tag = {
tag_name: {
@@ -110,18 +99,16 @@ def add_analysis_tag(self, file_object, tag_name, value, color=TagColor.LIGHT_BL
else:
file_object.processed_analysis[self.NAME]['tags'].update(new_tag)
-# ---- internal functions ----
-
def init_dict(self):
- results = {}
- results['analysis_date'] = time()
- results['plugin_version'] = self.VERSION
- return results
+ result_update = {'analysis_date': time(), 'plugin_version': self.VERSION}
+ if self.SYSTEM_VERSION:
+ result_update.update({'system_version': self.SYSTEM_VERSION})
+ return result_update
- def check_config(self, no_multihread):
+ def check_config(self, no_multithread):
if self.NAME not in self.config:
self.config.add_section(self.NAME)
- if 'threads' not in self.config[self.NAME] or no_multihread:
+ if 'threads' not in self.config[self.NAME] or no_multithread:
self.config.set(self.NAME, 'threads', '1')
def start_worker(self):
diff --git a/src/analysis/YaraPluginBase.py b/src/analysis/YaraPluginBase.py
index 527c593ed..b6a221967 100644
--- a/src/analysis/YaraPluginBase.py
+++ b/src/analysis/YaraPluginBase.py
@@ -3,6 +3,7 @@
import re
import os
import subprocess
+from pathlib import Path
from analysis.PluginBase import AnalysisBasePlugin
from helperFunctions.fileSystem import get_src_dir
@@ -12,9 +13,9 @@ class YaraBasePlugin(AnalysisBasePlugin):
'''
This should be the base for all YARA based analysis plugins
'''
- NAME = "Yara_Base_Plugin"
- DESCRIPTION = "this is a Yara plugin"
- VERSION = "0.0"
+ NAME = 'Yara_Base_Plugin'
+ DESCRIPTION = 'this is a Yara plugin'
+ VERSION = '0.0'
def __init__(self, plugin_administrator, config=None, recursive=True, plugin_path=None):
'''
@@ -23,8 +24,16 @@ def __init__(self, plugin_administrator, config=None, recursive=True, plugin_pat
'''
self.config = config
self._get_signature_file(plugin_path)
+ self.SYSTEM_VERSION = self.get_yara_system_version()
super().__init__(plugin_administrator, config=config, recursive=recursive, plugin_path=plugin_path)
+ def get_yara_system_version(self):
+ with subprocess.Popen(['yara', '--version'], stdout=subprocess.PIPE) as process:
+ yara_version = process.stdout.readline().decode().strip()
+
+ access_time = int(Path(self.signature_path).stat().st_mtime)
+ return '{}_{}'.format(yara_version, access_time)
+
def process_object(self, file_object):
if self.signature_path is not None:
with subprocess.Popen('yara --print-meta --print-strings {} {}'.format(self.signature_path, file_object.file_path), shell=True, stdout=subprocess.PIPE) as process:
@@ -101,7 +110,7 @@ def _parse_meta_data(meta_data_string):
for item in meta_data_string.split(','):
if '=' in item:
key, value = item.split('=', maxsplit=1)
- value = json.loads(value) if value in ['true', 'false'] else value.strip('\"')
+ value = json.loads(value) if value in ['true', 'false'] else value.strip('"')
meta_data[key] = value
else:
logging.warning('Malformed meta string \'{}\''.format(meta_data_string))
diff --git a/src/helperFunctions/dependency.py b/src/helperFunctions/dependency.py
index 57ec74b59..c40fa2a5f 100644
--- a/src/helperFunctions/dependency.py
+++ b/src/helperFunctions/dependency.py
@@ -1,10 +1,3 @@
-def schedule_dependencies(schedule_list, dependency_list, myself):
- for item in dependency_list:
- if item not in schedule_list:
- schedule_list.append(item)
- return [myself] + schedule_list
-
-
def get_unmatched_dependencies(fo_list, dependency_list):
missing_dependencies = []
for dependency in dependency_list:
diff --git a/src/helperFunctions/merge_generators.py b/src/helperFunctions/merge_generators.py
index bf610c311..99ad55adf 100644
--- a/src/helperFunctions/merge_generators.py
+++ b/src/helperFunctions/merge_generators.py
@@ -1,5 +1,9 @@
from itertools import zip_longest
from copy import deepcopy
+from random import sample, seed
+
+
+seed()
def merge_generators(*generators):
@@ -70,3 +74,7 @@ def avg(l):
if len(l) == 0:
return 0
return sum(l) / len(l)
+
+
+def shuffled(sequence):
+ return sample(sequence, len(sequence))
diff --git a/src/objects/file.py b/src/objects/file.py
index b638495b6..a1784c734 100644
--- a/src/objects/file.py
+++ b/src/objects/file.py
@@ -22,7 +22,6 @@ def __init__(self, binary=None, file_name=None, file_path=None, scheduled_analys
self.depth = 0
self.processed_analysis = {}
self.scheduled_analysis = scheduled_analysis
- self.analysis_dependency = set()
self.comments = []
self.parent_firmware_uids = set()
self.temporary_data = {}
diff --git a/src/plugins/analysis/crypto_material/view/crypto_material.html b/src/plugins/analysis/crypto_material/view/crypto_material.html
index 1f5db054a..b0b951a8e 100644
--- a/src/plugins/analysis/crypto_material/view/crypto_material.html
+++ b/src/plugins/analysis/crypto_material/view/crypto_material.html
@@ -3,7 +3,7 @@
{% block analysis_result_details %}
{% for key in firmware.processed_analysis[selected_analysis] %}
- {% if key not in ['summary', 'plugin_version', 'analysis_date', 'tags', 'skipped'] %}
+ {% if key not in ['summary', 'plugin_version', 'system_version', 'analysis_date', 'tags', 'skipped'] %}
| Description: |
{{ key }} |
diff --git a/src/plugins/analysis/known_vulnerabilities/view/known_vulnerabilities.html b/src/plugins/analysis/known_vulnerabilities/view/known_vulnerabilities.html
index e8fe148fa..64ca2b645 100644
--- a/src/plugins/analysis/known_vulnerabilities/view/known_vulnerabilities.html
+++ b/src/plugins/analysis/known_vulnerabilities/view/known_vulnerabilities.html
@@ -3,7 +3,7 @@
{% block analysis_result_details %}
{% for key in firmware.processed_analysis[selected_analysis] %}
- {% if key not in ['summary', 'plugin_version', 'analysis_date', 'skipped', 'tags'] %}
+ {% if key not in ['summary', 'plugin_version', 'analysis_date', 'skipped', 'system_version', 'tags'] %}
| {{ key }} |
{{ firmware.processed_analysis[selected_analysis][key]['description'] }}
diff --git a/src/plugins/analysis/software_components/view/software_components.html b/src/plugins/analysis/software_components/view/software_components.html
index 2ca5599ee..0e2457571 100644
--- a/src/plugins/analysis/software_components/view/software_components.html
+++ b/src/plugins/analysis/software_components/view/software_components.html
@@ -10,7 +10,7 @@
{% for key in firmware.processed_analysis[selected_analysis] %}
- {% if key not in ['summary', 'plugin_version', 'analysis_date', 'tags', 'skipped'] %}
+ {% if key not in ['summary', 'system_version', 'plugin_version', 'analysis_date', 'tags', 'skipped'] %}
|
| {{loop.index - 1}} |
Software Name: |
diff --git a/src/scheduler/Analysis.py b/src/scheduler/Analysis.py
index 13d5b2d17..6bf03f273 100644
--- a/src/scheduler/Analysis.py
+++ b/src/scheduler/Analysis.py
@@ -1,15 +1,17 @@
import logging
from concurrent.futures import ThreadPoolExecutor
from configparser import ConfigParser
+from distutils.version import LooseVersion
from multiprocessing import Queue, Value
-from random import shuffle
from queue import Empty
from time import sleep, time
-from typing import Tuple, List, Optional
+from typing import Tuple, List, Optional, Set, Iterable
from helperFunctions.compare_sets import substring_is_in_list
from helperFunctions.config import read_list_from_config
+from helperFunctions.fileSystem import get_file_type_from_binary
+from helperFunctions.merge_generators import shuffled
from helperFunctions.parsing import bcolors
from helperFunctions.plugin import import_plugins
from helperFunctions.process import ExceptionSafeProcess, terminate_process_and_childs
@@ -57,25 +59,48 @@ def shutdown(self):
self.process_queue.close()
logging.info('Analysis System offline')
- def add_update_task(self, fo):
+ def add_update_task(self, fo: FileObject):
for included_file in self.db_backend_service.get_list_of_all_included_files(fo):
child = self.db_backend_service.get_object(included_file)
- child.scheduled_analysis = fo.scheduled_analysis
- shuffle(child.scheduled_analysis)
+ child.scheduled_analysis = self._add_dependencies_recursively(fo.scheduled_analysis or [])
+ child.scheduled_analysis = self._smart_shuffle(child.scheduled_analysis)
self.check_further_process_or_complete(child)
self.check_further_process_or_complete(fo)
- def add_task(self, fo):
+ def add_task(self, fo: FileObject):
'''
This function should be used to add a new firmware object to the scheduler
'''
- if fo.scheduled_analysis is None:
- fo.scheduled_analysis = MANDATORY_PLUGINS
- else:
- shuffle(fo.scheduled_analysis)
- fo.scheduled_analysis = fo.scheduled_analysis + MANDATORY_PLUGINS
+ scheduled_plugins = self._add_dependencies_recursively(fo.scheduled_analysis or [])
+ fo.scheduled_analysis = self._smart_shuffle(scheduled_plugins + MANDATORY_PLUGINS)
self.check_further_process_or_complete(fo)
+ def _smart_shuffle(self, plugin_list: List[str]) -> List[str]:
+ scheduled_plugins = []
+ remaining_plugins = set(plugin_list)
+
+ while len(remaining_plugins) > 0:
+ next_plugins = self._get_plugins_with_met_dependencies(remaining_plugins, scheduled_plugins)
+ if not next_plugins:
+ logging.error('Error: Could not schedule plugins because dependencies cannot be fulfilled: {}'.format(remaining_plugins))
+ break
+ scheduled_plugins[:0] = shuffled(next_plugins)
+ remaining_plugins.difference_update(next_plugins)
+
+ # assure file type is first for blacklist functionality
+ if 'file_type' in scheduled_plugins and scheduled_plugins[-1] != 'file_type':
+ scheduled_plugins.remove('file_type')
+ scheduled_plugins.append('file_type')
+ return scheduled_plugins
+
+ def _get_plugins_with_met_dependencies(self, remaining_plugins: Set[str], scheduled_plugins: List[str]) -> List[str]:
+ met_dependencies = scheduled_plugins
+ return [
+ plugin
+ for plugin in remaining_plugins
+ if all(dependency in met_dependencies for dependency in self.analysis_plugins[plugin].DEPENDENCIES)
+ ]
+
def get_list_of_available_plugins(self):
'''
returns a list of all loaded plugins
@@ -84,6 +109,8 @@ def get_list_of_available_plugins(self):
plugin_list.sort(key=str.lower)
return plugin_list
+# ---- internal functions ----
+
def get_default_plugins_from_config(self):
try:
result = {}
@@ -114,14 +141,14 @@ def get_plugin_dict(self):
result['unpacker'] = ('Additional information provided by the unpacker', True, False)
return result
+# ---- scheduling functions ----
+
def get_scheduled_workload(self):
workload = {'analysis_main_scheduler': self.process_queue.qsize()}
for plugin in self.analysis_plugins:
workload[plugin] = self.analysis_plugins[plugin].in_queue.qsize()
return workload
-# ---- internal functions ----
-
def register_plugin(self, name, plugin_instance):
'''
This function is called upon plugin init to announce its presence
@@ -134,8 +161,6 @@ def load_plugins(self):
plugin = source.load_plugin(plugin_name)
plugin.AnalysisPlugin(self, config=self.config)
-# ---- scheduling functions ----
-
def start_scheduling_process(self):
logging.debug('Starting scheduler...')
self.schedule_process = ExceptionSafeProcess(target=self.scheduler)
@@ -150,6 +175,8 @@ def scheduler(self):
else:
self.process_next_analysis(task)
+ # ---- analysis skipping ----
+
def process_next_analysis(self, fw_object: FileObject):
self.pre_analysis(fw_object)
analysis_to_do = fw_object.scheduled_analysis.pop()
@@ -159,13 +186,53 @@ def process_next_analysis(self, fw_object: FileObject):
else:
self._start_or_skip_analysis(analysis_to_do, fw_object)
- def _start_or_skip_analysis(self, analysis_to_do, fw_object):
- if analysis_to_do in MANDATORY_PLUGINS or self._next_analysis_is_not_blacklisted(analysis_to_do, fw_object):
- self.analysis_plugins[analysis_to_do].add_job(fw_object)
- else:
+ def _start_or_skip_analysis(self, analysis_to_do: str, fw_object: FileObject):
+ if self._analysis_is_already_in_db_and_up_to_date(analysis_to_do, fw_object.get_uid()):
+ logging.debug('skipping analysis "{}" for {} (analysis already in DB)'.format(analysis_to_do, fw_object.get_uid()))
+ if analysis_to_do in self._get_cumulative_remaining_dependencies(fw_object.scheduled_analysis):
+ self._add_completed_analysis_results_to_file_object(analysis_to_do, fw_object)
+ self.check_further_process_or_complete(fw_object)
+ elif analysis_to_do not in MANDATORY_PLUGINS and self._next_analysis_is_blacklisted(analysis_to_do, fw_object):
logging.debug('skipping analysis "{}" for {} (blacklisted file type)'.format(analysis_to_do, fw_object.get_uid()))
fw_object.processed_analysis[analysis_to_do] = self._get_skipped_analysis_result(analysis_to_do)
self.check_further_process_or_complete(fw_object)
+ else:
+ self.analysis_plugins[analysis_to_do].add_job(fw_object)
+
+ def _add_completed_analysis_results_to_file_object(self, analysis_to_do: str, fw_object: FileObject):
+ db_entry = self.db_backend_service.get_specific_fields_of_db_entry(
+ fw_object.get_uid(), {'processed_analysis.{}'.format(analysis_to_do): 1}
+ )
+ desanitized_analysis = self.db_backend_service.retrieve_analysis(db_entry['processed_analysis'])
+ fw_object.processed_analysis[analysis_to_do] = desanitized_analysis[analysis_to_do]
+
+ def _analysis_is_already_in_db_and_up_to_date(self, analysis_to_do: str, uid: str):
+ db_entry = self.db_backend_service.get_specific_fields_of_db_entry(
+ uid,
+ {
+ 'processed_analysis.{}.plugin_version'.format(analysis_to_do): 1,
+ 'processed_analysis.{}.system_version'.format(analysis_to_do): 1
+ }
+ )
+ if not db_entry or analysis_to_do not in db_entry['processed_analysis']:
+ return False
+ elif 'plugin_version' not in db_entry['processed_analysis'][analysis_to_do]:
+ logging.error('Plugin Version missing: UID: {}, Plugin: {}'.format(uid, analysis_to_do))
+ return False
+
+ analysis_plugin_version = db_entry['processed_analysis'][analysis_to_do]['plugin_version']
+ analysis_system_version = db_entry['processed_analysis'][analysis_to_do]['system_version'] \
+ if 'system_version' in db_entry['processed_analysis'][analysis_to_do] else None
+ plugin_version = self.analysis_plugins[analysis_to_do].VERSION
+ system_version = self.analysis_plugins[analysis_to_do].SYSTEM_VERSION \
+ if hasattr(self.analysis_plugins[analysis_to_do], 'SYSTEM_VERSION') else None
+
+ if LooseVersion(analysis_plugin_version) < LooseVersion(plugin_version) or \
+ LooseVersion(analysis_system_version or '0') < LooseVersion(system_version or '0'):
+ return False
+ return True
+
+# ---- blacklist and whitelist ----
def _get_skipped_analysis_result(self, analysis_to_do):
return {
@@ -175,28 +242,27 @@ def _get_skipped_analysis_result(self, analysis_to_do):
'plugin_version': self.analysis_plugins[analysis_to_do].VERSION
}
- # ---- blacklist and whitelist ----
-
- def _next_analysis_is_not_blacklisted(self, next_analysis, fw_object: FileObject):
+ def _next_analysis_is_blacklisted(self, next_analysis: str, fw_object: FileObject):
blacklist, whitelist = self._get_blacklist_and_whitelist(next_analysis)
if not (blacklist or whitelist):
- return True
+ return False
if blacklist and whitelist:
logging.error('{}Configuration of plugin "{}" erroneous{}: found blacklist and whitelist. Ignoring blacklist.'.format(
bcolors.FAIL, next_analysis, bcolors.ENDC))
- try:
- file_type = fw_object.processed_analysis['file_type']['mime'].lower()
- except KeyError: # FIXME file_type analysis is missing (probably due to problem with analysis caching) -> re-schedule
- fw_object.scheduled_analysis.extend([next_analysis, 'file_type'])
- fw_object.analysis_dependency.add('file_type')
- return False
+ file_type = self._get_file_type_from_object_or_db(fw_object)
if whitelist:
- return substring_is_in_list(file_type, whitelist)
- return not substring_is_in_list(file_type, blacklist)
+ return not substring_is_in_list(file_type, whitelist)
+ return substring_is_in_list(file_type, blacklist)
+
+ def _get_file_type_from_object_or_db(self, fw_object: FileObject) -> Optional[str]:
+ if 'file_type' not in fw_object.processed_analysis:
+ self._add_completed_analysis_results_to_file_object('file_type', fw_object)
- def _get_blacklist_and_whitelist(self, next_analysis):
+ return fw_object.processed_analysis['file_type']['mime'].lower()
+
+ def _get_blacklist_and_whitelist(self, next_analysis: str) -> Tuple[List, List]:
blacklist, whitelist = self._get_blacklist_and_whitelist_from_config(next_analysis)
if not (blacklist or whitelist):
blacklist, whitelist = self._get_blacklist_and_whitelist_from_plugin(next_analysis)
@@ -207,18 +273,20 @@ def _get_blacklist_and_whitelist_from_config(self, analysis_plugin: str) -> Tupl
whitelist = read_list_from_config(self.config, analysis_plugin, 'mime_whitelist')
return blacklist, whitelist
+# ---- result collector functions ----
+
def _get_blacklist_and_whitelist_from_plugin(self, analysis_plugin: str) -> Tuple[List, List]:
blacklist = self.analysis_plugins[analysis_plugin].MIME_BLACKLIST if hasattr(self.analysis_plugins[analysis_plugin], 'MIME_BLACKLIST') else []
whitelist = self.analysis_plugins[analysis_plugin].MIME_WHITELIST if hasattr(self.analysis_plugins[analysis_plugin], 'MIME_WHITELIST') else []
return blacklist, whitelist
-# ---- result collector functions ----
-
def start_result_collector(self):
logging.debug('Starting result collector')
self.result_collector_process = ExceptionSafeProcess(target=self.result_collector)
self.result_collector_process.start()
+# ---- miscellaneous functions ----
+
def result_collector(self):
while self.stop_condition.value == 0:
nop = True
@@ -240,8 +308,6 @@ def _handle_analysis_tags(self, fw, plugin):
self.tag_queue.put(check_tags(fw, plugin))
return add_tags_to_object(fw, plugin)
-# ---- miscellaneous functions ----
-
def check_further_process_or_complete(self, fw_object):
if not fw_object.scheduled_analysis:
logging.info('Analysis Completed:\n{}'.format(fw_object))
@@ -261,8 +327,24 @@ def check_exceptions(self):
return True
for process in [self.schedule_process, self.result_collector_process]:
if process.exception:
- logging.error("{}Exception in scheduler process {}{}".format(bcolors.FAIL, bcolors.ENDC, process.name))
+ logging.error('{}Exception in scheduler process {}{}'.format(bcolors.FAIL, bcolors.ENDC, process.name))
logging.error(process.exception[1])
terminate_process_and_childs(process)
return True # Error here means nothing will ever get scheduled again. Thing should just break !
return False
+
+ def _add_dependencies_recursively(self, scheduled_analyses: List[str]) -> List[str]:
+ scheduled_analyses_set = set(scheduled_analyses)
+ while True:
+ new_dependencies = self._get_cumulative_remaining_dependencies(scheduled_analyses_set)
+ if not new_dependencies:
+ break
+ scheduled_analyses_set.update(new_dependencies)
+ return list(scheduled_analyses_set)
+
+ def _get_cumulative_remaining_dependencies(self, scheduled_analyses: Set[str]) -> Set[str]:
+ return {
+ dependency
+ for plugin in scheduled_analyses
+ for dependency in self.analysis_plugins[plugin].DEPENDENCIES
+ }.difference(scheduled_analyses)
diff --git a/src/storage/db_interface_common.py b/src/storage/db_interface_common.py
index ba0e151b0..b08e2d0f7 100644
--- a/src/storage/db_interface_common.py
+++ b/src/storage/db_interface_common.py
@@ -204,6 +204,9 @@ def _retrieve_binaries(self, sanitized_dict, key):
tmp_dict[analysis_key] = report
return tmp_dict
+ def get_specific_fields_of_db_entry(self, uid, field_dict):
+ return self.file_objects.find_one(uid, field_dict) or self.firmwares.find_one(uid, field_dict)
+
# --- summary recreation
def get_list_of_all_included_files(self, fo):
diff --git a/src/storage/db_interface_frontend.py b/src/storage/db_interface_frontend.py
index 6a15039c4..7e819a3e2 100644
--- a/src/storage/db_interface_frontend.py
+++ b/src/storage/db_interface_frontend.py
@@ -166,9 +166,6 @@ def get_other_versions_of_firmware(self, firmware_object):
results = self.firmwares.find(query, {'_id': 1, 'version': 1})
return [r for r in results if r['_id'] != firmware_object.get_uid()]
- def get_specific_fields_of_db_entry(self, uid, field_dict):
- return self.file_objects.find_one(uid, field_dict) or self.firmwares.find_one(uid, field_dict)
-
def get_specific_fields_for_multiple_entries(self, uid_list, field_dict):
query = self._build_search_query_for_uid_list(uid_list)
file_object_iterator = self.file_objects.find(query, field_dict)
diff --git a/src/test/common_helper.py b/src/test/common_helper.py
index 9e780d364..ffdd79bad 100644
--- a/src/test/common_helper.py
+++ b/src/test/common_helper.py
@@ -64,8 +64,7 @@ class MockFileObject(object):
def __init__(self, binary=b'test string', file_path='/bin/ls'):
self.binary = binary
self.file_path = file_path
- self.processed_analysis = {'file_type': {
- 'mime': 'application/x-executable'}}
+ self.processed_analysis = {'file_type': {'mime': 'application/x-executable'}}
class DatabaseMock:
@@ -330,6 +329,9 @@ def check_unpacking_lock(self, uid):
def drop_unpacking_locks(self):
self.locks = []
+ def get_specific_fields_of_db_entry(self, uid, field_dict):
+ return None # TODO
+
def fake_exit(self, *args):
pass
diff --git a/src/test/integration/common.py b/src/test/integration/common.py
index 19afaa728..9fef8e014 100644
--- a/src/test/integration/common.py
+++ b/src/test/integration/common.py
@@ -22,6 +22,9 @@ def existence_quick_check(self, uid):
def add_object(self, fo_fw):
self._objects[fo_fw.uid] = fo_fw
+ def get_specific_fields_of_db_entry(self, uid, field_dict):
+ pass
+
def initialize_config(tmp_dir):
config = get_config_for_testing(temp_dir=tmp_dir)
diff --git a/src/test/mock.py b/src/test/mock.py
new file mode 100644
index 000000000..f4a2fe355
--- /dev/null
+++ b/src/test/mock.py
@@ -0,0 +1,39 @@
+from contextlib import contextmanager
+from typing import Callable
+
+
+class MockSpy:
+ _called = False
+ _args = None
+
+ def spy_function(self, *args):
+ self._called = True
+ self._args = args
+
+ def was_called(self):
+ return self._called
+
+
+@contextmanager
+def mock_spy(o: object, method: str):
+ spy = MockSpy()
+ if not hasattr(o, method):
+ raise AttributeError('{} has no method {}'.format(type(o), method))
+ tmp = getattr(o, method)
+ try:
+ setattr(o, method, spy.spy_function)
+ yield spy
+ finally:
+ setattr(o, method, tmp)
+
+
+@contextmanager
+def mock_patch(o: object, method: str, replacement_method: Callable):
+ if not hasattr(o, method):
+ raise AttributeError('{} has no method {}'.format(type(o), method))
+ tmp = getattr(o, method)
+ try:
+ setattr(o, method, replacement_method)
+ yield o
+ finally:
+ setattr(o, method, tmp)
diff --git a/src/test/unit/analysis/test_plugin_base.py b/src/test/unit/analysis/test_plugin_base.py
index c87ce239a..e88c08089 100644
--- a/src/test/unit/analysis/test_plugin_base.py
+++ b/src/test/unit/analysis/test_plugin_base.py
@@ -15,7 +15,7 @@ class TestPluginBase(unittest.TestCase):
def setUp(self):
config = self.set_up_base_config()
- self.pBase = AnalysisBasePlugin(self, config)
+ self.base_plugin = AnalysisBasePlugin(self, config)
@staticmethod
def set_up_base_config():
@@ -27,7 +27,7 @@ def set_up_base_config():
return config
def tearDown(self):
- self.pBase.shutdown()
+ self.base_plugin.shutdown()
gc.collect()
def register_plugin(self, name, plugin_object):
@@ -45,8 +45,8 @@ def test_start_stop_workers(self):
def test_object_processing_no_childs(self):
root_object = FileObject(binary=b'root_file')
- self.pBase.in_queue.put(root_object)
- processed_object = self.pBase.out_queue.get()
+ self.base_plugin.in_queue.put(root_object)
+ processed_object = self.base_plugin.out_queue.get()
self.assertEqual(processed_object.get_uid(), root_object.get_uid(), 'uid changed')
self.assertTrue('base' in processed_object.processed_analysis, 'object not processed')
self.assertEqual(processed_object.processed_analysis['base']['plugin_version'], 'not set', 'plugin version missing in results')
@@ -56,97 +56,61 @@ def test_object_processing_one_child(self):
root_object = FileObject(binary=b'root_file')
child_object = FileObject(binary=b'first_child_object')
root_object.add_included_file(child_object)
- self.pBase.in_queue.put(root_object)
- processed_object = self.pBase.out_queue.get()
+ self.base_plugin.in_queue.put(root_object)
+ processed_object = self.base_plugin.out_queue.get()
self.assertEqual(processed_object.get_uid(), root_object.get_uid(), 'uid changed')
self.assertTrue(child_object.get_uid() in root_object.get_included_files_uids(), 'child object not in processed file')
def test_get_workload(self):
- assert self.pBase.get_workload() == 0
+ assert self.base_plugin.get_workload() == 0
class TestPluginBaseAddJob(TestPluginBase):
- def test_dependency_condition_check_no_deps(self):
- fo = FileObject(binary='test', scheduled_analysis=[])
- self.assertTrue(self.pBase._dependencies_are_fulfilled(fo), 'no deps specified')
-
- def test_dependency_condition_check_unmatched_deps(self):
- self.pBase.DEPENDENCIES = ['foo']
- fo = FileObject(binary='test', scheduled_analysis=[])
- self.assertFalse(self.pBase._dependencies_are_fulfilled(fo), 'deps specified and unmatched')
-
- def test_dependency_condition_check_matched_deps(self):
- self.pBase.DEPENDENCIES = ['foo']
- fo = FileObject(binary='test', scheduled_analysis=[])
- fo.processed_analysis.update({'foo': []})
- self.assertTrue(self.pBase._dependencies_are_fulfilled(fo), 'Fals but deps matched')
-
- def test_recursive_condition_is_set(self):
+ def test_analysis_depth_not_reached_yet(self):
fo = FileObject(binary='test', scheduled_analysis=[])
fo.depth = 1
- self.pBase.recursive = False
- self.assertFalse(self.pBase._recursive_condition_is_set(fo), 'positive but not root object')
+ self.base_plugin.recursive = False
+ self.assertFalse(self.base_plugin._analysis_depth_not_reached_yet(fo), 'positive but not root object')
fo.depth = 0
- self.pBase.recursive = False
- self.assertTrue(self.pBase._recursive_condition_is_set(fo))
+ self.base_plugin.recursive = False
+ self.assertTrue(self.base_plugin._analysis_depth_not_reached_yet(fo))
+
fo.depth = 1
- self.pBase.recursive = True
- self.assertTrue(self.pBase._recursive_condition_is_set(fo))
+ self.base_plugin.recursive = True
+ self.assertTrue(self.base_plugin._analysis_depth_not_reached_yet(fo))
fo.depth = 0
- self.pBase.recursive = True
- self.assertTrue(self.pBase._recursive_condition_is_set(fo))
+ self.base_plugin.recursive = True
+ self.assertTrue(self.base_plugin._analysis_depth_not_reached_yet(fo))
def test__add_job__recursive_is_set(self):
fo = FileObject(binary='test', scheduled_analysis=[])
fo.depth = 1
- self.pBase.recursive = False
- self.pBase.add_job(fo)
- out_fo = self.pBase.out_queue.get(timeout=5)
+ self.base_plugin.recursive = False
+ self.base_plugin.add_job(fo)
+ out_fo = self.base_plugin.out_queue.get(timeout=5)
self.assertIsInstance(out_fo, FileObject, 'not added to out_queue')
- self.pBase.recursive = True
- self.assertTrue(self.pBase._recursive_condition_is_set(fo), 'not positvie but recursive')
-
- def test_add_job_dependency_not_matched(self):
- self.pBase.DEPENDENCIES = ['foo']
- fo = FileObject(binary='test', scheduled_analysis=[])
- self.pBase.add_job(fo)
- fo = self.pBase.out_queue.get(timeout=5)
- self.assertEqual(fo.scheduled_analysis, ['base', 'foo'], 'analysis not scheduled')
- self.assertNotIn('base', fo.processed_analysis, 'base added to processed analysis, but is not processed')
+ self.base_plugin.recursive = True
+ self.assertTrue(self.base_plugin._analysis_depth_not_reached_yet(fo), 'not positive but recursive')
class TestPluginBaseOffline(TestPluginBase):
def setUp(self):
- self.pBase = AnalysisBasePlugin(self, config=self.set_up_base_config(), offline_testing=True)
-
- def test_object_history(self):
- test_fo = create_test_file_object()
- self.pBase.add_job(test_fo)
- result = self.pBase.in_queue.get(timeout=5)
- self.assertTrue(self.pBase.out_queue.empty(), 'added to outque but not in history')
- self.pBase.add_job(test_fo)
- result = self.pBase.out_queue.get(timeout=5)
- self.assertTrue(self.pBase.in_queue.empty(), 'added to inque but already in history')
- # required dependency check
- test_fo.analysis_dependency.add(self.pBase.NAME)
- self.pBase.add_job(test_fo)
- result = self.pBase.in_queue.get(timeout=5)
- self.assertTrue(self.pBase.out_queue.empty(), 'added to out queue but should be reanalyzed because of dependency request')
+ self.base_plugin = AnalysisBasePlugin(self, config=self.set_up_base_config(), offline_testing=True)
def test_get_view_file_path(self):
plugin_path = os.path.join(get_src_dir(), 'plugins/analysis/file_type/')
code_path = os.path.join(plugin_path, 'code/file_type.py')
estimated_view_path = os.path.join(plugin_path, 'view/file_type.html')
- assert self.pBase._get_view_file_path(code_path) == estimated_view_path
+ assert self.base_plugin._get_view_file_path(code_path) == estimated_view_path
plugin_path_without_view = os.path.join(get_src_dir(), 'plugins/analysis/dummy/code/dummy.py')
- assert self.pBase._get_view_file_path(plugin_path_without_view) is None
+ assert self.base_plugin._get_view_file_path(plugin_path_without_view) is None
class TestPluginNotRunning(TestPluginBase):
diff --git a/src/test/unit/helperFunctions/test_dependency.py b/src/test/unit/helperFunctions/test_dependency.py
index 151d344df..6260cf57b 100644
--- a/src/test/unit/helperFunctions/test_dependency.py
+++ b/src/test/unit/helperFunctions/test_dependency.py
@@ -5,7 +5,7 @@
'''
import unittest
-from helperFunctions.dependency import schedule_dependencies, get_unmatched_dependencies
+from helperFunctions.dependency import get_unmatched_dependencies
class MockFileObject:
@@ -13,22 +13,6 @@ def __init__(self, processed_analysis_list):
self.processed_analysis = processed_analysis_list
-class TestHelperFunctionsDependencySchedule(unittest.TestCase):
-
- def schedule_test(self, schedule_list, dependency_list, myself, out_list):
- new_schedule = schedule_dependencies(schedule_list, dependency_list, myself)
- self.assertEqual(new_schedule, out_list, 'schedule not correct')
-
- def test_schedule_simple_case(self):
- self.schedule_test(['a', 'b'], ['b'], 'c', ['c', 'a', 'b'])
-
- def test_schedule_not_in_list(self):
- self.schedule_test([], ['a'], 'b', ['b', 'a'])
-
- def test_schedule_multiple_in_not_in_list(self):
- self.schedule_test(['a', 'b'], ['b', 'c', 'd', 'a'], 'e', ['e', 'a', 'b', 'c', 'd'])
-
-
class TestHelperFunctionsDependencyMatch(unittest.TestCase):
def unmatched_dependency_test(self, processed_list, dependencies, out_list):
diff --git a/src/test/unit/scheduler/test_analysis.py b/src/test/unit/scheduler/test_analysis.py
index 6d514af35..89985c2d2 100644
--- a/src/test/unit/scheduler/test_analysis.py
+++ b/src/test/unit/scheduler/test_analysis.py
@@ -1,13 +1,17 @@
import gc
import os
+from contextlib import suppress
from multiprocessing import Queue
from unittest import TestCase, mock
+import pytest
+
from helperFunctions.config import get_config_for_testing
from helperFunctions.fileSystem import get_test_data_dir
from objects.firmware import Firmware
from scheduler.Analysis import AnalysisScheduler, MANDATORY_PLUGINS
from test.common_helper import DatabaseMock, fake_exit, MockFileObject
+from test.mock import mock_spy, mock_patch
class AnalysisSchedulerTest(TestCase):
@@ -93,8 +97,26 @@ def test_get_plugin_dict_version(self):
self.assertEqual(self.sched.analysis_plugins['file_type'].VERSION, result['file_type'][3], 'version not correct')
self.assertEqual(self.sched.analysis_plugins['file_hashes'].VERSION, result['file_hashes'][3], 'version not correct')
+ def test_process_next_analysis_unknown_plugin(self):
+ test_fw = Firmware(file_path=os.path.join(get_test_data_dir(), 'get_files_test/testfile1'))
+ test_fw.scheduled_analysis = ['unknown_plugin']
+
+ with mock_spy(self.sched, '_start_or_skip_analysis') as spy:
+ self.sched.process_next_analysis(test_fw)
+ assert not spy.was_called(), 'unknown plugin should simply be skipped'
+
+ def test_skip_analysis_because_whitelist(self):
+ self.sched.config.set('dummy_plugin_for_testing_only', 'mime_whitelist', 'foo, bar')
+ test_fw = Firmware(file_path=os.path.join(get_test_data_dir(), 'get_files_test/testfile1'))
+ test_fw.scheduled_analysis = ['file_hashes']
+ test_fw.processed_analysis['file_type'] = {'mime': 'text/plain'}
+ self.sched._start_or_skip_analysis('dummy_plugin_for_testing_only', test_fw)
+ test_fw = self.tmp_queue.get(timeout=10)
+ assert 'dummy_plugin_for_testing_only' in test_fw.processed_analysis
+ assert 'skipped' in test_fw.processed_analysis['dummy_plugin_for_testing_only']
+
-class TestAnalysisSchedulerBlacklist(AnalysisSchedulerTest):
+class TestAnalysisSchedulerBlacklist:
test_plugin = 'test_plugin'
fo = MockFileObject()
@@ -109,6 +131,18 @@ def __init__(self, blacklist=None, whitelist=None):
def shutdown(self):
pass
+ @classmethod
+ def setup_class(cls):
+ cls.init_patch = mock.patch(target='scheduler.Analysis.AnalysisScheduler.__init__', new=lambda *_: None)
+ cls.init_patch.start()
+ cls.sched = AnalysisScheduler()
+ cls.sched.analysis_plugins = {}
+ cls.plugin_list = ['no_deps', 'foo', 'bar']
+ cls.init_patch.stop()
+
+ def setup(self):
+ self.sched.config = get_config_for_testing()
+
def test_get_blacklist_and_whitelist_from_plugin(self):
self.sched.analysis_plugins['test_plugin'] = self.PluginMock(['foo'], ['bar'])
blacklist, whitelist = self.sched._get_blacklist_and_whitelist_from_plugin('test_plugin')
@@ -137,61 +171,200 @@ def test_get_blacklist_and_whitelist__plugin_only(self):
blacklist, whitelist = self.sched._get_blacklist_and_whitelist('test_plugin')
assert (blacklist, whitelist) == (['foo'], ['bar'])
- def test_next_analysis_is_not_blacklisted__blacklisted(self):
+ def test_next_analysis_is_blacklisted__blacklisted(self):
self.sched.analysis_plugins[self.test_plugin] = self.PluginMock(blacklist=['blacklisted_type'])
self.fo.processed_analysis['file_type']['mime'] = 'blacklisted_type'
- not_blacklisted = self.sched._next_analysis_is_not_blacklisted(self.test_plugin, self.fo)
- assert not_blacklisted is False
+ blacklisted = self.sched._next_analysis_is_blacklisted(self.test_plugin, self.fo)
+ assert blacklisted is True
- def test_next_analysis_is_not_blacklisted__not_blacklisted(self):
+ def test_next_analysis_is_blacklisted__not_blacklisted(self):
self.sched.analysis_plugins[self.test_plugin] = self.PluginMock(blacklist=[])
self.fo.processed_analysis['file_type']['mime'] = 'not_blacklisted_type'
- not_blacklisted = self.sched._next_analysis_is_not_blacklisted(self.test_plugin, self.fo)
- assert not_blacklisted is True
+ blacklisted = self.sched._next_analysis_is_blacklisted(self.test_plugin, self.fo)
+ assert blacklisted is False
- def test_next_analysis_is_not_blacklisted__whitelisted(self):
+ def test_next_analysis_is_blacklisted__whitelisted(self):
self.sched.analysis_plugins[self.test_plugin] = self.PluginMock(whitelist=['whitelisted_type'])
self.fo.processed_analysis['file_type']['mime'] = 'whitelisted_type'
- not_blacklisted = self.sched._next_analysis_is_not_blacklisted(self.test_plugin, self.fo)
- assert not_blacklisted is True
+ blacklisted = self.sched._next_analysis_is_blacklisted(self.test_plugin, self.fo)
+ assert blacklisted is False
- def test_next_analysis_is_not_blacklisted__not_whitelisted(self):
+ def test_next_analysis_is_blacklisted__not_whitelisted(self):
self.sched.analysis_plugins[self.test_plugin] = self.PluginMock(whitelist=['some_other_type'])
self.fo.processed_analysis['file_type']['mime'] = 'not_whitelisted_type'
- not_blacklisted = self.sched._next_analysis_is_not_blacklisted(self.test_plugin, self.fo)
- assert not_blacklisted is False
+ blacklisted = self.sched._next_analysis_is_blacklisted(self.test_plugin, self.fo)
+ assert blacklisted is True
- def test_next_analysis_is_not_blacklisted__whitelist_precedes_blacklist(self):
+ def test_next_analysis_is_blacklisted__whitelist_precedes_blacklist(self):
self.sched.analysis_plugins[self.test_plugin] = self.PluginMock(blacklist=['test_type'], whitelist=['test_type'])
self.fo.processed_analysis['file_type']['mime'] = 'test_type'
- not_blacklisted = self.sched._next_analysis_is_not_blacklisted(self.test_plugin, self.fo)
- assert not_blacklisted is True
+ blacklisted = self.sched._next_analysis_is_blacklisted(self.test_plugin, self.fo)
+ assert blacklisted is False
self.sched.analysis_plugins[self.test_plugin] = self.PluginMock(blacklist=[], whitelist=['some_other_type'])
self.fo.processed_analysis['file_type']['mime'] = 'test_type'
- not_blacklisted = self.sched._next_analysis_is_not_blacklisted(self.test_plugin, self.fo)
- assert not_blacklisted is False
+ blacklisted = self.sched._next_analysis_is_blacklisted(self.test_plugin, self.fo)
+ assert blacklisted is True
- def test_next_analysis_is_not_blacklisted__mime_missing(self):
- self.sched.analysis_plugins[self.test_plugin] = self.PluginMock(blacklist=['test_type'], whitelist=['test_type'])
- self.fo.processed_analysis['file_type'].pop('mime')
- self.fo.scheduled_analysis = []
- self.fo.analysis_dependency = set()
- not_blacklisted = self.sched._next_analysis_is_not_blacklisted(self.test_plugin, self.fo)
- assert not_blacklisted is False
- assert 'file_type' in self.fo.analysis_dependency
- assert self.fo.scheduled_analysis == [self.test_plugin, 'file_type']
-
- def test_start_or_skip_analysis(self):
- self.sched.config.set('dummy_plugin_for_testing_only', 'mime_whitelist', 'foo, bar')
- test_fw = Firmware(file_path=os.path.join(get_test_data_dir(), 'get_files_test/testfile1'))
- test_fw.scheduled_analysis = ['file_hashes']
- test_fw.processed_analysis['file_type'] = {'mime': 'text/plain'}
- self.sched._start_or_skip_analysis('dummy_plugin_for_testing_only', test_fw)
- test_fw = self.tmp_queue.get(timeout=10)
- assert 'dummy_plugin_for_testing_only' in test_fw.processed_analysis
- assert 'skipped' in test_fw.processed_analysis['dummy_plugin_for_testing_only']
+ def test_get_blacklist_file_type_from_database(self):
+ def add_file_type_mock(_, fo):
+ fo.processed_analysis['file_type'] = {'mime': 'foo_type'}
+
+ file_object = MockFileObject()
+ file_object.processed_analysis.pop('file_type')
+ with mock_patch(self.sched, '_add_completed_analysis_results_to_file_object', add_file_type_mock):
+ result = self.sched._get_file_type_from_object_or_db(file_object)
+ assert result == 'foo_type'
def _add_test_plugin_to_config(self):
self.sched.config.add_section('test_plugin')
self.sched.config.set('test_plugin', 'mime_blacklist', 'type1, type2')
+
+
+class TestUtilityFunctions:
+
+ class PluginMock:
+ def __init__(self, dependencies):
+ self.DEPENDENCIES = dependencies
+
+ @classmethod
+ def setup_class(cls):
+ cls.init_patch = mock.patch(target='scheduler.Analysis.AnalysisScheduler.__init__', new=lambda *_: None)
+ cls.init_patch.start()
+ cls.scheduler = AnalysisScheduler()
+ cls.plugin_list = ['no_deps', 'foo', 'bar']
+ cls.init_patch.stop()
+
+ def _add_plugins(self):
+ self.scheduler.analysis_plugins = {
+ 'no_deps': self.PluginMock(dependencies=[]),
+ 'foo': self.PluginMock(dependencies=['no_deps']),
+ 'bar': self.PluginMock(dependencies=['no_deps', 'foo'])
+ }
+
+ def _add_plugins_with_recursive_dependencies(self):
+ self.scheduler.analysis_plugins = {
+ 'p1': self.PluginMock(['p2', 'p3']),
+ 'p2': self.PluginMock(['p3']),
+ 'p3': self.PluginMock([]),
+ 'p4': self.PluginMock(['p5']),
+ 'p5': self.PluginMock(['p6']),
+ 'p6': self.PluginMock([])
+ }
+
+ @pytest.mark.parametrize('input_data, expected_output', [
+ (set(), set()),
+ ({'p1'}, {'p2', 'p3'}),
+ ({'p3'}, set()),
+ ({'p1', 'p2', 'p3', 'p4'}, {'p5'}),
+ ])
+ def test_get_cumulative_remaining_dependencies(self, input_data, expected_output):
+ self._add_plugins_with_recursive_dependencies()
+ result = self.scheduler._get_cumulative_remaining_dependencies(input_data)
+ assert result == expected_output
+
+ @pytest.mark.parametrize('input_data, expected_output', [
+ ([], set()),
+ (['p3'], {'p3'}),
+ (['p1'], {'p1', 'p2', 'p3'}),
+ (['p4'], {'p4', 'p5', 'p6'}),
+ ])
+ def test_add_dependencies_recursively(self, input_data, expected_output):
+ self._add_plugins_with_recursive_dependencies()
+ result = self.scheduler._add_dependencies_recursively(input_data)
+ assert set(result) == expected_output
+
+ @pytest.mark.parametrize('remaining, scheduled, expected_output', [
+ ({}, [], []),
+ ({'no_deps', 'foo', 'bar'}, [], ['no_deps']),
+ ({'foo', 'bar'}, ['no_deps'], ['foo']),
+ ({'bar'}, ['no_deps', 'foo'], ['bar']),
+ ])
+ def test_get_plugins_with_met_dependencies(self, remaining, scheduled, expected_output):
+ self._add_plugins()
+ assert self.scheduler._get_plugins_with_met_dependencies(remaining, scheduled) == expected_output
+
+ @pytest.mark.parametrize('remaining, scheduled, expected_output', [
+ ({'bar'}, ['no_deps', 'foo'], {'bar'}),
+ ({'foo', 'bar'}, ['no_deps', 'foo'], {'foo', 'bar'}),
+ ])
+ def test_get_plugins_with_met_dependencies__completed_analyses(self, remaining, scheduled, expected_output):
+ self._add_plugins()
+ assert set(self.scheduler._get_plugins_with_met_dependencies(remaining, scheduled)) == expected_output
+
+ def test_smart_shuffle(self):
+ self._add_plugins()
+ result = self.scheduler._smart_shuffle(self.plugin_list)
+ assert result == ['bar', 'foo', 'no_deps']
+
+ def test_smart_shuffle__impossible_dependency(self):
+ self._add_plugins()
+ self.scheduler.analysis_plugins['impossible'] = self.PluginMock(dependencies=['impossible to meet'])
+ result = self.scheduler._smart_shuffle(self.plugin_list + ['impossible'])
+ assert 'impossible' not in result
+ assert result == ['bar', 'foo', 'no_deps']
+
+ def test_smart_shuffle__circle_dependency(self):
+ self.scheduler.analysis_plugins = {
+ 'p1': self.PluginMock(['p2']),
+ 'p2': self.PluginMock(['p3']),
+ 'p3': self.PluginMock(['p1']),
+ }
+ result = self.scheduler._smart_shuffle(['p1', 'p2', 'p3'])
+ assert result == []
+
+
+class TestAnalysisSkipping:
+
+ class PluginMock:
+ def __init__(self, version, system_version):
+ self.VERSION = version
+ if system_version:
+ self.SYSTEM_VERSION = system_version
+
+ class BackendMock:
+ def __init__(self, analysis_entry=None):
+ self.analysis_entry = analysis_entry if analysis_entry else {}
+
+ def get_specific_fields_of_db_entry(self, *_):
+ return self.analysis_entry
+
+ @classmethod
+ def setup_class(cls):
+ cls.init_patch = mock.patch(target='scheduler.Analysis.AnalysisScheduler.__init__', new=lambda *_: None)
+ cls.init_patch.start()
+
+ cls.scheduler = AnalysisScheduler()
+ cls.scheduler.analysis_plugins = {}
+
+ cls.init_patch.stop()
+
+ @pytest.mark.parametrize(
+ 'plugin_version, plugin_system_version, analysis_plugin_version, '
+ 'analysis_system_version, expected_output', [
+ ('1.0', None, '1.0', None, True),
+ ('1.1', None, '1.0', None, False),
+ ('1.0', None, '1.1', None, True),
+ ('1.0', '2.0', '1.0', '2.0', True),
+ ('1.0', '2.0', '1.0', '2.1', True),
+ ('1.0', '2.1', '1.0', '2.0', False),
+ ('1.0', '2.0', '1.0', None, False),
+ ]
+ )
+ def test_analysis_is_already_in_db_and_up_to_date(
+ self, plugin_version, plugin_system_version, analysis_plugin_version, analysis_system_version, expected_output):
+ plugin = 'foo'
+ analysis_entry = {'processed_analysis': {plugin: {
+ 'plugin_version': analysis_plugin_version, 'system_version': analysis_system_version
+ }}}
+ self.scheduler.db_backend_service = self.BackendMock(analysis_entry)
+ self.scheduler.analysis_plugins[plugin] = self.PluginMock(
+ version=plugin_version, system_version=plugin_system_version)
+ assert self.scheduler._analysis_is_already_in_db_and_up_to_date(plugin, '') == expected_output
+
+ def test_analysis_is_already_in_db_and_up_to_date__missing_version(self):
+ plugin = 'foo'
+ analysis_entry = {'processed_analysis': {plugin: {}}}
+ self.scheduler.db_backend_service = self.BackendMock(analysis_entry)
+ self.scheduler.analysis_plugins[plugin] = self.PluginMock(version='1.0', system_version='1.0')
+ assert self.scheduler._analysis_is_already_in_db_and_up_to_date(plugin, '') is False