Skip to content

Commit fa5bd67

Browse files
authored
Merge branch 'master' into update-create-users
2 parents 0fe9f14 + 8e4a1ef commit fa5bd67

17 files changed

+651
-37
lines changed

blackduck/HubRestApi.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ def get_headers(self):
181181
return {
182182
'X-CSRF-TOKEN': self.csrf_token,
183183
'Authorization': 'Bearer {}'.format(self.token),
184+
'Accept': 'application/json',
184185
'Content-Type': 'application/json'}
185186
else:
186187
if self.bd_major_version == "3":
@@ -424,7 +425,7 @@ def _get_policy_url(self):
424425

425426
def get_policies(self, parameters={}):
426427
url = self._get_policy_url() + self._get_parameter_string(parameters)
427-
headers = {'Accept': 'application/vnd.blackducksoftware.policy-4+json'}
428+
headers = {'Accept': 'application/json'}
428429
response = self.execute_get(url, custom_headers=headers)
429430
return response.json()
430431

@@ -540,12 +541,12 @@ def create_version_reports(self, version, report_list, format="CSV"):
540541
version_reports_url = self.get_link(version, 'versionReport')
541542
return self.execute_post(version_reports_url, post_data)
542543

543-
valid_notices_formats = ["TEXT", "HTML"]
544+
valid_notices_formats = ["TEXT", "JSON"]
544545
def create_version_notices_report(self, version, format="TEXT"):
545546
assert format in HubInstance.valid_notices_formats, "Format must be one of {}".format(HubInstance.valid_notices_formats)
546547

