|  | 
|  | 1 | +''' | 
|  | 2 | +Created on Jan 22, 2024 | 
|  | 3 | +
 | 
|  | 4 | +@author: pedapati | 
|  | 5 | +
 | 
|  | 6 | +Update component version info for BOM Components with Unknown Versions based on matched filename in a given project version | 
|  | 7 | +
 | 
|  | 8 | +''' | 
|  | 9 | + | 
|  | 10 | +from blackduck import Client | 
|  | 11 | + | 
|  | 12 | +import requests | 
|  | 13 | +import argparse | 
|  | 14 | +import json | 
|  | 15 | +import logging | 
|  | 16 | +import sys | 
|  | 17 | +import time | 
|  | 18 | +from pprint import pprint | 
|  | 19 | + | 
|  | 20 | +import urllib3 | 
|  | 21 | +import urllib.parse | 
|  | 22 | + | 
|  | 23 | +NAME = 'update_component_version.py' | 
|  | 24 | +VERSION = '2024-01-22' | 
|  | 25 | + | 
|  | 26 | +print(f'{NAME} ({VERSION}). Copyright (c) 2023 Synopsys, Inc.') | 
|  | 27 | + | 
|  | 28 | + | 
|  | 29 | +logging.basicConfig( | 
|  | 30 | + level=logging.DEBUG, | 
|  | 31 | + format="[%(asctime)s] {%(module)s:%(lineno)d} %(levelname)s - %(message)s" | 
|  | 32 | +) | 
|  | 33 | + | 
|  | 34 | +parser = argparse.ArgumentParser(sys.argv[0]) | 
|  | 35 | +parser.add_argument("-u", "--bd-url", help="Hub server URL e.g. https://your.blackduck.url") | 
|  | 36 | +parser.add_argument("-t", "--token-file", help="File name of a file containing access token") | 
|  | 37 | +parser.add_argument("-nv", '--no-verify', dest='verify', action='store_false', help="disable TLS certificate verification") | 
|  | 38 | +parser.add_argument("project_name") | 
|  | 39 | +parser.add_argument("version_name") | 
|  | 40 | + | 
|  | 41 | +args = parser.parse_args() | 
|  | 42 | + | 
|  | 43 | +logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', stream=sys.stderr, level=logging.DEBUG) | 
|  | 44 | +logging.getLogger("requests").setLevel(logging.WARNING) | 
|  | 45 | +logging.getLogger("urllib3").setLevel(logging.WARNING) | 
|  | 46 | +logging.getLogger("blackduck").setLevel(logging.WARNING) | 
|  | 47 | + | 
|  | 48 | +with open(args.token_file, 'r') as tf: | 
|  | 49 | +access_token = tf.readline().strip() | 
|  | 50 | + | 
|  | 51 | +bd = Client(base_url=args.bd_url, token=access_token, verify=args.verify) | 
|  | 52 | + | 
|  | 53 | +params = { | 
|  | 54 | + 'q': [f"name:{args.project_name}"] | 
|  | 55 | +} | 
|  | 56 | +projects = [p for p in bd.get_resource('projects', params=params) if p['name'] == args.project_name] | 
|  | 57 | +assert len(projects) == 1, f"There should be one, and only one project named {args.project_name}. We found {len(projects)}" | 
|  | 58 | +project = projects[0] | 
|  | 59 | +project_id = project["_meta"]["href"].split("/")[-1] | 
|  | 60 | +print("Project ID: " + project_id) | 
|  | 61 | + | 
|  | 62 | +params = { | 
|  | 63 | + 'q': [f"versionName:{args.version_name}"] | 
|  | 64 | +} | 
|  | 65 | +versions = [v for v in bd.get_resource('versions', project, params=params) if v['versionName'] == args.version_name] | 
|  | 66 | +assert len(versions) == 1, f"There should be one, and only one version named {args.version_name}. We found {len(versions)}" | 
|  | 67 | +version = versions[0] | 
|  | 68 | +version_id = version["_meta"]["href"].split("/")[-1] | 
|  | 69 | +print("Version ID: " + version_id) | 
|  | 70 | + | 
|  | 71 | +logging.debug(f"Found {project['name']}:{version['versionName']}") | 
|  | 72 | + | 
|  | 73 | +def update_bom_unknown_versions(bd, project_id, version_id): | 
|  | 74 | +limit = 1000 | 
|  | 75 | +offset = 0 | 
|  | 76 | +paginated_url = f"{bd.base_url}/api/projects/{project_id}/versions/{version_id}/components?limit={limit}&offset={offset}&filter=unknownVersion:true" | 
|  | 77 | +print("Looking for BOM Components with Unknown Versions: " + paginated_url) | 
|  | 78 | +print() | 
|  | 79 | +components_json = bd.session.get(paginated_url).json() | 
|  | 80 | +total = str(components_json["totalCount"]) | 
|  | 81 | +print("Found " + total + " components with unknown versions") | 
|  | 82 | +print() | 
|  | 83 | +for component in components_json["items"]: | 
|  | 84 | +comp_name =component["componentName"] | 
|  | 85 | +print("Processing Component: " + comp_name) | 
|  | 86 | +comp_url = component["component"] | 
|  | 87 | +comp_bom_url = component["_meta"]["href"] | 
|  | 88 | +matched_files_url = component["_meta"]["href"] + "/matched-files" | 
|  | 89 | +matched_file_json = bd.session.get(matched_files_url).json() | 
|  | 90 | +archivecontext = matched_file_json["items"][0]["filePath"]["archiveContext"] | 
|  | 91 | +filename = matched_file_json["items"][0]["filePath"]["fileName"] | 
|  | 92 | +## Extract Component Name and Version from archivecontext to do a KB lookup | 
|  | 93 | +archive_strip = archivecontext.strip("/,!") | 
|  | 94 | +archive_partition = archive_strip.rpartition("-") | 
|  | 95 | +archive_final_list = archive_partition[0].rpartition("-") | 
|  | 96 | +kb_file_lookup_name = archive_final_list[0] | 
|  | 97 | +kb_comp_lookup_version = archive_final_list[2] | 
|  | 98 | +print("Processing Component Version: " + kb_comp_lookup_version) | 
|  | 99 | +## KB Lookup via Component Name | 
|  | 100 | +components_url = bd.base_url + "/api/components/autocomplete" | 
|  | 101 | +query = { "q": comp_name, | 
|  | 102 | + "filter": "componentType:kb_component" | 
|  | 103 | +	} | 
|  | 104 | +url = f"{components_url}?{urllib.parse.urlencode(query)}" | 
|  | 105 | +headers = {'Accept': '*/*'} | 
|  | 106 | +name_match = bd.session.get(url, headers=headers).json() | 
|  | 107 | +# Filtering results for exact name match  | 
|  | 108 | +exact_name_match = [x for x in name_match['items'] if x['name']==comp_name] | 
|  | 109 | +if len(exact_name_match) == 0 : | 
|  | 110 | +logging.debug(f"Component {comp_name} is not found in the KB") | 
|  | 111 | +return | 
|  | 112 | +else: | 
|  | 113 | +logging.debug(f"Component {comp_name} is found in the KB") | 
|  | 114 | +if kb_comp_lookup_version: | 
|  | 115 | +first_match_successful = False | 
|  | 116 | +# second_match_successful = False | 
|  | 117 | +for match in exact_name_match: # handling OSS components that share same name | 
|  | 118 | +url = match['_meta']['href']+'/versions?q=versionName:' + kb_comp_lookup_version | 
|  | 119 | +headers = {'Accept': 'application/vnd.blackducksoftware.summary-1+json'} | 
|  | 120 | +# Producing version matches | 
|  | 121 | +version_match = bd.session.get(url, headers=headers).json() | 
|  | 122 | +if version_match['totalCount'] > 0: | 
|  | 123 | +print(version_match["items"][0]["versionName"]) | 
|  | 124 | +print("Found version: " + kb_comp_lookup_version + " in the KB for component " + comp_name) | 
|  | 125 | +print("Updating component version for " + comp_name + " to " + kb_comp_lookup_version ) | 
|  | 126 | +# component_url = version_match[] | 
|  | 127 | +# print(version_match) | 
|  | 128 | +component_version_url = version_match['items'][0]['_meta']['href'] | 
|  | 129 | +component_url = component_version_url[:component_version_url.index("versions")-1] | 
|  | 130 | +# print(component_url) | 
|  | 131 | +post_data = {"component": component_url, "componentVersion": component_version_url} | 
|  | 132 | +headers = {'Accept': 'application/vnd.blackducksoftware.bill-of-materials-6+json', 'Content-Type': 'application/vnd.blackducksoftware.bill-of-materials-6+json'} | 
|  | 133 | +response = bd.session.put(comp_bom_url, headers=headers, data=json.dumps(post_data)) | 
|  | 134 | +# print(response) | 
|  | 135 | +if response.status_code == 200: | 
|  | 136 | +message = f"{response}" | 
|  | 137 | +print("Successfully updated " + comp_name + " with version " + kb_comp_lookup_version) | 
|  | 138 | +else: | 
|  | 139 | +message = f"{response.json()}" | 
|  | 140 | +logging.debug(f"Updating BOM component {comp_name} {kb_comp_lookup_version} failed with: {message}") | 
|  | 141 | +first_match_successful = True | 
|  | 142 | +print("### Proceeding to next component") | 
|  | 143 | +print() | 
|  | 144 | +break | 
|  | 145 | +else: | 
|  | 146 | +print("No matching version " + kb_comp_lookup_version + " found for " + comp_name) | 
|  | 147 | +if not first_match_successful: | 
|  | 148 | +## Trying to locate component name using source archive name | 
|  | 149 | +print("Proceeding to KB lookup via matched file name: " + kb_file_lookup_name) | 
|  | 150 | +components_url = bd.base_url + "/api/components/autocomplete" | 
|  | 151 | +query = { "q": kb_file_lookup_name, | 
|  | 152 | +"filter": "componentType:kb_component" | 
|  | 153 | +} | 
|  | 154 | +url = f"{components_url}?{urllib.parse.urlencode(query)}" | 
|  | 155 | +headers = {'Accept': '*/*'} | 
|  | 156 | +name_match = bd.session.get(url, headers=headers).json() | 
|  | 157 | +# Filtering results for exact name match  | 
|  | 158 | +exact_name_match = [x for x in name_match['items'] if x['name']==kb_file_lookup_name] | 
|  | 159 | +if len(exact_name_match) == 0 : | 
|  | 160 | +logging.debug(f"File Match KB Component {kb_file_lookup_name} is not found in the KB") | 
|  | 161 | +print("### Proceeding to next component") | 
|  | 162 | +print() | 
|  | 163 | +continue | 
|  | 164 | +else: | 
|  | 165 | +logging.debug(f"File Match KB Component {kb_file_lookup_name} is found in the KB") | 
|  | 166 | +if kb_comp_lookup_version: | 
|  | 167 | +for match in exact_name_match: # handling OSS components that share same name | 
|  | 168 | +url = match['_meta']['href']+'/versions?q=versionName:' + kb_comp_lookup_version | 
|  | 169 | +headers = {'Accept': 'application/vnd.blackducksoftware.summary-1+json'} | 
|  | 170 | +# Producing version matches | 
|  | 171 | +version_match = bd.session.get(url, headers=headers).json() | 
|  | 172 | +if version_match['totalCount'] > 0: | 
|  | 173 | +print(version_match["items"][0]["versionName"]) | 
|  | 174 | +print("Found version: " + kb_comp_lookup_version + " in the KB for component " + kb_file_lookup_name) | 
|  | 175 | +print("Updating component version for " + kb_file_lookup_name + " to " + kb_comp_lookup_version ) | 
|  | 176 | +# component_url = version_match[] | 
|  | 177 | +# print(version_match) | 
|  | 178 | +component_version_url = version_match['items'][0]['_meta']['href'] | 
|  | 179 | +component_url = component_version_url[:component_version_url.index("versions")-1] | 
|  | 180 | +# print(component_url) | 
|  | 181 | +post_data = {"component": component_url, "componentVersion": component_version_url} | 
|  | 182 | +headers = {'Accept': 'application/vnd.blackducksoftware.bill-of-materials-6+json', 'Content-Type': 'application/vnd.blackducksoftware.bill-of-materials-6+json'} | 
|  | 183 | +response = bd.session.put(comp_bom_url, headers=headers, data=json.dumps(post_data)) | 
|  | 184 | +# print(response) | 
|  | 185 | +if response.status_code == 200: | 
|  | 186 | +message = f"{response}" | 
|  | 187 | +print("Successfully updated " + kb_file_lookup_name + " with version " + kb_comp_lookup_version) | 
|  | 188 | +# second_match_successful = True | 
|  | 189 | +else: | 
|  | 190 | +message = f"{response.json()}" | 
|  | 191 | +logging.debug(f"Updating BOM component {kb_file_lookup_name} {kb_comp_lookup_version} failed with: {message}") | 
|  | 192 | +print("### Proceeding to next component") | 
|  | 193 | +print() | 
|  | 194 | +break | 
|  | 195 | +else: | 
|  | 196 | +print("No matching version " + kb_comp_lookup_version + " found for " + kb_file_lookup_name) | 
|  | 197 | +print("### Proceeding to next component") | 
|  | 198 | +print() | 
|  | 199 | + | 
|  | 200 | + | 
|  | 201 | + | 
|  | 202 | +bom = update_bom_unknown_versions(bd, project_id, version_id) | 
|  | 203 | + | 
|  | 204 | + | 
|  | 205 | + | 
0 commit comments