Skip to content

Commit c3d7c59

Browse files
author
Glenn Snyder
committed
adding sample for generating SBOM report
1 parent f7974b9 commit c3d7c59

File tree

3 files changed

+127
-17
lines changed

3 files changed

+127
-17
lines changed

README.md

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,6 @@
22

33
The hub-rest-api-python provides Python bindings for Hub REST API.
44

5-
:warning:Recently [CVE-2020-27589](https://nvd.nist.gov/vuln/detail/CVE-2020-27589), a medium severity security defect,
6-
was discovered in the [blackduck PyPi](https://pypi.org/project/blackduck) library which affects versions 0.0.25 – 0.0.52
7-
that could suppress certificate validation if the calling code used either the upload_scan or download_project_scans
8-
methods. These methods did not enforce certificate validation. Other methods in the library are not affected.
9-
The defect was fixed in version 0.0.53.
10-
11-
Customers using the [blackduck library](https://pypi.org/project/blackduck) should upgrade to version 0.0.53, or later, to implement the fix.
12-
135
# Paging and Black Duck v2022.2
146

157
In v2022.2 of Black Duck the REST API introduced a max page size to protect system resource usage. See the Black Duck [release notes on Synopsys Community](https://community.synopsys.com/s/article/Black-Duck-Release-Notes) for the details of which API endpoints are affected. Users of the the python bindings here should leverage the Client interface which provides automatic paging support to make best use of these endpoints.
@@ -23,17 +15,12 @@ Introducing the new Client class.
2315
In order to provide a more robust long-term connection, faster performance, and an overall better experience a new
2416
Client class has been designed.
2517

26-
It is backed by a [Requests session](https://docs.python-requests.org/en/master/user/advanced/#session-objects)
27-
object. The user specifies a base URL, timeout, retries, proxies, and TLS verification upon initialization and these
28-
attributes are persisted across all requests.
18+
It is backed by a [Requests session](https://docs.python-requests.org/en/master/user/advanced/#session-objects) object. The user specifies a base URL, timeout, retries, proxies, and TLS verification upon initialization and these attributes are persisted across all requests.
2919

3020
At the REST API level, the Client class provides a consistent way to discover and traverse public resources, uses a
31-
[generator](https://wiki.python.org/moin/Generators) to fetch all items using pagination, and automatically renews
32-
the bearer token.
21+
[generator](https://wiki.python.org/moin/Generators) to fetch all items using pagination, and automatically renews the bearer token.
3322

34-
See [Client versus HubInstance Comparison](https://github.com/blackducksoftware/hub-rest-api-python/wiki/Client-versus-HubInstance-Comparison)
35-
and also read the [Client User Guide](https://github.com/blackducksoftware/hub-rest-api-python/wiki/Client-User-Guide)
36-
on the [Hub REST API Python Wiki](https://github.com/blackducksoftware/hub-rest-api-python/wiki).
23+
See [Client versus HubInstance Comparison](https://github.com/blackducksoftware/hub-rest-api-python/wiki/Client-versus-HubInstance-Comparison) and also read the [Client User Guide](https://github.com/blackducksoftware/hub-rest-api-python/wiki/Client-User-Guide) on the [Hub REST API Python Wiki](https://github.com/blackducksoftware/hub-rest-api-python/wiki).
3724

3825
### Important Notes
3926
The old HubInstance (in HubRestApi.py) keeps its existing functionality for backwards compatibility and therefore does **not** currently leverage any of the new features in the Client class.

blackduck/__version__.py

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

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

examples/client/generate_sbom.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
'''
2+
Created on July 8, 2021
3+
4+
@author: gsnyder
5+
6+
Generate SBOM for a given project-version
7+
8+
'''
9+
10+
from blackduck import Client
11+
12+
import argparse
13+
import json
14+
import logging
15+
import sys
16+
import time
17+
18+
logging.basicConfig(
19+
level=logging.DEBUG,
20+
format="[%(asctime)s] {%(module)s:%(lineno)d} %(levelname)s - %(message)s"
21+
)
22+
23+
24+
# version_name_map = {
25+
# 'version': 'VERSION',
26+
# 'scans': 'CODE_LOCATIONS',
27+
# 'components': 'COMPONENTS',
28+
# 'vulnerabilities': 'SECURITY',
29+
# 'source':'FILES',
30+
# 'cryptography': 'CRYPTO_ALGORITHMS',
31+
# 'license_terms': 'LICENSE_TERM_FULFILLMENT',
32+
# 'component_additional_fields': 'BOM_COMPONENT_CUSTOM_FIELDS',
33+
# 'project_version_additional_fields': 'PROJECT_VERSION_CUSTOM_FIELDS',
34+
# 'vulnerability_matches': 'VULNERABILITY_MATCH'
35+
# }
36+
37+
# all_reports = list(version_name_map.keys())
38+
39+
class FailedReportDownload(Exception):
40+
pass
41+
42+
43+
parser = argparse.ArgumentParser("A program to create an SBOM for a given project-version")
44+
parser.add_argument("bd_url", help="Hub server URL e.g. https://your.blackduck.url")
45+
parser.add_argument("token_file", help="containing access token")
46+
parser.add_argument("project_name")
47+
parser.add_argument("version_name")
48+
parser.add_argument("-z", "--zip_file_name", default="reports.zip")
49+
# parser.add_argument("-r", "--reports",
50+
# default=",".join(all_reports),
51+
# help=f"Comma separated list (no spaces) of the reports to generate - {list(version_name_map.keys())}. Default is all reports.")
52+
# parser.add_argument('--format', default='CSV', choices=["CSV"], help="Report format - only CSV available for now")
53+
parser.add_argument('-t', '--tries', default=4, type=int, help="How many times to retry downloading the report, i.e. wait for the report to be generated")
54+
parser.add_argument('-s', '--sleep_time', default=5, type=int, help="The amount of time to sleep in-between (re-)tries to download the report")
55+
parser.add_argument('--no-verify', dest='verify', action='store_false', help="disable TLS certificate verification")
56+
57+
args = parser.parse_args()
58+
59+
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', stream=sys.stderr, level=logging.DEBUG)
60+
logging.getLogger("requests").setLevel(logging.WARNING)
61+
logging.getLogger("urllib3").setLevel(logging.WARNING)
62+
logging.getLogger("blackduck").setLevel(logging.WARNING)
63+
64+
65+
def download_report(bd_client, location, filename, retries=args.tries):
66+
report_id = location.split("/")[-1]
67+
if retries:
68+
logging.debug(f"Retrieving generated report from {location}")
69+
response = bd.session.get(location)
70+
report_status = response.json().get('status', 'Not Ready')
71+
if response.status_code == 200 and report_status == 'COMPLETED':
72+
response = bd.session.get(location + "/download.zip", headers={'Content-Type': 'application/zip', 'Accept':'application/zip'})
73+
if response.status_code == 200:
74+
with open(filename, "wb") as f:
75+
f.write(response.content)
76+
logging.info(f"Successfully downloaded zip file to {filename} for report {report_id}")
77+
else:
78+
logging.error("Ruh-roh, not sure what happened here")
79+
else:
80+
logging.debug(f"Failed to retrieve report {report_id}, report status: {report_status}")
81+
logging.debug("Probably not ready yet, waiting 5 seconds then retrying...")
82+
time.sleep(args.sleep_time)
83+
retries -= 1
84+
download_report(bd_client, location, filename, retries)
85+
else:
86+
raise FailedReportDownload(f"Failed to retrieve report {report_id} after multiple retries")
87+
88+
with open(args.token_file, 'r') as tf:
89+
access_token = tf.readline().strip()
90+
91+
bd = Client(base_url=args.bd_url, token=access_token, verify=args.verify)
92+
93+
params = {
94+
'q': [f"name:{args.project_name}"]
95+
}
96+
projects = [p for p in bd.get_resource('projects', params=params) if p['name'] == args.project_name]
97+
assert len(projects) == 1, f"There should be one, and only one project named {args.project_name}. We found {len(projects)}"
98+
project = projects[0]
99+
100+
params = {
101+
'q': [f"versionName:{args.version_name}"]
102+
}
103+
versions = [v for v in bd.get_resource('versions', project, params=params) if v['versionName'] == args.version_name]
104+
assert len(versions) == 1, f"There should be one, and only one version named {args.version_name}. We found {len(versions)}"
105+
version = versions[0]
106+
107+
logging.debug(f"Found {project['name']}:{version['versionName']}")
108+
109+
post_data = {
110+
'reportFormat': "JSON",
111+
'reportType': 'SBOM',
112+
'sbomType': "SPDX_22"
113+
}
114+
sbom_reports_url = version['_meta']['href'] + "/sbom-reports"
115+
116+
r = bd.session.post(sbom_reports_url, json=post_data)
117+
r.raise_for_status()
118+
location = r.headers.get('Location')
119+
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"
120+
121+
logging.debug(f"Created SBOM report for project {args.project_name}, version {args.version_name} at location {location}")
122+
download_report(bd, location, args.zip_file_name)
123+

0 commit comments

Comments
 (0)