# -*- coding:utf-8 -*- # # Copyright 2015 Hewlett-Packard Enterprise # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # ############################################################################# # 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. # ############################################################################# 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'] def main(): # 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 = "{}/{}".format(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(): 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(): 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(): 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, unknown = 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 = "{}.{}".format(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()