Skip to content

Commit 4dde441

Browse files
committed
script to convert BOM to flat SBOM added
1 parent 70180e2 commit 4dde441

File tree

1 file changed

+255
-0
lines changed

1 file changed

+255
-0
lines changed

examples/client/sbomify.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#!/usr/bin/env python3
2+
'''
3+
Created: Apr 2, 2024
4+
Author: @kumykov
5+
6+
Copyright (c) 2024, Synopsys, Inc.
7+
http://www.synopsys.com/
8+
9+
Licensed to the Apache Software Foundation (ASF) under one
10+
or more contributor license agreements. See the NOTICE file
11+
distributed with this work for additional information
12+
regarding copyright ownership. The ASF licenses this file
13+
to you under the Apache License, Version 2.0 (the
14+
"License"); you may not use this file except in compliance
15+
with the License. You may obtain a copy of the License at
16+
17+
http://www.apache.org/licenses/LICENSE-2.0
18+
19+
Unless required by applicable law or agreed to in writing,
20+
software distributed under the License is distributed on an
21+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
22+
KIND, either express or implied. See the License for the
23+
specific language governing permissions and limitations
24+
under the License.
25+
26+
usage: sbomify.py [-h] -u BASE_URL -t TOKEN_FILE [-nv] -sp SOURCE_PROJECT -sv SOURCE_VERSION -tp TARGET_PROJECT -tv TARGET_VERSION
27+
[-tg TARGET_PROJECT_GROUP] [-c] [--sbom-type SBOM_TYPE]
28+
29+
Generate and download SBOM for the source project version
30+
and upload it to the target project version
31+
32+
options:
33+
-h, --help show this help message and exit
34+
-u BASE_URL, --base-url BASE_URL
35+
Hub server URL e.g. https://your.blackduck.url
36+
-t TOKEN_FILE, --token-file TOKEN_FILE
37+
File containing access token
38+
-nv, --no-verify Disable TLS certificate verification
39+
-sp SOURCE_PROJECT, --source-project SOURCE_PROJECT
40+
Source Project Name
41+
-sv SOURCE_VERSION, --source-version SOURCE_VERSION
42+
Source Project Version Name
43+
-tp TARGET_PROJECT, --target-project TARGET_PROJECT
44+
Target Project Name
45+
-tv TARGET_VERSION, --target-version TARGET_VERSION
46+
Target Project Version Name
47+
-tg TARGET_PROJECT_GROUP, --target-project-group TARGET_PROJECT_GROUP
48+
Project Group to use for target
49+
-c, --create-target Create target project version if does not exist
50+
--sbom-type {SPDX_22,SPDX_23,CYCLONEDX_13,CYCLONEDX_14}
51+
SBOM type to use for transaction
52+
53+
Black Duck examples collection
54+
55+
56+
'''
57+
import argparse
58+
import io
59+
import json
60+
import sys
61+
import logging
62+
import time
63+
64+
from zipfile import ZipFile
65+
from blackduck import Client
66+
67+
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', stream=sys.stderr, level=logging.DEBUG)
68+
logging.getLogger("requests").setLevel(logging.WARNING)
69+
logging.getLogger("urllib3").setLevel(logging.WARNING)
70+
logging.getLogger("blackduck").setLevel(logging.WARNING)
71+
72+
73+
def find_project_by_name(bd, project_name):
74+
params = {
75+
'q': [f"name:{project_name}"]
76+
}
77+
projects = [p for p in bd.get_resource('projects', params=params) if p['name'].casefold() == project_name.casefold()]
78+
if len(projects) == 1:
79+
return projects[0]
80+
else:
81+
return None
82+
83+
def find_project_version_by_name(bd, project, version_name):
84+
params = {
85+
'q': [f"versionName:{version_name}"]
86+
}
87+
versions = [v for v in bd.get_resource('versions', project, params=params) if v['versionName'] == version_name]
88+
if len(versions) == 1:
89+
return versions[0]
90+
else:
91+
return None
92+
93+
def find_or_create_project_group(bd, group_name):
94+
url = '/api/project-groups'
95+
params = {
96+
'q': [f"name:{group_name}"]
97+
}
98+
groups = [p for p in bd.get_items(url, params=params) if p['name'] == group_name]
99+
if len(groups) == 0:
100+
headers = {
101+
'Accept': 'application/vnd.blackducksoftware.project-detail-5+json',
102+
'Content-Type': 'application/vnd.blackducksoftware.project-detail-5+json'
103+
}
104+
data = {
105+
'name': group_name
106+
}
107+
response = bd.session.post(url, headers=headers, json=data)
108+
return response.headers['Location']
109+
else:
110+
return groups[0]['_meta']['href']
111+
112+
def create_project_version(bd, project_name,version_name,project_group, nickname = None):
113+
version_data = {"distribution": "EXTERNAL", "phase": "DEVELOPMENT", "versionName": version_name}
114+
if nickname:
115+
version_data['nickname'] = nickname
116+
url = '/api/projects'
117+
project = find_project_by_name(bd, project_name)
118+
if project:
119+
data = version_data
120+
url = project['_meta']['href'] + '/versions'
121+
else:
122+
data = {"name": project_name,
123+
"projectGroup": find_or_create_project_group(bd, project_group),
124+
"versionRequest": version_data}
125+
return bd.session.post(url, json=data)
126+
127+
def locate_project_version(bd, project_name, version_name, group="Black Duck Project Groups", create=False):
128+
project = find_project_by_name(bd, project_name)
129+
version = None
130+
if project:
131+
version = find_project_version_by_name(bd, project, version_name)
132+
if version:
133+
pass
134+
elif create:
135+
version = create_project_version(bd, project_name, version_name, group)
136+
else:
137+
pass
138+
elif create:
139+
response = create_project_version(bd, project_name, version_name, group)
140+
logging.info(f"Project {project_name} : {version_name} creation completed with {response}")
141+
if response.ok:
142+
project = find_project_by_name(bd, project_name)
143+
version = find_project_version_by_name(bd, project, version_name)
144+
return version
145+
146+
def create_sbom_report(bd, version, type, include_subprojects):
147+
post_data = {
148+
'reportFormat': "JSON",
149+
'sbomType': type,
150+
'includeSubprojects': include_subprojects
151+
}
152+
sbom_reports_url = version['_meta']['href'] + "/sbom-reports"
153+
154+
bd.session.headers["Content-Type"] = "application/vnd.blackducksoftware.report-4+json"
155+
r = bd.session.post(sbom_reports_url, json=post_data)
156+
if (r.status_code == 403):
157+
logging.debug("Authorization Error - Please ensure the token you are using has write permissions!")
158+
r.raise_for_status()
159+
location = r.headers.get('Location')
160+
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"
161+
return location
162+
163+
def download_report(bd, location, retries):
164+
report_id = location.split("/")[-1]
165+
if retries:
166+
logging.debug(f"Retrieving generated report from {location}")
167+
response = bd.session.get(location)
168+
report_status = response.json().get('status', 'Not Ready')
169+
if response.status_code == 200 and report_status == 'COMPLETED':
170+
response = bd.session.get(location + "/download.zip", headers={'Content-Type': 'application/zip', 'Accept':'application/zip'})
171+
if response.status_code == 200:
172+
return response.content
173+
else:
174+
logging.error("Ruh-roh, not sure what happened here")
175+
return None
176+
else:
177+
logging.debug(f"Report status request {response.status_code} {report_status} ,waiting {retries} seconds then retrying...")
178+
time.sleep(60)
179+
retries -= 1
180+
return download_report(bd, location, retries)
181+
else:
182+
logging.debug(f"Failed to retrieve report {report_id} after multiple retries")
183+
return None
184+
185+
def produce_online_sbom_report(bd, project_name, project_version_name, sbom_type):
186+
project = find_project_by_name(bd, project_name)
187+
logging.debug(f"Project {project['name']} located")
188+
version = find_project_version_by_name(bd, project, project_version_name)
189+
logging.debug(f"Version {version['versionName']} located")
190+
location = create_sbom_report(bd, version, sbom_type, True)
191+
logging.debug(f"Created SBOM report of type {sbom_type} for project {project_name}, version {project_version_name} at location {location}")
192+
sbom_data_zip = download_report(bd, location, 60)
193+
logging.debug(f"Deleting report from Black Duck {bd.session.delete(location)}")
194+
zip=ZipFile(io.BytesIO(sbom_data_zip), "r")
195+
sbom_data = {name: zip.read(name) for name in zip.namelist()}
196+
filename = [i for i in sbom_data.keys() if i.endswith(".json")][0]
197+
return json.loads(sbom_data[filename])
198+
199+
def upload_sbom_file(bd, project_name, version_name, sbom_data):
200+
if sbom_data.get('bomFormat', None) == "CycloneDX":
201+
mime_type = 'application/vnd.cyclonedx'
202+
elif sbom_data.get('spdxVersion', None):
203+
mime_type = 'application/spdx'
204+
else:
205+
mime_type = None
206+
if not mime_type:
207+
logging.error(f"Could not identify file content for SBOM")
208+
sys.exit(1)
209+
logging.info(f"Mime type {mime_type} will be used for SBOM upload")
210+
files = {"file": ('sbom.json', json.dumps(sbom_data).encode('utf-8'), mime_type)}
211+
fields = {"projectName": project_name, "versionName": version_name}
212+
response = bd.session.post("/api/scan/data", files = files, data=fields)
213+
logging.info(f"SBOM Upload completed with {response}")
214+
if response.status_code == 409:
215+
logging.info(f"File SBOM is already mapped to a different project version")
216+
217+
def sbomify(bd, args):
218+
source = locate_project_version(bd, args.source_project, args.source_version)
219+
if not source:
220+
logging.error(f"Source project {args.source_project} : {args.source_version} not found. Exiting.")
221+
sys.exit(1)
222+
logging.info(f"Located source project {args.source_project} : {args.source_version}")
223+
sbom = produce_online_sbom_report(bd, args.source_project, args.source_version, args.sbom_type)
224+
bd.session.headers.pop('Content-Type')
225+
target = locate_project_version(bd, args.target_project, args.target_version, group=args.target_project_group, create=args.create_target)
226+
if not target:
227+
logging.error(f"Target project {args.target_project} : {args.target_version} not found. Exiting.")
228+
sys.exit(1)
229+
logging.info(f"Located target project {args.target_project} : {args.target_version}")
230+
upload_sbom_file(bd, args.target_project, args.target_version, sbom)
231+
232+
def parse_command_args():
233+
parser = argparse.ArgumentParser(prog = "sbomify.py", description="Generate and download SBOM and upload to the target project version", epilog="Blackduck examples collection")
234+
parser.add_argument("-u", "--base-url", required=True, help="Hub server URL e.g. https://your.blackduck.url")
235+
parser.add_argument("-t", "--token-file", required=True, help="File containing access token")
236+
parser.add_argument("-nv", "--no-verify", action='store_false', help="Disable TLS certificate verification")
237+
parser.add_argument("-sp", "--source-project", required=True, help="Source Project Name")
238+
parser.add_argument("-sv", "--source-version", required=True, help="Source Project Version Name")
239+
parser.add_argument("-tp", "--target-project", required=True, help="Target Project Name")
240+
parser.add_argument("-tv", "--target-version", required=True, help="Target Project Version Name")
241+
parser.add_argument("-tg", "--target-project-group", required=False, default='Black Duck Project Groups', help="Project Group to use for target")
242+
parser.add_argument("-c", "--create-target", action='store_true', help="Create target project version if does not exist")
243+
parser.add_argument("--sbom-type", required=False, default='SPDX_23', choices=["SPDX_22", "SPDX_23", "CYCLONEDX_13", "CYCLONEDX_14"], help="SBOM type to use for transaction")
244+
245+
return parser.parse_args()
246+
247+
def main():
248+
args = parse_command_args()
249+
with open(args.token_file, 'r') as tf:
250+
access_token = tf.readline().strip()
251+
bd = Client(base_url=args.base_url, token=access_token, verify=args.no_verify, timeout=60.0, retries=4)
252+
sbomify(bd, args)
253+
254+
if __name__ == "__main__":
255+
sys.exit(main())

0 commit comments

Comments
 (0)