Repository: PyCQA/bandit
Pull request #722 (2021-07-10 14:32:50)

fix reading initial values from .bandit
## How to fix reading .bandit initial values:

Previously bandit was not able to read initialization values from the **.bandit** file
like the `exclude` keyword. This has caused a lot of CI pipelines that were using
bandit to fail. In this pull, I have tried to fix this problem.

## What was the main problem that was causing this bug?

The CLI of bandit is written with python's popular library `argparse` .
The main problem was with the handling of default arguments' values
of the **bandit** command-line program. The program was not able to
distinguish between manually passed arguments and default arguments
values. Before In the first line of `_log_option_source` function in
`bandit.cli.main.py`, the function only checked whether `arg_val` exists
or not. At first glance, it seems that this is a correct action, but the truth is
that this function fails when the default value of an argument is a string
like `--exclude`.  When we didn't pass an argument to `--exclude`, the value
of `arg_val` would be equal to default value of the argument and passed
the `if` condition and will neglect the initial value in the **.bandit** file.

Resolves: [#499](https://github.com/PyCQA/bandit/issues/499), [#595](https://github.com/PyCQA/bandit/issues/595)



Patch:
diff --git a/bandit/cli/main.py b/bandit/cli/main.py
index 66f6b4d..bbefc93 100644
--- a/bandit/cli/main.py
+++ b/bandit/cli/main.py
@@ -80,16 +80,24 @@ def _init_extensions():
     return ext_loader.MANAGER
 
 
-def _log_option_source(arg_val, ini_val, option_name):
+def _log_option_source(default_val, arg_val, ini_val, option_name):
     """It's useful to show the source of each option."""
-    if arg_val:
-        LOG.info("Using command line arg for %s", option_name)
-        return arg_val
-    elif ini_val:
-        LOG.info("Using ini file for %s", option_name)
-        return ini_val
+    # When default value is not defined, arg_val and ini_val is deterministic
+    if default_val is None:
+        if arg_val:
+            LOG.info("Using command line arg for %s", option_name)
+            return arg_val
+        elif ini_val:
+            LOG.info("Using ini file for %s", option_name)
+            return ini_val
+        else:
+            return None
+    # No value passed to commad line and default value is used
+    elif default_val == arg_val:
+        return ini_val if ini_val else arg_val
+    # Certainly a value is passed to commad line
     else:
-        return None
+        return arg_val
 
 
 def _running_under_virtualenv():
@@ -354,16 +362,19 @@ def main():
     if ini_options:
         # prefer command line, then ini file
         args.excluded_paths = _log_option_source(
+            parser.get_default('excluded_paths'),
             args.excluded_paths,
             ini_options.get('exclude'),
             'excluded paths')
 
         args.skips = _log_option_source(
+            parser.get_default('skips'),
             args.skips,
             ini_options.get('skips'),
             'skipped tests')
 
         args.tests = _log_option_source(
+            parser.get_default('tests'),
             args.tests,
             ini_options.get('tests'),
             'selected tests')
@@ -373,6 +384,7 @@ def main():
             ini_targets = ini_targets.split(',')
 
         args.targets = _log_option_source(
+            parser.get_default('targets'),
             args.targets,
             ini_targets,
             'selected targets')
@@ -380,71 +392,85 @@ def main():
         # TODO(tmcpeak): any other useful options to pass from .bandit?
 
         args.recursive = _log_option_source(
+            parser.get_default('recursive'),
             args.recursive,
             ini_options.get('recursive'),
             'recursive scan')
 
         args.agg_type = _log_option_source(
+            parser.get_default('agg_type'),
             args.agg_type,
             ini_options.get('aggregate'),
             'aggregate output type')
 
         args.context_lines = _log_option_source(
+            parser.get_default('context_lines'),
             args.context_lines,
             ini_options.get('number'),
             'max code lines output for issue')
 
         args.profile = _log_option_source(
+            parser.get_default('profile'),
             args.profile,
             ini_options.get('profile'),
             'profile')
 
         args.severity = _log_option_source(
+            parser.get_default('severity'),
             args.severity,
             ini_options.get('level'),
             'severity level')
 
         args.confidence = _log_option_source(
+            parser.get_default('confidence'),
             args.confidence,
             ini_options.get('confidence'),
             'confidence level')
 
         args.output_format = _log_option_source(
+            parser.get_default('output_format'),
             args.output_format,
             ini_options.get('format'),
             'output format')
 
         args.msg_template = _log_option_source(
+            parser.get_default('msg_template'),
             args.msg_template,
             ini_options.get('msg-template'),
             'output message template')
 
         args.output_file = _log_option_source(
+            parser.get_default('output_file'),
             args.output_file,
             ini_options.get('output'),
             'output file')
 
         args.verbose = _log_option_source(
+            parser.get_default('verbose'),
             args.verbose,
             ini_options.get('verbose'),
             'output extra information')
 
         args.debug = _log_option_source(
+            parser.get_default('debug'),
             args.debug,
             ini_options.get('debug'),
             'debug mode')
 
         args.quiet = _log_option_source(
+            parser.get_default('quiet'),
             args.quiet,
             ini_options.get('quiet'),
             'silent mode')
 
         args.ignore_nosec = _log_option_source(
+            parser.get_default('ignore_nosec'),
             args.ignore_nosec,
             ini_options.get('ignore-nosec'),
             'do not skip lines with # nosec')
 
         args.baseline = _log_option_source(
+            parser.get_default('baseline'),
             args.baseline,
             ini_options.get('baseline'),
             'path of a baseline report')


Test patch:
diff --git a/tests/unit/cli/test_main.py b/tests/unit/cli/test_main.py
index b191699..ecc0f1f 100644
--- a/tests/unit/cli/test_main.py
+++ b/tests/unit/cli/test_main.py
@@ -126,25 +126,55 @@ class BanditCLIMainTests(testtools.TestCase):
 
     def test_log_option_source_arg_val(self):
         # Test that the command argument value is returned when provided
+        # with None or a string default value
         arg_val = 'file'
         ini_val = 'vuln'
         option_name = 'aggregate'
-        self.assertEqual(arg_val, bandit._log_option_source(arg_val, ini_val,
-                         option_name))
+        for default_val in (None, 'default'):
+            self.assertEqual(arg_val, bandit._log_option_source(
+                                default_val,
+                                arg_val,
+                                ini_val,
+                                option_name
+                            ))
 
     def test_log_option_source_ini_value(self):
         # Test that the ini value is returned when no command argument is
         # provided
+        default_val = None
         ini_val = 'vuln'
         option_name = 'aggregate'
-        self.assertEqual(ini_val, bandit._log_option_source(None, ini_val,
-                         option_name))
+        self.assertEqual(ini_val, bandit._log_option_source(
+                            default_val,
+                            None,
+                            ini_val,
+                            option_name
+                        ))
+
+    def test_log_option_source_ini_val_with_str_default_and_no_arg_val(self):
+        # Test that the ini value is returned when no command argument is
+        # provided
+        default_val = "file"
+        arg_val = 'file'
+        ini_val = 'vuln'
+        option_name = 'aggregate'
+        self.assertEqual(ini_val, bandit._log_option_source(
+                            default_val,
+                            arg_val,
+                            ini_val,
+                            option_name
+                        ))
 
     def test_log_option_source_no_values(self):
         # Test that None is returned when no command argument or ini value are
         # provided
         option_name = 'aggregate'
-        self.assertIsNone(bandit._log_option_source(None, None, option_name))
+        self.assertIsNone(bandit._log_option_source(
+                            None,
+                            None,
+                            None,
+                            option_name
+                          ))
 
     @mock.patch('sys.argv', ['bandit', '-c', 'bandit.yaml', 'test'])
     def test_main_config_unopenable(self):