From c364408faa25ccdc378b72c0a9e2150d20a94cba Mon Sep 17 00:00:00 2001 From: Travis McPeak Date: Thu, 21 Jan 2016 19:00:38 -0800 Subject: [PATCH] Allow certain command line arguments to be passed from file This commit allows the use of a .bandit file, which will be in the ini format and used to pass command line arguments. This is useful for multiple projects which are running in the same gate, want to be able to exclude different files per-project, and aren't using tox. Change-Id: I4256bdb7df2416f3cc01798882fb7e2e229790a3 Conflicts: README.rst tests/unit/core/test_util.py --- README.rst | 27 ++++++++++++++- bandit/bandit.py | 64 ++++++++++++++++++++++++++++++++++++ bandit/core/utils.py | 17 ++++++++++ tests/unit/core/test_util.py | 17 ++++++++++ 4 files changed, 124 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ff474c0f..2229ed29 100644 --- a/README.rst +++ b/README.rst @@ -107,7 +107,8 @@ Usage:: -b BASELINE, --baseline BASELINE Path to a baseline report, in JSON format. Note: baseline reports must be output in one of the - following formats: ['txt', 'html'] + --ini INI_PATH Path to a .bandit file which supplies command line + arguments to Bandit. The following plugin suites were discovered and loaded: any_other_function_with_shell_equals_true @@ -172,6 +173,29 @@ Mac OSX: - /usr/local/etc/bandit/bandit.yaml - /bandit/config/bandit.yaml (if running within virtualenv) +Per Project Command Line Args +----------------------------- +Projects may include a `.bandit` file that specifies command line arguments +that should be supplied for that project. The currently supported arguments +are: + + - exclude: comma separated list of excluded paths + - skips: comma separated list of tests to skip + - tests: comma separated list of tests to run + +To use this, put a .bandit file in your project's directory. For example: + +:: + + [bandit] + exclude: /test + +:: + + [bandit] + tests: B101,B102,B301 + + Exclusions ---------- In the event that a line of code triggers a Bandit issue, but that the line @@ -223,6 +247,7 @@ To write a test: vulnerability might present itself and extend the example file and the test function accordingly. + Extending Bandit ---------------- diff --git a/bandit/bandit.py b/bandit/bandit.py index 211eaecb..32c64cdb 100644 --- a/bandit/bandit.py +++ b/bandit/bandit.py @@ -16,6 +16,7 @@ from __future__ import absolute_import import argparse +import fnmatch import logging import os import sys @@ -59,11 +60,54 @@ def _init_logger(debug=False, log_format=None): logger.debug("logging initialized") +def _get_options_from_ini(ini_path, target): + """Return a dictionary of config options or None if we can't load any.""" + ini_file = None + + if ini_path: + ini_file = ini_path + else: + bandit_files = [] + + for t in target: + for root, dirnames, filenames in os.walk(t): + for filename in fnmatch.filter(filenames, '.bandit'): + bandit_files.append(os.path.join(root, filename)) + + if len(bandit_files) > 1: + logger.error('Multiple .bandit files found - scan separately or ' + 'choose one with --ini\n\t%s', + ', '.join(bandit_files)) + sys.exit(2) + + elif len(bandit_files) == 1: + ini_file = bandit_files[0] + logger.info('Found project level .bandit file: %s', + bandit_files[0]) + + if ini_file: + return utils.parse_ini_file(ini_file) + else: + return None + + def _init_extensions(): from bandit.core import extension_loader as ext_loader return ext_loader.MANAGER +def _log_option_source(arg_val, ini_val, option_name): + """It's useful to show the source of each option.""" + if arg_val: + logger.info("Using command line arg for %s", option_name) + return arg_val + elif ini_val: + logger.info("Using .bandit arg for %s", option_name) + return ini_val + else: + return None + + def _running_under_virtualenv(): if hasattr(sys, 'real_prefix'): return True @@ -192,6 +236,11 @@ def main(): 'Note: baseline reports must be output in one of ' 'the following formats: ' + str(baseline_formatters) ) + parser.add_argument( + '--ini', dest='ini_path', action='store', default=None, + help='Path to a .bandit file which supplies command line arguments to ' + 'Bandit.' + ) parser.set_defaults(debug=False) parser.set_defaults(verbose=False) parser.set_defaults(ignore_nosec=False) @@ -216,6 +265,21 @@ def main(): logger.error('%s', e) sys.exit(2) + # Handle .bandit files in projects to pass cmdline args from file + ini_options = _get_options_from_ini(args.ini_path, args.targets) + if ini_options: + # prefer command line, then ini file + args.excluded_paths = _log_option_source(args.excluded_paths, + ini_options.get('exclude'), + 'excluded paths') + + args.skips = _log_option_source(args.skips, ini_options.get('skips'), + 'skipped tests') + + args.tests = _log_option_source(args.tests, ini_options.get('tests'), + 'selected tests') + # TODO(tmcpeak): any other useful options to pass from .bandit? + # if the log format string was set in the options, reinitialize if b_conf.get_option('log_format'): log_format = b_conf.get_option('log_format') diff --git a/bandit/core/utils.py b/bandit/core/utils.py index 5843dbd6..06d34183 100644 --- a/bandit/core/utils.py +++ b/bandit/core/utils.py @@ -21,6 +21,10 @@ import logging import os.path import sys +try: + import configparser +except ImportError: + import ConfigParser as configparser logger = logging.getLogger(__name__) @@ -329,3 +333,16 @@ def get_path_for_function(f): else: logger.warn("Cannot resolve file path for module %s", module_name) return None + + +def parse_ini_file(f_loc): + config = configparser.ConfigParser() + try: + config.read(f_loc) + return {k: v for k, v in config.items('bandit')} + + except (configparser.Error, KeyError, TypeError): + logger.warning("Unable to parse config file %s or missing [bandit] " + "section", f_loc) + + return None diff --git a/tests/unit/core/test_util.py b/tests/unit/core/test_util.py index 715a96d6..91bf984b 100644 --- a/tests/unit/core/test_util.py +++ b/tests/unit/core/test_util.py @@ -272,3 +272,20 @@ class UtilTests(testtools.TestCase): self.assertEqual('deep value', b_utils.deepgetattr(a, 'b.c.d')) self.assertEqual('deep value 2', b_utils.deepgetattr(a, 'b.c.d2')) self.assertRaises(AttributeError, b_utils.deepgetattr, a.b, 'z') + + def test_parse_ini_file(self): + + tests = [{'content': "[bandit]\nexclude=/abc,/def", + 'expected': {'exclude': '/abc,/def'}}, + + {'content': '[Blabla]\nsomething=something', + 'expected': None}] + + with tempfile.NamedTemporaryFile('r+') as t: + for test in tests: + f = open(t.name, 'w') + f.write(test['content']) + f.close() + + self.assertEqual(b_utils.parse_ini_file(t.name), + test['expected'])