| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- #
- # 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
- import sys
- import tempfile
- import git
- 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)
- 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.",
- )
- 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 #################################
- 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()
|