Commit 89e4dda01b53c12b75b9af0a04fc51582cb23b87
Committed by
GitHub
Merge pull request #449 from christian-intra2net/use-log_helper
Use log helper
Showing
13 changed files
with
274 additions
and
159 deletions
oletools/common/log_helper/_json_formatter.py
| @@ -8,6 +8,10 @@ class JsonFormatter(logging.Formatter): | @@ -8,6 +8,10 @@ class JsonFormatter(logging.Formatter): | ||
| 8 | """ | 8 | """ |
| 9 | _is_first_line = True | 9 | _is_first_line = True |
| 10 | 10 | ||
| 11 | + def __init__(self, other_logger_has_first_line=False): | ||
| 12 | + if other_logger_has_first_line: | ||
| 13 | + self._is_first_line = False | ||
| 14 | + | ||
| 11 | def format(self, record): | 15 | def format(self, record): |
| 12 | """ | 16 | """ |
| 13 | Since we don't buffer messages, we always prepend messages with a comma to make | 17 | Since we don't buffer messages, we always prepend messages with a comma to make |
oletools/common/log_helper/_logger_adapter.py
| @@ -54,4 +54,9 @@ class OletoolsLoggerAdapter(logging.LoggerAdapter): | @@ -54,4 +54,9 @@ class OletoolsLoggerAdapter(logging.LoggerAdapter): | ||
| 54 | self._json_enabled = json_enabled | 54 | self._json_enabled = json_enabled |
| 55 | 55 | ||
| 56 | def level(self): | 56 | def level(self): |
| 57 | + """Return current level of logger.""" | ||
| 57 | return self.logger.level | 58 | return self.logger.level |
| 59 | + | ||
| 60 | + def setLevel(self, new_level): | ||
| 61 | + """Set level of underlying logger. Required only for python < 3.2.""" | ||
| 62 | + return self.logger.setLevel(new_level) |
oletools/common/log_helper/log_helper.py
| @@ -3,6 +3,26 @@ log_helper.py | @@ -3,6 +3,26 @@ log_helper.py | ||
| 3 | 3 | ||
| 4 | General logging helpers | 4 | General logging helpers |
| 5 | 5 | ||
| 6 | +Use as follows: | ||
| 7 | + | ||
| 8 | + # at the start of your file: | ||
| 9 | + # import logging <-- replace this with next line | ||
| 10 | + from oletools.common.log_helper import log_helper | ||
| 11 | + | ||
| 12 | + logger = log_helper.get_or_create_silent_logger("module_name") | ||
| 13 | + def enable_logging(): | ||
| 14 | + '''Enable logging in this module; for use by importing scripts''' | ||
| 15 | + logger.setLevel(log_helper.NOTSET) | ||
| 16 | + imported_oletool_module.enable_logging() | ||
| 17 | + other_imported_oletool_module.enable_logging() | ||
| 18 | + | ||
| 19 | + # ... your code; use logger instead of logging ... | ||
| 20 | + | ||
| 21 | + def main(): | ||
| 22 | + log_helper.enable_logging(level=...) # instead of logging.basicConfig | ||
| 23 | + # ... your main code ... | ||
| 24 | + log_helper.end_logging() | ||
| 25 | + | ||
| 6 | .. codeauthor:: Intra2net AG <info@intra2net>, Philippe Lagadec | 26 | .. codeauthor:: Intra2net AG <info@intra2net>, Philippe Lagadec |
| 7 | """ | 27 | """ |
| 8 | 28 | ||
| @@ -45,6 +65,7 @@ General logging helpers | @@ -45,6 +65,7 @@ General logging helpers | ||
| 45 | # TODO: | 65 | # TODO: |
| 46 | 66 | ||
| 47 | 67 | ||
| 68 | +from __future__ import print_function | ||
| 48 | from ._json_formatter import JsonFormatter | 69 | from ._json_formatter import JsonFormatter |
| 49 | from ._logger_adapter import OletoolsLoggerAdapter | 70 | from ._logger_adapter import OletoolsLoggerAdapter |
| 50 | from . import _root_logger_wrapper | 71 | from . import _root_logger_wrapper |
| @@ -61,15 +82,27 @@ LOG_LEVELS = { | @@ -61,15 +82,27 @@ LOG_LEVELS = { | ||
| 61 | 'critical': logging.CRITICAL | 82 | 'critical': logging.CRITICAL |
| 62 | } | 83 | } |
| 63 | 84 | ||
| 85 | +#: provide this constant to modules, so they do not have to import | ||
| 86 | +#: :py:mod:`logging` for themselves just for this one constant. | ||
| 87 | +NOTSET = logging.NOTSET | ||
| 88 | + | ||
| 64 | DEFAULT_LOGGER_NAME = 'oletools' | 89 | DEFAULT_LOGGER_NAME = 'oletools' |
| 65 | DEFAULT_MESSAGE_FORMAT = '%(levelname)-8s %(message)s' | 90 | DEFAULT_MESSAGE_FORMAT = '%(levelname)-8s %(message)s' |
| 66 | 91 | ||
| 67 | 92 | ||
| 68 | class LogHelper: | 93 | class LogHelper: |
| 94 | + """ | ||
| 95 | + Single helper class that creates and remembers loggers. | ||
| 96 | + """ | ||
| 97 | + | ||
| 98 | + #: for convenience: here again (see also :py:data:`log_helper.NOTSET`) | ||
| 99 | + NOTSET = logging.NOTSET | ||
| 100 | + | ||
| 69 | def __init__(self): | 101 | def __init__(self): |
| 70 | self._all_names = set() # set so we do not have duplicates | 102 | self._all_names = set() # set so we do not have duplicates |
| 71 | self._use_json = False | 103 | self._use_json = False |
| 72 | self._is_enabled = False | 104 | self._is_enabled = False |
| 105 | + self._target_stream = None | ||
| 73 | 106 | ||
| 74 | def get_or_create_silent_logger(self, name=DEFAULT_LOGGER_NAME, level=logging.CRITICAL + 1): | 107 | def get_or_create_silent_logger(self, name=DEFAULT_LOGGER_NAME, level=logging.CRITICAL + 1): |
| 75 | """ | 108 | """ |
| @@ -82,7 +115,8 @@ class LogHelper: | @@ -82,7 +115,8 @@ class LogHelper: | ||
| 82 | """ | 115 | """ |
| 83 | return self._get_or_create_logger(name, level, logging.NullHandler()) | 116 | return self._get_or_create_logger(name, level, logging.NullHandler()) |
| 84 | 117 | ||
| 85 | - def enable_logging(self, use_json=False, level='warning', log_format=DEFAULT_MESSAGE_FORMAT, stream=None): | 118 | + def enable_logging(self, use_json=False, level='warning', log_format=DEFAULT_MESSAGE_FORMAT, stream=None, |
| 119 | + other_logger_has_first_line=False): | ||
| 86 | """ | 120 | """ |
| 87 | This function initializes the root logger and enables logging. | 121 | This function initializes the root logger and enables logging. |
| 88 | We set the level of the root logger to the one passed by calling logging.basicConfig. | 122 | We set the level of the root logger to the one passed by calling logging.basicConfig. |
| @@ -93,15 +127,26 @@ class LogHelper: | @@ -93,15 +127,26 @@ class LogHelper: | ||
| 93 | which in turn will log to the stream set in this function. | 127 | which in turn will log to the stream set in this function. |
| 94 | Since the root logger is the one doing the work, when using JSON we set its formatter | 128 | Since the root logger is the one doing the work, when using JSON we set its formatter |
| 95 | so that every message logged is JSON-compatible. | 129 | so that every message logged is JSON-compatible. |
| 130 | + | ||
| 131 | + If other code also creates json output, all items should be pre-pended | ||
| 132 | + with a comma like the `JsonFormatter` does. Except the first; use param | ||
| 133 | + `other_logger_has_first_line` to clarify whether our logger or the | ||
| 134 | + other code will produce the first json item. | ||
| 96 | """ | 135 | """ |
| 97 | if self._is_enabled: | 136 | if self._is_enabled: |
| 98 | raise ValueError('re-enabling logging. Not sure whether that is ok...') | 137 | raise ValueError('re-enabling logging. Not sure whether that is ok...') |
| 99 | 138 | ||
| 100 | - if stream in (None, sys.stdout): | 139 | + if stream is None: |
| 140 | + self.target_stream = sys.stdout | ||
| 141 | + else: | ||
| 142 | + self.target_stream = stream | ||
| 143 | + | ||
| 144 | + if self.target_stream == sys.stdout: | ||
| 101 | ensure_stdout_handles_unicode() | 145 | ensure_stdout_handles_unicode() |
| 102 | 146 | ||
| 103 | log_level = LOG_LEVELS[level] | 147 | log_level = LOG_LEVELS[level] |
| 104 | - logging.basicConfig(level=log_level, format=log_format, stream=stream) | 148 | + logging.basicConfig(level=log_level, format=log_format, |
| 149 | + stream=self.target_stream) | ||
| 105 | self._is_enabled = True | 150 | self._is_enabled = True |
| 106 | 151 | ||
| 107 | self._use_json = use_json | 152 | self._use_json = use_json |
| @@ -115,8 +160,8 @@ class LogHelper: | @@ -115,8 +160,8 @@ class LogHelper: | ||
| 115 | 160 | ||
| 116 | # add a JSON formatter to the root logger, which will be used by every logger | 161 | # add a JSON formatter to the root logger, which will be used by every logger |
| 117 | if self._use_json: | 162 | if self._use_json: |
| 118 | - _root_logger_wrapper.set_formatter(JsonFormatter()) | ||
| 119 | - print('[') | 163 | + _root_logger_wrapper.set_formatter(JsonFormatter(other_logger_has_first_line)) |
| 164 | + print('[', file=self.target_stream) | ||
| 120 | 165 | ||
| 121 | def end_logging(self): | 166 | def end_logging(self): |
| 122 | """ | 167 | """ |
| @@ -133,7 +178,7 @@ class LogHelper: | @@ -133,7 +178,7 @@ class LogHelper: | ||
| 133 | 178 | ||
| 134 | # end json list | 179 | # end json list |
| 135 | if self._use_json: | 180 | if self._use_json: |
| 136 | - print(']') | 181 | + print(']', file=self.target_stream) |
| 137 | self._use_json = False | 182 | self._use_json = False |
| 138 | 183 | ||
| 139 | def _get_except_hook(self, old_hook): | 184 | def _get_except_hook(self, old_hook): |
oletools/crypto.py
| @@ -95,6 +95,7 @@ http://www.decalage.info/python/oletools | @@ -95,6 +95,7 @@ http://www.decalage.info/python/oletools | ||
| 95 | # 2019-05-23 PL: - added DEFAULT_PASSWORDS list | 95 | # 2019-05-23 PL: - added DEFAULT_PASSWORDS list |
| 96 | # 2021-05-22 v0.60 PL: - added PowerPoint transparent password | 96 | # 2021-05-22 v0.60 PL: - added PowerPoint transparent password |
| 97 | # '/01Hannes Ruescher/01' (issue #627) | 97 | # '/01Hannes Ruescher/01' (issue #627) |
| 98 | +# 2019-05-24 CH: - use log_helper | ||
| 98 | 99 | ||
| 99 | __version__ = '0.60' | 100 | __version__ = '0.60' |
| 100 | 101 | ||
| @@ -104,7 +105,6 @@ import os | @@ -104,7 +105,6 @@ import os | ||
| 104 | from os.path import splitext, isfile | 105 | from os.path import splitext, isfile |
| 105 | from tempfile import mkstemp | 106 | from tempfile import mkstemp |
| 106 | import zipfile | 107 | import zipfile |
| 107 | -import logging | ||
| 108 | 108 | ||
| 109 | from olefile import OleFileIO | 109 | from olefile import OleFileIO |
| 110 | 110 | ||
| @@ -134,44 +134,20 @@ MAX_NESTING_DEPTH = 10 | @@ -134,44 +134,20 @@ MAX_NESTING_DEPTH = 10 | ||
| 134 | 134 | ||
| 135 | # === LOGGING ================================================================= | 135 | # === LOGGING ================================================================= |
| 136 | 136 | ||
| 137 | -# TODO: use log_helper instead | ||
| 138 | - | ||
| 139 | -def get_logger(name, level=logging.CRITICAL+1): | ||
| 140 | - """ | ||
| 141 | - Create a suitable logger object for this module. | ||
| 142 | - The goal is not to change settings of the root logger, to avoid getting | ||
| 143 | - other modules' logs on the screen. | ||
| 144 | - If a logger exists with same name, reuse it. (Else it would have duplicate | ||
| 145 | - handlers and messages would be doubled.) | ||
| 146 | - The level is set to CRITICAL+1 by default, to avoid any logging. | ||
| 147 | - """ | ||
| 148 | - # First, test if there is already a logger with the same name, else it | ||
| 149 | - # will generate duplicate messages (due to duplicate handlers): | ||
| 150 | - if name in logging.Logger.manager.loggerDict: | ||
| 151 | - # NOTE: another less intrusive but more "hackish" solution would be to | ||
| 152 | - # use getLogger then test if its effective level is not default. | ||
| 153 | - logger = logging.getLogger(name) | ||
| 154 | - # make sure level is OK: | ||
| 155 | - logger.setLevel(level) | ||
| 156 | - return logger | ||
| 157 | - # get a new logger: | ||
| 158 | - logger = logging.getLogger(name) | ||
| 159 | - # only add a NullHandler for this logger, it is up to the application | ||
| 160 | - # to configure its own logging: | ||
| 161 | - logger.addHandler(logging.NullHandler()) | ||
| 162 | - logger.setLevel(level) | ||
| 163 | - return logger | ||
| 164 | - | ||
| 165 | # a global logger object used for debugging: | 137 | # a global logger object used for debugging: |
| 166 | -log = get_logger('crypto') | 138 | +log = log_helper.get_or_create_silent_logger('crypto') |
| 139 | + | ||
| 167 | 140 | ||
| 168 | def enable_logging(): | 141 | def enable_logging(): |
| 169 | """ | 142 | """ |
| 170 | Enable logging for this module (disabled by default). | 143 | Enable logging for this module (disabled by default). |
| 144 | + | ||
| 145 | + For use by third-party libraries that import `crypto` as module. | ||
| 146 | + | ||
| 171 | This will set the module-specific logger level to NOTSET, which | 147 | This will set the module-specific logger level to NOTSET, which |
| 172 | means the main application controls the actual logging level. | 148 | means the main application controls the actual logging level. |
| 173 | """ | 149 | """ |
| 174 | - log.setLevel(logging.NOTSET) | 150 | + log.setLevel(log_helper.NOTSET) |
| 175 | 151 | ||
| 176 | 152 | ||
| 177 | def is_encrypted(some_file): | 153 | def is_encrypted(some_file): |
oletools/mraptor.py
| @@ -71,7 +71,7 @@ __version__ = '0.56.2' | @@ -71,7 +71,7 @@ __version__ = '0.56.2' | ||
| 71 | 71 | ||
| 72 | #--- IMPORTS ------------------------------------------------------------------ | 72 | #--- IMPORTS ------------------------------------------------------------------ |
| 73 | 73 | ||
| 74 | -import sys, logging, optparse, re, os | 74 | +import sys, optparse, re, os |
| 75 | 75 | ||
| 76 | # IMPORTANT: it should be possible to run oletools directly as scripts | 76 | # IMPORTANT: it should be possible to run oletools directly as scripts |
| 77 | # in any directory without installing them with pip or setup.py. | 77 | # in any directory without installing them with pip or setup.py. |
| @@ -90,11 +90,12 @@ from oletools.thirdparty.tablestream import tablestream | @@ -90,11 +90,12 @@ from oletools.thirdparty.tablestream import tablestream | ||
| 90 | 90 | ||
| 91 | from oletools import olevba | 91 | from oletools import olevba |
| 92 | from oletools.olevba import TYPE2TAG | 92 | from oletools.olevba import TYPE2TAG |
| 93 | +from oletools.common.log_helper import log_helper | ||
| 93 | 94 | ||
| 94 | # === LOGGING ================================================================= | 95 | # === LOGGING ================================================================= |
| 95 | 96 | ||
| 96 | # a global logger object used for debugging: | 97 | # a global logger object used for debugging: |
| 97 | -log = olevba.get_logger('mraptor') | 98 | +log = log_helper.get_or_create_silent_logger('mraptor') |
| 98 | 99 | ||
| 99 | 100 | ||
| 100 | #--- CONSTANTS ---------------------------------------------------------------- | 101 | #--- CONSTANTS ---------------------------------------------------------------- |
| @@ -230,15 +231,7 @@ def main(): | @@ -230,15 +231,7 @@ def main(): | ||
| 230 | """ | 231 | """ |
| 231 | Main function, called when olevba is run from the command line | 232 | Main function, called when olevba is run from the command line |
| 232 | """ | 233 | """ |
| 233 | - global log | ||
| 234 | DEFAULT_LOG_LEVEL = "warning" # Default log level | 234 | DEFAULT_LOG_LEVEL = "warning" # Default log level |
| 235 | - LOG_LEVELS = { | ||
| 236 | - 'debug': logging.DEBUG, | ||
| 237 | - 'info': logging.INFO, | ||
| 238 | - 'warning': logging.WARNING, | ||
| 239 | - 'error': logging.ERROR, | ||
| 240 | - 'critical': logging.CRITICAL | ||
| 241 | - } | ||
| 242 | 235 | ||
| 243 | usage = 'usage: mraptor [options] <filename> [filename2 ...]' | 236 | usage = 'usage: mraptor [options] <filename> [filename2 ...]' |
| 244 | parser = optparse.OptionParser(usage=usage) | 237 | parser = optparse.OptionParser(usage=usage) |
| @@ -272,9 +265,9 @@ def main(): | @@ -272,9 +265,9 @@ def main(): | ||
| 272 | print('MacroRaptor %s - http://decalage.info/python/oletools' % __version__) | 265 | print('MacroRaptor %s - http://decalage.info/python/oletools' % __version__) |
| 273 | print('This is work in progress, please report issues at %s' % URL_ISSUES) | 266 | print('This is work in progress, please report issues at %s' % URL_ISSUES) |
| 274 | 267 | ||
| 275 | - logging.basicConfig(level=LOG_LEVELS[options.loglevel], format='%(levelname)-8s %(message)s') | 268 | + log_helper.enable_logging(level=options.loglevel) |
| 276 | # enable logging in the modules: | 269 | # enable logging in the modules: |
| 277 | - log.setLevel(logging.NOTSET) | 270 | + olevba.enable_logging() |
| 278 | 271 | ||
| 279 | t = tablestream.TableStream(style=tablestream.TableStyleSlim, | 272 | t = tablestream.TableStream(style=tablestream.TableStyleSlim, |
| 280 | header_row=['Result', 'Flags', 'Type', 'File'], | 273 | header_row=['Result', 'Flags', 'Type', 'File'], |
| @@ -346,6 +339,7 @@ def main(): | @@ -346,6 +339,7 @@ def main(): | ||
| 346 | global_result = result | 339 | global_result = result |
| 347 | exitcode = result.exit_code | 340 | exitcode = result.exit_code |
| 348 | 341 | ||
| 342 | + log_helper.end_logging() | ||
| 349 | print('') | 343 | print('') |
| 350 | print('Flags: A=AutoExec, W=Write, X=Execute') | 344 | print('Flags: A=AutoExec, W=Write, X=Execute') |
| 351 | print('Exit code: %d - %s' % (exitcode, global_result.name)) | 345 | print('Exit code: %d - %s' % (exitcode, global_result.name)) |
oletools/olevba.py
| @@ -239,7 +239,6 @@ __version__ = '0.60.1.dev6' | @@ -239,7 +239,6 @@ __version__ = '0.60.1.dev6' | ||
| 239 | 239 | ||
| 240 | #------------------------------------------------------------------------------ | 240 | #------------------------------------------------------------------------------ |
| 241 | # TODO: | 241 | # TODO: |
| 242 | -# + setup logging (common with other oletools) | ||
| 243 | # + add xor bruteforcing like bbharvest | 242 | # + add xor bruteforcing like bbharvest |
| 244 | # + options -a and -c should imply -d | 243 | # + options -a and -c should imply -d |
| 245 | 244 | ||
| @@ -272,7 +271,6 @@ __version__ = '0.60.1.dev6' | @@ -272,7 +271,6 @@ __version__ = '0.60.1.dev6' | ||
| 272 | import traceback | 271 | import traceback |
| 273 | import sys | 272 | import sys |
| 274 | import os | 273 | import os |
| 275 | -import logging | ||
| 276 | import struct | 274 | import struct |
| 277 | from io import BytesIO, StringIO | 275 | from io import BytesIO, StringIO |
| 278 | import math | 276 | import math |
| @@ -346,6 +344,7 @@ from oletools import crypto | @@ -346,6 +344,7 @@ from oletools import crypto | ||
| 346 | from oletools.common.io_encoding import ensure_stdout_handles_unicode | 344 | from oletools.common.io_encoding import ensure_stdout_handles_unicode |
| 347 | from oletools.common import codepages | 345 | from oletools.common import codepages |
| 348 | from oletools import ftguess | 346 | from oletools import ftguess |
| 347 | +from oletools.common.log_helper import log_helper | ||
| 349 | 348 | ||
| 350 | # === PYTHON 2+3 SUPPORT ====================================================== | 349 | # === PYTHON 2+3 SUPPORT ====================================================== |
| 351 | 350 | ||
| @@ -423,47 +422,29 @@ def bytes2str(bytes_string, encoding='utf8'): | @@ -423,47 +422,29 @@ def bytes2str(bytes_string, encoding='utf8'): | ||
| 423 | 422 | ||
| 424 | # === LOGGING ================================================================= | 423 | # === LOGGING ================================================================= |
| 425 | 424 | ||
| 426 | -def get_logger(name, level=logging.CRITICAL+1): | ||
| 427 | - """ | ||
| 428 | - Create a suitable logger object for this module. | ||
| 429 | - The goal is not to change settings of the root logger, to avoid getting | ||
| 430 | - other modules' logs on the screen. | ||
| 431 | - If a logger exists with same name, reuse it. (Else it would have duplicate | ||
| 432 | - handlers and messages would be doubled.) | ||
| 433 | - The level is set to CRITICAL+1 by default, to avoid any logging. | ||
| 434 | - """ | ||
| 435 | - # First, test if there is already a logger with the same name, else it | ||
| 436 | - # will generate duplicate messages (due to duplicate handlers): | ||
| 437 | - if name in logging.Logger.manager.loggerDict: | ||
| 438 | - # NOTE: another less intrusive but more "hackish" solution would be to | ||
| 439 | - # use getLogger then test if its effective level is not default. | ||
| 440 | - logger = logging.getLogger(name) | ||
| 441 | - # make sure level is OK: | ||
| 442 | - logger.setLevel(level) | ||
| 443 | - return logger | ||
| 444 | - # get a new logger: | ||
| 445 | - logger = logging.getLogger(name) | ||
| 446 | - # only add a NullHandler for this logger, it is up to the application | ||
| 447 | - # to configure its own logging: | ||
| 448 | - logger.addHandler(logging.NullHandler()) | ||
| 449 | - logger.setLevel(level) | ||
| 450 | - return logger | ||
| 451 | 425 | ||
| 452 | # a global logger object used for debugging: | 426 | # a global logger object used for debugging: |
| 453 | -log = get_logger('olevba') | 427 | +log = log_helper.get_or_create_silent_logger('olevba') |
| 454 | 428 | ||
| 455 | 429 | ||
| 456 | def enable_logging(): | 430 | def enable_logging(): |
| 457 | """ | 431 | """ |
| 458 | Enable logging for this module (disabled by default). | 432 | Enable logging for this module (disabled by default). |
| 459 | - This will set the module-specific logger level to NOTSET, which | 433 | + |
| 434 | + For use by third-party libraries that import `olevba` as module. | ||
| 435 | + | ||
| 436 | + This will set the module-specific logger level to `NOTSET`, which | ||
| 460 | means the main application controls the actual logging level. | 437 | means the main application controls the actual logging level. |
| 438 | + | ||
| 439 | + This also enables logging for the modules used by us, but not the global | ||
| 440 | + common logging mechanism (:py:mod:`oletools.common.log_helper.log_helper`). | ||
| 441 | + Use :py:func:`oletools.common.log_helper.log_helper.enable_logging` for | ||
| 442 | + that. | ||
| 461 | """ | 443 | """ |
| 462 | - log.setLevel(logging.NOTSET) | ||
| 463 | - # Also enable logging in the ppt_parser module: | 444 | + log.setLevel(log_helper.NOTSET) |
| 464 | ppt_parser.enable_logging() | 445 | ppt_parser.enable_logging() |
| 465 | crypto.enable_logging() | 446 | crypto.enable_logging() |
| 466 | - | 447 | + # TODO: do not have enable_logging yet: oleform, rtfobj |
| 467 | 448 | ||
| 468 | 449 | ||
| 469 | #=== EXCEPTIONS ============================================================== | 450 | #=== EXCEPTIONS ============================================================== |
| @@ -2462,18 +2443,18 @@ def json2ascii(json_obj, encoding='utf8', errors='replace'): | @@ -2462,18 +2443,18 @@ def json2ascii(json_obj, encoding='utf8', errors='replace'): | ||
| 2462 | return json_obj | 2443 | return json_obj |
| 2463 | 2444 | ||
| 2464 | 2445 | ||
| 2465 | -def print_json(json_dict=None, _json_is_first=False, _json_is_last=False, | ||
| 2466 | - **json_parts): | 2446 | +def print_json(json_dict=None, _json_is_first=False, **json_parts): |
| 2467 | """ line-wise print of json.dumps(json2ascii(..)) with options and indent+1 | 2447 | """ line-wise print of json.dumps(json2ascii(..)) with options and indent+1 |
| 2468 | 2448 | ||
| 2469 | can use in two ways: | 2449 | can use in two ways: |
| 2470 | (1) print_json(some_dict) | 2450 | (1) print_json(some_dict) |
| 2471 | (2) print_json(key1=value1, key2=value2, ...) | 2451 | (2) print_json(key1=value1, key2=value2, ...) |
| 2472 | 2452 | ||
| 2473 | - :param bool _json_is_first: set to True only for very first entry to complete | ||
| 2474 | - the top-level json-list | ||
| 2475 | - :param bool _json_is_last: set to True only for very last entry to complete | ||
| 2476 | - the top-level json-list | 2453 | + This is compatible with :py:mod:`oletools.common.log_helper`: log messages |
| 2454 | + can be mixed if arg `use_json` was `True` in | ||
| 2455 | + :py:func:`log_helper.enable_logging` provided this function is called | ||
| 2456 | + before the first "regular" logging with `_json_is_first=True` (and | ||
| 2457 | + non-empty input) but after log_helper.enable_logging. | ||
| 2477 | """ | 2458 | """ |
| 2478 | if json_dict and json_parts: | 2459 | if json_dict and json_parts: |
| 2479 | raise ValueError('Invalid json argument: want either single dict or ' | 2460 | raise ValueError('Invalid json argument: want either single dict or ' |
| @@ -2485,18 +2466,18 @@ def print_json(json_dict=None, _json_is_first=False, _json_is_last=False, | @@ -2485,18 +2466,18 @@ def print_json(json_dict=None, _json_is_first=False, _json_is_last=False, | ||
| 2485 | if json_parts: | 2466 | if json_parts: |
| 2486 | json_dict = json_parts | 2467 | json_dict = json_parts |
| 2487 | 2468 | ||
| 2488 | - if _json_is_first: | ||
| 2489 | - print('[') | ||
| 2490 | - | ||
| 2491 | lines = json.dumps(json2ascii(json_dict), check_circular=False, | 2469 | lines = json.dumps(json2ascii(json_dict), check_circular=False, |
| 2492 | - indent=4, ensure_ascii=False).splitlines() | ||
| 2493 | - for line in lines[:-1]: | ||
| 2494 | - print(' {0}'.format(line)) | ||
| 2495 | - if _json_is_last: | ||
| 2496 | - print(' {0}'.format(lines[-1])) # print last line without comma | ||
| 2497 | - print(']') | 2470 | + indent=4, ensure_ascii=False).splitlines() |
| 2471 | + if not lines: | ||
| 2472 | + return | ||
| 2473 | + | ||
| 2474 | + if _json_is_first: | ||
| 2475 | + print(' ' + lines[0]) | ||
| 2498 | else: | 2476 | else: |
| 2499 | - print(' {0},'.format(lines[-1])) # print last line with comma | 2477 | + print(', ' + lines[0]) |
| 2478 | + | ||
| 2479 | + for line in lines[1:]: | ||
| 2480 | + print(' ' + line.rstrip()) | ||
| 2500 | 2481 | ||
| 2501 | 2482 | ||
| 2502 | class VBA_Scanner(object): | 2483 | class VBA_Scanner(object): |
| @@ -4358,13 +4339,6 @@ def parse_args(cmd_line_args=None): | @@ -4358,13 +4339,6 @@ def parse_args(cmd_line_args=None): | ||
| 4358 | """ parse command line arguments (given ones or per default sys.argv) """ | 4339 | """ parse command line arguments (given ones or per default sys.argv) """ |
| 4359 | 4340 | ||
| 4360 | DEFAULT_LOG_LEVEL = "warning" # Default log level | 4341 | DEFAULT_LOG_LEVEL = "warning" # Default log level |
| 4361 | - LOG_LEVELS = { | ||
| 4362 | - 'debug': logging.DEBUG, | ||
| 4363 | - 'info': logging.INFO, | ||
| 4364 | - 'warning': logging.WARNING, | ||
| 4365 | - 'error': logging.ERROR, | ||
| 4366 | - 'critical': logging.CRITICAL | ||
| 4367 | - } | ||
| 4368 | 4342 | ||
| 4369 | usage = 'usage: olevba [options] <filename> [filename2 ...]' | 4343 | usage = 'usage: olevba [options] <filename> [filename2 ...]' |
| 4370 | parser = argparse.ArgumentParser(usage=usage) | 4344 | parser = argparse.ArgumentParser(usage=usage) |
| @@ -4459,8 +4433,6 @@ def parse_args(cmd_line_args=None): | @@ -4459,8 +4433,6 @@ def parse_args(cmd_line_args=None): | ||
| 4459 | if options.show_pcode and options.no_pcode: | 4433 | if options.show_pcode and options.no_pcode: |
| 4460 | parser.error('You cannot combine options --no-pcode and --show-pcode') | 4434 | parser.error('You cannot combine options --no-pcode and --show-pcode') |
| 4461 | 4435 | ||
| 4462 | - options.loglevel = LOG_LEVELS[options.loglevel] | ||
| 4463 | - | ||
| 4464 | return options | 4436 | return options |
| 4465 | 4437 | ||
| 4466 | 4438 | ||
| @@ -4474,6 +4446,8 @@ def process_file(filename, data, container, options, crypto_nesting=0): | @@ -4474,6 +4446,8 @@ def process_file(filename, data, container, options, crypto_nesting=0): | ||
| 4474 | Returns a single code summarizing the status of processing of this file | 4446 | Returns a single code summarizing the status of processing of this file |
| 4475 | """ | 4447 | """ |
| 4476 | try: | 4448 | try: |
| 4449 | + vba_parser = None | ||
| 4450 | + | ||
| 4477 | # Open the file | 4451 | # Open the file |
| 4478 | vba_parser = VBA_Parser_CLI(filename, data=data, container=container, | 4452 | vba_parser = VBA_Parser_CLI(filename, data=data, container=container, |
| 4479 | relaxed=options.relaxed, | 4453 | relaxed=options.relaxed, |
| @@ -4501,6 +4475,7 @@ def process_file(filename, data, container, options, crypto_nesting=0): | @@ -4501,6 +4475,7 @@ def process_file(filename, data, container, options, crypto_nesting=0): | ||
| 4501 | no_xlm=options.no_xlm)) | 4475 | no_xlm=options.no_xlm)) |
| 4502 | else: # (should be impossible) | 4476 | else: # (should be impossible) |
| 4503 | raise ValueError('unexpected output mode: "{0}"!'.format(options.output_mode)) | 4477 | raise ValueError('unexpected output mode: "{0}"!'.format(options.output_mode)) |
| 4478 | + vba_parser.close() | ||
| 4504 | 4479 | ||
| 4505 | # even if processing succeeds, file might still be encrypted | 4480 | # even if processing succeeds, file might still be encrypted |
| 4506 | log.debug('Checking for encryption (normal)') | 4481 | log.debug('Checking for encryption (normal)') |
| @@ -4508,6 +4483,10 @@ def process_file(filename, data, container, options, crypto_nesting=0): | @@ -4508,6 +4483,10 @@ def process_file(filename, data, container, options, crypto_nesting=0): | ||
| 4508 | log.debug('no encryption detected') | 4483 | log.debug('no encryption detected') |
| 4509 | return RETURN_OK | 4484 | return RETURN_OK |
| 4510 | except Exception as exc: | 4485 | except Exception as exc: |
| 4486 | + log.debug('Caught exception:', exc_info=True) | ||
| 4487 | + if vba_parser: | ||
| 4488 | + vba_parser.close() | ||
| 4489 | + | ||
| 4511 | log.debug('Checking for encryption (after exception)') | 4490 | log.debug('Checking for encryption (after exception)') |
| 4512 | if crypto.is_encrypted(filename): | 4491 | if crypto.is_encrypted(filename): |
| 4513 | pass # deal with this below | 4492 | pass # deal with this below |
| @@ -4582,10 +4561,15 @@ def main(cmd_line_args=None): | @@ -4582,10 +4561,15 @@ def main(cmd_line_args=None): | ||
| 4582 | """ | 4561 | """ |
| 4583 | options = parse_args(cmd_line_args) | 4562 | options = parse_args(cmd_line_args) |
| 4584 | 4563 | ||
| 4564 | + # enable logging in the modules (for json, this prints the opening '['): | ||
| 4565 | + log_helper.enable_logging(options.output_mode=='json', options.loglevel, | ||
| 4566 | + other_logger_has_first_line=True) | ||
| 4567 | + | ||
| 4585 | # provide info about tool and its version | 4568 | # provide info about tool and its version |
| 4586 | if options.output_mode == 'json': | 4569 | if options.output_mode == 'json': |
| 4587 | - # print first json entry with meta info and opening '[' | 4570 | + # print first json entry with meta info |
| 4588 | print_json(script_name='olevba', version=__version__, | 4571 | print_json(script_name='olevba', version=__version__, |
| 4572 | + python_version=sys.version_info[0:3], | ||
| 4589 | url='http://decalage.info/python/oletools', | 4573 | url='http://decalage.info/python/oletools', |
| 4590 | type='MetaInformation', _json_is_first=True) | 4574 | type='MetaInformation', _json_is_first=True) |
| 4591 | else: | 4575 | else: |
| @@ -4594,10 +4578,6 @@ def main(cmd_line_args=None): | @@ -4594,10 +4578,6 @@ def main(cmd_line_args=None): | ||
| 4594 | print('olevba %s on Python %s - http://decalage.info/python/oletools' % | 4578 | print('olevba %s on Python %s - http://decalage.info/python/oletools' % |
| 4595 | (__version__, python_version)) | 4579 | (__version__, python_version)) |
| 4596 | 4580 | ||
| 4597 | - logging.basicConfig(level=options.loglevel, format='%(levelname)-8s %(message)s') | ||
| 4598 | - # enable logging in the modules: | ||
| 4599 | - enable_logging() | ||
| 4600 | - | ||
| 4601 | # with the option --reveal, make sure --deobf is also enabled: | 4581 | # with the option --reveal, make sure --deobf is also enabled: |
| 4602 | if options.show_deobfuscated_code and not options.deobfuscate: | 4582 | if options.show_deobfuscated_code and not options.deobfuscate: |
| 4603 | log.debug('set --deobf because --reveal was set') | 4583 | log.debug('set --deobf because --reveal was set') |
| @@ -4682,11 +4662,6 @@ def main(cmd_line_args=None): | @@ -4682,11 +4662,6 @@ def main(cmd_line_args=None): | ||
| 4682 | 'A=Auto-executable, S=Suspicious keywords, I=IOCs, H=Hex strings, ' \ | 4662 | 'A=Auto-executable, S=Suspicious keywords, I=IOCs, H=Hex strings, ' \ |
| 4683 | 'B=Base64 strings, D=Dridex strings, V=VBA strings, ?=Unknown)\n') | 4663 | 'B=Base64 strings, D=Dridex strings, V=VBA strings, ?=Unknown)\n') |
| 4684 | 4664 | ||
| 4685 | - if options.output_mode == 'json': | ||
| 4686 | - # print last json entry (a last one without a comma) and closing ] | ||
| 4687 | - print_json(type='MetaInformation', return_code=return_code, | ||
| 4688 | - n_processed=count, _json_is_last=True) | ||
| 4689 | - | ||
| 4690 | except crypto.CryptoErrorBase as exc: | 4665 | except crypto.CryptoErrorBase as exc: |
| 4691 | log.exception('Problems with encryption in main: {}'.format(exc), | 4666 | log.exception('Problems with encryption in main: {}'.format(exc), |
| 4692 | exc_info=True) | 4667 | exc_info=True) |
| @@ -4704,6 +4679,7 @@ def main(cmd_line_args=None): | @@ -4704,6 +4679,7 @@ def main(cmd_line_args=None): | ||
| 4704 | 4679 | ||
| 4705 | # done. exit | 4680 | # done. exit |
| 4706 | log.debug('will exit now with code %s' % return_code) | 4681 | log.debug('will exit now with code %s' % return_code) |
| 4682 | + log_helper.end_logging() | ||
| 4707 | sys.exit(return_code) | 4683 | sys.exit(return_code) |
| 4708 | 4684 | ||
| 4709 | if __name__ == '__main__': | 4685 | if __name__ == '__main__': |
oletools/record_base.py
| @@ -42,6 +42,7 @@ from __future__ import print_function | @@ -42,6 +42,7 @@ from __future__ import print_function | ||
| 42 | # 2018-09-11 v0.54 PL: - olefile is now a dependency | 42 | # 2018-09-11 v0.54 PL: - olefile is now a dependency |
| 43 | # 2019-01-30 PL: - fixed import to avoid mixing installed oletools | 43 | # 2019-01-30 PL: - fixed import to avoid mixing installed oletools |
| 44 | # and dev version | 44 | # and dev version |
| 45 | +# 2019-05-24 CH: - use log_helper | ||
| 45 | 46 | ||
| 46 | __version__ = '0.60.dev1' | 47 | __version__ = '0.60.dev1' |
| 47 | 48 | ||
| @@ -64,7 +65,6 @@ __version__ = '0.60.dev1' | @@ -64,7 +65,6 @@ __version__ = '0.60.dev1' | ||
| 64 | import sys | 65 | import sys |
| 65 | import os.path | 66 | import os.path |
| 66 | from io import SEEK_CUR | 67 | from io import SEEK_CUR |
| 67 | -import logging | ||
| 68 | 68 | ||
| 69 | import olefile | 69 | import olefile |
| 70 | 70 | ||
| @@ -74,6 +74,7 @@ PARENT_DIR = os.path.normpath(os.path.dirname(os.path.dirname( | @@ -74,6 +74,7 @@ PARENT_DIR = os.path.normpath(os.path.dirname(os.path.dirname( | ||
| 74 | if PARENT_DIR not in sys.path: | 74 | if PARENT_DIR not in sys.path: |
| 75 | sys.path.insert(0, PARENT_DIR) | 75 | sys.path.insert(0, PARENT_DIR) |
| 76 | del PARENT_DIR | 76 | del PARENT_DIR |
| 77 | +from oletools.common.log_helper import log_helper | ||
| 77 | 78 | ||
| 78 | 79 | ||
| 79 | ############################################################################### | 80 | ############################################################################### |
| @@ -100,11 +101,26 @@ ENTRY_TYPE2STR = { | @@ -100,11 +101,26 @@ ENTRY_TYPE2STR = { | ||
| 100 | } | 101 | } |
| 101 | 102 | ||
| 102 | 103 | ||
| 104 | +logger = log_helper.get_or_create_silent_logger('record_base') | ||
| 105 | + | ||
| 106 | + | ||
| 103 | def enable_olefile_logging(): | 107 | def enable_olefile_logging(): |
| 104 | - """ enable logging olefile e.g., to get debug info from OleFileIO """ | 108 | + """ enable logging in olefile e.g., to get debug info from OleFileIO """ |
| 105 | olefile.enable_logging() | 109 | olefile.enable_logging() |
| 106 | 110 | ||
| 107 | 111 | ||
| 112 | +def enable_logging(): | ||
| 113 | + """ | ||
| 114 | + Enable logging for this module (disabled by default). | ||
| 115 | + | ||
| 116 | + For use by third-party libraries that import `record_base` as module. | ||
| 117 | + | ||
| 118 | + This will set the module-specific logger level to NOTSET, which | ||
| 119 | + means the main application controls the actual logging level. | ||
| 120 | + """ | ||
| 121 | + logger.setLevel(log_helper.NOTSET) | ||
| 122 | + | ||
| 123 | + | ||
| 108 | ############################################################################### | 124 | ############################################################################### |
| 109 | # Base Classes | 125 | # Base Classes |
| 110 | ############################################################################### | 126 | ############################################################################### |
| @@ -139,7 +155,7 @@ class OleRecordFile(olefile.OleFileIO): | @@ -139,7 +155,7 @@ class OleRecordFile(olefile.OleFileIO): | ||
| 139 | 155 | ||
| 140 | def iter_streams(self): | 156 | def iter_streams(self): |
| 141 | """ find all streams, including orphans """ | 157 | """ find all streams, including orphans """ |
| 142 | - logging.debug('Finding streams in ole file') | 158 | + logger.debug('Finding streams in ole file') |
| 143 | 159 | ||
| 144 | for sid, direntry in enumerate(self.direntries): | 160 | for sid, direntry in enumerate(self.direntries): |
| 145 | is_orphan = direntry is None | 161 | is_orphan = direntry is None |
| @@ -147,7 +163,7 @@ class OleRecordFile(olefile.OleFileIO): | @@ -147,7 +163,7 @@ class OleRecordFile(olefile.OleFileIO): | ||
| 147 | # this direntry is not part of the tree --> unused or orphan | 163 | # this direntry is not part of the tree --> unused or orphan |
| 148 | direntry = self._load_direntry(sid) | 164 | direntry = self._load_direntry(sid) |
| 149 | is_stream = direntry.entry_type == olefile.STGTY_STREAM | 165 | is_stream = direntry.entry_type == olefile.STGTY_STREAM |
| 150 | - logging.debug('direntry {:2d} {}: {}'.format( | 166 | + logger.debug('direntry {:2d} {}: {}'.format( |
| 151 | sid, '[orphan]' if is_orphan else direntry.name, | 167 | sid, '[orphan]' if is_orphan else direntry.name, |
| 152 | 'is stream of size {}'.format(direntry.size) if is_stream else | 168 | 'is stream of size {}'.format(direntry.size) if is_stream else |
| 153 | 'no stream ({})'.format(ENTRY_TYPE2STR[direntry.entry_type]))) | 169 | 'no stream ({})'.format(ENTRY_TYPE2STR[direntry.entry_type]))) |
| @@ -216,8 +232,8 @@ class OleRecordStream(object): | @@ -216,8 +232,8 @@ class OleRecordStream(object): | ||
| 216 | 232 | ||
| 217 | # read first few bytes, determine record type and size | 233 | # read first few bytes, determine record type and size |
| 218 | rec_type, rec_size, other = self.read_record_head() | 234 | rec_type, rec_size, other = self.read_record_head() |
| 219 | - # logging.debug('Record type {0} of size {1}' | ||
| 220 | - # .format(rec_type, rec_size)) | 235 | + # logger.debug('Record type {0} of size {1}' |
| 236 | + # .format(rec_type, rec_size)) | ||
| 221 | 237 | ||
| 222 | # determine what class to wrap this into | 238 | # determine what class to wrap this into |
| 223 | rec_clz, force_read = self.record_class_for_type(rec_type) | 239 | rec_clz, force_read = self.record_class_for_type(rec_type) |
| @@ -237,6 +253,7 @@ class OleRecordStream(object): | @@ -237,6 +253,7 @@ class OleRecordStream(object): | ||
| 237 | yield rec_object | 253 | yield rec_object |
| 238 | 254 | ||
| 239 | def close(self): | 255 | def close(self): |
| 256 | + """Close this stream (i.e. the stream given in constructor).""" | ||
| 240 | self.stream.close() | 257 | self.stream.close() |
| 241 | 258 | ||
| 242 | def __str__(self): | 259 | def __str__(self): |
| @@ -348,25 +365,25 @@ def test(filenames, ole_file_class=OleRecordFile, | @@ -348,25 +365,25 @@ def test(filenames, ole_file_class=OleRecordFile, | ||
| 348 | if an error occurs while parsing a stream of type in must_parse, the error | 365 | if an error occurs while parsing a stream of type in must_parse, the error |
| 349 | will be raised. Otherwise a message is printed | 366 | will be raised. Otherwise a message is printed |
| 350 | """ | 367 | """ |
| 351 | - logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO) | 368 | + log_helper.enable_logging(False, 'debug' if verbose else 'info') |
| 352 | if do_per_record is None: | 369 | if do_per_record is None: |
| 353 | def do_per_record(record): # pylint: disable=function-redefined | 370 | def do_per_record(record): # pylint: disable=function-redefined |
| 354 | pass # do nothing | 371 | pass # do nothing |
| 355 | if not filenames: | 372 | if not filenames: |
| 356 | - logging.info('need file name[s]') | 373 | + logger.info('need file name[s]') |
| 357 | return 2 | 374 | return 2 |
| 358 | for filename in filenames: | 375 | for filename in filenames: |
| 359 | - logging.info('checking file {0}'.format(filename)) | 376 | + logger.info('checking file {0}'.format(filename)) |
| 360 | if not olefile.isOleFile(filename): | 377 | if not olefile.isOleFile(filename): |
| 361 | - logging.info('not an ole file - skip') | 378 | + logger.info('not an ole file - skip') |
| 362 | continue | 379 | continue |
| 363 | ole = ole_file_class(filename) | 380 | ole = ole_file_class(filename) |
| 364 | 381 | ||
| 365 | for stream in ole.iter_streams(): | 382 | for stream in ole.iter_streams(): |
| 366 | - logging.info(' parse ' + str(stream)) | 383 | + logger.info(' parse ' + str(stream)) |
| 367 | try: | 384 | try: |
| 368 | for record in stream.iter_records(): | 385 | for record in stream.iter_records(): |
| 369 | - logging.info(' ' + str(record)) | 386 | + logger.info(' ' + str(record)) |
| 370 | do_per_record(record) | 387 | do_per_record(record) |
| 371 | except Exception: | 388 | except Exception: |
| 372 | if not must_parse: | 389 | if not must_parse: |
| @@ -374,7 +391,9 @@ def test(filenames, ole_file_class=OleRecordFile, | @@ -374,7 +391,9 @@ def test(filenames, ole_file_class=OleRecordFile, | ||
| 374 | elif isinstance(stream, must_parse): | 391 | elif isinstance(stream, must_parse): |
| 375 | raise | 392 | raise |
| 376 | else: | 393 | else: |
| 377 | - logging.info(' failed to parse', exc_info=True) | 394 | + logger.info(' failed to parse', exc_info=True) |
| 395 | + | ||
| 396 | + log_helper.end_logging() | ||
| 378 | return 0 | 397 | return 0 |
| 379 | 398 | ||
| 380 | 399 |
tests/common/log_helper/log_helper_test_imported.py
| @@ -4,7 +4,6 @@ by the main test file | @@ -4,7 +4,6 @@ by the main test file | ||
| 4 | """ | 4 | """ |
| 5 | 5 | ||
| 6 | from oletools.common.log_helper import log_helper | 6 | from oletools.common.log_helper import log_helper |
| 7 | -import logging | ||
| 8 | 7 | ||
| 9 | DEBUG_MESSAGE = 'imported: debug log' | 8 | DEBUG_MESSAGE = 'imported: debug log' |
| 10 | INFO_MESSAGE = 'imported: info log' | 9 | INFO_MESSAGE = 'imported: info log' |
| @@ -14,7 +13,11 @@ CRITICAL_MESSAGE = 'imported: critical log' | @@ -14,7 +13,11 @@ CRITICAL_MESSAGE = 'imported: critical log' | ||
| 14 | RESULT_MESSAGE = 'imported: result log' | 13 | RESULT_MESSAGE = 'imported: result log' |
| 15 | RESULT_TYPE = 'imported: result' | 14 | RESULT_TYPE = 'imported: result' |
| 16 | 15 | ||
| 17 | -logger = log_helper.get_or_create_silent_logger('test_imported', logging.ERROR) | 16 | +logger = log_helper.get_or_create_silent_logger('test_imported') |
| 17 | + | ||
| 18 | +def enable_logging(): | ||
| 19 | + """Enable logging if imported by third party modules.""" | ||
| 20 | + logger.setLevel(log_helper.NOTSET) | ||
| 18 | 21 | ||
| 19 | 22 | ||
| 20 | def log(): | 23 | def log(): |
tests/common/log_helper/log_helper_test_main.py
| 1 | """ Test log_helpers """ | 1 | """ Test log_helpers """ |
| 2 | 2 | ||
| 3 | import sys | 3 | import sys |
| 4 | +import logging | ||
| 4 | from tests.common.log_helper import log_helper_test_imported | 5 | from tests.common.log_helper import log_helper_test_imported |
| 5 | from oletools.common.log_helper import log_helper | 6 | from oletools.common.log_helper import log_helper |
| 6 | 7 | ||
| @@ -15,7 +16,13 @@ RESULT_TYPE = 'main: result' | @@ -15,7 +16,13 @@ RESULT_TYPE = 'main: result' | ||
| 15 | logger = log_helper.get_or_create_silent_logger('test_main') | 16 | logger = log_helper.get_or_create_silent_logger('test_main') |
| 16 | 17 | ||
| 17 | 18 | ||
| 18 | -def init_logging_and_log(args): | 19 | +def enable_logging(): |
| 20 | + """Enable logging if imported by third party modules.""" | ||
| 21 | + logger.setLevel(log_helper.NOTSET) | ||
| 22 | + log_helper_test_imported.enable_logging() | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +def main(args): | ||
| 19 | """ | 26 | """ |
| 20 | Try to cover possible logging scenarios. For each scenario covered, here's the expected args and outcome: | 27 | Try to cover possible logging scenarios. For each scenario covered, here's the expected args and outcome: |
| 21 | - Log without enabling: ['<level>'] | 28 | - Log without enabling: ['<level>'] |
| @@ -36,13 +43,12 @@ def init_logging_and_log(args): | @@ -36,13 +43,12 @@ def init_logging_and_log(args): | ||
| 36 | throw = 'throw' in args | 43 | throw = 'throw' in args |
| 37 | percent_autoformat = '%-autoformat' in args | 44 | percent_autoformat = '%-autoformat' in args |
| 38 | 45 | ||
| 46 | + log_helper_test_imported.logger.setLevel(logging.ERROR) | ||
| 47 | + | ||
| 39 | if 'enable' in args: | 48 | if 'enable' in args: |
| 40 | log_helper.enable_logging(use_json, level, stream=sys.stdout) | 49 | log_helper.enable_logging(use_json, level, stream=sys.stdout) |
| 41 | 50 | ||
| 42 | - _log() | ||
| 43 | - | ||
| 44 | - if percent_autoformat: | ||
| 45 | - logger.info('The %s is %d.', 'answer', 47) | 51 | + do_log(percent_autoformat) |
| 46 | 52 | ||
| 47 | if throw: | 53 | if throw: |
| 48 | raise Exception('An exception occurred before ending the logging') | 54 | raise Exception('An exception occurred before ending the logging') |
| @@ -50,7 +56,10 @@ def init_logging_and_log(args): | @@ -50,7 +56,10 @@ def init_logging_and_log(args): | ||
| 50 | log_helper.end_logging() | 56 | log_helper.end_logging() |
| 51 | 57 | ||
| 52 | 58 | ||
| 53 | -def _log(): | 59 | +def do_log(percent_autoformat=False): |
| 60 | + if percent_autoformat: | ||
| 61 | + logger.info('The %s is %d.', 'answer', 47) | ||
| 62 | + | ||
| 54 | logger.debug(DEBUG_MESSAGE) | 63 | logger.debug(DEBUG_MESSAGE) |
| 55 | logger.info(INFO_MESSAGE) | 64 | logger.info(INFO_MESSAGE) |
| 56 | logger.warning(WARNING_MESSAGE) | 65 | logger.warning(WARNING_MESSAGE) |
| @@ -61,4 +70,4 @@ def _log(): | @@ -61,4 +70,4 @@ def _log(): | ||
| 61 | 70 | ||
| 62 | 71 | ||
| 63 | if __name__ == '__main__': | 72 | if __name__ == '__main__': |
| 64 | - init_logging_and_log(sys.argv[1:]) | 73 | + main(sys.argv[1:]) |
tests/common/log_helper/test_log_helper.py
| @@ -15,18 +15,16 @@ from os.path import dirname, join, relpath, abspath | @@ -15,18 +15,16 @@ from os.path import dirname, join, relpath, abspath | ||
| 15 | 15 | ||
| 16 | from tests.test_utils import PROJECT_ROOT | 16 | from tests.test_utils import PROJECT_ROOT |
| 17 | 17 | ||
| 18 | -# this is the common base of "tests" and "oletools" dirs | 18 | +# test file we use as "main" module |
| 19 | TEST_FILE = relpath(join(dirname(abspath(__file__)), 'log_helper_test_main.py'), | 19 | TEST_FILE = relpath(join(dirname(abspath(__file__)), 'log_helper_test_main.py'), |
| 20 | PROJECT_ROOT) | 20 | PROJECT_ROOT) |
| 21 | -PYTHON_EXECUTABLE = sys.executable | ||
| 22 | 21 | ||
| 23 | -MAIN_LOG_MESSAGES = [ | ||
| 24 | - log_helper_test_main.DEBUG_MESSAGE, | ||
| 25 | - log_helper_test_main.INFO_MESSAGE, | ||
| 26 | - log_helper_test_main.WARNING_MESSAGE, | ||
| 27 | - log_helper_test_main.ERROR_MESSAGE, | ||
| 28 | - log_helper_test_main.CRITICAL_MESSAGE | ||
| 29 | -] | 22 | +# test file simulating a third party main module that only imports oletools |
| 23 | +TEST_FILE_3RD_PARTY = relpath(join(dirname(abspath(__file__)), | ||
| 24 | + 'third_party_importer.py'), | ||
| 25 | + PROJECT_ROOT) | ||
| 26 | + | ||
| 27 | +PYTHON_EXECUTABLE = sys.executable | ||
| 30 | 28 | ||
| 31 | 29 | ||
| 32 | class TestLogHelper(unittest.TestCase): | 30 | class TestLogHelper(unittest.TestCase): |
| @@ -127,6 +125,22 @@ class TestLogHelper(unittest.TestCase): | @@ -127,6 +125,22 @@ class TestLogHelper(unittest.TestCase): | ||
| 127 | log_helper_test_imported.CRITICAL_MESSAGE | 125 | log_helper_test_imported.CRITICAL_MESSAGE |
| 128 | ]) | 126 | ]) |
| 129 | 127 | ||
| 128 | + def test_import_by_third_party_disabled(self): | ||
| 129 | + """Test that when imported by third party, logging is still disabled.""" | ||
| 130 | + output = self._run_test([], run_third_party=True).splitlines() | ||
| 131 | + self.assertEqual(len(output), 2) | ||
| 132 | + self.assertEqual(output[0], | ||
| 133 | + 'INFO:root:Start message from 3rd party importer') | ||
| 134 | + self.assertEqual(output[1], | ||
| 135 | + 'INFO:root:End message from 3rd party importer') | ||
| 136 | + | ||
| 137 | + def test_import_by_third_party_enabled(self): | ||
| 138 | + """Test that when imported by third party, logging can be enabled.""" | ||
| 139 | + output = self._run_test(['enable', ], run_third_party=True).splitlines() | ||
| 140 | + self.assertEqual(len(output), 12) | ||
| 141 | + self.assertIn('INFO:test_main:main: info log', output) | ||
| 142 | + self.assertIn('INFO:test_imported:imported: info log', output) | ||
| 143 | + | ||
| 130 | def _assert_json_messages(self, output, messages): | 144 | def _assert_json_messages(self, output, messages): |
| 131 | try: | 145 | try: |
| 132 | json_data = json.loads(output) | 146 | json_data = json.loads(output) |
| @@ -139,14 +153,24 @@ class TestLogHelper(unittest.TestCase): | @@ -139,14 +153,24 @@ class TestLogHelper(unittest.TestCase): | ||
| 139 | 153 | ||
| 140 | self.assertNotEqual(len(json_data), 0, msg='Output was empty') | 154 | self.assertNotEqual(len(json_data), 0, msg='Output was empty') |
| 141 | 155 | ||
| 142 | - def _run_test(self, args, should_succeed=True): | 156 | + def _run_test(self, args, should_succeed=True, run_third_party=False): |
| 143 | """ | 157 | """ |
| 144 | Use subprocess to better simulate the real scenario and avoid | 158 | Use subprocess to better simulate the real scenario and avoid |
| 145 | logging conflicts when running multiple tests (since logging depends on singletons, | 159 | logging conflicts when running multiple tests (since logging depends on singletons, |
| 146 | we might get errors or false positives between sequential tests runs) | 160 | we might get errors or false positives between sequential tests runs) |
| 161 | + | ||
| 162 | + When arg `run_third_party` is `True`, we do not run the `TEST_FILE` as | ||
| 163 | + main moduel but the `TEST_FILE_3RD_PARTY` and return contents of | ||
| 164 | + `stderr` instead of `stdout`. | ||
| 147 | """ | 165 | """ |
| 166 | + all_args = [PYTHON_EXECUTABLE, ] | ||
| 167 | + if run_third_party: | ||
| 168 | + all_args.append(TEST_FILE_3RD_PARTY) | ||
| 169 | + else: | ||
| 170 | + all_args.append(TEST_FILE) | ||
| 171 | + all_args.extend(args) | ||
| 148 | child = subprocess.Popen( | 172 | child = subprocess.Popen( |
| 149 | - [PYTHON_EXECUTABLE, TEST_FILE] + args, | 173 | + all_args, |
| 150 | shell=False, | 174 | shell=False, |
| 151 | env={'PYTHONPATH': PROJECT_ROOT}, | 175 | env={'PYTHONPATH': PROJECT_ROOT}, |
| 152 | universal_newlines=True, | 176 | universal_newlines=True, |
| @@ -157,6 +181,16 @@ class TestLogHelper(unittest.TestCase): | @@ -157,6 +181,16 @@ class TestLogHelper(unittest.TestCase): | ||
| 157 | ) | 181 | ) |
| 158 | (output, output_err) = child.communicate() | 182 | (output, output_err) = child.communicate() |
| 159 | 183 | ||
| 184 | + if False: # DEBUG | ||
| 185 | + print() | ||
| 186 | + for line in output_err.splitlines(): | ||
| 187 | + print('ERR: {}'.format(line.rstrip())) | ||
| 188 | + for line in output.splitlines(): | ||
| 189 | + print('OUT: {}'.format(line.rstrip())) | ||
| 190 | + | ||
| 191 | + if run_third_party: | ||
| 192 | + output = output_err | ||
| 193 | + | ||
| 160 | if not isinstance(output, str): | 194 | if not isinstance(output, str): |
| 161 | output = output.decode('utf-8') | 195 | output = output.decode('utf-8') |
| 162 | 196 |
tests/common/log_helper/third_party_importer.py
0 โ 100644
| 1 | +#!/usr/bin/env python3 | ||
| 2 | + | ||
| 3 | +""" | ||
| 4 | +Module for testing import of common logging modules by third party modules. | ||
| 5 | + | ||
| 6 | +This module behaves like a third party module. It does not use the common | ||
| 7 | +logging and enables logging on its own. But it imports log_helper_test_main. | ||
| 8 | +""" | ||
| 9 | + | ||
| 10 | +import sys | ||
| 11 | +import logging | ||
| 12 | + | ||
| 13 | +from tests.common.log_helper import log_helper_test_main | ||
| 14 | + | ||
| 15 | + | ||
| 16 | +def main(args): | ||
| 17 | + """ | ||
| 18 | + Main function, called when running file as script | ||
| 19 | + | ||
| 20 | + see module doc for more info | ||
| 21 | + """ | ||
| 22 | + logging.basicConfig(level=logging.INFO) | ||
| 23 | + if 'enable' in args: | ||
| 24 | + log_helper_test_main.enable_logging() | ||
| 25 | + | ||
| 26 | + logging.debug('Should not show.') | ||
| 27 | + logging.info('Start message from 3rd party importer') | ||
| 28 | + | ||
| 29 | + log_helper_test_main.do_log() | ||
| 30 | + | ||
| 31 | + logging.debug('Returning 0, but you will never see that ... .') | ||
| 32 | + logging.info('End message from 3rd party importer') | ||
| 33 | + return 0 | ||
| 34 | + | ||
| 35 | + | ||
| 36 | +if __name__ == '__main__': | ||
| 37 | + sys.exit(main(sys.argv[1:])) |
tests/olevba/test_basic.py
| @@ -76,14 +76,14 @@ class TestOlevbaBasic(unittest.TestCase): | @@ -76,14 +76,14 @@ class TestOlevbaBasic(unittest.TestCase): | ||
| 76 | 76 | ||
| 77 | def test_crypt_return(self): | 77 | def test_crypt_return(self): |
| 78 | """ | 78 | """ |
| 79 | - Tests that encrypted files give a certain return code. | 79 | + Test that encrypted files give a certain return code. |
| 80 | 80 | ||
| 81 | Currently, only the encryption applied by Office 2010 (CryptoApi RC4 | 81 | Currently, only the encryption applied by Office 2010 (CryptoApi RC4 |
| 82 | Encryption) is tested. | 82 | Encryption) is tested. |
| 83 | """ | 83 | """ |
| 84 | CRYPT_DIR = join(DATA_BASE_DIR, 'encrypted') | 84 | CRYPT_DIR = join(DATA_BASE_DIR, 'encrypted') |
| 85 | CRYPT_RETURN_CODE = 9 | 85 | CRYPT_RETURN_CODE = 9 |
| 86 | - ADD_ARGS = [], ['-d', ], ['-a', ], ['-j', ], ['-t', ] | 86 | + ADD_ARGS = [], ['-d', ], ['-a', ], ['-j', ], ['-t', ] # only 1st file |
| 87 | EXCEPTIONS = ['autostart-encrypt-standardpassword.xls', # These ... | 87 | EXCEPTIONS = ['autostart-encrypt-standardpassword.xls', # These ... |
| 88 | 'autostart-encrypt-standardpassword.xlsm', # files ... | 88 | 'autostart-encrypt-standardpassword.xlsm', # files ... |
| 89 | 'autostart-encrypt-standardpassword.xlsb', # are ... | 89 | 'autostart-encrypt-standardpassword.xlsb', # are ... |
| @@ -103,6 +103,10 @@ class TestOlevbaBasic(unittest.TestCase): | @@ -103,6 +103,10 @@ class TestOlevbaBasic(unittest.TestCase): | ||
| 103 | msg='Wrong return code {} for args {}'\ | 103 | msg='Wrong return code {} for args {}'\ |
| 104 | .format(ret_code, args + [filename, ])) | 104 | .format(ret_code, args + [filename, ])) |
| 105 | 105 | ||
| 106 | + # test only first file with all arg combinations, others just | ||
| 107 | + # without arg (test takes too long otherwise | ||
| 108 | + ADD_ARGS = ([], ) | ||
| 109 | + | ||
| 106 | 110 | ||
| 107 | # just in case somebody calls this file as a script | 111 | # just in case somebody calls this file as a script |
| 108 | if __name__ == '__main__': | 112 | if __name__ == '__main__': |
tests/olevba/test_crypto.py
| @@ -40,26 +40,35 @@ class OlevbaCryptoWriteProtectTest(unittest.TestCase): | @@ -40,26 +40,35 @@ class OlevbaCryptoWriteProtectTest(unittest.TestCase): | ||
| 40 | exclude_stderr=True) | 40 | exclude_stderr=True) |
| 41 | data = json.loads(output, object_pairs_hook=OrderedDict) | 41 | data = json.loads(output, object_pairs_hook=OrderedDict) |
| 42 | # debug: json.dump(data, sys.stdout, indent=4) | 42 | # debug: json.dump(data, sys.stdout, indent=4) |
| 43 | - self.assertEqual(len(data), 4) | 43 | + self.assertIn(len(data), (3, 4)) |
| 44 | + | ||
| 45 | + # first 2 parts: general info about script and file | ||
| 44 | self.assertIn('script_name', data[0]) | 46 | self.assertIn('script_name', data[0]) |
| 45 | self.assertIn('version', data[0]) | 47 | self.assertIn('version', data[0]) |
| 46 | self.assertEqual(data[0]['type'], 'MetaInformation') | 48 | self.assertEqual(data[0]['type'], 'MetaInformation') |
| 47 | - self.assertIn('return_code', data[-1]) | ||
| 48 | - self.assertEqual(data[-1]['type'], 'MetaInformation') | ||
| 49 | self.assertEqual(data[1]['container'], None) | 49 | self.assertEqual(data[1]['container'], None) |
| 50 | self.assertEqual(data[1]['file'], example_file) | 50 | self.assertEqual(data[1]['file'], example_file) |
| 51 | self.assertEqual(data[1]['analysis'], None) | 51 | self.assertEqual(data[1]['analysis'], None) |
| 52 | self.assertEqual(data[1]['macros'], []) | 52 | self.assertEqual(data[1]['macros'], []) |
| 53 | self.assertEqual(data[1]['type'], 'OLE') | 53 | self.assertEqual(data[1]['type'], 'OLE') |
| 54 | - self.assertEqual(data[2]['container'], example_file) | ||
| 55 | - self.assertNotEqual(data[2]['file'], example_file) | ||
| 56 | - self.assertEqual(data[2]['type'], "OpenXML") | ||
| 57 | - analysis = data[2]['analysis'] | 54 | + self.assertTrue(data[1]['json_conversion_successful']) |
| 55 | + | ||
| 56 | + # possible VBA stomping warning | ||
| 57 | + if len(data) == 4: | ||
| 58 | + self.assertEqual(data[2]['type'], 'msg') | ||
| 59 | + self.assertIn('VBA stomping', data[2]['msg']) | ||
| 60 | + | ||
| 61 | + # last part is the actual result | ||
| 62 | + self.assertEqual(data[-1]['container'], example_file) | ||
| 63 | + self.assertNotEqual(data[-1]['file'], example_file) | ||
| 64 | + self.assertEqual(data[-1]['type'], "OpenXML") | ||
| 65 | + analysis = data[-1]['analysis'] | ||
| 58 | self.assertEqual(analysis[0]['type'], 'AutoExec') | 66 | self.assertEqual(analysis[0]['type'], 'AutoExec') |
| 59 | self.assertEqual(analysis[0]['keyword'], 'Auto_Open') | 67 | self.assertEqual(analysis[0]['keyword'], 'Auto_Open') |
| 60 | - macros = data[2]['macros'] | 68 | + macros = data[-1]['macros'] |
| 61 | self.assertEqual(macros[0]['vba_filename'], 'Modul1.bas') | 69 | self.assertEqual(macros[0]['vba_filename'], 'Modul1.bas') |
| 62 | self.assertIn('Sub Auto_Open()', macros[0]['code']) | 70 | self.assertIn('Sub Auto_Open()', macros[0]['code']) |
| 71 | + self.assertTrue(data[-1]['json_conversion_successful']) | ||
| 63 | 72 | ||
| 64 | 73 | ||
| 65 | if __name__ == '__main__': | 74 | if __name__ == '__main__': |