From 06aeadda32fdee16ffe5a41d930855012126204a Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 28 Jul 2022 17:10:13 +0200 Subject: [PATCH 1/2] Add test for default .bandit ini parsing --- tests/unit/cli/test_main.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/unit/cli/test_main.py b/tests/unit/cli/test_main.py index 5d0fd7b76..3d13229d5 100644 --- a/tests/unit/cli/test_main.py +++ b/tests/unit/cli/test_main.py @@ -46,6 +46,28 @@ } """ +bandit_default_ini = """ +[bandit] +targets = +recursive = false +aggregate = file +number = 3 +profile = +tests = +skips = +level = 1 +confidence = 1 +format = txt +msg-template = +output = +verbose = false +debug = false +quiet = false +ignore-nosec = false +exclude = +baseline = +""" + class BanditCLIMainLoggerTests(testtools.TestCase): def setUp(self): @@ -218,6 +240,16 @@ def test_main_handle_ini_options(self): "Unknown test found in profile: some_test", ) + @mock.patch("sys.argv", ["bandit", "--ini", ".bandit", "test"]) + def test_main_handle_default_ini_file(self): + # Test that bandit can parse a default .bandit ini file + temp_directory = self.useFixture(fixtures.TempDir()).path + os.chdir(temp_directory) + with open(".bandit", "wt") as fd: + fd.write(bandit_default_ini) + # should run without errors + self.assertRaisesRegex(SystemExit, "0", bandit.main) + @mock.patch( "sys.argv", ["bandit", "-c", "bandit.yaml", "-t", "badID", "test"] ) From 59f3ff597d311276337746fef33e0e7ccc79ed5c Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 28 Jul 2022 15:05:21 +0200 Subject: [PATCH 2/2] Use the argparse parser to validate the ini config Closes #938 Additionally this provides better error messages for .bandit ini config files and ensures that the ini options are parsed correctly by reusing the argparse parser in `bandit.cli.main` --- bandit/cli/main.py | 9 ++-- bandit/core/utils.py | 97 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/bandit/cli/main.py b/bandit/cli/main.py index 47588859d..194acc1ba 100644 --- a/bandit/cli/main.py +++ b/bandit/cli/main.py @@ -459,6 +459,7 @@ def main(): # Handle .bandit files in projects to pass cmdline args from file ini_options = _get_options_from_ini(args.ini_path, args.targets) + ini_options = utils.validate_ini_options(ini_options, parser) if ini_options: # prefer command line, then ini file args.excluded_paths = _log_option_source( @@ -482,14 +483,10 @@ def main(): "selected tests", ) - ini_targets = ini_options.get("targets") - if ini_targets: - ini_targets = ini_targets.split(",") - args.targets = _log_option_source( parser.get_default("targets"), args.targets, - ini_targets, + ini_options.get("targets"), "selected targets", ) @@ -512,7 +509,7 @@ def main(): args.context_lines = _log_option_source( parser.get_default("context_lines"), args.context_lines, - int(ini_options.get("number") or 0) or None, + ini_options.get("number"), "max code lines output for issue", ) diff --git a/bandit/core/utils.py b/bandit/core/utils.py index 3ac78f54f..d34d01b18 100644 --- a/bandit/core/utils.py +++ b/bandit/core/utils.py @@ -6,6 +6,7 @@ import logging import os.path import sys +from argparse import ArgumentError try: import configparser @@ -360,6 +361,102 @@ def parse_ini_file(f_loc): return None +def value_option(key, value): + if not value: + return [] + return [f"--{key}", value] + + +def multi_option(key, value): + if not value.isdigit(): + LOG.warning(f"INI config '{key}' is not a number: using default") + value = 0 + return [f"--{key}"] * (int(value) - 1) + + +def flag_option(key, value): + try: + opt = {"false": False, "true": True}[value.lower()] + except KeyError: + LOG.warning(f"INI config '{key}' not 'True/False': using default") + opt = False + return [f"--{key}"] if opt else [] + + +INI_KEY_TO_ARGS = { + "targets": lambda k, v: v.split(","), + "recursive": flag_option, + "aggregate": value_option, + "number": value_option, + "profile": value_option, + "tests": value_option, + "skips": lambda k, v: value_option("skip", v), + "level": multi_option, + "confidence": multi_option, + "format": value_option, + "msg-template": value_option, + "output": value_option, + "verbose": flag_option, + "debug": flag_option, + "quiet": flag_option, + "ignore-nosec": flag_option, + "exclude": value_option, + "baseline": value_option, +} +INI_KEY_RENAME = { + "aggregate": "agg_type", + "number": "context_lines", + "level": "severity", + "format": "output_format", + "msg-template": "msg_template", + "output": "output_file", + "ignore-nosec": "ignore_nosec", + "exclude": "excluded_paths", +} +ARGPARSE_KEY_RENAME = {v: k for k, v in INI_KEY_RENAME.items()} + + +def validate_ini_options(ini_config, parser): + """Validate the ini config dict by reusing the argparse ArgumentParser""" + if ini_config is None: + return None + + invalid_keys = set(ini_config) - set(INI_KEY_TO_ARGS) + # gracefully continue + for key in invalid_keys: + LOG.warning( + "INI config file contains invalid key %s in section [bandit]", + repr(key), + ) + ini_config.pop(key) + + ini_args = [] + for key, value in ini_config.items(): + key_args = INI_KEY_TO_ARGS[key](key, value) + ini_args.extend(key_args) + + # nicer output on 3.9 + if sys.version_info >= (3, 9): + parser.exit_on_error = False + + try: + args = parser.parse_args(ini_args) + except SystemExit: + # python < 3.9 will have to catch SystemExit here. + LOG.error("INI config: parsing failed") + raise + except ArgumentError as err: + action, msg = err.args + ini_name = ARGPARSE_KEY_RENAME.get(action.dest, action.dest) + LOG.error(f"INI config '{ini_name}': {msg}") + sys.exit(2) + + for key in ini_config: + ini_config[key] = getattr(args, INI_KEY_RENAME.get(key, key)) + + return ini_config + + def check_ast_node(name): "Check if the given name is that of a valid AST node." try: