Skip to content

Commit 2770afb

Browse files
Merge pull request #284 from blackducksoftware/LuckySkyWalker-refresh1
Create refresh_project_copyrights.py
2 parents 47cab2e + 5ed3923 commit 2770afb

File tree

1 file changed

+295
-0
lines changed

1 file changed

+295
-0
lines changed
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
# Iterate through components within a named project (or all) and named version (or all)
2+
# and refresh the copyrights of each - equivalent to clicking the UI refresh button
3+
# Use --debug for some feedback on progress
4+
# Ian Ashworth, May 2025
5+
#
6+
import http.client
7+
from sys import api_version
8+
import sys
9+
import csv
10+
import datetime
11+
from blackduck import Client
12+
import argparse
13+
import logging
14+
from pprint import pprint
15+
import array as arr
16+
17+
http.client._MAXHEADERS = 1000
18+
19+
logging.basicConfig(
20+
level=logging.INFO,
21+
format="[%(asctime)s] {%(module)s:%(lineno)d} %(levelname)s - %(message)s"
22+
)
23+
24+
def RepDebug(level, msg):
25+
if hasattr(args, 'debug') and level <= args.debug:
26+
print("dbg{" + str(level) + "} " + msg)
27+
return True
28+
return False
29+
30+
def RepWarning(msg):
31+
print("WARNING: " + msg)
32+
return True
33+
34+
35+
# Parse command line arguments
36+
parser = argparse.ArgumentParser("Refresh copyrights for project/version components")
37+
38+
parser.add_argument("--base-url", required=True, help="BD Hub server URL e.g. https://your.blackduck.url")
39+
parser.add_argument("--token-file", dest='token_file', required=True, help="File containing your access token")
40+
41+
parser.add_argument("--dump-data", dest='dump_data', action='store_true', help="Retain analysed data")
42+
parser.add_argument("--csv-file", dest='csv_file', help="File name for dumped data formatted as CSV")
43+
44+
parser.add_argument("--project", dest='project_name', help="Project name")
45+
parser.add_argument("--version", dest='version_name', help="Version name")
46+
47+
parser.add_argument("--max-projects", dest='max_projects', type=int, help="Maximum projects to inspect else all")
48+
parser.add_argument("--max-versions-per-project", dest='max_versions_per_project', type=int, help="Maximum versions per project to inspect else all")
49+
parser.add_argument("--max-components", dest='max_components', type=int, help="Maximum components to inspect in total else all")
50+
51+
parser.add_argument("--debug", dest='debug', type=int, default=0, help="Debug verbosity (0=none)")
52+
53+
parser.add_argument("--no-verify", dest='verify', action='store_false', help="Disable TLS certificate verification")
54+
parser.add_argument("-t", "--timeout", default=15, type=int, help="Adjust the (HTTP) session timeout value (default: 15s)")
55+
parser.add_argument("-r", "--retries", default=3, type=int, help="Adjust the number of retries on failure (default: 3)")
56+
57+
args = parser.parse_args()
58+
59+
# open the access token file
60+
with open(args.token_file, 'r') as tf:
61+
access_token = tf.readline().strip()
62+
63+
# access the Black Duck platform
64+
bd = Client(
65+
base_url=args.base_url,
66+
token=access_token,
67+
verify=args.verify,
68+
timeout=args.timeout,
69+
retries=args.retries,
70+
)
71+
72+
# initialise
73+
all_my_comp_data = []
74+
my_statistics = {}
75+
76+
77+
# version of components API to call
78+
comp_api_version = 6
79+
80+
comp_accept_version = "application/vnd.blackducksoftware.bill-of-materials-" + str(comp_api_version) + "+json"
81+
#comp_accept_version = "application/json"
82+
83+
comp_content_type = comp_accept_version
84+
85+
# header keys
86+
comp_lc_keys = {}
87+
comp_lc_keys['accept'] = comp_accept_version
88+
comp_lc_keys['content-type'] = comp_accept_version
89+
90+
# keyword arguments to pass to API call
91+
comp_kwargs={}
92+
comp_kwargs['headers'] = comp_lc_keys
93+
94+
95+
# version of API to call
96+
refresh_api_version = 4
97+
98+
refresh_accept_version = "application/vnd.blackducksoftware.copyright-" + str(refresh_api_version) + "+json"
99+
#refresh_accept_version = "application/json"
100+
101+
refresh_content_type = refresh_accept_version
102+
103+
104+
# header keys
105+
refresh_lc_keys = {}
106+
refresh_lc_keys['accept'] = refresh_accept_version
107+
refresh_lc_keys['content-type'] = refresh_accept_version
108+
109+
# keyword arguments to pass to API call
110+
refresh_kwargs={}
111+
refresh_kwargs['headers'] = refresh_lc_keys
112+
113+
114+
# zero our main counters
115+
my_statistics['_cntProjects'] = 0
116+
my_statistics['_cntVersions'] = 0
117+
my_statistics['_cntComponents'] = 0
118+
my_statistics['_cntRefresh'] = 0
119+
my_statistics['_cntNoOrigins'] = 0
120+
my_statistics['_cntNoIDs'] = 0
121+
122+
123+
# record any control values
124+
if args.project_name:
125+
my_statistics['_namedProject'] = args.project_name
126+
if args.version_name:
127+
my_statistics['_namedVersion'] = args.version_name
128+
129+
if args.max_projects:
130+
my_statistics['_maxProjects'] = args.max_projects
131+
if args.max_versions_per_project:
132+
my_statistics['_maxVersionsPerProject'] = args.max_versions_per_project
133+
if args.max_components:
134+
my_statistics['_maxComponents'] = args.max_components
135+
136+
now = datetime.datetime.now()
137+
print('Started: %s' % now.strftime("%Y-%m-%d %H:%M:%S"))
138+
139+
# check named project of specific interest
140+
if args.project_name:
141+
params = {
142+
'q': [f"name:{args.project_name}"]
143+
}
144+
projects = [p for p in bd.get_resource('projects', params=params) if p['name'] == args.project_name]
145+
146+
# must exist
147+
assert len(projects) > 0, f"There should be at least one - {len(projects)} project(s) noted"
148+
else:
149+
# all projects are in scope
150+
projects = bd.get_resource('projects')
151+
152+
# loop through projects list
153+
for this_project in projects:
154+
155+
# check if we have hit any limit
156+
if args.max_components and my_statistics['_cntComponents'] >= args.max_components:
157+
break
158+
159+
if args.max_projects and my_statistics['_cntProjects'] >= args.max_projects:
160+
break
161+
162+
my_statistics['_cntProjects'] += 1
163+
RepDebug(1, '## Project %d: %s' % (my_statistics['_cntProjects'], this_project['name']))
164+
165+
if args.version_name:
166+
# note the specific project version of interest
167+
params = {
168+
'q': [f"versionName:{args.version_name}"]
169+
}
170+
versions = [v for v in bd.get_resource('versions', this_project, params=params) if v['versionName'] == args.version_name]
171+
172+
# it must exist
173+
assert len(versions) > 0, f"There should be at least one - {len(versions)} version(s) noted"
174+
else:
175+
# all versions for this project are in scope
176+
versions = bd.get_resource('versions', this_project)
177+
178+
nVersionsPerProject = 0
179+
180+
for this_version in versions:
181+
182+
# check if we have hit any limit
183+
if args.max_components and my_statistics['_cntComponents'] >= args.max_components:
184+
# exit component loop - at the limit
185+
break
186+
187+
if args.max_versions_per_project and nVersionsPerProject >= args.max_versions_per_project:
188+
# exit loop - at the version per project limit
189+
break
190+
191+
nVersionsPerProject += 1
192+
my_statistics['_cntVersions'] += 1
193+
194+
# Announce
195+
# logging.debug(f"Found {this_project['name']}:{this_version['versionName']}")
196+
RepDebug(3, ' Version: %s' % this_version['versionName'])
197+
198+
199+
# iterate through all components for this project version
200+
for this_comp_data in bd.get_resource('components', this_version, **comp_kwargs):
201+
202+
if args.max_components and my_statistics['_cntComponents'] >= args.max_components:
203+
# exit component loop - at the limit
204+
break
205+
206+
my_statistics['_cntComponents'] += 1
207+
RepDebug(4, ' Component: %s (%s)' %
208+
(this_comp_data['componentName'], this_comp_data['componentVersionName']))
209+
210+
if this_comp_data['inputExternalIds'].__len__() > 0:
211+
inputExternalIds = this_comp_data['inputExternalIds'][0]
212+
else:
213+
my_statistics['_cntNoIDs'] += 1
214+
inputExternalIds = "n/a"
215+
RepDebug(2, ' ID: %s' % inputExternalIds)
216+
217+
218+
# refresh the copyrights for this component
219+
if this_comp_data['origins'].__len__() > 0:
220+
url = this_comp_data['origins'][0]['origin']
221+
else:
222+
# no origins
223+
RepWarning('No origin defined for [%s]' % this_comp_data['componentVersion'])
224+
# url = this_comp_data['componentVersion']
225+
url = ''
226+
227+
if len(url) > 0:
228+
# refresh end point
229+
url += "/copyrights-refresh"
230+
231+
try:
232+
response = bd.session.put(url, data=None, **refresh_kwargs)
233+
RepDebug(5,'Refresh response %s' % response)
234+
except urllib3.exceptions.ReadTimeoutError:
235+
print('Failed to confirm copyrights refresh')
236+
237+
my_statistics['_cntRefresh'] += 1
238+
else:
239+
my_statistics['_cntNoOrigins'] += 1
240+
url = 'n/a'
241+
242+
243+
# if recording the data - perhaps outputting to a CSV file
244+
if args.dump_data:
245+
my_data = {}
246+
my_data['componentName'] = this_comp_data['componentName']
247+
my_data['componentVersion'] = this_comp_data['componentVersionName']
248+
my_data['url'] = url
249+
250+
if hasattr(args, 'debug') and 5 <= args.debug:
251+
pprint(my_data)
252+
253+
# add to our list
254+
all_my_comp_data.append(my_data)
255+
256+
257+
# end of processing loop
258+
259+
now = datetime.datetime.now()
260+
print('Finished: %s' % now.strftime("%Y-%m-%d %H:%M:%S"))
261+
print('Summary:')
262+
pprint(my_statistics)
263+
264+
# if dumping data
265+
if args.dump_data:
266+
# if outputting to a CSV file
267+
if args.csv_file:
268+
'''Note: See the BD API doc and in particular .../api-doc/public.html#_bom_vulnerability_endpoints
269+
for a complete list of the fields available. The below code shows a subset of them just to
270+
illustrate how to write out the data into a CSV format.
271+
'''
272+
logging.info(f"Exporting {len(all_my_comp_data)} records to CSV file {args.csv_file}")
273+
274+
with open(args.csv_file, 'w') as csv_f:
275+
field_names = [
276+
'Component',
277+
'Component Version',
278+
'Url'
279+
]
280+
281+
writer = csv.DictWriter(csv_f, fieldnames=field_names)
282+
writer.writeheader()
283+
284+
for my_comp_data in all_my_comp_data:
285+
row_data = {
286+
'Component': my_comp_data['componentName'],
287+
'Component Version': my_comp_data['componentVersion'],
288+
'Url': my_comp_data['url']
289+
}
290+
writer.writerow(row_data)
291+
else:
292+
# print to screen
293+
pprint(all_my_comp_data)
294+
295+
#end

0 commit comments

Comments
 (0)