547548
post_data = {
548-
'categories': HubInstance.valid_categories,
549+
'categories': ["COPYRIGHT_TEXT"],
549550
'versionId': version['_meta']['href'].split("/")[-1],
550551
'reportType': 'VERSION_LICENSE',
551552
'reportFormat': format
@@ -554,9 +555,48 @@ def create_version_notices_report(self, version, format="TEXT"):
554555
return self.execute_post(notices_report_url, post_data)
555556

556557
def download_report(self, report_id):
558+
# TODO: Fix me, looks like the reports should be downloaded from different paths than the one here, and depending on the type and format desired the path can change
557559
url = self.get_urlbase() + "/api/reports/{}".format(report_id)
558560
return self.execute_get(url, {'Content-Type': 'application/zip', 'Accept':'application/zip'})
559561

562+
def download_notification_report(self, report_location_url):
563+
'''Download the notices report using the report URL. Inspect the report object to determine
564+
the format and use the appropriate media header'''
565+
custom_headers = {'Accept': 'application/vnd.blackducksoftware.report-4+json'}
566+
response = self.execute_get(report_location_url, custom_headers=custom_headers)
567+
report_obj = response.json()
568+
569+
if report_obj['reportFormat'] == 'TEXT':
570+
download_url = self.get_link(report_obj, "download") + ".json"
571+
logging.debug("downloading report from {}".format(download_url))
572+
response = self.execute_get(download_url, {'Accept': 'application/zip'})
573+
else:
574+
# JSON
575+
contents_url = self.get_link(report_obj, "content")
576+
logging.debug("retrieving report contents from {}".format(contents_url))
577+
response = self.execute_get(contents_url, {'Accept': 'application/json'})
578+
return response, report_obj['reportFormat']
579+
580+
##
581+
#
582+
# (Global) Vulnerability reports
583+
#
584+
##
585+
valid_vuln_status_report_formats = ["CSV", "JSON"]
586+
def create_vuln_status_report(self, format="CSV"):
587+
assert format in HubInstance.valid_vuln_status_report_formats, "Format must be one of {}".format(HubInstance.valid_vuln_status_report_formats)
588+
589+
post_data = {
590+
"reportFormat": format,
591+
"locale": "en_US"
592+
}
593+
url = self.get_apibase() + "/vulnerability-status-reports"
594+
custom_headers = {
595+
'Content-Type': 'application/vnd.blackducksoftware.report-4+json',
596+
'Accept': 'application/vnd.blackducksoftware.report-4+json'
597+
}
598+
return self.execute_post(url, custom_headers=custom_headers, data=post_data)
599+
560600
##
561601
#
562602
# License stuff

blackduck/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
VERSION = (0, 0, 40)
1+
VERSION = (0, 0, 43)
22

33
__version__ = '.'.join(map(str, VERSION))

examples/add_custom_field_options.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
#!/usr/bin/env python
3+
4+
import argparse
5+
import json
6+
import logging
7+
import sys
8+
9+
from blackduck.HubRestApi import HubInstance
10+
11+
parser = argparse.ArgumentParser("Modify a custom field")
12+
parser.add_argument("object", choices=["BOM Component", "Component", "Component Version", "Project", "Project Version"], help="The object that the custom field should be attached to")
13+
parser.add_argument("field_id", help="The ID of the custom field to modify")
14+
parser.add_argument("-o", "--options", action='append', nargs=2, metavar=('label', 'position'), help="The options to add. To add more than one option repeat the -o option, supply a label and position for each possible selection. Used for DROPDOWN, MULTISELECT, and RADIO field types.")
15+
args = parser.parse_args()
16+
17+
18+
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', stream=sys.stderr, level=logging.DEBUG)
19+
logging.getLogger("requests").setLevel(logging.DEBUG)
20+
logging.getLogger("urllib3").setLevel(logging.WARNING)
21+
22+
options = [{"label": io[0], "position": io[1]} for io in args.options]
23+
24+
hub = HubInstance()
25+
26+
# delete all custom fields for the specified object type
27+
custom_fields = hub.get_custom_fields(args.object).get('items', [])
28+
for custom_field in custom_fields:
29+
url = custom_field['_meta']['href']
30+
field_id = url.split("/")[-1]
31+
if field_id == args.field_id:
32+
field_obj = hub.execute_get(url).json()
33+
34+
options_url = hub.get_link(field_obj, "custom-field-option-list")
35+
for option in options:
36+
response = hub.execute_post(options_url, data=option)
37+
if response.status_code == 201:
38+
print("Successfully added option {} to custom field {}".format(option, url))
39+
else:
40+
print("Failed to add option {} for custom field {}, status code: {}".format(
41+
option, url, response.status_code))
42+

examples/create_fix_it_message.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env python
2+
3+
import argparse
4+
from datetime import datetime
5+
import logging
6+
import json
7+
import sys
8+
9+
parser = argparse.ArgumentParser("Process the JSON output from get_bom_component_policy_violations.py to create a FIX IT message that guides the project team how to resolve the issues identified through policy rule violations")
10+
parser.add_argument("-f", "--policy_violations_file", help="By default, program reads JSON doc from stdin, but you can alternatively give a file name")
11+
parser.add_argument("-o", "--output_file", help="By default, the fix it message is written to stdout. Use this option to instead write to a file")
12+
13+
args = parser.parse_args()
14+
15+
logging.basicConfig(format='%(asctime)s%(levelname)s:%(message)s', stream=sys.stderr, level=logging.DEBUG)
16+
logging.getLogger("requests").setLevel(logging.WARNING)
17+
logging.getLogger("urllib3").setLevel(logging.WARNING)
18+
19+
if args.policy_violations_file:
20+
with open(args.policy_violations_file, 'r') as pvf:
21+
policy_violations = json.load(pvf)
22+
else:
23+
policy_violations = json.load(sys.stdin)
24+
25+
project_name = policy_violations['project']['name']
26+
version_label = policy_violations['version']['versionName']
27+
version_url = policy_violations['version']['_meta']['href']
28+
version_license = policy_violations['version']['license']['licenseDisplay']
29+
if version_license.lower() == "unknown license":
30+
license_statement = """This project version does not appear to have a declared license.
31+
You should probably declare one.. You can find more info on how to choose the appropriate license
32+
for your project <a href=https://our-company/choosing-license>here</a>"""
33+
else:
34+
license_statement = "This project version is governed by the {} license.".format(version_license)
35+
36+
html = """
37+
<html>
38+
<body>
39+
<p>This is a summary of the policy violations found in project {}, version {}. It was created {}.
40+
This summary provides guidance on what to do to resolve these violations. </p>
41+
<p>{}</p>
42+
<p>You can view the project version and more details regarding it
43+
on Black Duck <a href={}>here</a></p>
44+
<p>For more information on SCA (Software Composition Analysis) and
45+
how it fits into our SDLC go to <a href=https://our-company/sca-info>https://our-company/sca-info</a></p>
46+
47+
<table border=1>
48+
<tr>
49+
<th>Component</th>
50+
<th>Version</th>
51+
<th>Component License</th>
52+
<th>URL</th>
53+
<th>Violation</th>
54+
<th>Guidance to Fix</th>
55+
<th>Overrideable</th>
56+
<th>Severity</th>
57+
</tr>
58+
""".format(project_name, version_label, datetime.now(), license_statement, version_url)
59+
60+
del policy_violations['project']
61+
del policy_violations['version']
62+
63+
for violation_info in policy_violations.values():
64+
component_name = violation_info['bom_component']['componentName']
65+
component_version = violation_info['bom_component'].get('componentVersionName')
66+
component_url = violation_info['bom_component']['component']
67+
# if component-version URL is available use it, but if not revert to the component URL
68+
component_version_url = violation_info['bom_component'].get('componentVersion', component_url)
69+
component_license = ",".join([l['licenseDisplay'] for l in violation_info['bom_component']['licenses']])
70+
policies_in_violation = violation_info['policies_in_violation']['totalCount']
71+
for policy_violation in violation_info['policies_in_violation']['items']:
72+
pv_name = policy_violation['name']
73+
pv_description = policy_violation['description']
74+
overridable = policy_violation['overridable']
75+
severity = policy_violation['severity']
76+
html += """
77+
<tr>
78+
<td>{}</td>
79+
<td>{}</td>
80+
<td>{}</td>
81+
<td><a href={}>click here to view more info on this component from Black Duck</a></td>
82+
<td>{}</td>
83+
<td>{}</td>
84+
<td>{}</td>
85+
<td>{}</td>
86+
</tr>
87+
""".format(component_name, component_version, component_license, component_version_url, pv_name, pv_description, overridable, severity)
88+
89+
html += """
90+
</table>
91+
</body>
92+
</html>
93+
"""
94+
95+
if args.output_file:
96+
output_file = open(args.output_file, 'w')
97+
else:
98+
output_file = sys.stdout
99+
100+
output_file.write(html)
101+
102+
103+
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
#!/usr/bin/env python
3+
4+
import argparse
5+
import json
6+
import logging
7+
import sys
8+
9+
from blackduck.HubRestApi import HubInstance
10+
11+
parser = argparse.ArgumentParser("Modify a custom field option")
12+
parser.add_argument("object", choices=["BOM Component", "Component", "Component Version", "Project", "Project Version"], help="The object that the custom field should be attached to")
13+
parser.add_argument("field_id", help="The ID of the custom field to modify")
14+
parser.add_argument("option_id", help="The ID of the custom field option to modify")
15+
args = parser.parse_args()
16+
17+
18+
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', stream=sys.stderr, level=logging.DEBUG)
19+
logging.getLogger("requests").setLevel(logging.DEBUG)
20+
logging.getLogger("urllib3").setLevel(logging.WARNING)
21+
22+
hub = HubInstance()
23+
24+
# delete all custom fields for the specified object type
25+
custom_fields = hub.get_custom_fields(args.object).get('items', [])
26+
for custom_field in custom_fields:
27+
field_url = custom_field['_meta']['href']
28+
field_id = field_url.split("/")[-1]
29+
if field_id == args.field_id:
30+
field_obj = hub.execute_get(field_url).json()
31+
32+
options_url = hub.get_link(field_obj, "custom-field-option-list")
33+
options = hub.execute_get(options_url).json().get('items', [])
34+
for option in options:
35+
option_url = option['_meta']['href']
36+
option_id = option_url.split("/")[-1]
37+
if option_id == args.option_id:
38+
response = hub.execute_delete(option_url)
39+
if response.status_code == 204:
40+
print("Deleted option {}".format(option_url))
41+
else:
42+
print("Failed to delete option {}, status code: {}".format(option_url, response.status_code))

examples/generate_notices_report_for_project_version.py

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,42 +10,51 @@
1010

1111
import argparse
1212
import json
13+
import logging
14+
import sys
1315
import time
1416
import zipfile
1517

1618
parser = argparse.ArgumentParser("A program to generate the notices file for a given project-version")
1719
parser.add_argument("project_name")
1820
parser.add_argument("version_name")
19-
parser.add_argument("--zip_file_name", default="notices_report.zip")
20-
parser.add_argument("--reports",
21-
default="version,scans,components,vulnerabilities,source",
22-
help="Comma separated list (no spaces) of the reports to generate - version, scans, components, vulnerabilities, source, and cryptography reports (default: all, except cryptography")
23-
parser.add_argument('--format', default='TEXT', choices=["HTML", "TEXT"], help="Report format - choices are TEXT or HTML")
21+
parser.add_argument('-f', "--file_name_base", default="notices_report", help="Base file name to write the report data into. If the report format is TEXT a .zip file will be created, otherwise a .json file")
22+
parser.add_argument('-r', '--report_format', default='TEXT', choices=["JSON", "TEXT"], help="Report format - choices are TEXT or HTML")
2423

2524
args = parser.parse_args()
2625

2726
hub = HubInstance()
2827

29-
# TODO: Promote this to the API?
28+
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', stream=sys.stderr, level=logging.DEBUG)
29+
3030
class FailedReportDownload(Exception):
3131
pass
3232

33-
def download_report(location, filename, retries=4):
33+
def download_report(location, file_name_base, retries=10):
3434
report_id = location.split("/")[-1]
3535

3636
if retries:
37-
print("Retrieving generated report from {}".format(location))
38-
response = hub.download_report(report_id)
37+
logging.debug("Retrieving generated report from {}".format(location))
38+
# response = hub.download_report(report_id)
39+
response, report_format = hub.download_notification_report(location)
3940
if response.status_code == 200:
40-
with open(filename, "wb") as f:
41-
f.write(response.content)
42-
print("Successfully downloaded zip file to {} for report {}".format(filename, report_id))
41+
if report_format == "TEXT":
42+
filename = file_name_base + ".zip"
43+
with open(filename, "wb") as f:
44+
f.write(response.content)
45+
else:
46+
# JSON format
47+
filename = file_name_base + ".json"
48+
with open(filename, "w") as f:
49+
json.dump(response.json(), f, indent=3)
50+
logging.info("Successfully downloaded json file to {} for report {}".format(
51+
filename, report_id))
4352
else:
44-
print("Failed to retrieve report {}".format(report_id))
45-
print("Probably not ready yet, waiting 5 seconds then retrying...")
53+
logging.warning("Failed to retrieve report {}".format(report_id))
54+
logging.warning("Probably not ready yet, waiting 5 seconds then retrying (remaining retries={}".format(retries))
4655
time.sleep(5)
4756
retries -= 1
48-
download_report(location, filename, retries)
57+
download_report(location, file_name_base, retries)
4958
else:
5059
raise FailedReportDownload("Failed to retrieve report {} after multiple retries".format(report_id))
5160

@@ -54,22 +63,23 @@ def download_report(location, filename, retries=4):
5463
if project:
5564
version = hub.get_version_by_name(project, args.version_name)
5665

57-
response = hub.create_version_notices_report(version, args.format)
66+
response = hub.create_version_notices_report(version, args.report_format)
5867

5968
if response.status_code == 201:
60-
print("Successfully created reports ({}) for project {} and version {}".format(
61-
args.reports, args.project_name, args.version_name))
69+
logging.info("Successfully created notices report in {} format for project {} and version {}".format(
70+
args.report_format, args.project_name, args.version_name))
6271
location = response.headers['Location']
63-
download_report(location, args.zip_file_name)
72+
download_report(location, args.file_name_base)
73+
6474

6575
# Showing how you can interact with the downloaded zip and where to find the
6676
# output content. Uncomment the lines below to see how it works.
6777

68-
# with zipfile.ZipFile(zip_file_name, 'r') as zipf:
78+
# with zipfile.ZipFile(zip_file_name_base, 'r') as zipf:
6979
# with zipf.open("{}/{}/version-license.txt".format(args.project_name, args.version_name), "r") as license_file:
7080
# print(license_file.read())
7181
else:
72-
print("Failed to create reports for project {} version {}, status code returned {}".format(
82+
logging.error("Failed to create reports for project {} version {}, status code returned {}".format(
7383
args.project_name, args.version_name, response.status_code))
7484
else:
75-
print("Did not find project with name {}".format(args.project_name))
85+
logging.warning("Did not find project with name {}".format(args.project_name))

0 commit comments

Comments
 (0)