classAzureProvider.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import base64
  2. import json
  3. import random
  4. # import re
  5. import os
  6. # import shutil
  7. import sys
  8. # import tempfile
  9. from azure.devops.connection import Connection
  10. from msrest.authentication import BasicAuthentication
  11. from bdscan import classSCMProvider
  12. from bdscan import globals
  13. # from bdscan import utils
  14. # import azure
  15. # import azure.devops
  16. import requests
  17. from azure.devops.v6_0.git import GitPushRef, GitRefUpdate, GitPush, GitCommitRef, GitPullRequest, \
  18. GitPullRequestCommentThread, Comment, GitPullRequestSearchCriteria
  19. class AzureProvider(classSCMProvider.SCMProvider):
  20. def __init__(self):
  21. super().__init__()
  22. self.scm = 'azure'
  23. self.azure_base_url = ''
  24. self.azure_api_token = ''
  25. self.azure_pull_request_id = ''
  26. self.azure_project = ''
  27. self.azure_project_id = ''
  28. self.azure_repo_id = ''
  29. self.azure_build_source_branch = ''
  30. self.azure_credentials = None
  31. self.azure_connection = None
  32. self.azure_git_client = None
  33. def init(self):
  34. globals.printdebug(f"DEBUG: Initializing Azure DevOps SCM Provider")
  35. self.azure_base_url = os.getenv('SYSTEM_COLLECTIONURI')
  36. self.azure_api_token = os.getenv('SYSTEM_ACCESSTOKEN')
  37. if not self.azure_api_token:
  38. self.azure_api_token = os.getenv('AZURE_API_TOKEN')
  39. self.azure_pull_request_id = os.getenv('SYSTEM_PULLREQUEST_PULLREQUESTID')
  40. self.azure_project = os.getenv('SYSTEM_TEAMPROJECT')
  41. self.azure_project_id = os.getenv('SYSTEM_TEAMPROJECTID')
  42. self.azure_repo_id = os.getenv('BUILD_REPOSITORY_ID')
  43. self.azure_build_source_branch = os.getenv('BUILD_SOURCEBRANCH')
  44. globals.printdebug(f'DEBUG: Azure DevOps base_url={self.azure_base_url} api_token={self.azure_api_token} '
  45. f'pull_request_id={self.azure_pull_request_id} project={self.azure_project} '
  46. f'project_id={self.azure_project_id} repo_id={self.azure_repo_id}')
  47. if not self.azure_base_url or not self.azure_project or not self.azure_repo_id or not self.azure_api_token \
  48. or not self.azure_project_id:
  49. print(f'BD-Scan-Action: ERROR: Azure DevOps requires that SYSTEM_COLLECTIONURI, SYSTEM_TEAMPROJECT,'
  50. 'SYSTEM_TEAMPROJECTID, SYSTEM_ACCESSTOKEN or AZURE_API_TOKEN, and BUILD_REPOSITORY_ID be set.')
  51. sys.exit(1)
  52. if globals.args.comment_on_pr and not self.azure_pull_request_id:
  53. print(f'BD-Scan-Action: ERROR: Azure DevOps requires that SYSTEM_PULLREQUEST_PULLREQUESTID be set'
  54. 'when operating on a pull request')
  55. sys.exit(1)
  56. if globals.args.fix_pr and not self.azure_build_source_branch:
  57. print(f'BD-Scan-Action: ERROR: Azure DevOps requires that BUILD_SOURCEBRANCH be set'
  58. 'when operating on a pull request')
  59. sys.exit(1)
  60. self.azure_credentials = BasicAuthentication('', self.azure_api_token)
  61. self.azure_connection = Connection(base_url=self.azure_base_url, creds=self.azure_credentials)
  62. # Get a client (the "core" client provides access to projects, teams, etc)
  63. self.azure_git_client = self.azure_connection.clients.get_git_client()
  64. return True
  65. def azure_create_branch(self, from_ref, branch_name):
  66. authorization = str(base64.b64encode(bytes(':' + self.azure_api_token, 'ascii')), 'ascii')
  67. url = f"{self.azure_base_url}/_apis/git/repositories/{self.azure_repo_id}/refs?api-version=6.0"
  68. headers = {
  69. 'Authorization': 'Basic ' + authorization
  70. }
  71. body = [
  72. {
  73. 'name': f"refs/heads/{branch_name}",
  74. 'oldObjectId': '0000000000000000000000000000000000000000',
  75. 'newObjectId': from_ref
  76. }
  77. ]
  78. if globals.debug > 0:
  79. print("DEBUG: perform API Call to ADO: " + url + " : " + json.dumps(body, indent=4, sort_keys=True) + "\n")
  80. r = requests.post(url, json=body, headers=headers)
  81. if r.status_code == 200:
  82. if globals.debug > 0:
  83. print(f"DEBUG: Success creating branch")
  84. print(r.text)
  85. return True
  86. else:
  87. print(f"BD-Scan-Action: ERROR: Failure creating branch: Error {r.status_code}")
  88. print(r.text)
  89. return False
  90. def comp_commit_file_and_create_fixpr(self, comp, files_to_patch):
  91. if len(files_to_patch) == 0:
  92. print('BD-Scan-Action: WARN: Unable to apply fix patch - cannot determine containing package file')
  93. return False
  94. new_branch_seed = '%030x' % random.randrange(16 ** 30)
  95. new_branch_name = f"synopsys-enablement-{new_branch_seed}"
  96. globals.printdebug(f"DEBUG: Get commit for head of {self.azure_build_source_branch}'")
  97. commits = self.azure_git_client.get_commits(self.azure_repo_id, None)
  98. head_commit = commits[0]
  99. globals.printdebug(f"DEBUG: Head commit={head_commit.commit_id}")
  100. globals.printdebug(f"DEBUG: Creating new ref 'refs/heads/{new_branch_name}'")
  101. self.azure_create_branch(head_commit.commit_id, new_branch_name)
  102. gitRefUpdate = GitRefUpdate()
  103. gitRefUpdate.name = f"refs/heads/{new_branch_name}"
  104. gitRefUpdate.old_object_id = head_commit.commit_id
  105. gitPush = GitPush()
  106. gitPush.commits = []
  107. gitPush.ref_updates = [gitRefUpdate]
  108. # for file_to_patch in globals.files_to_patch:
  109. for pkgfile in files_to_patch:
  110. globals.printdebug(f"DEBUG: Upload file '{pkgfile}'")
  111. try:
  112. with open(files_to_patch[pkgfile], 'r') as fp:
  113. new_contents = fp.read()
  114. except Exception as exc:
  115. print(f"BD-Scan-Action: ERROR: Unable to open package file '{files_to_patch[pkgfile]}'"
  116. f" - {str(exc)}")
  117. return False
  118. gitCommitRef = GitCommitRef()
  119. gitCommitRef.comment = "Added Synopsys pipeline template"
  120. gitCommitRef.changes = [
  121. {
  122. 'changeType': 'edit',
  123. 'item': {
  124. 'path': pkgfile
  125. },
  126. 'newContent': {
  127. 'content': new_contents,
  128. 'contentType': 'rawText'
  129. }
  130. }
  131. ]
  132. gitPush.commits.append(gitCommitRef)
  133. # globals.printdebug(f"DEBUG: Update file '{pkgfile}' with commit message '{commit_message}'")
  134. # file = repo.update_file(pkgfile, commit_message, new_contents, orig_contents.sha, branch=new_branch_name)
  135. push = self.azure_git_client.create_push(gitPush, self.azure_repo_id)
  136. if not push:
  137. print(f"BD-Scan-Action: ERROR: Create push failed")
  138. sys.exit(1)
  139. pr_title = f"Black Duck: Upgrade {comp.name} to version {comp.goodupgrade} fix known security vulerabilities"
  140. pr_body = f"\n# Synopsys Black Duck Auto Pull Request\n" \
  141. f"Upgrade {comp.name} from version {comp.version} to " \
  142. f"{comp.goodupgrade} in order to fix security vulnerabilities:\n\n"
  143. gitPullRequest = GitPullRequest()
  144. gitPullRequest.source_ref_name = f"refs/heads/{new_branch_name}"
  145. gitPullRequest.target_ref_name = self.azure_build_source_branch
  146. gitPullRequest.title = pr_title
  147. gitPullRequest.description = pr_body
  148. pull = self.azure_git_client.create_pull_request(gitPullRequest, self.azure_repo_id)
  149. if not pull:
  150. print(f"BD-Scan-Action: ERROR: Create pull request failed")
  151. sys.exit(1)
  152. return True
  153. def comp_fix_pr(self, comp):
  154. ret = True
  155. globals.printdebug(f"DEBUG: Fix '{comp.name}' version '{comp.version}' in "
  156. f"file '{comp.projfiles}' using ns '{comp.ns}' to version "
  157. f"'{comp.goodupgrade}'")
  158. pull_request_title = f"Black Duck: Upgrade {comp.name} to version " \
  159. f"{comp.goodupgrade} to fix known security vulnerabilities"
  160. search_criteria = None # GitPullRequestSearchCriteria()
  161. pulls = self.azure_git_client.get_pull_requests(self.azure_repo_id, search_criteria)
  162. for pull in pulls:
  163. if pull_request_title in pull.title:
  164. globals.printdebug(f"DEBUG: Skipping pull request for {comp.name}' version "
  165. f"'{comp.goodupgrade} as it is already present")
  166. return
  167. files_to_patch = comp.do_upgrade_dependency()
  168. if len(files_to_patch) == 0:
  169. print('BD-Scan-Action: WARN: Unable to apply fix patch - cannot determine containing package file')
  170. return False
  171. if not self.comp_commit_file_and_create_fixpr(comp, files_to_patch):
  172. ret = False
  173. return ret
  174. def pr_comment(self, comment):
  175. pr_threads = self.azure_git_client.get_threads(self.azure_repo_id, self.azure_pull_request_id)
  176. existing_thread = None
  177. existing_comment = None
  178. for pr_thread in pr_threads:
  179. for pr_thread_comment in pr_thread.comments:
  180. if pr_thread_comment.content and globals.comment_on_pr_header in pr_thread_comment.content:
  181. existing_thread = pr_thread
  182. existing_comment = pr_thread_comment
  183. comments_markdown = f"# {globals.comment_on_pr_header}\n{comment}"
  184. if len(comments_markdown) > 65535:
  185. comments_markdown = comments_markdown[:65535]
  186. if existing_comment is not None:
  187. globals.printdebug(f"DEBUG: Update/edit existing comment for PR #{self.azure_pull_request_id}\n"
  188. f"{comments_markdown}")
  189. pr_thread_comment = Comment()
  190. pr_thread_comment.parent_comment_id = 0
  191. pr_thread_comment.content = comments_markdown
  192. pr_thread_comment.comment_type = 1
  193. retval = self.azure_git_client.update_comment(pr_thread_comment, self.azure_repo_id,
  194. self.azure_pull_request_id, existing_thread.id,
  195. existing_comment.id)
  196. globals.printdebug(f"DEBUG: Updated thread, retval={retval}")
  197. else:
  198. globals.printdebug(f"DEBUG: Create new thread for PR #{self.azure_pull_request_id}")
  199. pr_thread_comment = Comment()
  200. pr_thread_comment.parent_comment_id = 0
  201. pr_thread_comment.content = comments_markdown
  202. pr_thread_comment.comment_type = 1
  203. pr_thread = GitPullRequestCommentThread()
  204. pr_thread.comments = [pr_thread_comment]
  205. pr_thread.status = 1
  206. retval = self.azure_git_client.create_thread(pr_thread, self.azure_repo_id, self.azure_pull_request_id)
  207. globals.printdebug(f"DEBUG: Created thread, retval={retval}")
  208. return True
  209. def set_commit_status(self, is_ok):
  210. globals.printdebug(f"WARNING: Azure DevOps does not support set_commit_status")
  211. return
  212. def check_files_in_pull_request(self):
  213. globals.printdebug(f"WARNING: Azure DevOps does not support querying changed files, returning True")
  214. found = True
  215. return found
  216. def check_files_in_commit(self):
  217. globals.printdebug(f"WARNING: Azure DevOps does not support querying committed files, returning True")
  218. found = True
  219. return found