28
28
'''
29
29
30
30
import argparse
31
+ import csv
31
32
import logging
32
33
import sys
33
34
import io
47
48
'''
48
49
49
50
# BD report general
50
- BLACKDUCK_REPORT_MEDIATYPE = "application/vnd.blackducksoftware.report-4+json"
51
- blackduck_report_download_api = "/api/projects/{projectId}/versions/{projectVersionId}/reports/{reportId}/download"
52
- # BD version details report
53
- blackduck_create_version_report_api = "/api/versions/{projectVersionId}/reports"
54
- blackduck_version_report_filename = "./blackduck_version_report_for_{projectVersionId}.zip"
55
- # Consolidated report
56
51
BLACKDUCK_VERSION_MEDIATYPE = "application/vnd.blackducksoftware.status-4+json"
57
52
BLACKDUCK_VERSION_API = "/api/current-version"
58
- REPORT_DIR = "./blackduck_component_source_report"
59
53
# Retries to wait for BD report creation. RETRY_LIMIT can be overwritten by the script parameter.
60
54
RETRY_LIMIT = 30
61
55
RETRY_TIMER = 30
@@ -122,7 +116,7 @@ def create_version_details_report(bd, version):
122
116
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"
123
117
return location
124
118
125
- def download_report (bd , location , retries ):
119
+ def download_report (bd , location , retries , timeout ):
126
120
report_id = location .split ("/" )[- 1 ]
127
121
logging .debug (f"Report location { location } " )
128
122
url_data = location .split ('/' )
@@ -142,10 +136,10 @@ def download_report(bd, location, retries):
142
136
logging .error ("Ruh-roh, not sure what happened here" )
143
137
return None
144
138
else :
145
- logging .debug (f"Report status request { response .status_code } { report_status } ,waiting { retries } seconds then retrying..." )
146
- time .sleep (60 )
139
+ logging .debug (f"Report status request { response .status_code } { report_status } ,waiting { timeout } seconds then retrying..." )
140
+ time .sleep (timeout )
147
141
retries -= 1
148
- return download_report (bd , location , retries )
142
+ return download_report (bd , location , retries , timeout )
149
143
else :
150
144
logging .debug (f"Failed to retrieve report { report_id } after multiple retries" )
151
145
return None
@@ -158,6 +152,47 @@ def get_blackduck_version(hub_client):
158
152
else :
159
153
sys .exit (f"Get BlackDuck version failed with status { res .status_code } " )
160
154
155
+ def reduce (path_set ):
156
+ path_set .sort ()
157
+ for path in path_set :
158
+ if len (path ) < 3 :
159
+ continue
160
+ index = path_set .index (path )
161
+ while index + 1 < len (path_set ) and path in path_set [index + 1 ]:
162
+ logging .debug (f"{ path } is in { path_set [index + 1 ]} deleting the sub-path from the list" )
163
+ path_set .pop (index + 1 )
164
+ return path_set
165
+
166
+ def trim_version_report (version_report , reduced_path_set ):
167
+ file_bom_entries = version_report ['detailedFileBomViewEntries' ]
168
+ aggregate_bom_view_entries = version_report ['aggregateBomViewEntries' ]
169
+
170
+ reduced_file_bom_entries = [e for e in file_bom_entries if f"{ e .get ('archiveContext' , "" )} !{ e ['path' ]} " in reduced_path_set ]
171
+ version_report ['detailedFileBomViewEntries' ] = reduced_file_bom_entries
172
+
173
+ component_identifiers = [f"{ e ['projectId' ]} :{ e ['versionId' ]} " for e in reduced_file_bom_entries ]
174
+ deduplicated = list (dict .fromkeys (component_identifiers ))
175
+
176
+ reduced_aggregate_bom_view_entries = [e for e in aggregate_bom_view_entries if f"{ e ['producerProject' ]['id' ]} :{ e ['producerReleases' ][0 ]['id' ]} " in deduplicated ]
177
+ version_report ['aggregateBomViewEntries' ] = reduced_aggregate_bom_view_entries
178
+
179
+ def write_output_file (version_report , output_file ):
180
+ if output_file .lower ().endswith (".csv" ):
181
+ logging .info (f"Writing CSV output into { output_file } " )
182
+ field_names = list (version_report ['aggregateBomViewEntries' ][0 ].keys ())
183
+ with open (output_file , "w" ) as f :
184
+ writer = csv .DictWriter (f , fieldnames = field_names )
185
+ writer .writeheader ()
186
+ writer .writerows (version_report ['aggregateBomViewEntries' ])
187
+
188
+ return
189
+ # If it's neither, then .json
190
+ if not output_file .lower ().endswith (".json" ):
191
+ output_file += ".json"
192
+ logging .info (f"Writing JSON output into { output_file } " )
193
+ with open (output_file ,"w" ) as f :
194
+ json .dump (version_report , f )
195
+
161
196
def parse_command_args ():
162
197
parser = argparse .ArgumentParser (description = program_description , formatter_class = argparse .RawTextHelpFormatter )
163
198
parser .add_argument ("-u" , "--base-url" , required = True , help = "Hub server URL e.g. https://your.blackduck.url" )
@@ -166,8 +201,10 @@ def parse_command_args():
166
201
parser .add_argument ("-d" , "--debug" , action = 'store_true' , help = "Set debug output on" )
167
202
parser .add_argument ("-pn" , "--project-name" , required = True , help = "Project Name" )
168
203
parser .add_argument ("-pv" , "--project-version-name" , required = True , help = "Project Version Name" )
204
+ parser .add_argument ("-o" , "--output-file" , required = False , help = "File name to write output. File extension determines format .json and .csv, json is the default." )
169
205
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." )
170
206
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." )
207
+ parser .add_argument ("--report-timeout" , metavar = "" , type = int , default = RETRY_TIMER , help = "Wait time between subsequent download attempts." )
171
208
parser .add_argument ("--timeout" , metavar = "" , type = int , default = 60 , help = "Timeout for REST-API. Some API may take longer than the default 60 seconds" )
172
209
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" )
173
210
return parser .parse_args ()
@@ -176,6 +213,9 @@ def main():
176
213
args = parse_command_args ()
177
214
with open (args .token_file , 'r' ) as tf :
178
215
token = tf .readline ().strip ()
216
+ output_file = args .output_file
217
+ if not args .output_file :
218
+ output_file = f"{ args .project_name } -{ args .project_version_name } .json" .replace (" " ,"_" )
179
219
try :
180
220
log_config (args .debug )
181
221
hub_client = Client (token = token ,
@@ -187,7 +227,7 @@ def main():
187
227
project = find_project_by_name (hub_client , args .project_name )
188
228
version = find_project_version_by_name (hub_client , project , args .project_version_name )
189
229
location = create_version_details_report (hub_client , version )
190
- report_zip = download_report (hub_client , location , args .report_retries )
230
+ report_zip = download_report (hub_client , location , args .report_retries , args . report_timeout )
191
231
logging .debug (f"Deleting report from Black Duck { hub_client .session .delete (location )} " )
192
232
zip = ZipFile (io .BytesIO (report_zip ), "r" )
193
233
pprint (zip .namelist ())
@@ -198,10 +238,24 @@ def main():
198
238
json .dump (version_report , f )
199
239
# TODO items
200
240
# Process file section of report data to identify primary paths
241
+ path_set = [f"{ entry .get ('archiveContext' , "" )} !{ entry ['path' ]} " for entry in version_report ['detailedFileBomViewEntries' ]]
242
+ reduced_path_set = reduce (path_set .copy ())
243
+ logging .info (f"{ len (path_set )- len (reduced_path_set )} path entries were scrubbed from the dataset." )
244
+
245
+ # Remove component entries that correspond to removed path entries.
246
+
247
+ logging .info (f"Original dataset contains { len (version_report ['aggregateBomViewEntries' ])} bom entries and { len (version_report ['detailedFileBomViewEntries' ])} file view entries" )
248
+ if not args .keep_hierarchy :
249
+ trim_version_report (version_report , reduced_path_set )
250
+ logging .info (f"Truncated dataset contains { len (version_report ['aggregateBomViewEntries' ])} bom entries and { len (version_report ['detailedFileBomViewEntries' ])} file view entries" )
251
+
252
+ write_output_file (version_report , output_file )
253
+
201
254
# Combine component data with selected file data
202
255
# Output result with CSV anf JSON as options.
203
256
204
257
258
+
205
259
except (Exception , BaseException ) as err :
206
260
logging .error (f"Exception by { str (err )} . See the stack trace" )
207
261
traceback .print_exc ()
0 commit comments