Skip to content

Commit 37978f2

Browse files
author
Murat Kumykov
committed
Merge branch 'master' into mkumykov-multi-image
2 parents 6d817e4 + 726e1f7 commit 37978f2

11 files changed

+672
-24
lines changed

blackduck/Core.py

+11
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,17 @@ def execute_put(self, url, data, custom_headers={}):
146146
response = requests.put(url, headers=headers, data=json_data, verify = not self.config['insecure'])
147147
return response
148148

149+
def execute_patch(self, url, data, custom_headers={}) -> requests.Response:
150+
'''
151+
Work-around to add missing execute_patch()
152+
'''
153+
json_data = self._validated_json_data(data)
154+
headers = self.get_headers()
155+
headers["Content-Type"] = "application/json"
156+
headers.update(custom_headers)
157+
response = requests.patch(url, headers=headers, data=json_data, verify=not self.config['insecure'])
158+
return response
159+
149160
def _create(self, url, json_body):
150161
response = self.execute_post(url, json_body)
151162
# v4+ returns the newly created location in the response headers

blackduck/HubRestApi.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ class HubInstance(object):
6868
_create,_get_hub_rest_api_version_info,_get_major_version,_get_parameter_string,_validated_json_data,
6969
execute_delete,execute_get,execute_post,execute_put,get_api_version,get_apibase,get_auth_token,get_headers,
7070
get_limit_paramstring,get_link,get_matched_components,get_tags_url,get_urlbase,read_config,write_config,
71-
_check_version_compatibility
71+
_check_version_compatibility,execute_patch
7272
)
7373
from .Roles import (
7474
_get_role_url, assign_role_given_role_url, assign_role_to_user_or_group,

blackduck/Reporting.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def create_version_reports(self, version, report_list, format="CSV"):
2525
return self.execute_post(version_reports_url, post_data)
2626

2727
valid_notices_formats = ["TEXT", "JSON"]
28-
def create_version_notices_report(self, version, format="TEXT", include_copyright_info=True):
28+
def create_version_notices_report(self, version, format="TEXT", include_copyright_info=True, include_license_info=True):
2929
assert format in valid_notices_formats, "Format must be one of {}".format(valid_notices_formats)
3030

3131
post_data = {
@@ -35,6 +35,8 @@ def create_version_notices_report(self, version, format="TEXT", include_copyrigh
3535
}
3636
if include_copyright_info:
3737
post_data.update({'categories': ["COPYRIGHT_TEXT"] })
38+
if include_license_info:
39+
post_data.update({'categories': ["COPYRIGHT_TEXT","LICENSE_DATA","LICENSE_TEXT"] })
3840

3941
notices_report_url = self.get_link(version, 'licenseReports')
4042
return self.execute_post(notices_report_url, post_data)

examples/client/batch_generate_sbom.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ def sanitize_filename(filename):
262262

263263
def parse_command_args():
264264

265-
parser = argparse.ArgumentParser("Generate and download reports for projets in a spreadsheet")
265+
parser = argparse.ArgumentParser("Generate and download reports for projects in a spreadsheet")
266266
parser.add_argument("-u", "--base-url", required=True, help="Hub server URL e.g. https://your.blackduck.url")
267267
parser.add_argument("-t", "--token-file", required=True, help="File containing access token")
268268
parser.add_argument("-i", "--input-file", required=True, help="Project Name")

examples/client/generate_sbom.py

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def download_report(bd_client, location, filename, retries=args.retries):
116116
logging.debug("Authorization Error - Please ensure the token you are using has write permissions!")
117117
r.raise_for_status()
118118
location = r.headers.get('Location')
119+
print(f"location {location}")
119120
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"
120121

121122
logging.debug(f"Created SBOM report of type {args.type} for project {args.project_name}, version {args.version_name} at location {location}")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env python
2+
3+
'''
4+
Copyright (C) 2021 Synopsys, Inc.
5+
http://www.blackducksoftware.com/
6+
7+
Licensed to the Apache Software Foundation (ASF) under one
8+
or more contributor license agreements. See the NOTICE file
9+
distributed with this work for additional information
10+
regarding copyright ownership. The ASF licenses this file
11+
to you under the Apache License, Version 2.0 (the
12+
"License"); you may not use this file except in compliance
13+
with the License. You may obtain a copy of the License at
14+
15+
http://www.apache.org/licenses/LICENSE-2.0
16+
17+
Unless required by applicable law or agreed to in writing,
18+
software distributed under the License is distributed on an
19+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20+
KIND, either express or implied. See the License for the
21+
specific language governing permissions and limitations
22+
under the License.
23+
24+
'''
25+
import argparse
26+
from datetime import datetime
27+
import json
28+
import logging
29+
import requests
30+
import sys
31+
import time
32+
import csv
33+
34+
from blackduck import Client
35+
36+
DEFAULT_OUTPUT_FILE="vuln_status_report.csv"
37+
38+
parser = argparse.ArgumentParser("Generate a vulnerability status report")
39+
parser.add_argument("--base-url", help="Hub server URL e.g. https://your.blackduck.url")
40+
parser.add_argument("--token-file", help="containing access token")
41+
parser.add_argument("--projects", nargs="+", help="The list of projects to include in the report")
42+
parser.add_argument('-t', '--tries', default=10, type=int, help="How many times to retry downloading the report, i.e. wait for the report to be generated")
43+
parser.add_argument("-o", "--output-file-name",
44+
dest="file_name",
45+
default=DEFAULT_OUTPUT_FILE,
46+
help=f"Name of the output file (default: {DEFAULT_OUTPUT_FILE})")
47+
parser.add_argument("--no-verify",
48+
dest='verify',
49+
action='store_false',
50+
help="disable TLS certificate verification")
51+
args = parser.parse_args()
52+
53+
54+
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', stream=sys.stdout, level=logging.DEBUG)
55+
logging.getLogger("requests").setLevel(logging.WARNING)
56+
logging.getLogger("urllib3").setLevel(logging.WARNING)
57+
logging.getLogger("blackduck").setLevel(logging.WARNING)
58+
59+
class FailedReportDownload(Exception):
60+
pass
61+
62+
def download_vuln_report(bd_client, location, filename, report_format, retries=args.tries):
63+
if retries:
64+
report_status = bd_client.session.get(location).json()
65+
if report_status['status'] == 'COMPLETED':
66+
download_url = bd_client.list_resources(report_status)['download']
67+
contents_url = download_url + "/contents"
68+
report_contents = bd_client.session.get(contents_url).json()
69+
if report_format == 'JSON':
70+
with open(filename, 'w') as f:
71+
json.dump(report_contents, f, indent=3)
72+
logging.info(f"Wrote vulnerability status report contents to {filename}")
73+
elif report_format == 'CSV':
74+
csv_data = report_contents['reportContent'][0]['fileContent']
75+
with open(filename, 'w') as f:
76+
f.write(csv_data)
77+
logging.info(f"Wrote vulnerability status report contents to {filename}")
78+
else:
79+
logging.error(f"Unrecognized format ({report_format}) given. Exiting")
80+
81+
else:
82+
sleep_time = 25
83+
retries -= 1
84+
logging.debug(f"Report is not ready to download yet, waiting {sleep_time} seconds and then retrying {retries} more times")
85+
time.sleep(sleep_time)
86+
download_vuln_report(bd_client, location, filename, report_format, retries)
87+
else:
88+
raise FailedReportDownload(f"Failed to retrieve report from {location} after {retries} attempts")
89+
90+
91+
def get_projects(client, project_names):
92+
print (project_names)
93+
'''Given a list of project names return a list of the corresponding project URLs'''
94+
project_urls = list()
95+
for project in client.get_items("/api/projects"):
96+
if project['name'] in project_names:
97+
project_urls.append(project['_meta']['href'])
98+
return project_urls
99+
100+
def augment_filename(filename):
101+
if filename.endswith('.csv'):
102+
index = filename.index('.csv')
103+
return filename[:index] + '_augmented' + filename[index:]
104+
else:
105+
return filename + '_augmented.csv'
106+
107+
def correct_vuln_ids(bd, filename, new_filename):
108+
logging.debug(f"Generating file with augmented vuln ids as {new_filename}")
109+
input = open(filename, 'r')
110+
reader = csv.DictReader(input)
111+
fieldnames = reader.fieldnames
112+
rowcount = 0
113+
with open(new_filename, 'w') as output:
114+
writer = csv.DictWriter(output, fieldnames=fieldnames)
115+
writer.writeheader()
116+
for row in reader:
117+
vuln_id = row['Vulnerability id']
118+
related_vuln_id = get_related_vuln_id(bd, vuln_id)
119+
if related_vuln_id:
120+
correct_vuln_id = f"{vuln_id} ({related_vuln_id})"
121+
else:
122+
correct_vuln_id = vuln_id
123+
row['Vulnerability id'] = correct_vuln_id
124+
writer.writerow(row)
125+
rowcount+=1
126+
if rowcount % 100 == 0:
127+
logging.debug(f"{rowcount:15} rows written into {new_filename}")
128+
logging.debug(f"Total of {rowcount} rows written into {new_filename}")
129+
logging.info(f"Wrote vulnerability status report contents to {filename}")
130+
131+
def get_related_vuln_id(bd, vuln_id):
132+
related_id = None
133+
if vuln_id.startswith('BDSA') and 'CVE' not in vuln_id:
134+
vuln_data = bd.get_json(f"/api/vulnerabilities/{vuln_id}")
135+
vuln_resources = bd.list_resources(vuln_data)
136+
related_url = vuln_resources.get('related-vulnerability', None)
137+
if related_url:
138+
related_id = related_url.split('/')[-1:][0]
139+
return related_id
140+
141+
with open(args.token_file, 'r') as tf:
142+
access_token = tf.readline().strip()
143+
144+
bd = Client(
145+
base_url=args.base_url,
146+
token=access_token,
147+
verify=args.verify
148+
)
149+
150+
project_urls = get_projects(bd, args.projects)
151+
logging.debug(f"Generating vulnerability status report for the following projects: {args.projects}")
152+
logging.debug(f"Project list resulted in following project URLs {project_urls}")
153+
post_data = {
154+
'reportFormat': 'CSV',
155+
'projects': project_urls,
156+
'locale': 'en_US'
157+
}
158+
159+
try:
160+
r = bd.session.post("/api/vulnerability-status-reports", json=post_data)
161+
r.raise_for_status()
162+
report_url = r.headers['Location']
163+
logging.debug(f"created vulnerability status report {report_url}")
164+
except requests.HTTPError as err:
165+
# more fine grained error handling here; otherwise:
166+
bd.http_error_handler(err)
167+
logging.error("Failed to generate the report")
168+
sys.exit(1)
169+
170+
download_vuln_report(bd, report_url, args.file_name, 'CSV', retries=args.tries)
171+
172+
correct_vuln_ids(bd, args.file_name, augment_filename(args.file_name))
173+
174+
175+
176+
177+
178+
179+
180+
181+

examples/client/get_bom_component_vuln_info.py

+23-20
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,23 @@
4949
all_bom_component_vulns = []
5050

5151
for bom_component_vuln in bd.get_resource('vulnerable-components', version):
52-
vuln_name = bom_component_vuln['vulnerabilityWithRemediation']['vulnerabilityName']
53-
vuln_source = bom_component_vuln['vulnerabilityWithRemediation']['source']
52+
vulnerabilities = bd.get_resource('vulnerabilities', bom_component_vuln)
5453
upgrade_guidance = bd.get_json(f"{bom_component_vuln['componentVersion']}/upgrade-guidance")
5554
bom_component_vuln['upgrade_guidance'] = upgrade_guidance
55+
all_bom_component_vulns.append(bom_component_vuln)
56+
#for vuln in vulnerabilities:
57+
#pprint(vuln)
58+
#vuln_name = vuln['name']
59+
#vuln_source = vuln['source']
5660

57-
vuln_details = bd.get_json(f"/api/vulnerabilities/{vuln_name}")
58-
bom_component_vuln['vulnerability_details'] = vuln_details
61+
#vuln_details = bd.get_json(f"/api/vulnerabilities/{vuln_name}")
62+
#bom_component_vuln['vulnerability_details'] = vuln_details
5963

60-
if 'related-vulnerability' in bd.list_resources(vuln_details):
61-
related_vuln = bd.get_resource("related-vulnerability", vuln_details, items=False)
62-
else:
63-
related_vuln = None
64-
bom_component_vuln['related_vulnerability'] = related_vuln
65-
all_bom_component_vulns.append(bom_component_vuln)
64+
#if 'related-vulnerability' in bd.list_resources(vuln_details):
65+
# related_vuln = bd.get_resource("related-vulnerability", vuln_details, items=False)
66+
#else:
67+
# related_vuln = None
68+
#bom_component_vuln['related_vulnerability'] = related_vuln
6669

6770
if args.csv_file:
6871
'''Note: See the BD API doc and in particular .../api-doc/public.html#_bom_vulnerability_endpoints
@@ -73,28 +76,28 @@
7376
with open(args.csv_file, 'w') as csv_f:
7477
field_names = [
7578
'Vulnerability Name',
76-
'Vulnerability Description',
79+
#'Vulnerability Description',
7780
'Remediation Status',
7881
'Component',
7982
'Component Version',
80-
'Exploit Available',
81-
'Workaround Available',
82-
'Solution Available',
83+
#'Exploit Available',
84+
#'Workaround Available',
85+
#'Solution Available',
8386
'Upgrade Guidance - short term',
8487
'Upgrade Guidance - long term',
8588
]
8689
writer = csv.DictWriter(csv_f, fieldnames = field_names)
8790
writer.writeheader()
8891
for comp_vuln in all_bom_component_vulns:
8992
row_data = {
90-
'Vulnerability Name': comp_vuln['vulnerabilityWithRemediation']['vulnerabilityName'],
91-
'Vulnerability Description': comp_vuln['vulnerabilityWithRemediation']['description'],
92-
'Remediation Status': comp_vuln['vulnerabilityWithRemediation']['remediationStatus'],
93+
'Vulnerability Name': comp_vuln['vulnerability']['vulnerabilityId'],
94+
#'Vulnerability Description': comp_vuln['vulnerabilityWithRemediation']['description'],
95+
'Remediation Status': comp_vuln['vulnerability']['remediationStatus'],
9396
'Component': comp_vuln['componentName'],
9497
'Component Version': comp_vuln['componentVersionName'],
95-
'Exploit Available': comp_vuln['vulnerability_details'].get('exploitPublishDate', 'None available'),
96-
'Workaround Available': comp_vuln['vulnerability_details'].get('workaround', 'None available'),
97-
'Solution Available': comp_vuln['vulnerability_details'].get('solution', 'None available'),
98+
#'Exploit Available': comp_vuln['vulnerability_details'].get('exploitPublishDate', 'None available'),
99+
#'Workaround Available': comp_vuln['vulnerability_details'].get('workaround', 'None available'),
100+
#'Solution Available': comp_vuln['vulnerability_details'].get('solution', 'None available'),
98101
'Upgrade Guidance - short term': comp_vuln['upgrade_guidance'].get('shortTerm', 'None available'),
99102
'Upgrade Guidance - long term': comp_vuln['upgrade_guidance'].get('longTerm', 'None available')
100103
}

0 commit comments

Comments
 (0)