Commit 89e4dda01b53c12b75b9af0a04fc51582cb23b87

Authored by Philippe Lagadec
Committed by GitHub
2 parents dfbcabb9 b8ba847b

Merge pull request #449 from christian-intra2net/use-log_helper

Use log helper
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__ = &#39;0.56.2&#39; @@ -71,7 +71,7 @@ __version__ = &#39;0.56.2&#39;
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__ = &#39;0.60.1.dev6&#39; @@ -239,7 +239,6 @@ __version__ = &#39;0.60.1.dev6&#39;
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__ = &#39;0.60.1.dev6&#39; @@ -272,7 +271,6 @@ __version__ = &#39;0.60.1.dev6&#39;
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=&#39;utf8&#39;): @@ -423,47 +422,29 @@ def bytes2str(bytes_string, encoding=&#39;utf8&#39;):
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=&#39;utf8&#39;, errors=&#39;replace&#39;): @@ -2462,18 +2443,18 @@ def json2ascii(json_obj, encoding=&#39;utf8&#39;, errors=&#39;replace&#39;):
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__ = &#39;0.60.dev1&#39; @@ -64,7 +65,6 @@ __version__ = &#39;0.60.dev1&#39;
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 = &#39;imported: critical log&#39; @@ -14,7 +13,11 @@ CRITICAL_MESSAGE = &#39;imported: critical log&#39;
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 = &#39;main: result&#39; @@ -15,7 +16,13 @@ RESULT_TYPE = &#39;main: result&#39;
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__':