Skip to content

Commit 18cd9c0

Browse files
authored
Merge pull request #275 from blackducksoftware/snps-steve-patch-1
Add files via upload
2 parents 084b557 + 7d32fb2 commit 18cd9c0

File tree

1 file changed

+246
-0
lines changed

1 file changed

+246
-0
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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"\rProcessing 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("\nProcessing 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

Comments
 (0)