2828'''
2929
3030import argparse
31+ import csv
3132import logging
3233import sys
3334import io
4748'''
4849
4950# 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
5651BLACKDUCK_VERSION_MEDIATYPE = "application/vnd.blackducksoftware.status-4+json"
5752BLACKDUCK_VERSION_API = "/api/current-version"
58- REPORT_DIR = "./blackduck_component_source_report"
5953# Retries to wait for BD report creation. RETRY_LIMIT can be overwritten by the script parameter.
6054RETRY_LIMIT = 30
6155RETRY_TIMER = 30
@@ -122,7 +116,7 @@ def create_version_details_report(bd, version):
122116 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"
123117 return location
124118
125- def download_report (bd , location , retries ):
119+ def download_report (bd , location , retries , timeout ):
126120 report_id = location .split ("/" )[- 1 ]
127121 logging .debug (f"Report location { location } " )
128122 url_data = location .split ('/' )
@@ -142,10 +136,10 @@ def download_report(bd, location, retries):
142136 logging .error ("Ruh-roh, not sure what happened here" )
143137 return None
144138 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 )
147141 retries -= 1
148- return download_report (bd , location , retries )
142+ return download_report (bd , location , retries , timeout )
149143 else :
150144 logging .debug (f"Failed to retrieve report { report_id } after multiple retries" )
151145 return None
@@ -158,6 +152,47 @@ def get_blackduck_version(hub_client):
158152 else :
159153 sys .exit (f"Get BlackDuck version failed with status { res .status_code } " )
160154
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+
161196def parse_command_args ():
162197 parser = argparse .ArgumentParser (description = program_description , formatter_class = argparse .RawTextHelpFormatter )
163198 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():
166201 parser .add_argument ("-d" , "--debug" , action = 'store_true' , help = "Set debug output on" )
167202 parser .add_argument ("-pn" , "--project-name" , required = True , help = "Project Name" )
168203 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." )
169205 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." )
170206 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." )
171208 parser .add_argument ("--timeout" , metavar = "" , type = int , default = 60 , help = "Timeout for REST-API. Some API may take longer than the default 60 seconds" )
172209 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" )
173210 return parser .parse_args ()
@@ -176,6 +213,9 @@ def main():
176213 args = parse_command_args ()
177214 with open (args .token_file , 'r' ) as tf :
178215 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 (" " ,"_" )
179219 try :
180220 log_config (args .debug )
181221 hub_client = Client (token = token ,
@@ -187,7 +227,7 @@ def main():
187227 project = find_project_by_name (hub_client , args .project_name )
188228 version = find_project_version_by_name (hub_client , project , args .project_version_name )
189229 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 )
191231 logging .debug (f"Deleting report from Black Duck { hub_client .session .delete (location )} " )
192232 zip = ZipFile (io .BytesIO (report_zip ), "r" )
193233 pprint (zip .namelist ())
@@ -198,10 +238,24 @@ def main():
198238 json .dump (version_report , f )
199239 # TODO items
200240 # 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+
201254 # Combine component data with selected file data
202255 # Output result with CSV anf JSON as options.
203256
204257
258+
205259 except (Exception , BaseException ) as err :
206260 logging .error (f"Exception by { str (err )} . See the stack trace" )
207261 traceback .print_exc ()
0 commit comments