From 59f3ff597d311276337746fef33e0e7ccc79ed5c Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 28 Jul 2022 15:05:21 +0200 Subject: [PATCH] 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: