30
30
import argparse
31
31
import logging
32
32
import sys
33
+ import io
33
34
import os
34
35
import re
35
36
import time
40
41
import ijson
41
42
from blackduck import Client
42
43
from zipfile import ZipFile
44
+ from pprint import pprint
43
45
44
46
program_description = \
45
47
'''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).
47
49
48
50
This script assumes a project version exists and has scans associated with it (i.e. the project is not scanned as part of this process).
49
51
@@ -83,39 +85,21 @@ def log_config(debug):
83
85
logging .getLogger ("urllib3" ).setLevel (logging .WARNING )
84
86
logging .getLogger ("blackduck" ).setLevel (logging .WARNING )
85
87
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 ]
119
103
120
104
def get_bd_project_data (hub_client , project_name , version_name ):
121
105
""" Get and return project ID, version ID. """
@@ -136,55 +120,53 @@ def get_bd_project_data(hub_client, project_name, version_name):
136
120
137
121
return project_id , version_id
138
122
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 = {
176
126
'reportFormat' : 'JSON' ,
177
127
'locale' : 'en_US' ,
178
- 'versionId' : f' { version_id } ' ,
128
+ 'versionId' : version [ '_meta' ][ 'href' ]. split ( "/" )[ - 1 ] ,
179
129
'categories' : [ 'COMPONENTS' , 'FILES' ] # Generating "project version" report including components and files
180
130
}
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
188
170
189
171
def get_blackduck_version (hub_client ):
190
172
url = hub_client .base_url + BLACKDUCK_VERSION_API
@@ -194,68 +176,46 @@ def get_blackduck_version(hub_client):
194
176
else :
195
177
sys .exit (f"Get BlackDuck version failed with status { res .status_code } " )
196
178
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 ()
221
192
222
193
def 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 ()
225
197
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 ,
248
202
timeout = args .timeout ,
249
203
retries = args .retries )
250
204
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 ]))
252
218
253
- generate_file_report (hub_client ,
254
- project_id ,
255
- version_id ,
256
- args .keep_hierarchy ,
257
- args .report_retries
258
- )
259
219
260
220
except (Exception , BaseException ) as err :
261
221
logging .error (f"Exception by { str (err )} . See the stack trace" )
0 commit comments