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 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 @@ def test_deepgetattr(self): 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'])