Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ini config parsing #939

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 3 additions & 6 deletions bandit/cli/main.py
Expand Up @@ -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(
Expand All @@ -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",
)

Expand All @@ -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",
)

Expand Down
97 changes: 97 additions & 0 deletions bandit/core/utils.py
Expand Up @@ -6,6 +6,7 @@
import logging
import os.path
import sys
from argparse import ArgumentError

try:
import configparser
Expand Down Expand Up @@ -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:
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/cli/test_main.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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"]
)
Expand Down