You need to sign in before continuing.
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 | 8 | """ |
| 9 | 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 | 15 | def format(self, record): |
| 12 | 16 | """ |
| 13 | 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 | 54 | self._json_enabled = json_enabled |
| 55 | 55 | |
| 56 | 56 | def level(self): |
| 57 | + """Return current level of logger.""" | |
| 57 | 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 | 3 | |
| 4 | 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 | 26 | .. codeauthor:: Intra2net AG <info@intra2net>, Philippe Lagadec |
| 7 | 27 | """ |
| 8 | 28 | |
| ... | ... | @@ -45,6 +65,7 @@ General logging helpers |
| 45 | 65 | # TODO: |
| 46 | 66 | |
| 47 | 67 | |
| 68 | +from __future__ import print_function | |
| 48 | 69 | from ._json_formatter import JsonFormatter |
| 49 | 70 | from ._logger_adapter import OletoolsLoggerAdapter |
| 50 | 71 | from . import _root_logger_wrapper |
| ... | ... | @@ -61,15 +82,27 @@ LOG_LEVELS = { |
| 61 | 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 | 89 | DEFAULT_LOGGER_NAME = 'oletools' |
| 65 | 90 | DEFAULT_MESSAGE_FORMAT = '%(levelname)-8s %(message)s' |
| 66 | 91 | |
| 67 | 92 | |
| 68 | 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 | 101 | def __init__(self): |
| 70 | 102 | self._all_names = set() # set so we do not have duplicates |
| 71 | 103 | self._use_json = False |
| 72 | 104 | self._is_enabled = False |
| 105 | + self._target_stream = None | |
| 73 | 106 | |
| 74 | 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 | 115 | """ |
| 83 | 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 | 121 | This function initializes the root logger and enables logging. |
| 88 | 122 | We set the level of the root logger to the one passed by calling logging.basicConfig. |
| ... | ... | @@ -93,15 +127,26 @@ class LogHelper: |
| 93 | 127 | which in turn will log to the stream set in this function. |
| 94 | 128 | Since the root logger is the one doing the work, when using JSON we set its formatter |
| 95 | 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 | 136 | if self._is_enabled: |
| 98 | 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 | 145 | ensure_stdout_handles_unicode() |
| 102 | 146 | |
| 103 | 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 | 150 | self._is_enabled = True |
| 106 | 151 | |
| 107 | 152 | self._use_json = use_json |
| ... | ... | @@ -115,8 +160,8 @@ class LogHelper: |
| 115 | 160 | |
| 116 | 161 | # add a JSON formatter to the root logger, which will be used by every logger |
| 117 | 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 | 166 | def end_logging(self): |
| 122 | 167 | """ |
| ... | ... | @@ -133,7 +178,7 @@ class LogHelper: |
| 133 | 178 | |
| 134 | 179 | # end json list |
| 135 | 180 | if self._use_json: |
| 136 | - print(']') | |
| 181 | + print(']', file=self.target_stream) | |
| 137 | 182 | self._use_json = False |
| 138 | 183 | |
| 139 | 184 | def _get_except_hook(self, old_hook): | ... | ... |
oletools/crypto.py
| ... | ... | @@ -95,6 +95,7 @@ http://www.decalage.info/python/oletools |
| 95 | 95 | # 2019-05-23 PL: - added DEFAULT_PASSWORDS list |
| 96 | 96 | # 2021-05-22 v0.60 PL: - added PowerPoint transparent password |
| 97 | 97 | # '/01Hannes Ruescher/01' (issue #627) |
| 98 | +# 2019-05-24 CH: - use log_helper | |
| 98 | 99 | |
| 99 | 100 | __version__ = '0.60' |
| 100 | 101 | |
| ... | ... | @@ -104,7 +105,6 @@ import os |
| 104 | 105 | from os.path import splitext, isfile |
| 105 | 106 | from tempfile import mkstemp |
| 106 | 107 | import zipfile |
| 107 | -import logging | |
| 108 | 108 | |
| 109 | 109 | from olefile import OleFileIO |
| 110 | 110 | |
| ... | ... | @@ -134,44 +134,20 @@ MAX_NESTING_DEPTH = 10 |
| 134 | 134 | |
| 135 | 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 | 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 | 141 | def enable_logging(): |
| 169 | 142 | """ |
| 170 | 143 | Enable logging for this module (disabled by default). |
| 144 | + | |
| 145 | + For use by third-party libraries that import `crypto` as module. | |
| 146 | + | |
| 171 | 147 | This will set the module-specific logger level to NOTSET, which |
| 172 | 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 | 153 | def is_encrypted(some_file): | ... | ... |
oletools/mraptor.py
| ... | ... | @@ -71,7 +71,7 @@ __version__ = '0.56.2' |
| 71 | 71 | |
| 72 | 72 | #--- IMPORTS ------------------------------------------------------------------ |
| 73 | 73 | |
| 74 | -import sys, logging, optparse, re, os | |
| 74 | +import sys, optparse, re, os | |
| 75 | 75 | |
| 76 | 76 | # IMPORTANT: it should be possible to run oletools directly as scripts |
| 77 | 77 | # in any directory without installing them with pip or setup.py. |
| ... | ... | @@ -90,11 +90,12 @@ from oletools.thirdparty.tablestream import tablestream |
| 90 | 90 | |
| 91 | 91 | from oletools import olevba |
| 92 | 92 | from oletools.olevba import TYPE2TAG |
| 93 | +from oletools.common.log_helper import log_helper | |
| 93 | 94 | |
| 94 | 95 | # === LOGGING ================================================================= |
| 95 | 96 | |
| 96 | 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 | 101 | #--- CONSTANTS ---------------------------------------------------------------- |
| ... | ... | @@ -230,15 +231,7 @@ def main(): |
| 230 | 231 | """ |
| 231 | 232 | Main function, called when olevba is run from the command line |
| 232 | 233 | """ |
| 233 | - global log | |
| 234 | 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 | 236 | usage = 'usage: mraptor [options] <filename> [filename2 ...]' |
| 244 | 237 | parser = optparse.OptionParser(usage=usage) |
| ... | ... | @@ -272,9 +265,9 @@ def main(): |
| 272 | 265 | print('MacroRaptor %s - http://decalage.info/python/oletools' % __version__) |
| 273 | 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 | 269 | # enable logging in the modules: |
| 277 | - log.setLevel(logging.NOTSET) | |
| 270 | + olevba.enable_logging() | |
| 278 | 271 | |
| 279 | 272 | t = tablestream.TableStream(style=tablestream.TableStyleSlim, |
| 280 | 273 | header_row=['Result', 'Flags', 'Type', 'File'], |
| ... | ... | @@ -346,6 +339,7 @@ def main(): |
| 346 | 339 | global_result = result |
| 347 | 340 | exitcode = result.exit_code |
| 348 | 341 | |
| 342 | + log_helper.end_logging() | |
| 349 | 343 | print('') |
| 350 | 344 | print('Flags: A=AutoExec, W=Write, X=Execute') |
| 351 | 345 | print('Exit code: %d - %s' % (exitcode, global_result.name)) | ... | ... |
oletools/olevba.py
| ... | ... | @@ -239,7 +239,6 @@ __version__ = '0.60.1.dev6' |
| 239 | 239 | |
| 240 | 240 | #------------------------------------------------------------------------------ |
| 241 | 241 | # TODO: |
| 242 | -# + setup logging (common with other oletools) | |
| 243 | 242 | # + add xor bruteforcing like bbharvest |
| 244 | 243 | # + options -a and -c should imply -d |
| 245 | 244 | |
| ... | ... | @@ -272,7 +271,6 @@ __version__ = '0.60.1.dev6' |
| 272 | 271 | import traceback |
| 273 | 272 | import sys |
| 274 | 273 | import os |
| 275 | -import logging | |
| 276 | 274 | import struct |
| 277 | 275 | from io import BytesIO, StringIO |
| 278 | 276 | import math |
| ... | ... | @@ -346,6 +344,7 @@ from oletools import crypto |
| 346 | 344 | from oletools.common.io_encoding import ensure_stdout_handles_unicode |
| 347 | 345 | from oletools.common import codepages |
| 348 | 346 | from oletools import ftguess |
| 347 | +from oletools.common.log_helper import log_helper | |
| 349 | 348 | |
| 350 | 349 | # === PYTHON 2+3 SUPPORT ====================================================== |
| 351 | 350 | |
| ... | ... | @@ -423,47 +422,29 @@ def bytes2str(bytes_string, encoding='utf8'): |
| 423 | 422 | |
| 424 | 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 | 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 | 430 | def enable_logging(): |
| 457 | 431 | """ |
| 458 | 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 | 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 | 445 | ppt_parser.enable_logging() |
| 465 | 446 | crypto.enable_logging() |
| 466 | - | |
| 447 | + # TODO: do not have enable_logging yet: oleform, rtfobj | |
| 467 | 448 | |
| 468 | 449 | |
| 469 | 450 | #=== EXCEPTIONS ============================================================== |
| ... | ... | @@ -2462,18 +2443,18 @@ def json2ascii(json_obj, encoding='utf8', errors='replace'): |
| 2462 | 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 | 2447 | """ line-wise print of json.dumps(json2ascii(..)) with options and indent+1 |
| 2468 | 2448 | |
| 2469 | 2449 | can use in two ways: |
| 2470 | 2450 | (1) print_json(some_dict) |
| 2471 | 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 | 2459 | if json_dict and json_parts: |
| 2479 | 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 | 2466 | if json_parts: |
| 2486 | 2467 | json_dict = json_parts |
| 2487 | 2468 | |
| 2488 | - if _json_is_first: | |
| 2489 | - print('[') | |
| 2490 | - | |
| 2491 | 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 | 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 | 2483 | class VBA_Scanner(object): |
| ... | ... | @@ -4358,13 +4339,6 @@ def parse_args(cmd_line_args=None): |
| 4358 | 4339 | """ parse command line arguments (given ones or per default sys.argv) """ |
| 4359 | 4340 | |
| 4360 | 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 | 4343 | usage = 'usage: olevba [options] <filename> [filename2 ...]' |
| 4370 | 4344 | parser = argparse.ArgumentParser(usage=usage) |
| ... | ... | @@ -4459,8 +4433,6 @@ def parse_args(cmd_line_args=None): |
| 4459 | 4433 | if options.show_pcode and options.no_pcode: |
| 4460 | 4434 | parser.error('You cannot combine options --no-pcode and --show-pcode') |
| 4461 | 4435 | |
| 4462 | - options.loglevel = LOG_LEVELS[options.loglevel] | |
| 4463 | - | |
| 4464 | 4436 | return options |
| 4465 | 4437 | |
| 4466 | 4438 | |
| ... | ... | @@ -4474,6 +4446,8 @@ def process_file(filename, data, container, options, crypto_nesting=0): |
| 4474 | 4446 | Returns a single code summarizing the status of processing of this file |
| 4475 | 4447 | """ |
| 4476 | 4448 | try: |
| 4449 | + vba_parser = None | |
| 4450 | + | |
| 4477 | 4451 | # Open the file |
| 4478 | 4452 | vba_parser = VBA_Parser_CLI(filename, data=data, container=container, |
| 4479 | 4453 | relaxed=options.relaxed, |
| ... | ... | @@ -4501,6 +4475,7 @@ def process_file(filename, data, container, options, crypto_nesting=0): |
| 4501 | 4475 | no_xlm=options.no_xlm)) |
| 4502 | 4476 | else: # (should be impossible) |
| 4503 | 4477 | raise ValueError('unexpected output mode: "{0}"!'.format(options.output_mode)) |
| 4478 | + vba_parser.close() | |
| 4504 | 4479 | |
| 4505 | 4480 | # even if processing succeeds, file might still be encrypted |
| 4506 | 4481 | log.debug('Checking for encryption (normal)') |
| ... | ... | @@ -4508,6 +4483,10 @@ def process_file(filename, data, container, options, crypto_nesting=0): |
| 4508 | 4483 | log.debug('no encryption detected') |
| 4509 | 4484 | return RETURN_OK |
| 4510 | 4485 | except Exception as exc: |
| 4486 | + log.debug('Caught exception:', exc_info=True) | |
| 4487 | + if vba_parser: | |
| 4488 | + vba_parser.close() | |
| 4489 | + | |
| 4511 | 4490 | log.debug('Checking for encryption (after exception)') |
| 4512 | 4491 | if crypto.is_encrypted(filename): |
| 4513 | 4492 | pass # deal with this below |
| ... | ... | @@ -4582,10 +4561,15 @@ def main(cmd_line_args=None): |
| 4582 | 4561 | """ |
| 4583 | 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 | 4568 | # provide info about tool and its version |
| 4586 | 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 | 4571 | print_json(script_name='olevba', version=__version__, |
| 4572 | + python_version=sys.version_info[0:3], | |
| 4589 | 4573 | url='http://decalage.info/python/oletools', |
| 4590 | 4574 | type='MetaInformation', _json_is_first=True) |
| 4591 | 4575 | else: |
| ... | ... | @@ -4594,10 +4578,6 @@ def main(cmd_line_args=None): |
| 4594 | 4578 | print('olevba %s on Python %s - http://decalage.info/python/oletools' % |
| 4595 | 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 | 4581 | # with the option --reveal, make sure --deobf is also enabled: |
| 4602 | 4582 | if options.show_deobfuscated_code and not options.deobfuscate: |
| 4603 | 4583 | log.debug('set --deobf because --reveal was set') |
| ... | ... | @@ -4682,11 +4662,6 @@ def main(cmd_line_args=None): |
| 4682 | 4662 | 'A=Auto-executable, S=Suspicious keywords, I=IOCs, H=Hex strings, ' \ |
| 4683 | 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 | 4665 | except crypto.CryptoErrorBase as exc: |
| 4691 | 4666 | log.exception('Problems with encryption in main: {}'.format(exc), |
| 4692 | 4667 | exc_info=True) |
| ... | ... | @@ -4704,6 +4679,7 @@ def main(cmd_line_args=None): |
| 4704 | 4679 | |
| 4705 | 4680 | # done. exit |
| 4706 | 4681 | log.debug('will exit now with code %s' % return_code) |
| 4682 | + log_helper.end_logging() | |
| 4707 | 4683 | sys.exit(return_code) |
| 4708 | 4684 | |
| 4709 | 4685 | if __name__ == '__main__': | ... | ... |
oletools/record_base.py
| ... | ... | @@ -42,6 +42,7 @@ from __future__ import print_function |
| 42 | 42 | # 2018-09-11 v0.54 PL: - olefile is now a dependency |
| 43 | 43 | # 2019-01-30 PL: - fixed import to avoid mixing installed oletools |
| 44 | 44 | # and dev version |
| 45 | +# 2019-05-24 CH: - use log_helper | |
| 45 | 46 | |
| 46 | 47 | __version__ = '0.60.dev1' |
| 47 | 48 | |
| ... | ... | @@ -64,7 +65,6 @@ __version__ = '0.60.dev1' |
| 64 | 65 | import sys |
| 65 | 66 | import os.path |
| 66 | 67 | from io import SEEK_CUR |
| 67 | -import logging | |
| 68 | 68 | |
| 69 | 69 | import olefile |
| 70 | 70 | |
| ... | ... | @@ -74,6 +74,7 @@ PARENT_DIR = os.path.normpath(os.path.dirname(os.path.dirname( |
| 74 | 74 | if PARENT_DIR not in sys.path: |
| 75 | 75 | sys.path.insert(0, PARENT_DIR) |
| 76 | 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 | 101 | } |
| 101 | 102 | |
| 102 | 103 | |
| 104 | +logger = log_helper.get_or_create_silent_logger('record_base') | |
| 105 | + | |
| 106 | + | |
| 103 | 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 | 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 | 125 | # Base Classes |
| 110 | 126 | ############################################################################### |
| ... | ... | @@ -139,7 +155,7 @@ class OleRecordFile(olefile.OleFileIO): |
| 139 | 155 | |
| 140 | 156 | def iter_streams(self): |
| 141 | 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 | 160 | for sid, direntry in enumerate(self.direntries): |
| 145 | 161 | is_orphan = direntry is None |
| ... | ... | @@ -147,7 +163,7 @@ class OleRecordFile(olefile.OleFileIO): |
| 147 | 163 | # this direntry is not part of the tree --> unused or orphan |
| 148 | 164 | direntry = self._load_direntry(sid) |
| 149 | 165 | is_stream = direntry.entry_type == olefile.STGTY_STREAM |
| 150 | - logging.debug('direntry {:2d} {}: {}'.format( | |
| 166 | + logger.debug('direntry {:2d} {}: {}'.format( | |
| 151 | 167 | sid, '[orphan]' if is_orphan else direntry.name, |
| 152 | 168 | 'is stream of size {}'.format(direntry.size) if is_stream else |
| 153 | 169 | 'no stream ({})'.format(ENTRY_TYPE2STR[direntry.entry_type]))) |
| ... | ... | @@ -216,8 +232,8 @@ class OleRecordStream(object): |
| 216 | 232 | |
| 217 | 233 | # read first few bytes, determine record type and size |
| 218 | 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 | 238 | # determine what class to wrap this into |
| 223 | 239 | rec_clz, force_read = self.record_class_for_type(rec_type) |
| ... | ... | @@ -237,6 +253,7 @@ class OleRecordStream(object): |
| 237 | 253 | yield rec_object |
| 238 | 254 | |
| 239 | 255 | def close(self): |
| 256 | + """Close this stream (i.e. the stream given in constructor).""" | |
| 240 | 257 | self.stream.close() |
| 241 | 258 | |
| 242 | 259 | def __str__(self): |
| ... | ... | @@ -348,25 +365,25 @@ def test(filenames, ole_file_class=OleRecordFile, |
| 348 | 365 | if an error occurs while parsing a stream of type in must_parse, the error |
| 349 | 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 | 369 | if do_per_record is None: |
| 353 | 370 | def do_per_record(record): # pylint: disable=function-redefined |
| 354 | 371 | pass # do nothing |
| 355 | 372 | if not filenames: |
| 356 | - logging.info('need file name[s]') | |
| 373 | + logger.info('need file name[s]') | |
| 357 | 374 | return 2 |
| 358 | 375 | for filename in filenames: |
| 359 | - logging.info('checking file {0}'.format(filename)) | |
| 376 | + logger.info('checking file {0}'.format(filename)) | |
| 360 | 377 | if not olefile.isOleFile(filename): |
| 361 | - logging.info('not an ole file - skip') | |
| 378 | + logger.info('not an ole file - skip') | |
| 362 | 379 | continue |
| 363 | 380 | ole = ole_file_class(filename) |
| 364 | 381 | |
| 365 | 382 | for stream in ole.iter_streams(): |
| 366 | - logging.info(' parse ' + str(stream)) | |
| 383 | + logger.info(' parse ' + str(stream)) | |
| 367 | 384 | try: |
| 368 | 385 | for record in stream.iter_records(): |
| 369 | - logging.info(' ' + str(record)) | |
| 386 | + logger.info(' ' + str(record)) | |
| 370 | 387 | do_per_record(record) |
| 371 | 388 | except Exception: |
| 372 | 389 | if not must_parse: |
| ... | ... | @@ -374,7 +391,9 @@ def test(filenames, ole_file_class=OleRecordFile, |
| 374 | 391 | elif isinstance(stream, must_parse): |
| 375 | 392 | raise |
| 376 | 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 | 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 | 4 | """ |
| 5 | 5 | |
| 6 | 6 | from oletools.common.log_helper import log_helper |
| 7 | -import logging | |
| 8 | 7 | |
| 9 | 8 | DEBUG_MESSAGE = 'imported: debug log' |
| 10 | 9 | INFO_MESSAGE = 'imported: info log' |
| ... | ... | @@ -14,7 +13,11 @@ CRITICAL_MESSAGE = 'imported: critical log' |
| 14 | 13 | RESULT_MESSAGE = 'imported: result log' |
| 15 | 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 | 23 | def log(): | ... | ... |
tests/common/log_helper/log_helper_test_main.py
| 1 | 1 | """ Test log_helpers """ |
| 2 | 2 | |
| 3 | 3 | import sys |
| 4 | +import logging | |
| 4 | 5 | from tests.common.log_helper import log_helper_test_imported |
| 5 | 6 | from oletools.common.log_helper import log_helper |
| 6 | 7 | |
| ... | ... | @@ -15,7 +16,13 @@ RESULT_TYPE = 'main: result' |
| 15 | 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 | 27 | Try to cover possible logging scenarios. For each scenario covered, here's the expected args and outcome: |
| 21 | 28 | - Log without enabling: ['<level>'] |
| ... | ... | @@ -36,13 +43,12 @@ def init_logging_and_log(args): |
| 36 | 43 | throw = 'throw' in args |
| 37 | 44 | percent_autoformat = '%-autoformat' in args |
| 38 | 45 | |
| 46 | + log_helper_test_imported.logger.setLevel(logging.ERROR) | |
| 47 | + | |
| 39 | 48 | if 'enable' in args: |
| 40 | 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 | 53 | if throw: |
| 48 | 54 | raise Exception('An exception occurred before ending the logging') |
| ... | ... | @@ -50,7 +56,10 @@ def init_logging_and_log(args): |
| 50 | 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 | 63 | logger.debug(DEBUG_MESSAGE) |
| 55 | 64 | logger.info(INFO_MESSAGE) |
| 56 | 65 | logger.warning(WARNING_MESSAGE) |
| ... | ... | @@ -61,4 +70,4 @@ def _log(): |
| 61 | 70 | |
| 62 | 71 | |
| 63 | 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 | 15 | |
| 16 | 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 | 19 | TEST_FILE = relpath(join(dirname(abspath(__file__)), 'log_helper_test_main.py'), |
| 20 | 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 | 30 | class TestLogHelper(unittest.TestCase): |
| ... | ... | @@ -127,6 +125,22 @@ class TestLogHelper(unittest.TestCase): |
| 127 | 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 | 144 | def _assert_json_messages(self, output, messages): |
| 131 | 145 | try: |
| 132 | 146 | json_data = json.loads(output) |
| ... | ... | @@ -139,14 +153,24 @@ class TestLogHelper(unittest.TestCase): |
| 139 | 153 | |
| 140 | 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 | 158 | Use subprocess to better simulate the real scenario and avoid |
| 145 | 159 | logging conflicts when running multiple tests (since logging depends on singletons, |
| 146 | 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 | 172 | child = subprocess.Popen( |
| 149 | - [PYTHON_EXECUTABLE, TEST_FILE] + args, | |
| 173 | + all_args, | |
| 150 | 174 | shell=False, |
| 151 | 175 | env={'PYTHONPATH': PROJECT_ROOT}, |
| 152 | 176 | universal_newlines=True, |
| ... | ... | @@ -157,6 +181,16 @@ class TestLogHelper(unittest.TestCase): |
| 157 | 181 | ) |
| 158 | 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 | 194 | if not isinstance(output, str): |
| 161 | 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 | 76 | |
| 77 | 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 | 81 | Currently, only the encryption applied by Office 2010 (CryptoApi RC4 |
| 82 | 82 | Encryption) is tested. |
| 83 | 83 | """ |
| 84 | 84 | CRYPT_DIR = join(DATA_BASE_DIR, 'encrypted') |
| 85 | 85 | CRYPT_RETURN_CODE = 9 |
| 86 | - ADD_ARGS = [], ['-d', ], ['-a', ], ['-j', ], ['-t', ] | |
| 86 | + ADD_ARGS = [], ['-d', ], ['-a', ], ['-j', ], ['-t', ] # only 1st file | |
| 87 | 87 | EXCEPTIONS = ['autostart-encrypt-standardpassword.xls', # These ... |
| 88 | 88 | 'autostart-encrypt-standardpassword.xlsm', # files ... |
| 89 | 89 | 'autostart-encrypt-standardpassword.xlsb', # are ... |
| ... | ... | @@ -103,6 +103,10 @@ class TestOlevbaBasic(unittest.TestCase): |
| 103 | 103 | msg='Wrong return code {} for args {}'\ |
| 104 | 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 | 111 | # just in case somebody calls this file as a script |
| 108 | 112 | if __name__ == '__main__': | ... | ... |
tests/olevba/test_crypto.py
| ... | ... | @@ -40,26 +40,35 @@ class OlevbaCryptoWriteProtectTest(unittest.TestCase): |
| 40 | 40 | exclude_stderr=True) |
| 41 | 41 | data = json.loads(output, object_pairs_hook=OrderedDict) |
| 42 | 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 | 46 | self.assertIn('script_name', data[0]) |
| 45 | 47 | self.assertIn('version', data[0]) |
| 46 | 48 | self.assertEqual(data[0]['type'], 'MetaInformation') |
| 47 | - self.assertIn('return_code', data[-1]) | |
| 48 | - self.assertEqual(data[-1]['type'], 'MetaInformation') | |
| 49 | 49 | self.assertEqual(data[1]['container'], None) |
| 50 | 50 | self.assertEqual(data[1]['file'], example_file) |
| 51 | 51 | self.assertEqual(data[1]['analysis'], None) |
| 52 | 52 | self.assertEqual(data[1]['macros'], []) |
| 53 | 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 | 66 | self.assertEqual(analysis[0]['type'], 'AutoExec') |
| 59 | 67 | self.assertEqual(analysis[0]['keyword'], 'Auto_Open') |
| 60 | - macros = data[2]['macros'] | |
| 68 | + macros = data[-1]['macros'] | |
| 61 | 69 | self.assertEqual(macros[0]['vba_filename'], 'Modul1.bas') |
| 62 | 70 | self.assertIn('Sub Auto_Open()', macros[0]['code']) |
| 71 | + self.assertTrue(data[-1]['json_conversion_successful']) | |
| 63 | 72 | |
| 64 | 73 | |
| 65 | 74 | if __name__ == '__main__': | ... | ... |