3030import argparse
3131import logging
3232import sys
33+ import io
3334import os
3435import re
3536import time
4041import ijson
4142from blackduck import Client
4243from zipfile import ZipFile
44+ from pprint import pprint
4345
4446program_description = \
4547'''Generate version detail reports (source and components) and consolidate information on source matches, with license
46- and component matched. Removes matches found underneith other matched components in the source tree (configurable).
48+ and component matched. Removes matches found underneath other matched components in the source tree (configurable).
4749
4850This script assumes a project version exists and has scans associated with it (i.e. the project is not scanned as part of this process).
4951
@@ -83,39 +85,21 @@ def log_config(debug):
8385 logging .getLogger ("urllib3" ).setLevel (logging .WARNING )
8486 logging .getLogger ("blackduck" ).setLevel (logging .WARNING )
8587
86- def parse_parameter ():
87- parser = argparse .ArgumentParser (description = program_description , formatter_class = argparse .RawTextHelpFormatter )
88- parser .add_argument ("project" ,
89- metavar = "project" ,
90- type = str ,
91- help = "Provide the BlackDuck project name." )
92- parser .add_argument ("version" ,
93- metavar = "version" ,
94- type = str ,
95- help = "Provide the BlackDuck project version name." )
96- parser .add_argument ("-kh" ,
97- "--keep_hierarchy" ,
98- action = 'store_true' ,
99- help = "Set to keep all entries in the sources report. Will not remove components found under others." )
100- parser .add_argument ("-rr" ,
101- "--report_retries" ,
102- metavar = "" ,
103- type = int ,
104- default = RETRY_LIMIT ,
105- help = "Retries for receiving the generated BlackDuck report. Generating copyright report tends to take longer minutes." )
106- parser .add_argument ("-t" ,
107- "--timeout" ,
108- metavar = "" ,
109- type = int ,
110- default = 15 ,
111- help = "Timeout for REST-API. Some API may take longer than the default 15 seconds" )
112- parser .add_argument ("-r" ,
113- "--retries" ,
114- metavar = "" ,
115- type = int ,
116- default = 3 ,
117- help = "Retries for REST-API. Some API may need more retries than the default 3 times" )
118- return parser .parse_args ()
88+ def find_project_by_name (bd , project_name ):
89+ params = {
90+ 'q' : [f"name:{ project_name } " ]
91+ }
92+ projects = [p for p in bd .get_resource ('projects' , params = params ) if p ['name' ] == project_name ]
93+ assert len (projects ) == 1 , f"Project { project_name } not found."
94+ return projects [0 ]
95+
96+ def find_project_version_by_name (bd , project , version_name ):
97+ params = {
98+ 'q' : [f"versionName:{ version_name } " ]
99+ }
100+ versions = [v for v in bd .get_resource ('versions' , project , params = params ) if v ['versionName' ] == version_name ]
101+ assert len (versions ) == 1 , f"Project version { version_name } for project { project ['name' ]} not found"
102+ return versions [0 ]
119103
120104def get_bd_project_data (hub_client , project_name , version_name ):
121105 """ Get and return project ID, version ID. """
@@ -136,55 +120,53 @@ def get_bd_project_data(hub_client, project_name, version_name):
136120
137121 return project_id , version_id
138122
139- def report_create (hub_client , url , body ):
140- """
141- Request BlackDuck to create report. Requested report is included in the request payload.
142- """
143- res = hub_client .session .post (url , headers = {'Content-Type' : BLACKDUCK_REPORT_MEDIATYPE }, json = body )
144- if res .status_code != 201 :
145- sys .exit (f"BlackDuck report creation failed with status { res .status_code } !" )
146- return res .headers ['Location' ] # return report_url
147-
148- def report_download (hub_client , report_url , project_id , version_id , retries ):
149- """
150- Download the generated report after the report completion. We will retry until reaching the retry-limit.
151- """
152- while retries :
153- res = hub_client .session .get (report_url , headers = {'Accept' : BLACKDUCK_REPORT_MEDIATYPE })
154- if res .status_code == 200 and (json .loads (res .content ))['status' ] == "COMPLETED" :
155- report_id = report_url .split ("reports/" , 1 )[1 ]
156- download_url = (((blackduck_report_download_api .replace ("{projectId}" , project_id ))
157- .replace ("{projectVersionId}" , version_id ))
158- .replace ("{reportId}" , report_id ))
159- res = hub_client .session .get (download_url ,
160- headers = {'Content-Type' : 'application/zip' , 'Accept' :'application/zip' })
161- if res .status_code != 200 :
162- sys .exit (f"BlackDuck report download failed with status { res .status_code } for { download_url } !" )
163- return res .content
164- elif res .status_code != 200 :
165- sys .exit (f"BlackDuck report creation not completed successfully with status { res .status_code } " )
166- else :
167- retries -= 1
168- logging .info (f"Waiting for the report generation for { report_url } with the remaining retries { retries } times." )
169- time .sleep (RETRY_TIMER )
170- sys .exit (f"BlackDuck report for { report_url } was not generated after retries { RETRY_TIMER } sec * { retries } times!" )
171-
172- def get_version_detail_report (hub_client , project_id , version_id , retries ):
173- """ Create and get BOM component and BOM source file report in json. """
174- create_version_url = blackduck_create_version_report_api .replace ("{projectVersionId}" , version_id )
175- body = {
123+ def create_version_details_report (bd , version ):
124+ version_reports_url = bd .list_resources (version ).get ('versionReport' )
125+ post_data = {
176126 'reportFormat' : 'JSON' ,
177127 'locale' : 'en_US' ,
178- 'versionId' : f' { version_id } ' ,
128+ 'versionId' : version [ '_meta' ][ 'href' ]. split ( "/" )[ - 1 ] ,
179129 'categories' : [ 'COMPONENTS' , 'FILES' ] # Generating "project version" report including components and files
180130 }
181- report_url = report_create (hub_client , create_version_url , body )
182- # Zipped report content is received and write the content to a local zip file
183- content = report_download (hub_client , report_url , project_id , version_id , retries )
184- output_file = blackduck_version_report_filename .replace ("{projectVersionId}" , version_id )
185- with open (output_file , "wb" ) as f :
186- f .write (content )
187- return output_file
131+
132+ bd .session .headers ["Content-Type" ] = "application/vnd.blackducksoftware.report-4+json"
133+ r = bd .session .post (version_reports_url , json = post_data )
134+ if (r .status_code == 403 ):
135+ logging .debug ("Authorization Error - Please ensure the token you are using has write permissions!" )
136+ r .raise_for_status ()
137+ pprint (r .headers )
138+ location = r .headers .get ('Location' )
139+ assert location , "Hmm, this does not make sense. If we successfully created a report then there needs to be a location where we can get it from"
140+ return location
141+
142+ def download_report (bd , location , retries ):
143+ report_id = location .split ("/" )[- 1 ]
144+ print (location )
145+ url_data = location .split ('/' )
146+ url_data .pop (4 )
147+ url_data .pop (4 )
148+ download_link = '/' .join (url_data )
149+ print (download_link )
150+ if retries :
151+ logging .debug (f"Retrieving generated report from { location } " )
152+ response = bd .session .get (location )
153+ report_status = response .json ().get ('status' , 'Not Ready' )
154+ if response .status_code == 200 and report_status == 'COMPLETED' :
155+ response = bd .session .get (download_link , headers = {'Content-Type' : 'application/zip' , 'Accept' :'application/zip' })
156+ pprint (response )
157+ if response .status_code == 200 :
158+ return response .content
159+ else :
160+ logging .error ("Ruh-roh, not sure what happened here" )
161+ return None
162+ else :
163+ logging .debug (f"Report status request { response .status_code } { report_status } ,waiting { retries } seconds then retrying..." )
164+ time .sleep (60 )
165+ retries -= 1
166+ return download_report (bd , location , retries )
167+ else :
168+ logging .debug (f"Failed to retrieve report { report_id } after multiple retries" )
169+ return None
188170
189171def get_blackduck_version (hub_client ):
190172 url = hub_client .base_url + BLACKDUCK_VERSION_API
@@ -194,68 +176,46 @@ def get_blackduck_version(hub_client):
194176 else :
195177 sys .exit (f"Get BlackDuck version failed with status { res .status_code } " )
196178
197- def generate_file_report (hub_client , project_id , version_id , keep_hierarchy , retries ):
198- """
199- Create a consolidated file report from BlackDuck project version source and components reports.
200- Remarks:
201- """
202- if not os .path .exists (REPORT_DIR ):
203- os .makedirs (REPORT_DIR )
204-
205- # Report body - Component BOM, file BOM with Discoveries data
206- version_report_zip = get_version_detail_report (hub_client , project_id , version_id , retries )
207- with ZipFile (f"./{ version_report_zip } " , "r" ) as vzf :
208- vzf .extractall ()
209- for i , unzipped_version in enumerate (vzf .namelist ()):
210- if re .search (r"\bversion.+json\b" , unzipped_version ) is not None :
211- break
212- if i + 1 >= len (vzf .namelist ()):
213- sys .exit (f"Version detail file not found in the downloaded report: { version_report_zip } !" )
214-
215- # Report body - Component BOM report
216- with open (f"./{ unzipped_version } " , "r" ) as uvf :
217- for i , comp_bom in enumerate (ijson .items (uvf , 'aggregateBomViewEntries.item' )):
218- logging .info (f"{ comp_bom ['componentName' ]} " )
219- logging .info (f"Number of the reported components { i + 1 } " )
220-
179+ def parse_command_args ():
180+ parser = argparse .ArgumentParser (description = program_description , formatter_class = argparse .RawTextHelpFormatter )
181+ parser .add_argument ("-u" , "--base-url" , required = True , help = "Hub server URL e.g. https://your.blackduck.url" )
182+ parser .add_argument ("-t" , "--token-file" , required = True , help = "File containing access token" )
183+ parser .add_argument ("-nv" , "--no-verify" , action = 'store_false' , help = "Disable TLS certificate verification" )
184+ parser .add_argument ("-d" , "--debug" , action = 'store_true' , help = "Set debug output on" )
185+ parser .add_argument ("-pn" , "--project-name" , required = True , help = "Project Name" )
186+ parser .add_argument ("-pv" , "--project-version-name" , required = True , help = "Project Version Name" )
187+ parser .add_argument ("-kh" , "--keep_hierarchy" , action = 'store_true' , help = "Set to keep all entries in the sources report. Will not remove components found under others." )
188+ parser .add_argument ("--report-retries" , metavar = "" , type = int , default = RETRY_LIMIT , help = "Retries for receiving the generated BlackDuck report. Generating copyright report tends to take longer minutes." )
189+ parser .add_argument ("--timeout" , metavar = "" , type = int , default = 60 , help = "Timeout for REST-API. Some API may take longer than the default 60 seconds" )
190+ parser .add_argument ("--retries" , metavar = "" , type = int , default = 4 , help = "Retries for REST-API. Some API may need more retries than the default 4 times" )
191+ return parser .parse_args ()
221192
222193def main ():
223- args = parse_parameter ()
224- debug = 0
194+ args = parse_command_args ()
195+ with open (args .token_file , 'r' ) as tf :
196+ token = tf .readline ().strip ()
225197 try :
226- if args .project == "" :
227- sys .exit ("Please set BlackDuck project name!" )
228- if args .version == "" :
229- sys .exit ("Please set BlackDuck project version name!" )
230-
231- with open (".restconfig.json" , "r" ) as f :
232- config = json .load (f )
233- # Remove last slash if there is, otherwise REST API may fail.
234- if re .search (r".+/$" , config ['baseurl' ]):
235- bd_url = config ['baseurl' ][:- 1 ]
236- else :
237- bd_url = config ['baseurl' ]
238- bd_token = config ['api_token' ]
239- bd_insecure = not config ['insecure' ]
240- if config ['debug' ]:
241- debug = 1
242-
243- log_config (debug )
244-
245- hub_client = Client (token = bd_token ,
246- base_url = bd_url ,
247- verify = bd_insecure ,
198+ log_config (args .debug )
199+ hub_client = Client (token = token ,
200+ base_url = args .base_url ,
201+ verify = args .no_verify ,
248202 timeout = args .timeout ,
249203 retries = args .retries )
250204
251- project_id , version_id = get_bd_project_data (hub_client , args .project , args .version )
205+ project = find_project_by_name (hub_client , args .project_name )
206+ version = find_project_version_by_name (hub_client , project , args .project_version_name )
207+ pprint (version )
208+ location = create_version_details_report (hub_client , version )
209+ pprint (location )
210+ report_zip = download_report (hub_client , location , args .report_retries )
211+ pprint (report_zip )
212+ logging .debug (f"Deleting report from Black Duck { hub_client .session .delete (location )} " )
213+ zip = ZipFile (io .BytesIO (report_zip ), "r" )
214+ pprint (zip .namelist ())
215+ report_data = {name : zip .read (name ) for name in zip .namelist ()}
216+ filename = [i for i in report_data .keys () if i .endswith (".json" )][0 ]
217+ pprint (json .loads (report_data [filename ]))
252218
253- generate_file_report (hub_client ,
254- project_id ,
255- version_id ,
256- args .keep_hierarchy ,
257- args .report_retries
258- )
259219
260220 except (Exception , BaseException ) as err :
261221 logging .error (f"Exception by { str (err )} . See the stack trace" )
0 commit comments