baseline.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. #
  2. # Copyright 2015 Hewlett-Packard Enterprise
  3. #
  4. # SPDX-License-Identifier: Apache-2.0
  5. # #############################################################################
  6. # Bandit Baseline is a tool that runs Bandit against a Git commit, and compares
  7. # the current commit findings to the parent commit findings.
  8. # To do this it checks out the parent commit, runs Bandit (with any provided
  9. # filters or profiles), checks out the current commit, runs Bandit, and then
  10. # reports on any new findings.
  11. # #############################################################################
  12. """Bandit is a tool designed to find common security issues in Python code."""
  13. import argparse
  14. import contextlib
  15. import logging
  16. import os
  17. import shutil
  18. import subprocess
  19. import sys
  20. import tempfile
  21. import git
  22. bandit_args = sys.argv[1:]
  23. baseline_tmp_file = "_bandit_baseline_run.json_"
  24. current_commit = None
  25. default_output_format = "terminal"
  26. LOG = logging.getLogger(__name__)
  27. repo = None
  28. report_basename = "bandit_baseline_result"
  29. valid_baseline_formats = ["txt", "html", "json"]
  30. """baseline.py"""
  31. def main():
  32. """Execute Bandit."""
  33. # our cleanup function needs this and can't be passed arguments
  34. global current_commit
  35. global repo
  36. parent_commit = None
  37. output_format = None
  38. repo = None
  39. report_fname = None
  40. init_logger()
  41. output_format, repo, report_fname = initialize()
  42. if not repo:
  43. sys.exit(2)
  44. # #################### Find current and parent commits ####################
  45. try:
  46. commit = repo.commit()
  47. current_commit = commit.hexsha
  48. LOG.info("Got current commit: [%s]", commit.name_rev)
  49. commit = commit.parents[0]
  50. parent_commit = commit.hexsha
  51. LOG.info("Got parent commit: [%s]", commit.name_rev)
  52. except git.GitCommandError:
  53. LOG.error("Unable to get current or parent commit")
  54. sys.exit(2)
  55. except IndexError:
  56. LOG.error("Parent commit not available")
  57. sys.exit(2)
  58. # #################### Run Bandit against both commits ####################
  59. output_type = (
  60. ["-f", "txt"]
  61. if output_format == default_output_format
  62. else ["-o", report_fname]
  63. )
  64. with baseline_setup() as t:
  65. bandit_tmpfile = f"{t}/{baseline_tmp_file}"
  66. steps = [
  67. {
  68. "message": "Getting Bandit baseline results",
  69. "commit": parent_commit,
  70. "args": bandit_args + ["-f", "json", "-o", bandit_tmpfile],
  71. },
  72. {
  73. "message": "Comparing Bandit results to baseline",
  74. "commit": current_commit,
  75. "args": bandit_args + ["-b", bandit_tmpfile] + output_type,
  76. },
  77. ]
  78. return_code = None
  79. for step in steps:
  80. repo.head.reset(commit=step["commit"], working_tree=True)
  81. LOG.info(step["message"])
  82. bandit_command = ["bandit"] + step["args"]
  83. try:
  84. output = subprocess.check_output(bandit_command)
  85. except subprocess.CalledProcessError as e:
  86. output = e.output
  87. return_code = e.returncode
  88. else:
  89. return_code = 0
  90. output = output.decode("utf-8") # subprocess returns bytes
  91. if return_code not in [0, 1]:
  92. LOG.error(
  93. "Error running command: %s\nOutput: %s\n",
  94. bandit_args,
  95. output,
  96. )
  97. # #################### Output and exit ####################################
  98. # print output or display message about written report
  99. if output_format == default_output_format:
  100. print(output)
  101. else:
  102. LOG.info("Successfully wrote %s", report_fname)
  103. # exit with the code the last Bandit run returned
  104. sys.exit(return_code)
  105. # #################### Clean up before exit ###################################
  106. @contextlib.contextmanager
  107. def baseline_setup():
  108. """Baseline setup by creating temp folder and resetting repo."""
  109. d = tempfile.mkdtemp()
  110. yield d
  111. shutil.rmtree(d, True)
  112. if repo:
  113. repo.head.reset(commit=current_commit, working_tree=True)
  114. # #################### Setup logging ##########################################
  115. def init_logger():
  116. """Init logger."""
  117. LOG.handlers = []
  118. log_level = logging.INFO
  119. log_format_string = "[%(levelname)7s ] %(message)s"
  120. logging.captureWarnings(True)
  121. LOG.setLevel(log_level)
  122. handler = logging.StreamHandler(sys.stdout)
  123. handler.setFormatter(logging.Formatter(log_format_string))
  124. LOG.addHandler(handler)
  125. # #################### Perform initialization and validate assumptions ########
  126. def initialize():
  127. """Initialize arguments and output formats."""
  128. valid = True
  129. # #################### Parse Args #########################################
  130. parser = argparse.ArgumentParser(
  131. description="Bandit Baseline - Generates Bandit results compared to "
  132. "a baseline",
  133. formatter_class=argparse.RawDescriptionHelpFormatter,
  134. epilog="Additional Bandit arguments such as severity filtering (-ll) "
  135. "can be added and will be passed to Bandit.",
  136. )
  137. parser.add_argument(
  138. "targets",
  139. metavar="targets",
  140. type=str,
  141. nargs="+",
  142. help="source file(s) or directory(s) to be tested",
  143. )
  144. parser.add_argument(
  145. "-f",
  146. dest="output_format",
  147. action="store",
  148. default="terminal",
  149. help="specify output format",
  150. choices=valid_baseline_formats,
  151. )
  152. args, _ = parser.parse_known_args()
  153. # #################### Setup Output #######################################
  154. # set the output format, or use a default if not provided
  155. output_format = (
  156. args.output_format if args.output_format else default_output_format
  157. )
  158. if output_format == default_output_format:
  159. LOG.info("No output format specified, using %s", default_output_format)
  160. # set the report name based on the output format
  161. report_fname = f"{report_basename}.{output_format}"
  162. # #################### Check Requirements #################################
  163. try:
  164. repo = git.Repo(os.getcwd())
  165. except git.exc.InvalidGitRepositoryError:
  166. LOG.error("Bandit baseline must be called from a git project root")
  167. valid = False
  168. except git.exc.GitCommandNotFound:
  169. LOG.error("Git command not found")
  170. valid = False
  171. else:
  172. if repo.is_dirty():
  173. LOG.error(
  174. "Current working directory is dirty and must be " "resolved"
  175. )
  176. valid = False
  177. # if output format is specified, we need to be able to write the report
  178. if output_format != default_output_format and os.path.exists(report_fname):
  179. LOG.error("File %s already exists, aborting", report_fname)
  180. valid = False
  181. # Bandit needs to be able to create this temp file
  182. if os.path.exists(baseline_tmp_file):
  183. LOG.error(
  184. "Temporary file %s needs to be removed prior to running",
  185. baseline_tmp_file,
  186. )
  187. valid = False
  188. # we must validate -o is not provided, as it will mess up Bandit baseline
  189. if "-o" in bandit_args:
  190. LOG.error("Bandit baseline must not be called with the -o option")
  191. valid = False
  192. return (output_format, repo, report_fname) if valid else (None, None, None)
  193. if __name__ == "__main__":
  194. main()