summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Weiser <michael.weiser@gmx.de>2019-04-18 09:04:55 +0000
committerMichael Weiser <michael.weiser@gmx.de>2019-04-25 12:20:20 +0000
commit41d65dab0f05b5551af496dfd2381c43b8f3eb15 (patch)
tree600c5624ff0bf483afb57df8c3a4d89ddb8aed98
parent77d2ff1348a6f1ade05b54b297fc771abd0e14cd (diff)
Detect unknown config sections and options
Check for and report unknown configuration sections and options to help the user detect misconfiguration and typos. Add respective logic to the main configuration as well as the ruleset config.
-rw-r--r--peekaboo/config.py78
-rw-r--r--peekaboo/ruleset/engine.py15
-rw-r--r--peekaboo/ruleset/rules.py10
-rwxr-xr-xtest.py165
4 files changed, 261 insertions, 7 deletions
diff --git a/peekaboo/config.py b/peekaboo/config.py
index 51a4870..acba03d 100644
--- a/peekaboo/config.py
+++ b/peekaboo/config.py
@@ -208,6 +208,66 @@ class PeekabooConfigParser( # pylint: disable=too-many-ancestors
return fallback
+ def check_config(self, known_options):
+ """ Check this configuration against a list of known options. Raise an
+ exception if any unknown options are found.
+
+ @param known_options: A dict of sections and options, the key being the
+ section name and the value a list of option names.
+ @type known_options: dict
+
+ @returns: None
+ @raises PeekabooConfigException: if any unknown sections or options are
+ found.
+ """
+ known_sections = known_options.keys()
+ self.check_sections(known_sections)
+
+ # go over sections both allowed and in the config
+ for section in known_sections:
+ self.check_section_options(section, known_options[section])
+
+ def check_sections(self, known_sections):
+ """ Check a list of known section names against this configuration
+
+ @param known_sections: names of known sections
+ @type known_sections: list(string)
+
+ @returns: None
+ @raises PeekabooConfigException: if any unknown sections are found in
+ the configuration.
+ """
+ section_diff = set(self.sections()) - set(known_sections)
+ if section_diff:
+ raise PeekabooConfigException(
+ 'Unknown section(s) found in config: %s'
+ % ', '.join(section_diff))
+
+ def check_section_options(self, section, known_options):
+ """ Check a config section for unknown options.
+
+ @param section: name of section to check
+ @type section: string
+ @param known_options: list of names of known options to check against
+ @type known_options: list(string)
+
+ @returns: None
+ @raises PeekabooConfigException: if any unknown options are found. """
+ try:
+ section_options = map(
+ # account for option.1 list syntax
+ lambda x: x.split('.')[0],
+ self.options(section))
+ except configparser.NoSectionError:
+ # a non-existant section can have no non-allowed options :)
+ return
+
+ option_diff = set(section_options) - set(known_options)
+ if option_diff:
+ raise PeekabooConfigException(
+ 'Unknown config option(s) found in section %s: %s'
+ % (section, ', '.join(option_diff)))
+
class PeekabooConfig(PeekabooConfigParser):
""" This class represents the Peekaboo configuration. """
@@ -299,7 +359,16 @@ class PeekabooConfig(PeekabooConfigParser):
# overwrite above defaults in our member variables via indirect access
settings = vars(self)
- for (option, config_names) in config_options.items():
+ check_options = {}
+ for (setting, config_names) in config_options.items():
+ section = config_names[0]
+ option = config_names[1]
+
+ # remember for later checking for unknown options
+ if section not in check_options:
+ check_options[section] = []
+ check_options[section].append(option)
+
# maybe use special getter
getter = self.get_by_default_type
if len(config_names) == 3:
@@ -308,8 +377,11 @@ class PeekabooConfig(PeekabooConfigParser):
# e.g.:
# self.log_format = self.get('logging', 'log_format',
# self.log_format)
- settings[option] = getter(
- config_names[0], config_names[1], fallback=settings[option])
+ settings[setting] = getter(
+ section, option, fallback=settings[setting])
+
+ # now check for unknown options
+ self.check_config(check_options)
# Update logging with what we just parsed from the config
self.setup_logging()
diff --git a/peekaboo/ruleset/engine.py b/peekaboo/ruleset/engine.py
index 376cc08..90c83f2 100644
--- a/peekaboo/ruleset/engine.py
+++ b/peekaboo/ruleset/engine.py
@@ -56,6 +56,11 @@ class RulesetEngine(object):
]
def __init__(self, ruleset_config, db_con):
+ """ Initialise the engine, validate its and the individual rules'
+ configuration.
+
+ @raises PeekabooRulesetConfigError: if configuration errors are found
+ """
self.config = ruleset_config
self.db_con = db_con
@@ -77,6 +82,8 @@ class RulesetEngine(object):
@returns: None
@raises PeekabooRulesetConfigError: if configuration errors are found
+ @raises KeyError, ValueError, PeekabooConfigException: by failed config
+ object accesses
"""
if not self.enabled_rules:
raise PeekabooRulesetConfigError(
@@ -89,7 +96,13 @@ class RulesetEngine(object):
raise PeekabooRulesetConfigError(
'Unknown rule(s) enabled: %s' % ', '.join(unknown_rules))
- config_sections = []
+ # check for unknown config sections by using rule names as rules'
+ # config section names. Allow all known rules not only the enabled ones
+ # because some might be temporarily disabled but should be allowed to
+ # retain their configuration.
+ self.config.check_sections(['rules'] + known_rule_names)
+
+ # have enabled rules check their configuration
for rule in self.enabled_rules:
# not passing database connection. Needs revisiting if a rule
# ever wants to retrieve configuration from the database. For
diff --git a/peekaboo/ruleset/rules.py b/peekaboo/ruleset/rules.py
index 90f9dcb..953a7a7 100644
--- a/peekaboo/ruleset/rules.py
+++ b/peekaboo/ruleset/rules.py
@@ -48,7 +48,13 @@ class Rule(object):
self.config = config
# initialise and validate configuration
+ self.config_options = {}
self.get_config()
+ # if this rule has (tried to) read any options from the config, it must
+ # believe them to be known and allowed
+ if self.config_options:
+ self.config.check_section_options(
+ self.rule_name, self.config_options.keys())
def result(self, result, reason, further_analysis):
""" Construct a RuleResult for returning to the engine. """
@@ -88,7 +94,9 @@ class Rule(object):
@returns: configuration value read from config
"""
- # additional common logic to go here
+ # mark this config option as known
+ self.config_options[option] = True
+
return getter(self.rule_name, option, *args, **kwargs)
def get_config_int(self, option, default=None):
diff --git a/test.py b/test.py
index 6309efa..36f1b2e 100755
--- a/test.py
+++ b/test.py
@@ -49,7 +49,9 @@ from peekaboo.sample import SampleFactory
from peekaboo.ruleset import RuleResult, Result
from peekaboo.ruleset.engine import RulesetEngine
from peekaboo.ruleset.rules import FileTypeOnWhitelistRule, \
- FileTypeOnGreylistRule, CuckooAnalysisFailedRule
+ FileTypeOnGreylistRule, CuckooAnalysisFailedRule, \
+ KnownRule, FileLargerThanRule, CuckooEvilSigRule, \
+ CuckooScoreRule, RequestsEvilDomainRule, FinalRule
from peekaboo.toolbox.cuckoo import CuckooReport
from peekaboo.db import PeekabooDatabase, PeekabooDatabaseError
# pylint: enable=wrong-import-position
@@ -281,7 +283,24 @@ user; peekaboo''')
r'\[Errno 2\] No such file or directory' % config_file):
PeekabooConfig(config_file)
- def test_4_unknown_loglevel(self):
+ def test_4_unknown_section(self):
+ """ Test correct error is thrown if an unknown section name is given.
+ """
+ with self.assertRaisesRegexp(
+ PeekabooConfigException,
+ r'Unknown section\(s\) found in config: globl'):
+ CreatingPeekabooConfig('''[globl]''')
+
+ def test_5_unknown_option(self):
+ """ Test correct error is thrown if an unknown option name is given.
+ """
+ with self.assertRaisesRegexp(
+ PeekabooConfigException,
+ r'Unknown config option\(s\) found in section global: foo'):
+ CreatingPeekabooConfig('''[global]
+foo: bar''')
+
+ def test_6_unknown_loglevel(self):
""" Test with an unknown log level """
with self.assertRaisesRegexp(
PeekabooConfigException,
@@ -628,6 +647,18 @@ higher_than: foo''')
r'could not convert string to float: foo'):
RulesetEngine(ruleset_config=config, db_con=None)
+ def test_disabled_config(self):
+ """ Test that no error is shown if disabled rule has config. """
+
+ config = CreatingConfigParser('''[rules]
+rule.1: known
+#rule.2: cuckoo_score
+
+[cuckoo_score]
+higher_than: 4.0''')
+ RulesetEngine(ruleset_config=config, db_con=None)
+
+
class MimetypeSample(object): # pylint: disable=too-few-public-methods
""" A dummy sample class that only contains a set of MIME types for testing
whitelist and greylist rules with it. """
@@ -659,6 +690,31 @@ greylist.3 : application/msword
failure.1: end of analysis reached!
success.1: analysis completed successfully''')
+ def test_config_known(self): # pylint: disable=no-self-use
+ """ Test the known rule configuration. """
+ config = '''[known]
+unknown : baz'''
+ # there is no exception here since empty config is acceptable
+ KnownRule(CreatingConfigParser())
+ # there is no exception here since the known rule simply does
+ # not look at the configuration at all - maybe we should have a
+ # 'unknown section' error here
+ KnownRule(CreatingConfigParser(config))
+
+ def test_config_file_larger_than(self):
+ """ Test the file larger than rule configuration. """
+ config = '''[file_larger_than]
+bytes : 10
+unknown : baz'''
+ # there is no exception here since empty config is acceptable
+ FileLargerThanRule(CreatingConfigParser())
+
+ with self.assertRaisesRegexp(
+ PeekabooConfigException,
+ r'Unknown config option\(s\) found in section '
+ r'file_larger_than: unknown'):
+ FileLargerThanRule(CreatingConfigParser(config))
+
def test_rule_file_type_on_whitelist(self):
""" Test whitelist rule. """
combinations = [
@@ -674,6 +730,22 @@ success.1: analysis completed successfully''')
result = rule.evaluate(MimetypeSample(types))
self.assertEqual(result.further_analysis, expected)
+ def test_config_file_type_on_whitelist(self):
+ """ Test whitelist rule configuration. """
+ config = '''[file_type_on_whitelist]
+whitelist.1 : foo/bar
+unknown : baz'''
+ with self.assertRaisesRegexp(
+ PeekabooRulesetConfigError,
+ r'Empty whitelist, check file_type_on_whitelist rule config.'):
+ FileTypeOnWhitelistRule(CreatingConfigParser())
+
+ with self.assertRaisesRegexp(
+ PeekabooConfigException,
+ r'Unknown config option\(s\) found in section '
+ r'file_type_on_whitelist: unknown'):
+ FileTypeOnWhitelistRule(CreatingConfigParser(config))
+
def test_rule_file_type_on_greylist(self):
""" Test greylist rule. """
combinations = [
@@ -690,6 +762,22 @@ success.1: analysis completed successfully''')
result = rule.evaluate(MimetypeSample(types))
self.assertEqual(result.further_analysis, expected)
+ def test_config_file_type_on_greylist(self):
+ """ Test greylist rule configuration. """
+ config = '''[file_type_on_greylist]
+greylist.1 : foo/bar
+unknown : baz'''
+ with self.assertRaisesRegexp(
+ PeekabooRulesetConfigError,
+ r'Empty greylist, check file_type_on_greylist rule config.'):
+ FileTypeOnGreylistRule(CreatingConfigParser())
+
+ with self.assertRaisesRegexp(
+ PeekabooConfigException,
+ r'Unknown config option\(s\) found in section '
+ r'file_type_on_greylist: unknown'):
+ FileTypeOnGreylistRule(CreatingConfigParser(config))
+
def test_rule_analysis_failed(self):
""" Test the Cuckoo analysis failed rule """
# create some test samples
@@ -735,6 +823,79 @@ success.1: analysis completed successfully''')
self.assertEqual(result.result, Result.failed)
self.assertEqual(result.further_analysis, False)
+ def test_config_evil_sig(self):
+ """ Test the Cuckoo evil signature rule configuration. """
+ config = '''[cuckoo_evil_sig]
+signature.1 : foo
+unknown : baz'''
+ with self.assertRaisesRegexp(
+ PeekabooRulesetConfigError,
+ r'Empty bad signature list, check cuckoo_evil_sig rule '
+ r'config.'):
+ CuckooEvilSigRule(CreatingConfigParser())
+
+ with self.assertRaisesRegexp(
+ PeekabooConfigException,
+ r'Unknown config option\(s\) found in section '
+ r'cuckoo_evil_sig: unknown'):
+ CuckooEvilSigRule(CreatingConfigParser(config))
+
+ def test_config_score(self):
+ """ Test the Cuckoo score rule configuration. """
+ config = '''[cuckoo_score]
+higher_than : 10
+unknown : baz'''
+ CuckooScoreRule(CreatingConfigParser())
+
+ with self.assertRaisesRegexp(
+ PeekabooConfigException,
+ r'Unknown config option\(s\) found in section '
+ r'cuckoo_score: unknown'):
+ CuckooScoreRule(CreatingConfigParser(config))
+
+ def test_config_evil_domain(self):
+ """ Test the Cuckoo requests evil domain rule configuration. """
+ config = '''[requests_evil_domain]
+domain.1 : foo
+unknown : baz'''
+ with self.assertRaisesRegexp(
+ PeekabooRulesetConfigError,
+ r'Empty evil domain list, check requests_evil_domain rule '
+ r'config.'):
+ RequestsEvilDomainRule(CreatingConfigParser())
+
+ with self.assertRaisesRegexp(
+ PeekabooConfigException,
+ r'Unknown config option\(s\) found in section '
+ r'requests_evil_domain: unknown'):
+ RequestsEvilDomainRule(CreatingConfigParser(config))
+
+ def test_config_analysis_failed(self):
+ """ Test the Cuckoo analysis failed rule configuration. """
+ config = '''[cuckoo_analysis_failed]
+failure.1: end of analysis reached!
+success.1: analysis completed successfully
+unknown : baz'''
+ # there should be no exception here since empty config is acceptable
+ CuckooAnalysisFailedRule(CreatingConfigParser())
+
+ with self.assertRaisesRegexp(
+ PeekabooConfigException,
+ r'Unknown config option\(s\) found in section '
+ r'cuckoo_analysis_failed: unknown'):
+ CuckooAnalysisFailedRule(CreatingConfigParser(config))
+
+ def test_config_final(self): # pylint: disable=no-self-use
+ """ Test the final rule configuration. """
+ config = '''[final]
+unknown : baz'''
+ # there is no exception here since empty config is acceptable
+ FinalRule(CreatingConfigParser())
+ # there is no exception here since the final rule simply does
+ # not look at the configuration at all - maybe we should have a
+ # 'unknown section' error here
+ FinalRule(CreatingConfigParser(config))
+
class PeekabooTestResult(unittest.TextTestResult):
""" Subclassed test result for custom formatting. """