summaryrefslogtreecommitdiffstats
path: root/peekaboo
diff options
context:
space:
mode:
Diffstat (limited to 'peekaboo')
-rw-r--r--peekaboo/config.py288
-rw-r--r--peekaboo/daemon.py20
-rw-r--r--peekaboo/exceptions.py5
-rw-r--r--peekaboo/locale/de/LC_MESSAGES/peekaboo.mobin4219 -> 4074 bytes
-rw-r--r--peekaboo/locale/de/LC_MESSAGES/peekaboo.po54
-rw-r--r--peekaboo/locale/peekaboo.pot52
-rw-r--r--peekaboo/queuing.py4
-rw-r--r--peekaboo/ruleset/engine.py52
-rw-r--r--peekaboo/ruleset/rules.py180
9 files changed, 392 insertions, 263 deletions
diff --git a/peekaboo/config.py b/peekaboo/config.py
index 9215011..51a4870 100644
--- a/peekaboo/config.py
+++ b/peekaboo/config.py
@@ -26,6 +26,7 @@
defaults as well as reading a configuration file. """
+import re
import sys
import logging
import configparser
@@ -55,8 +56,160 @@ class PeekabooConfigParser( # pylint: disable=too-many-ancestors
'Configuration file "%s" can not be parsed: %s' %
(config_file, cperror))
+ self.lists = {}
+ self.relists = {}
+
+ def getlist(self, section, option, raw=False, vars=None, fallback=None):
+ """ Special getter where multiple options in the config file
+ distinguished by a .<no> suffix form a list. Matches the signature for
+ configparser getters. """
+ # cache results because the following is somewhat inefficient
+ if section not in self.lists:
+ self.lists[section] = {}
+
+ if option in self.lists[section]:
+ return self.lists[section][option]
+
+ if section not in self:
+ self.lists[section][option] = fallback
+ return fallback
+
+ # Go over all options in this section we want to allow "holes" in
+ # the lists, i.e setting.1, setting.2 but no setting.3 followed by
+ # setting.4. We use here that ConfigParser retains option order from
+ # the file.
+ value = []
+ for setting in self[section]:
+ if not setting.startswith(option):
+ continue
+
+ # Parse 'setting' into (key) and 'setting.subscript' into
+ # (key, subscript) and use it to determine if this setting is a
+ # list. Note how we do not use the subscript at all here.
+ name_parts = setting.split('.')
+ key = name_parts[0]
+ is_list = len(name_parts) > 1
+
+ if key != option:
+ continue
+
+ if not is_list:
+ raise PeekabooConfigException(
+ 'Option %s in section %s is supposed to be a list '
+ 'but given as individual setting' % (setting, section))
+
+ # Potential further checks:
+ # - There are no duplicate settings with ConfigParser. The last
+ # one always wins.
+
+ value.append(self[section].get(setting, raw=raw, vars=vars))
+
+ # it's not gonna get any better on the next call, so cache even the
+ # default
+ if not value:
+ value = fallback
+
+ self.lists[section][option] = value
+ return value
+
+ def getrelist(self, section, option, raw=False, vars=None, fallback=None):
+ """ Special getter for lists of regular expressions. Returns the
+ compiled expression objects in a list ready for matching and searching.
+ """
+ if section not in self.relists:
+ self.relists[section] = {}
+
+ if option in self.relists[section]:
+ return self.relists[section][option]
+
+ if section not in self:
+ self.relists[section][option] = fallback
+ return fallback
+
+ strlist = self[section].getlist(option, raw=raw, vars=vars,
+ fallback=fallback)
+ if strlist is None:
+ self.relists[section][option] = None
+ return None
+
+ compiled_res = []
+ for regex in strlist:
+ try:
+ compiled_res.append(re.compile(regex))
+ except (ValueError, TypeError) as error:
+ raise PeekabooConfigException(
+ 'Failed to compile regular expression "%s" (section %s, '
+ 'option %s): %s' % (re, section, option, error))
+
+ # it's not gonna get any better on the next call, so cache even the
+ # default
+ if not compiled_res:
+ compiled_res = fallback
+
+ self.relists[section][option] = compiled_res
+ return compiled_res
+
+ def get_log_level(self, section, option, raw=False, vars=None,
+ fallback=None):
+ """ Get the log level from the configuration file and parse the string
+ into a logging loglevel such as logging.CRITICAL. Raises config
+ exception if the log level is unknown. Options identical to get(). """
+ levels = {
+ 'CRITICAL': logging.CRITICAL,
+ 'ERROR': logging.ERROR,
+ 'WARNING': logging.WARNING,
+ 'INFO': logging.INFO,
+ 'DEBUG': logging.DEBUG
+ }
+
+ level = self.get(section, option, raw=raw, vars=vars, fallback=None)
+ if level is None:
+ return fallback
-class PeekabooConfig(object): # pylint: disable=too-many-instance-attributes
+ if level not in levels:
+ raise PeekabooConfigException('Unknown log level %s' % level)
+
+ return levels[level]
+
+ def get_by_default_type(self, section, option, fallback=None,
+ option_type=None):
+ """ Get an option from the configuration file parser. Automatically
+ detects the type from the type of the default if given and calls the
+ right getter method to coerce the value to the correct type.
+
+ @param section: Which section to look for option in.
+ @type section: string
+ @param option: The option to read.
+ @type option: string
+ @param fallback: (optional) Default value to return if option is not
+ found. Defaults itself to None so that the method will
+ return None if the option is not found.
+ @type fallback: int, bool, str or None.
+ @param option_type: Override the option type.
+ @type option_type: int, bool, str or None. """
+ if option_type is None and fallback is not None:
+ option_type = type(fallback)
+
+ getter = {
+ int: self.getint,
+ bool: self.getboolean,
+ str: self.get,
+ None: self.get,
+ }
+
+ try:
+ return getter[option_type](section, option)
+ except configparser.NoSectionError:
+ logger.debug('Configuration section %s not found - using '
+ 'default %s', section, fallback)
+ except configparser.NoOptionError:
+ logger.debug('Configuration option %s not found in section '
+ '%s - using default: %s', option, section, fallback)
+
+ return fallback
+
+
+class PeekabooConfig(PeekabooConfigParser):
""" This class represents the Peekaboo configuration. """
def __init__(self, config_file=None, log_level=None):
""" Initialise the configuration with defaults, overwrite with command
@@ -142,21 +295,21 @@ class PeekabooConfig(object): # pylint: disable=too-many-instance-attributes
# read configuration file. Note that we require a configuration file
# here. We may change that if we decide that we want to allow the user
# to run us with the above defaults only.
- self.__config = PeekabooConfigParser(self.config_file)
+ PeekabooConfigParser.__init__(self, self.config_file)
# overwrite above defaults in our member variables via indirect access
settings = vars(self)
for (option, config_names) in config_options.items():
# maybe use special getter
- get = self.get
+ getter = self.get_by_default_type
if len(config_names) == 3:
- get = config_names[2]
+ getter = config_names[2]
# e.g.:
# self.log_format = self.get('logging', 'log_format',
# self.log_format)
- settings[option] = get(config_names[0], config_names[1],
- settings[option])
+ settings[option] = getter(
+ config_names[0], config_names[1], fallback=settings[option])
# Update logging with what we just parsed from the config
self.setup_logging()
@@ -164,63 +317,6 @@ class PeekabooConfig(object): # pylint: disable=too-many-instance-attributes
# here we could overwrite defaults and config file with additional
# command line arguments if required
- def get(self, section, option, default=None, option_type=None):
- """ Get an option from the configuration file parser. Automatically
- detects the type from the type of the default if given and calls the
- right getter method to coerce the value to the correct type.
-
- @param section: Which section to look for option in.
- @type section: string
- @param option: The option to read.
- @type option: string
- @param default: (optional) Default value to return if option is not
- found. Defaults itself to None so that the method will
- return None if the option is not found.
- @type default: int, bool, str or None.
- @param option_type: Override the option type.
- @type option_type: int, bool, str or None. """
- if option_type is None and default is not None:
- option_type = type(default)
-
- getter = {
- int: self.__config.getint,
- bool: self.__config.getboolean,
- str: self.__config.get,
- None: self.__config.get,
- }
-
- try:
- return getter[option_type](section, option)
- except configparser.NoSectionError:
- logger.debug('Configuration section %s not found - using '
- 'default %s', section, default)
- except configparser.NoOptionError:
- logger.debug('Configuration option %s not found in section '
- '%s - using default: %s', option, section, default)
-
- return default
-
- def get_log_level(self, section, option, default=None):
- """ Get the log level from the configuration file and parse the string
- into a logging loglevel such as logging.CRITICAL. Raises config
- exception if the log level is unknown. Options identical to get(). """
- levels = {
- 'CRITICAL': logging.CRITICAL,
- 'ERROR': logging.ERROR,
- 'WARNING': logging.WARNING,
- 'INFO': logging.INFO,
- 'DEBUG': logging.DEBUG
- }
-
- level = self.get(section, option, None)
- if level is None:
- return default
-
- if level not in levels:
- raise PeekabooConfigException('Unknown log level %s' % level)
-
- return levels[level]
-
def setup_logging(self):
""" Setup logging to console by reconfiguring the root logger so that
it affects all loggers everywhere. """
@@ -248,69 +344,3 @@ class PeekabooConfig(object): # pylint: disable=too-many-instance-attributes
return '<PeekabooConfig(%s)>' % settings
__repr__ = __str__
-
-
-class PeekabooRulesetConfig(object):
- """
- This class represents the ruleset configuration file "ruleset.conf".
-
- The ruleset configuration is stored as a dictionary in the form of
- ruleset_config[rule_name][config_option] = value | [value1, value2, ...]
-
- @since: 1.6
- """
- def __init__(self, config_file):
- self.config_file = config_file
- self.ruleset_config = {}
-
- config = PeekabooConfigParser(self.config_file)
- sections = config.sections()
- for section in sections:
- self.ruleset_config[section] = {}
-
- for section in sections:
- for setting in config.options(section):
- # Parse 'setting' into (key) and 'setting.subscript' into
- # (key, subscript) and use it to determine if this setting is a
- # list. Note how we do not use the subscript at all here.
- name_parts = setting.split('.')
- key = name_parts[0]
- is_list = len(name_parts) > 1
-
- saved_val = self.ruleset_config[section].get(key)
- if saved_val is None and is_list:
- saved_val = []
-
- # If the setting wants to add to a list the saved or freshly
- # initialised value from above should be a list. Otherwise it
- # should of course not be.
- if is_list != isinstance(saved_val, list):
- raise PeekabooConfigException(
- 'Setting %s in section %s specified as list as well '
- 'as individual setting' % (setting, section))
-
- # Potential further checks:
- # - There are no duplicate settings with ConfigParser. The last
- # one always wins.
-
- if is_list:
- saved_val.append(config.get(section, setting))
- else:
- saved_val = config.get(section, setting)
-
- self.ruleset_config[section][key] = saved_val
-
- def rule_config(self, rule):
- """ Get the configuration for a rule.
-
- @param rule: Name of the rule whose configuration to return.
- @type rule: string
- @return: dict of rule configuration settings or None if no
- configuration is present. """
- return self.ruleset_config.get(rule)
-
- def __str__(self):
- return '<PeekabooRulesetConfiguration(filepath="%s", %s)>' % \
- (self.config_file, self.ruleset_config)
-
- __repr__ = __str__
diff --git a/peekaboo/daemon.py b/peekaboo/daemon.py
index c7fbf5e..13169a4 100644
--- a/peekaboo/daemon.py
+++ b/peekaboo/daemon.py
@@ -38,12 +38,14 @@ from argparse import ArgumentParser
from sdnotify import SystemdNotifier
from sqlalchemy.exc import SQLAlchemyError
from peekaboo import PEEKABOO_OWL, __version__
-from peekaboo.config import PeekabooConfig, PeekabooRulesetConfig
+from peekaboo.config import PeekabooConfig, PeekabooConfigParser
from peekaboo.db import PeekabooDatabase
from peekaboo.queuing import JobQueue
+from peekaboo.ruleset.engine import RulesetEngine
from peekaboo.sample import SampleFactory
from peekaboo.server import PeekabooServer
-from peekaboo.exceptions import PeekabooDatabaseError, PeekabooConfigException
+from peekaboo.exceptions import PeekabooDatabaseError, \
+ PeekabooConfigException, PeekabooRulesetConfigError
from peekaboo.toolbox.cuckoo import CuckooEmbed, CuckooApi
@@ -321,12 +323,22 @@ def run():
# workers of the job queue need the ruleset configuration to create the
# ruleset engine with it
try:
- ruleset_config = PeekabooRulesetConfig(config.ruleset_config)
- logger.debug(ruleset_config)
+ ruleset_config = PeekabooConfigParser(config.ruleset_config)
except PeekabooConfigException as error:
logging.critical(error)
sys.exit(1)
+ # verify the ruleset configuration by spawning a ruleset engine and having
+ # it verify it
+ try:
+ engine = RulesetEngine(ruleset_config, db_con)
+ except (KeyError, ValueError, PeekabooConfigException) as error:
+ logging.critical('Ruleset configuration error: %s', error)
+ sys.exit(1)
+ except PeekabooRulesetConfigError as error:
+ logging.critical(error)
+ sys.exit(1)
+
job_queue = JobQueue(
worker_count=config.worker_count, ruleset_config=ruleset_config,
db_con=db_con,
diff --git a/peekaboo/exceptions.py b/peekaboo/exceptions.py
index 1ee8097..298cd21 100644
--- a/peekaboo/exceptions.py
+++ b/peekaboo/exceptions.py
@@ -40,6 +40,11 @@ class PeekabooRulesetException(PeekabooException):
pass
+class PeekabooRulesetConfigError(PeekabooException):
+ """ Used to signal that a rule is unhappy with its configuration. """
+ pass
+
+
class PeekabooAnalysisDeferred(PeekabooRulesetException):
""" Analysis has been deferred to a later point in time.
diff --git a/peekaboo/locale/de/LC_MESSAGES/peekaboo.mo b/peekaboo/locale/de/LC_MESSAGES/peekaboo.mo
index 6cca488..c89b24b 100644
--- a/peekaboo/locale/de/LC_MESSAGES/peekaboo.mo
+++ b/peekaboo/locale/de/LC_MESSAGES/peekaboo.mo
Binary files differ
diff --git a/peekaboo/locale/de/LC_MESSAGES/peekaboo.po b/peekaboo/locale/de/LC_MESSAGES/peekaboo.po
index 3a2be9f..d82f699 100644
--- a/peekaboo/locale/de/LC_MESSAGES/peekaboo.po
+++ b/peekaboo/locale/de/LC_MESSAGES/peekaboo.po
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PeekabooAV 1.6.2\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2019-04-17 09:12+0000\n"
+"POT-Creation-Date: 2019-04-17 09:26+0000\n"
"PO-Revision-Date: 2019-02-14 22:02+0000\n"
"Last-Translator: Michael Weiser <michael.weiser@gmx.de>\n"
"Language: de\n"
@@ -100,55 +100,55 @@ msgstr "Ja"
msgid "No"
msgstr "Nein"
-#: peekaboo/ruleset/engine.py:97
+#: peekaboo/ruleset/engine.py:118
msgid "Rule aborted with error"
msgstr "Regel mit Fehler abgebrochen"
-#: peekaboo/ruleset/rules.py:86
+#: peekaboo/ruleset/rules.py:133
msgid "File is not yet known to the system"
msgstr "Datei ist dem System noch nicht bekannt"
-#: peekaboo/ruleset/rules.py:106
+#: peekaboo/ruleset/rules.py:154
#, python-format
msgid "Failure to determine sample file size: %s"
msgstr "Ermittlung der Dateigröße fehlgeschlagen: %s"
-#: peekaboo/ruleset/rules.py:111
+#: peekaboo/ruleset/rules.py:159
#, python-format
msgid "File has more than %d bytes"
msgstr "Datei hat mehr als %d bytes"
-#: peekaboo/ruleset/rules.py:116
+#: peekaboo/ruleset/rules.py:165
#, python-format
-msgid "File is more than %d bytes long"
-msgstr "Datei ist nur %d bytes lang"
+msgid "File is only %d bytes long"
+msgstr ""
-#: peekaboo/ruleset/rules.py:135
+#: peekaboo/ruleset/rules.py:187
msgid "File type is on whitelist"
msgstr "Dateityp ist auf Whitelist"
-#: peekaboo/ruleset/rules.py:139
+#: peekaboo/ruleset/rules.py:191
msgid "File type is not on whitelist"
msgstr "Dateityp ist nicht auf Whitelist"
-#: peekaboo/ruleset/rules.py:158
+#: peekaboo/ruleset/rules.py:213
msgid "File type is on the list of types to analyze"
msgstr "Dateityp ist auf der Liste der zu analysiserenden Typen"
-#: peekaboo/ruleset/rules.py:163
+#: peekaboo/ruleset/rules.py:218
#, python-format
msgid "File type is not on the list of types to analyse (%s)"
msgstr "Dateityp ist nicht auf der Liste der zu analysierenden Typen (%s)"
-#: peekaboo/ruleset/rules.py:176
+#: peekaboo/ruleset/rules.py:231
msgid "The file contains an Office macro"
msgstr "Die Datei beinhaltet ein Office-Makro"
-#: peekaboo/ruleset/rules.py:180
+#: peekaboo/ruleset/rules.py:235
msgid "The file does not contain a recognizable Office macro"
msgstr "Die Datei beinhaltet kein erkennbares Office-Makro"
-#: peekaboo/ruleset/rules.py:210 peekaboo/ruleset/rules.py:332
+#: peekaboo/ruleset/rules.py:265 peekaboo/ruleset/rules.py:402
msgid ""
"Behavioral analysis by Cuckoo has produced an error and did not finish "
"successfully"
@@ -156,48 +156,40 @@ msgstr ""
"Die Verhaltensanalyse durch Cuckoo hat einen Fehler produziert und konnte"
" nicht erfolgreich abgeschlossen werden"
-#: peekaboo/ruleset/rules.py:244
-msgid "Empty list of malicious signatures"
-msgstr "Leere Liste schädlicher Signaturen"
-
-#: peekaboo/ruleset/rules.py:262
+#: peekaboo/ruleset/rules.py:322
msgid "No signature suggesting malware detected"
msgstr "Keine Signatur erkannt die auf Schadcode hindeutet"
-#: peekaboo/ruleset/rules.py:267
+#: peekaboo/ruleset/rules.py:327
#, python-format
msgid "The following signatures have been recognized: %s"
msgstr "Folgende Signaturen wurden erkannt: %s"
-#: peekaboo/ruleset/rules.py:284
+#: peekaboo/ruleset/rules.py:346
#, python-format
msgid "Cuckoo score >= %s: %s"
msgstr ""
-#: peekaboo/ruleset/rules.py:289
+#: peekaboo/ruleset/rules.py:351
#, python-format
msgid "Cuckoo score < %s: %s"
msgstr ""
-#: peekaboo/ruleset/rules.py:305
-msgid "Empty domain list"
-msgstr "Leere Domainliste"
-
-#: peekaboo/ruleset/rules.py:310
+#: peekaboo/ruleset/rules.py:375
#, python-format
msgid "The file attempts to contact at least one domain on the blacklist (%s)"
msgstr ""
"Die Datei versucht mindestens eine Domain aus der Blacklist zu "
"kontaktieren (%s)"
-#: peekaboo/ruleset/rules.py:316
+#: peekaboo/ruleset/rules.py:381
msgid "File does not seem to attempt contact with domains on the blacklist"
msgstr "Datei scheint keine Domains aus der Blacklist kontaktieren zu wollen"
-#: peekaboo/ruleset/rules.py:352
+#: peekaboo/ruleset/rules.py:418
msgid "Behavioral analysis by Cuckoo completed successfully"
msgstr "Die Verhaltensanalyse durch Cuckoo wurde erfolgreich abgeschlossen"
-#: peekaboo/ruleset/rules.py:369
+#: peekaboo/ruleset/rules.py:435
msgid "File does not seem to exhibit recognizable malicious behaviour"
msgstr "Datei scheint keine erkennbaren Schadroutinen zu starten"
diff --git a/peekaboo/locale/peekaboo.pot b/peekaboo/locale/peekaboo.pot
index d2231b7..56b6113 100644
--- a/peekaboo/locale/peekaboo.pot
+++ b/peekaboo/locale/peekaboo.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2019-04-17 09:12+0000\n"
+"POT-Creation-Date: 2019-04-17 09:26+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -99,101 +99,93 @@ msgstr ""
msgid "No"
msgstr ""
-#: peekaboo/ruleset/engine.py:97
+#: peekaboo/ruleset/engine.py:118
msgid "Rule aborted with error"
msgstr ""
-#: peekaboo/ruleset/rules.py:86
+#: peekaboo/ruleset/rules.py:133
msgid "File is not yet known to the system"
msgstr ""
-#: peekaboo/ruleset/rules.py:106
+#: peekaboo/ruleset/rules.py:154
#, python-format
msgid "Failure to determine sample file size: %s"
msgstr ""
-#: peekaboo/ruleset/rules.py:111
+#: peekaboo/ruleset/rules.py:159
#, python-format
msgid "File has more than %d bytes"
msgstr ""
-#: peekaboo/ruleset/rules.py:116
+#: peekaboo/ruleset/rules.py:165
#, python-format
-msgid "File is more than %d bytes long"
+msgid "File is only %d bytes long"
msgstr ""
-#: peekaboo/ruleset/rules.py:135
+#: peekaboo/ruleset/rules.py:187
msgid "File type is on whitelist"
msgstr ""
-#: peekaboo/ruleset/rules.py:139
+#: peekaboo/ruleset/rules.py:191
msgid "File type is not on whitelist"
msgstr ""
-#: peekaboo/ruleset/rules.py:158
+#: peekaboo/ruleset/rules.py:213
msgid "File type is on the list of types to analyze"
msgstr ""
-#: peekaboo/ruleset/rules.py:163
+#: peekaboo/ruleset/rules.py:218
#, python-format
msgid "File type is not on the list of types to analyse (%s)"
msgstr ""
-#: peekaboo/ruleset/rules.py:176
+#: peekaboo/ruleset/rules.py:231
msgid "The file contains an Office macro"
msgstr ""
-#: peekaboo/ruleset/rules.py:180
+#: peekaboo/ruleset/rules.py:235
msgid "The file does not contain a recognizable Office macro"
msgstr ""
-#: peekaboo/ruleset/rules.py:210 peekaboo/ruleset/rules.py:332
+#: peekaboo/ruleset/rules.py:265 peekaboo/ruleset/rules.py:402
msgid ""
"Behavioral analysis by Cuckoo has produced an error and did not finish "
"successfully"
msgstr ""
-#: peekaboo/ruleset/rules.py:244
-msgid "Empty list of malicious signatures"
-msgstr ""
-
-#: peekaboo/ruleset/rules.py:262
+#: peekaboo/ruleset/rules.py:322
msgid "No signature suggesting malware detected"
msgstr ""
-#: peekaboo/ruleset/rules.py:267
+#: peekaboo/ruleset/rules.py:327
#, python-format
msgid "The following signatures have been recognized: %s"
msgstr ""
-#: peekaboo/ruleset/rules.py:284
+#: peekaboo/ruleset/rules.py:346
#, python-format
msgid "Cuckoo score >= %s: %s"
msgstr ""
-#: peekaboo/ruleset/rules.py:289
+#: peekaboo/ruleset/rules.py:351
#, python-format
msgid "Cuckoo score < %s: %s"
msgstr ""
-#: peekaboo/ruleset/rules.py:305
-msgid "Empty domain list"
-msgstr ""
-
-#: peekaboo/ruleset/rules.py:310
+#: peekaboo/ruleset/rules.py:375
#, python-format
msgid "The file attempts to contact at least one domain on the blacklist (%s)"
msgstr ""
-#: peekaboo/ruleset/rules.py:316
+#: peekaboo/ruleset/rules.py:381
msgid "File does not seem to attempt contact with domains on the blacklist"
msgstr ""
-#: peekaboo/ruleset/rules.py:352
+#: peekaboo/ruleset/rules.py:418
msgid "Behavioral analysis by Cuckoo completed successfully"
msgstr ""
-#: peekaboo/ruleset/rules.py:369
+#: peekaboo/ruleset/rules.py:435
msgid "File does not seem to exhibit recognizable malicious behaviour"
msgstr ""
diff --git a/peekaboo/queuing.py b/peekaboo/queuing.py
index 8a4215f..160a4b1 100644
--- a/peekaboo/queuing.py
+++ b/peekaboo/queuing.py
@@ -384,9 +384,9 @@ class Worker(Thread):
self.job_queue.done(sample.sha256sum)
continue
- engine = RulesetEngine(sample, self.ruleset_config, self.db_con)
+ engine = RulesetEngine(self.ruleset_config, self.db_con)
try:
- engine.run()
+ engine.run(sample)
except PeekabooAnalysisDeferred:
logger.debug("Report for sample %s still pending", sample)
continue
diff --git a/peekaboo/ruleset/engine.py b/peekaboo/ruleset/engine.py
index 86b95e8..376cc08 100644
--- a/peekaboo/ruleset/engine.py
+++ b/peekaboo/ruleset/engine.py
@@ -28,7 +28,8 @@ import logging
from peekaboo.ruleset import Result, RuleResult
from peekaboo.ruleset.rules import *
from peekaboo.toolbox.peekabooyar import ContainsPeekabooYarRule
-from peekaboo.exceptions import PeekabooAnalysisDeferred
+from peekaboo.exceptions import PeekabooAnalysisDeferred, \
+ PeekabooConfigException, PeekabooRulesetConfigError
logger = logging.getLogger(__name__)
@@ -54,19 +55,51 @@ class RulesetEngine(object):
FinalRule
]
- def __init__(self, sample, ruleset_config, db_con):
- self.sample = sample
+ def __init__(self, ruleset_config, db_con):
self.config = ruleset_config
self.db_con = db_con
# create a lookup table from rule name to class
- self.rules = {}
+ self.rule_classes = {}
for known_rule in self.known_rules:
- self.rules[known_rule.rule_name] = known_rule
+ self.rule_classes[known_rule.rule_name] = known_rule
- def run(self):
- for rule in self.config.rule_config('rules').get('rule'):
- result = self.__exec_rule(self.sample, self.rules[rule])
+ try:
+ self.enabled_rules = self.config.getlist('rules', 'rule')
+ except PeekabooConfigException as error:
+ raise PeekabooRulesetConfigError(
+ 'Ruleset configuration error: %s' % error)
+
+ self.validate_rule_config()
+
+ def validate_rule_config(self):
+ """ Validate the rule configuration in various ways.
+
+ @returns: None
+ @raises PeekabooRulesetConfigError: if configuration errors are found
+ """
+ if not self.enabled_rules:
+ raise PeekabooRulesetConfigError(
+ 'No enabled rules found, check ruleset config.')
+
+ # check if unknown rules are enabled
+ known_rule_names = self.rule_classes.keys()
+ unknown_rules = set(self.enabled_rules) - set(known_rule_names)
+ if unknown_rules:
+ raise PeekabooRulesetConfigError(
+ 'Unknown rule(s) enabled: %s' % ', '.join(unknown_rules))
+
+ config_sections = []
+ for rule in self.enabled_rules:
+ # not passing database connection. Needs revisiting if a rule
+ # ever wants to retrieve configuration from the database. For
+ # now at least rule constructor and get_config() need to be
+ # able to cope without it.
+ rule = self.rule_classes[rule](self.config)
+
+ def run(self, sample):
+ for rule in self.enabled_rules:
+ result = self.__exec_rule(sample, self.rule_classes[rule])
if not result.further_analysis:
return
@@ -80,8 +113,7 @@ class RulesetEngine(object):
logger.debug("Processing rule '%s' for %s" % (rule_name, sample))
try:
- rule_config = self.config.rule_config(rule_name)
- rule = rule_class(config=rule_config, db_con=self.db_con)
+ rule = rule_class(config=self.config, db_con=self.db_con)
result = rule.evaluate(sample)
sample.add_rule_result(result)
except PeekabooAnalysisDeferred:
diff --git a/peekaboo/ruleset/rules.py b/peekaboo/ruleset/rules.py
index a631cc1..90f9dcb 100644
--- a/peekaboo/ruleset/rules.py
+++ b/peekaboo/ruleset/rules.py
@@ -30,7 +30,7 @@ import re
import logging
from peekaboo.ruleset import Result, RuleResult
from peekaboo.exceptions import PeekabooAnalysisDeferred, \
- CuckooSubmitFailedException
+ CuckooSubmitFailedException, PeekabooRulesetConfigError
logger = logging.getLogger(__name__)
@@ -45,13 +45,10 @@ class Rule(object):
def __init__(self, config=None, db_con=None):
""" Initialize common configuration and resources """
self.db_con = db_con
+ self.config = config
- # initialise and retain config as empty dict if no rule config is given
- # to us so the rule can rely on it and does not need to do any type
- # checking
- self.config = {}
- if config is not None:
- self.config = config
+ # initialise and validate configuration
+ self.get_config()
def result(self, result, reason, further_analysis):
""" Construct a RuleResult for returning to the engine. """
@@ -68,6 +65,56 @@ class Rule(object):
"""
raise NotImplementedError
+ def get_config(self):
+ """ Extract this rule's configuration out of the ruleset configuration
+ object given at creation. To be overridden by child classes if they
+ have configuration options. """
+ # pass
+
+ # the following getters are somewhat boilerplate but unavoidable for now.
+ # They serve the purpose of keeping config access specifics out of rules for
+ # the sake of readablility.
+ def get_config_value(self, getter, option, *args, **kwargs):
+ """ Get a configuation value for this rule from the ruleset
+ configuration. Getter routine and option name to be provided by caller.
+ The rule's name is always used as configuration section name.
+
+ @param getter: getter routine to use
+ @type getter: getter method of PeekabooConfigParser
+ @param option: name of option to read
+ @type option: string
+ @param args, kwargs: additional arguments passed to the getter routine,
+ such as fallback.
+
+ @returns: configuration value read from config
+ """
+ # additional common logic to go here
+ return getter(self.rule_name, option, *args, **kwargs)
+
+ def get_config_int(self, option, default=None):
+ """ Get an integer from the ruleset configuration. See get_config_value
+ for parameters. """
+ return self.get_config_value(
+ self.config.getint, option, fallback=default)
+
+ def get_config_float(self, option, default=None):
+ """ Get a float from the ruleset configuration. See get_config_value
+ for parameters. """
+ return self.get_config_value(
+ self.config.getfloat, option, fallback=default)
+
+ def get_config_list(self, option, default=None):
+ """ Get a list from the ruleset configuration. See get_config_value
+ for parameters. """
+ return self.get_config_value(
+ self.config.getlist, option, fallback=default)
+
+ def get_config_relist(self, option, default=None):
+ """ Get a list of compiled regular expressions from the ruleset. See
+ get_config_value for parameters. """
+ return self.get_config_value(
+ self.config.getrelist, option, fallback=default)
+
class KnownRule(Rule):
""" A rule determining if a sample is known by looking at the database for
@@ -92,12 +139,13 @@ class FileLargerThanRule(Rule):
"""
rule_name = 'file_larger_than'
+ def get_config(self):
+ self.size_threshold = self.get_config_int('bytes', 5)
+
def evaluate(self, sample):
""" Evaluate whether the sample is larger than a certain threshold.
Advise the engine to stop processing if the size is below the
threshold. """
- size = int(self.config.get('bytes', 5))
-
try:
sample_size = sample.file_size
except OSError as oserr:
@@ -106,14 +154,15 @@ class FileLargerThanRule(Rule):
_("Failure to determine sample file size: %s") % oserr,
False)
- if sample_size > size:
+ if sample_size > self.size_threshold:
return self.result(Result.unknown,
- _("File has more than %d bytes") % size,
+ _("File has more than %d bytes")
+ % self.size_threshold,
True)
return self.result(
Result.ignored,
- _("File is more than %d bytes long") % sample.file_size,
+ _("File is only %d bytes long") % sample_size,
False)
@@ -122,15 +171,18 @@ class FileTypeOnWhitelistRule(Rule):
whitelist. """
rule_name = 'file_type_on_whitelist'
+ def get_config(self):
+ whitelist = self.get_config_list('whitelist')
+ if not whitelist:
+ raise PeekabooRulesetConfigError(
+ "Empty whitelist, check %s rule config." % self.rule_name)
+
+ self.whitelist = set(whitelist)
+
def evaluate(self, sample):
""" Ignore the file only if *all* of its mime types are on the
whitelist and we could determine at least one. """
- whitelist = self.config.get('whitelist', ())
- if not whitelist:
- logger.warning("Empty whitelist, check ruleset config.")
- return self.result(Result.unknown, "Whitelist ist leer", True)
-
- if sample.mimetypes and sample.mimetypes.issubset(set(whitelist)):
+ if sample.mimetypes and sample.mimetypes.issubset(self.whitelist):
return self.result(Result.ignored,
_("File type is on whitelist"),
False)
@@ -145,15 +197,18 @@ class FileTypeOnGreylistRule(Rule):
greylist, i.e. enabled for analysis. """
rule_name = 'file_type_on_greylist'
+ def get_config(self):
+ greylist = self.get_config_list('greylist')
+ if not greylist:
+ raise PeekabooRulesetConfigError(
+ "Empty greylist, check %s rule config." % self.rule_name)
+
+ self.greylist = set(greylist)
+
def evaluate(self, sample):