Skip to content

Commit 4e35214

Browse files
dnicholdnichol
authored andcommitted
Hierarchy source report - basic structure and report generation
1 parent 03ec6d2 commit 4e35214

File tree

1 file changed

+265
-0
lines changed

1 file changed

+265
-0
lines changed
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
'''
2+
Created on June 25, 2024
3+
4+
@author: dnichol and kumykov
5+
6+
Generate version detail reports (source and components) and consolidate information on source matches, with license
7+
and component matched. Removes matches found underneith other matched components in the source tree (configurable).
8+
9+
Copyright (C) 2023 Synopsys, Inc.
10+
http://www.synopsys.com/
11+
12+
Licensed to the Apache Software Foundation (ASF) under one
13+
or more contributor license agreements. See the NOTICE file
14+
distributed with this work for additional information
15+
regarding copyright ownership. The ASF licenses this file
16+
to you under the Apache License, Version 2.0 (the
17+
"License"); you may not use this file except in compliance
18+
with the License. You may obtain a copy of the License at
19+
20+
http://www.apache.org/licenses/LICENSE-2.0
21+
22+
Unless required by applicable law or agreed to in writing,
23+
software distributed under the License is distributed on an
24+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
25+
KIND, either express or implied. See the License for the
26+
specific language governing permissions and limitations
27+
under the License.
28+
'''
29+
30+
import argparse
31+
import logging
32+
import sys
33+
import os
34+
import re
35+
import time
36+
import subprocess
37+
import json
38+
import traceback
39+
import copy
40+
import ijson
41+
from blackduck import Client
42+
from zipfile import ZipFile
43+
44+
program_description = \
45+
'''Generate version detail reports (source and components) and consolidate information on source matches, with license
46+
and component matched. Removes matches found underneith other matched components in the source tree (configurable).
47+
48+
This script assumes a project version exists and has scans associated with it (i.e. the project is not scanned as part of this process).
49+
50+
Config file:
51+
API Token and Black Duck URL need to be placed in the .restconfig.json file which must be placed in the same folder where this script resides.
52+
{
53+
"baseurl": "https://hub-hostname",
54+
"api_token": "<API token goes here>",
55+
"insecure": true or false <Default is false>,
56+
"debug": true or false <Default is false>
57+
}
58+
59+
Remarks:
60+
This script uses 3rd party PyPI package "ijson". This package must be installed.
61+
'''
62+
63+
# BD report general
64+
BLACKDUCK_REPORT_MEDIATYPE = "application/vnd.blackducksoftware.report-4+json"
65+
blackduck_report_download_api = "/api/projects/{projectId}/versions/{projectVersionId}/reports/{reportId}/download"
66+
# BD version details report
67+
blackduck_create_version_report_api = "/api/versions/{projectVersionId}/reports"
68+
blackduck_version_report_filename = "./blackduck_version_report_for_{projectVersionId}.zip"
69+
# Consolidated report
70+
BLACKDUCK_VERSION_MEDIATYPE = "application/vnd.blackducksoftware.status-4+json"
71+
BLACKDUCK_VERSION_API = "/api/current-version"
72+
REPORT_DIR = "./blackduck_component_source_report"
73+
# Retries to wait for BD report creation. RETRY_LIMIT can be overwritten by the script parameter.
74+
RETRY_LIMIT = 30
75+
RETRY_TIMER = 30
76+
77+
def log_config(debug):
78+
if debug:
79+
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(module)s: %(message)s', stream=sys.stderr, level=logging.DEBUG)
80+
else:
81+
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(module)s: %(message)s', stream=sys.stderr, level=logging.INFO)
82+
logging.getLogger("requests").setLevel(logging.WARNING)
83+
logging.getLogger("urllib3").setLevel(logging.WARNING)
84+
logging.getLogger("blackduck").setLevel(logging.WARNING)
85+
86+
def parse_parameter():
87+
parser = argparse.ArgumentParser(description=program_description, formatter_class=argparse.RawTextHelpFormatter)
88+
parser.add_argument("project",
89+
metavar="project",
90+
type=str,
91+
help="Provide the BlackDuck project name.")
92+
parser.add_argument("version",
93+
metavar="version",
94+
type=str,
95+
help="Provide the BlackDuck project version name.")
96+
parser.add_argument("-kh",
97+
"--keep_hierarchy",
98+
action='store_true',
99+
help="Set to keep all entries in the sources report. Will not remove components found under others.")
100+
parser.add_argument("-rr",
101+
"--report_retries",
102+
metavar="",
103+
type=int,
104+
default=RETRY_LIMIT,
105+
help="Retries for receiving the generated BlackDuck report. Generating copyright report tends to take longer minutes.")
106+
parser.add_argument("-t",
107+
"--timeout",
108+
metavar="",
109+
type=int,
110+
default=15,
111+
help="Timeout for REST-API. Some API may take longer than the default 15 seconds")
112+
parser.add_argument("-r",
113+
"--retries",
114+
metavar="",
115+
type=int,
116+
default=3,
117+
help="Retries for REST-API. Some API may need more retries than the default 3 times")
118+
return parser.parse_args()
119+
120+
def get_bd_project_data(hub_client, project_name, version_name):
121+
""" Get and return project ID, version ID. """
122+
project_id = ""
123+
for project in hub_client.get_resource("projects"):
124+
if project['name'] == project_name:
125+
project_id = (project['_meta']['href']).split("projects/", 1)[1]
126+
break
127+
if project_id == "":
128+
sys.exit(f"No project for {project_name} was found!")
129+
version_id = codelocations = ""
130+
for version in hub_client.get_resource("versions", project):
131+
if version['versionName'] == version_name:
132+
version_id = (version['_meta']['href']).split("versions/", 1)[1]
133+
break
134+
if version_id == "":
135+
sys.exit(f"No project version for {version_name} was found!")
136+
137+
return project_id, version_id
138+
139+
def report_create(hub_client, url, body):
140+
"""
141+
Request BlackDuck to create report. Requested report is included in the request payload.
142+
"""
143+
res = hub_client.session.post(url, headers={'Content-Type': BLACKDUCK_REPORT_MEDIATYPE}, json=body)
144+
if res.status_code != 201:
145+
sys.exit(f"BlackDuck report creation failed with status {res.status_code}!")
146+
return res.headers['Location'] # return report_url
147+
148+
def report_download(hub_client, report_url, project_id, version_id, retries):
149+
"""
150+
Download the generated report after the report completion. We will retry until reaching the retry-limit.
151+
"""
152+
while retries:
153+
res = hub_client.session.get(report_url, headers={'Accept': BLACKDUCK_REPORT_MEDIATYPE})
154+
if res.status_code == 200 and (json.loads(res.content))['status'] == "COMPLETED":
155+
report_id = report_url.split("reports/", 1)[1]
156+
download_url = (((blackduck_report_download_api.replace("{projectId}", project_id))
157+
.replace("{projectVersionId}", version_id))
158+
.replace("{reportId}", report_id))
159+
res = hub_client.session.get(download_url,
160+
headers={'Content-Type': 'application/zip', 'Accept':'application/zip'})
161+
if res.status_code != 200:
162+
sys.exit(f"BlackDuck report download failed with status {res.status_code} for {download_url}!")
163+
return res.content
164+
elif res.status_code != 200:
165+
sys.exit(f"BlackDuck report creation not completed successfully with status {res.status_code}")
166+
else:
167+
retries -= 1
168+
logging.info(f"Waiting for the report generation for {report_url} with the remaining retries {retries} times.")
169+
time.sleep(RETRY_TIMER)
170+
sys.exit(f"BlackDuck report for {report_url} was not generated after retries {RETRY_TIMER} sec * {retries} times!")
171+
172+
def get_version_detail_report(hub_client, project_id, version_id, retries):
173+
""" Create and get BOM component and BOM source file report in json. """
174+
create_version_url = blackduck_create_version_report_api.replace("{projectVersionId}", version_id)
175+
body = {
176+
'reportFormat' : 'JSON',
177+
'locale' : 'en_US',
178+
'versionId' : f'{version_id}',
179+
'categories' : [ 'COMPONENTS', 'FILES' ] # Generating "project version" report including components and files
180+
}
181+
report_url = report_create(hub_client, create_version_url, body)
182+
# Zipped report content is received and write the content to a local zip file
183+
content = report_download(hub_client, report_url, project_id, version_id, retries)
184+
output_file = blackduck_version_report_filename.replace("{projectVersionId}", version_id)
185+
with open(output_file, "wb") as f:
186+
f.write(content)
187+
return output_file
188+
189+
def get_blackduck_version(hub_client):
190+
url = hub_client.base_url + BLACKDUCK_VERSION_API
191+
res = hub_client.session.get(url)
192+
if res.status_code == 200 and res.content:
193+
return json.loads(res.content)['version']
194+
else:
195+
sys.exit(f"Get BlackDuck version failed with status {res.status_code}")
196+
197+
def generate_file_report(hub_client, project_id, version_id, keep_hierarchy, retries):
198+
"""
199+
Create a consolidated file report from BlackDuck project version source and components reports.
200+
Remarks:
201+
"""
202+
if not os.path.exists(REPORT_DIR):
203+
os.makedirs(REPORT_DIR)
204+
205+
# Report body - Component BOM, file BOM with Discoveries data
206+
version_report_zip = get_version_detail_report(hub_client, project_id, version_id, retries)
207+
with ZipFile(f"./{version_report_zip}", "r") as vzf:
208+
vzf.extractall()
209+
for i, unzipped_version in enumerate(vzf.namelist()):
210+
if re.search(r"\bversion.+json\b", unzipped_version) is not None:
211+
break
212+
if i + 1 >= len(vzf.namelist()):
213+
sys.exit(f"Version detail file not found in the downloaded report: {version_report_zip}!")
214+
215+
# Report body - Component BOM report
216+
with open(f"./{unzipped_version}", "r") as uvf:
217+
for i, comp_bom in enumerate(ijson.items(uvf, 'aggregateBomViewEntries.item')):
218+
logging.info(f"{comp_bom['componentName']}")
219+
logging.info(f"Number of the reported components {i+1}")
220+
221+
222+
def main():
223+
args = parse_parameter()
224+
debug = 0
225+
try:
226+
if args.project == "":
227+
sys.exit("Please set BlackDuck project name!")
228+
if args.version == "":
229+
sys.exit("Please set BlackDuck project version name!")
230+
231+
with open(".restconfig.json", "r") as f:
232+
config = json.load(f)
233+
# Remove last slash if there is, otherwise REST API may fail.
234+
if re.search(r".+/$", config['baseurl']):
235+
bd_url = config['baseurl'][:-1]
236+
else:
237+
bd_url = config['baseurl']
238+
bd_token = config['api_token']
239+
bd_insecure = not config['insecure']
240+
if config['debug']:
241+
debug = 1
242+
243+
log_config(debug)
244+
245+
hub_client = Client(token=bd_token,
246+
base_url=bd_url,
247+
verify=bd_insecure,
248+
timeout=args.timeout,
249+
retries=args.retries)
250+
251+
project_id, version_id = get_bd_project_data(hub_client, args.project, args.version)
252+
253+
generate_file_report(hub_client,
254+
project_id,
255+
version_id,
256+
args.keep_hierarchy,
257+
args.report_retries
258+
)
259+
260+
except (Exception, BaseException) as err:
261+
logging.error(f"Exception by {str(err)}. See the stack trace")
262+
traceback.print_exc()
263+
264+
if __name__ == '__main__':
265+
sys.exit(main())

0 commit comments

Comments
 (0)