1
+ '''
2
+ Created on Dec 19, 2018
3
+ Updated on Sept 20, 2024
4
+
5
+ @author: gsnyder
6
+ @contributor: smiths
7
+
8
+ Generate a CSV report for a given project-version and enhance with "File Paths", "How to Fix", and
9
+ "References and Related Links"
10
+ '''
11
+
12
+ import argparse
13
+ import csv
14
+ import io
15
+ import json
16
+ import logging
17
+ import time
18
+ import zipfile
19
+ from blackduck .HubRestApi import HubInstance
20
+ from requests .exceptions import MissingSchema
21
+
22
+ logging .basicConfig (
23
+ level = logging .DEBUG ,
24
+ format = "[%(asctime)s] {%(module)s:%(lineno)d} %(levelname)s - %(message)s"
25
+ )
26
+
27
+ version_name_map = {
28
+ 'version' : 'VERSION' ,
29
+ 'scans' : 'CODE_LOCATIONS' ,
30
+ 'components' : 'COMPONENTS' ,
31
+ 'vulnerabilities' : 'SECURITY' ,
32
+ 'source' : 'FILES' ,
33
+ 'cryptography' : 'CRYPTO_ALGORITHMS' ,
34
+ 'license_terms' : 'LICENSE_TERM_FULFILLMENT' ,
35
+ 'component_additional_fields' : 'BOM_COMPONENT_CUSTOM_FIELDS' ,
36
+ 'project_version_additional_fields' : 'PROJECT_VERSION_CUSTOM_FIELDS' ,
37
+ 'vulnerability_matches' : 'VULNERABILITY_MATCH'
38
+ }
39
+
40
+ all_reports = list (version_name_map .keys ())
41
+
42
+ parser = argparse .ArgumentParser ("A program to create reports for a given project-version" )
43
+ parser .add_argument ("project_name" )
44
+ parser .add_argument ("version_name" )
45
+ parser .add_argument ("-z" , "--zip_file_name" , default = "reports.zip" )
46
+ parser .add_argument ("-r" , "--reports" ,
47
+ default = "," .join (all_reports ),
48
+ help = f"Comma separated list (no spaces) of the reports to generate - { list (version_name_map .keys ())} . Default is all reports." ,
49
+ type = lambda s : s .upper ())
50
+ parser .add_argument ('--format' , default = 'CSV' , choices = ["CSV" ], help = "Report format - only CSV available for now" )
51
+ parser .add_argument ('-t' , '--tries' , default = 5 , type = int , help = "How many times to retry downloading the report, i.e. wait for the report to be generated" )
52
+ parser .add_argument ('-s' , '--sleep_time' , default = 30 , type = int , help = "The amount of time to sleep in-between (re-)tries to download the report" )
53
+
54
+ args = parser .parse_args ()
55
+
56
+ hub = HubInstance ()
57
+
58
+ class FailedReportDownload (Exception ):
59
+ pass
60
+
61
+ def download_report (location , filename , retries = args .tries ):
62
+ report_id = location .split ("/" )[- 1 ]
63
+
64
+ for attempt in range (retries ):
65
+
66
+ # Wait for 30 seconds before attempting to download
67
+ print (f"Waiting 30 seconds before attempting to download..." )
68
+ time .sleep (30 )
69
+
70
+ # Retries
71
+ print (f"Attempt { attempt + 1 } of { retries } to retrieve report { report_id } " )
72
+
73
+ # Report Retrieval
74
+ print (f"Retrieving generated report from { location } " )
75
+ response = hub .download_report (report_id )
76
+
77
+ if response .status_code == 200 :
78
+ with open (filename , "wb" ) as f :
79
+ f .write (response .content )
80
+ print (f"Successfully downloaded zip file to { filename } for report { report_id } " )
81
+ return response .content
82
+ else :
83
+ print (f"Failed to retrieve report { report_id } " )
84
+ if attempt < retries - 1 : # If it's not the last attempt
85
+ wait_time = args .sleep_time
86
+ print (f"Waiting { wait_time } seconds before retrying..." )
87
+ time .sleep (wait_time )
88
+ else :
89
+ print (f"Maximum retries reached. Unable to download report." )
90
+
91
+ raise FailedReportDownload (f"Failed to retrieve report { report_id } after { retries } tries" )
92
+
93
+ def get_file_paths (hub , project_id , project_version_id , component_id , component_version_id , component_origin_id ):
94
+ url = f"{ hub .get_urlbase ()} /api/projects/{ project_id } /versions/{ project_version_id } /components/{ component_id } /versions/{ component_version_id } /origins/{ component_origin_id } /matched-files"
95
+ headers = {
96
+ "Accept" : "application/vnd.blackducksoftware.bill-of-materials-6+json" ,
97
+ "Authorization" : f"Bearer { hub .token } "
98
+ }
99
+
100
+ logging .debug (f"Making API call to: { url } " )
101
+
102
+ try :
103
+ response = hub .execute_get (url )
104
+ if response .status_code == 200 :
105
+ data = response .json ()
106
+ file_paths = []
107
+ for item in data .get ('items' , []):
108
+ file_path = item .get ('filePath' , {})
109
+ composite_path = file_path .get ('compositePathContext' , '' )
110
+ if composite_path :
111
+ file_paths .append (composite_path )
112
+ return file_paths
113
+ else :
114
+ logging .error (f"Failed to fetch matched files. Status code: { response .status_code } " )
115
+ return []
116
+ except Exception as e :
117
+ logging .error (f"Error making API request: { str (e )} " )
118
+ return []
119
+
120
+ def get_vulnerability_details (hub , vulnerability_id ):
121
+ url = f"{ hub .get_urlbase ()} /api/vulnerabilities/{ vulnerability_id } "
122
+
123
+ try :
124
+ response = hub .execute_get (url )
125
+ if response .status_code == 200 :
126
+ data = response .json ()
127
+ solution = data .get ('solution' , '' )
128
+ references = []
129
+ meta_data = data .get ('_meta' , {})
130
+ links = meta_data .get ('links' , [])
131
+ for link in links :
132
+ references .append ({
133
+ 'rel' : link .get ('rel' , '' ),
134
+ 'href' : link .get ('href' , '' )
135
+ })
136
+ return solution , references
137
+ else :
138
+ logging .error (f"Failed to fetch vulnerability details. Status code: { response .status_code } " )
139
+ return '' , []
140
+ except Exception as e :
141
+ logging .error (f"Error making API request for vulnerability details: { str (e )} " )
142
+ return '' , []
143
+
144
+ def enhance_security_report (hub , zip_content , project_id , project_version_id ):
145
+ logging .info (f"Enhancing security report for Project ID: { project_id } , Project Version ID: { project_version_id } " )
146
+
147
+ with zipfile .ZipFile (io .BytesIO (zip_content ), 'r' ) as zin :
148
+ csv_files = [f for f in zin .namelist () if f .endswith ('.csv' )]
149
+ for csv_file in csv_files :
150
+ csv_content = zin .read (csv_file ).decode ('utf-8' )
151
+ reader = csv .DictReader (io .StringIO (csv_content ))
152
+
153
+ # Count total rows
154
+ total_rows = sum (1 for row in reader )
155
+ reader = csv .DictReader (io .StringIO (csv_content )) # Reset reader
156
+
157
+ fieldnames = reader .fieldnames + ["File Paths" , "How to Fix" , "References and Related Links" ]
158
+ output = io .StringIO ()
159
+ writer = csv .DictWriter (output , fieldnames = fieldnames )
160
+ writer .writeheader ()
161
+
162
+ processed_components = 0
163
+ skipped_components = 0
164
+
165
+ for index , row in enumerate (reader , 1 ):
166
+ print (f"\r Processing row { index } of { total_rows } ({ index / total_rows * 100 :.2f} %)" , end = '' , flush = True )
167
+
168
+ component_id = row .get ('Component id' , '' )
169
+ component_version_id = row .get ('Version id' , '' )
170
+ component_origin_id = row .get ('Origin id' , '' )
171
+ vulnerability_id = row .get ('Vulnerability id' , '' )
172
+
173
+ if not all ([component_id , component_version_id , component_origin_id ]):
174
+ logging .warning (f"Missing component information. Component ID: { component_id } , Component Version ID: { component_version_id } , Origin ID: { component_origin_id } " )
175
+ skipped_components += 1
176
+ file_paths = []
177
+ else :
178
+ file_paths = get_file_paths (hub , project_id , project_version_id , component_id , component_version_id , component_origin_id )
179
+ processed_components += 1
180
+
181
+ if vulnerability_id :
182
+ solution , references = get_vulnerability_details (hub , vulnerability_id )
183
+ else :
184
+ solution , references = '' , []
185
+
186
+ row ["File Paths" ] = '; ' .join (file_paths ) if file_paths else "No file paths available"
187
+ row ["How to Fix" ] = solution
188
+ row ["References and Related Links" ] = json .dumps (references )
189
+
190
+ writer .writerow (row )
191
+
192
+ print ("\n Processing complete." )
193
+
194
+ # Generate a unique filename for the enhanced report
195
+ timestamp = time .strftime ("%Y%m%d-%H%M%S" )
196
+ enhanced_filename = f"enhanced_security_report_{ timestamp } .csv"
197
+
198
+ # Update zip file with modified CSV
199
+ with zipfile .ZipFile (args .zip_file_name , 'a' ) as zout :
200
+ zout .writestr (enhanced_filename , output .getvalue ())
201
+
202
+ logging .info (f"Enhanced security report saved to { args .zip_file_name } " )
203
+ logging .info (f"Processed components: { processed_components } " )
204
+ logging .info (f"Skipped components: { skipped_components } " )
205
+
206
+ def main ():
207
+ hub = HubInstance ()
208
+
209
+ project = hub .get_project_by_name (args .project_name )
210
+
211
+ if project :
212
+ project_id = project ['_meta' ]['href' ].split ('/' )[- 1 ]
213
+ logging .info (f"Project ID: { project_id } " )
214
+
215
+ version = hub .get_version_by_name (project , args .version_name )
216
+ if version :
217
+ project_version_id = version ['_meta' ]['href' ].split ('/' )[- 1 ]
218
+ logging .info (f"Project Version ID: { project_version_id } " )
219
+
220
+ reports_l = [version_name_map .get (r .strip ().lower (), r .strip ()) for r in args .reports .split ("," )]
221
+
222
+ valid_reports = set (version_name_map .values ())
223
+ invalid_reports = [r for r in reports_l if r not in valid_reports ]
224
+ if invalid_reports :
225
+ print (f"Error: Invalid report type(s): { ', ' .join (invalid_reports )} " )
226
+ print (f"Valid report types are: { ', ' .join (valid_reports )} " )
227
+ exit (1 )
228
+
229
+ response = hub .create_version_reports (version , reports_l , args .format )
230
+
231
+ if response .status_code == 201 :
232
+ print (f"Successfully created reports ({ args .reports } ) for project { args .project_name } and version { args .version_name } " )
233
+ location = response .headers ['Location' ]
234
+ zip_content = download_report (location , args .zip_file_name )
235
+
236
+ if 'SECURITY' in reports_l :
237
+ enhance_security_report (hub , zip_content , project_id , project_version_id )
238
+ else :
239
+ print (f"Failed to create reports for project { args .project_name } version { args .version_name } , status code returned { response .status_code } " )
240
+ else :
241
+ print (f"Did not find version { args .version_name } for project { args .project_name } " )
242
+ else :
243
+ print (f"Did not find project with name { args .project_name } " )
244
+
245
+ if __name__ == "__main__" :
246
+ main ()
0 commit comments