# # Copyright 2015 Hewlett-Packard Enterprise # # SPDX-License-Identifier: Apache-2.0 # ############################################################################# # Bandit Baseline is a tool that runs Bandit against a Git commit, and compares # the current commit findings to the parent commit findings. # To do this it checks out the parent commit, runs Bandit (with any provided # filters or profiles), checks out the current commit, runs Bandit, and then # reports on any new findings. # ############################################################################# """Bandit is a tool designed to find common security issues in Python code.""" import argparse import contextlib import logging import os import shutil import subprocess # nosec: B404 import sys import tempfile try: import git except ImportError: git = None bandit_args = sys.argv[1:] baseline_tmp_file = "_bandit_baseline_run.json_" current_commit = None default_output_format = "terminal" LOG = logging.getLogger(__name__) repo = None report_basename = "bandit_baseline_result" valid_baseline_formats = ["txt", "html", "json"] """baseline.py""" def main(): """Execute Bandit.""" # our cleanup function needs this and can't be passed arguments global current_commit global repo parent_commit = None output_format = None repo = None report_fname = None init_logger() output_format, repo, report_fname = initialize() if not repo: sys.exit(2) # #################### Find current and parent commits #################### try: commit = repo.commit() current_commit = commit.hexsha LOG.info("Got current commit: [%s]", commit.name_rev) commit = commit.parents[0] parent_commit = commit.hexsha LOG.info("Got parent commit: [%s]", commit.name_rev) except git.GitCommandError: LOG.error("Unable to get current or parent commit") sys.exit(2) except IndexError: LOG.error("Parent commit not available") sys.exit(2) # #################### Run Bandit against both commits #################### output_type = ( ["-f", "txt"] if output_format == default_output_format else ["-o", report_fname] ) with baseline_setup() as t: bandit_tmpfile = f"{t}/{baseline_tmp_file}" steps = [ { "message": "Getting Bandit baseline results", "commit": parent_commit, "args": bandit_args + ["-f", "json", "-o", bandit_tmpfile], }, { "message": "Comparing Bandit results to baseline", "commit": current_commit, "args": bandit_args + ["-b", bandit_tmpfile] + output_type, }, ] return_code = None for step in steps: repo.head.reset(commit=step["commit"], working_tree=True) LOG.info(step["message"]) bandit_command = ["bandit"] + step["args"] try: output = subprocess.check_output(bandit_command) # nosec: B603 except subprocess.CalledProcessError as e: output = e.output return_code = e.returncode else: return_code = 0 output = output.decode("utf-8") # subprocess returns bytes if return_code not in [0, 1]: LOG.error( "Error running command: %s\nOutput: %s\n", bandit_args, output, ) # #################### Output and exit #################################### # print output or display message about written report if output_format == default_output_format: print(output) else: LOG.info("Successfully wrote %s", report_fname) # exit with the code the last Bandit run returned sys.exit(return_code) # #################### Clean up before exit ################################### @contextlib.contextmanager def baseline_setup(): """Baseline setup by creating temp folder and resetting repo.""" d = tempfile.mkdtemp() yield d shutil.rmtree(d, True) if repo: repo.head.reset(commit=current_commit, working_tree=True) # #################### Setup logging ########################################## def init_logger(): """Init logger.""" LOG.handlers = [] log_level = logging.INFO log_format_string = "[%(levelname)7s ] %(message)s" logging.captureWarnings(True) LOG.setLevel(log_level) handler = logging.StreamHandler(sys.stdout) handler.setFormatter(logging.Formatter(log_format_string)) LOG.addHandler(handler) # #################### Perform initialization and validate assumptions ######## def initialize(): """Initialize arguments and output formats.""" valid = True # #################### Parse Args ######################################### parser = argparse.ArgumentParser( description="Bandit Baseline - Generates Bandit results compared to " "a baseline", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="Additional Bandit arguments such as severity filtering (-ll) " "can be added and will be passed to Bandit.", ) if sys.version_info >= (3, 14): parser.suggest_on_error = True parser.color = False parser.add_argument( "targets", metavar="targets", type=str, nargs="+", help="source file(s) or directory(s) to be tested", ) parser.add_argument( "-f", dest="output_format", action="store", default="terminal", help="specify output format", choices=valid_baseline_formats, ) args, _ = parser.parse_known_args() # #################### Setup Output ####################################### # set the output format, or use a default if not provided output_format = ( args.output_format if args.output_format else default_output_format ) if output_format == default_output_format: LOG.info("No output format specified, using %s", default_output_format) # set the report name based on the output format report_fname = f"{report_basename}.{output_format}" # #################### Check Requirements ################################# if git is None: LOG.error("Git not available, reinstall with baseline extra") valid = False return (None, None, None) try: repo = git.Repo(os.getcwd()) except git.exc.InvalidGitRepositoryError: LOG.error("Bandit baseline must be called from a git project root") valid = False except git.exc.GitCommandNotFound: LOG.error("Git command not found") valid = False else: if repo.is_dirty(): LOG.error( "Current working directory is dirty and must be " "resolved" ) valid = False # if output format is specified, we need to be able to write the report if output_format != default_output_format and os.path.exists(report_fname): LOG.error("File %s already exists, aborting", report_fname) valid = False # Bandit needs to be able to create this temp file if os.path.exists(baseline_tmp_file): LOG.error( "Temporary file %s needs to be removed prior to running", baseline_tmp_file, ) valid = False # we must validate -o is not provided, as it will mess up Bandit baseline if "-o" in bandit_args: LOG.error("Bandit baseline must not be called with the -o option") valid = False return (output_format, repo, report_fname) if valid else (None, None, None) if __name__ == "__main__": main()