50
50
pprint
51
51
spdx_tools
52
52
re
53
+ pathlib
53
54
54
55
- Blackduck instance
55
56
- API token with sufficient privileges to perform project version phase
87
88
import json
88
89
import re
89
90
from pprint import pprint
91
+ from pathlib import Path
90
92
from spdx_tools .spdx .model .document import Document
91
93
from spdx_tools .spdx .validation .document_validator import validate_full_spdx_document
92
94
from spdx_tools .spdx .parser .error import SPDXParsingError
93
95
from spdx_tools .spdx .parser .parse_anything import parse_file
94
96
95
-
96
- # Returns SPDX Document object on success, otherwise exits on parse failure
97
+ # TODO what happens if file doesn't exist?
98
+ # Returns SPDX Document object on success, otherwise exits on parse failure
99
+ # Input: file = Filename to process
100
+ # Returns: SPDX document object
97
101
def spdx_parse (file ):
98
102
print ("Parsing SPDX file..." )
99
103
start = time .process_time ()
@@ -121,6 +125,7 @@ def spdx_validate(document):
121
125
# sample data.
122
126
logging .warning (validation_message .validation_message )
123
127
128
+ # TODO is it possible to make this a case-insensitive match?
124
129
# Lookup the given matchname in the KB
125
130
# Logs a successful match
126
131
# Return the boolean purlmatch and matchname, which we might change from
@@ -144,6 +149,7 @@ def find_comp_in_kb(matchname, extref):
144
149
return (purlmatch , result ['componentName' ])
145
150
return (purlmatch , matchname )
146
151
152
+ # TODO is it possible to make this a case-insensitive match?
147
153
# Locate component name + version in BOM
148
154
# Returns True on success, False on failure
149
155
def find_comp_in_bom (bd , compname , compver , projver ):
@@ -173,6 +179,7 @@ def find_comp_in_bom(bd, compname, compver, projver):
173
179
return False
174
180
175
181
182
+ # TODO is it possible to make this a case-insensitive match?
176
183
# Returns:
177
184
# CompMatch - Contains matched component url, None for no match
178
185
# VerMatch - Contains matched component verison url, None for no match
@@ -202,6 +209,7 @@ def find_cust_comp(cust_comp_name, cust_comp_version):
202
209
203
210
# Returns URL of matching license
204
211
# Exits on failure, we assume it must pre-exist - TODO could probably just create this?
212
+ # Note: License name search is case-sensitive
205
213
def get_license_url (license_name ):
206
214
params = {
207
215
'q' : [f"name:{ license_name } " ]
@@ -214,8 +222,13 @@ def get_license_url(license_name):
214
222
logging .error (f"Failed to find license { license_name } " )
215
223
sys .exit (1 )
216
224
225
+ # Create a custom component
226
+ # Inputs:
227
+ # name - Name of component to add
228
+ # version - Version of component to add
229
+ # license - License name
217
230
# Returns the URL for the newly created component version URL if successful
218
- def create_cust_comp (name , version , license , approval ):
231
+ def create_cust_comp (name , version , license ):
219
232
print (f"Adding custom component: { name } { version } " )
220
233
license_url = get_license_url (license )
221
234
data = {
@@ -225,21 +238,34 @@ def create_cust_comp(name, version, license, approval):
225
238
'license' : {
226
239
'license' : license_url
227
240
},
228
- },
229
- 'approvalStatus' : approval
241
+ }
230
242
}
231
- # TODO validate response
232
- # looks like a 412 if it already existed
233
243
response = bd .session .post ("api/components" , json = data )
234
- # should be guaranteed 1 version because we just created it!
235
- # TODO put in a fail-safe
244
+ logging .debug (response )
245
+ if response .status_code == 412 :
246
+ # Shouldn't be possible. We checked for existence earlier.
247
+ logging .error (f"Component { name } already exists" )
248
+ sys .exit (1 )
249
+
250
+ if response .status_code != 201 :
251
+ # Shouldn't be possible. We checked for existence earlier.
252
+ logging .error (response .json ()['errors' ][0 ]['errorMessage' ])
253
+ logging .error (f"Status code { response .status_code } " )
254
+ sys .exit (1 )
255
+
256
+ # Should be guaranteed 1 version because we just created it!
236
257
for version in bd .get_items (response .links ['versions' ]['url' ]):
237
258
return (version ['_meta' ]['href' ])
238
259
239
- #return(response.links['self']['url'])
240
260
241
261
# Create a version for a custom component that already exists
242
- # Returns the component version url just created
262
+ #
263
+ # Inputs:
264
+ # comp_url - API URL of the component to update
265
+ # version - Version to add to existing component
266
+ # license - License to use for version
267
+ #
268
+ # Returns: component version url just created
243
269
def create_cust_comp_ver (comp_url , version , license ):
244
270
license_url = get_license_url (license )
245
271
data = {
@@ -248,18 +274,33 @@ def create_cust_comp_ver(comp_url, version, license):
248
274
'license' : license_url
249
275
},
250
276
}
251
- #response = bd.session.post(comp['_meta']['href'] + "/versions", json=data)
252
277
response = bd .session .post (comp_url + "/versions" , json = data )
253
- # TODO validate response
254
- return (response .links ['versions' ]['url' ])
278
+ logging .debug (response )
279
+ if response .status_code == 412 :
280
+ # Shouldn't be possible. We checked for existence earlier.
281
+ logging .error (f"Version { version } already exists for component" )
282
+ sys .exit (1 )
283
+
284
+ # necessary?
285
+ if response .status_code != 201 :
286
+ logging .error (f"Failed to add Version { version } to component" )
287
+ sys .exit (1 )
288
+
289
+ return (response .links ['self' ]['url' ])
255
290
256
291
# Add specified component version url to our project+version SBOM
292
+ # Inputs:
293
+ # proj_version_url: API URL for a project+version to update
294
+ # comp_ver_url: API URL of a component+version to add
257
295
def add_to_sbom (proj_version_url , comp_ver_url ):
258
296
data = {
259
297
'component' : comp_ver_url
260
298
}
261
- # TODO validate response
262
299
response = bd .session .post (proj_version_url + "/components" , json = data )
300
+ if (response .status_code != 200 ):
301
+ logging .error (response .json ()['errors' ][0 ]['errorMessage' ])
302
+ logging .error (f"Status code { response .status_code } " )
303
+ sys .exit (1 )
263
304
264
305
parser = argparse .ArgumentParser (description = "Parse SPDX file and verify if component names are in current SBOM for given project-version" )
265
306
parser .add_argument ("--base-url" , required = True , help = "Hub server URL e.g. https://your.blackduck.url" )
@@ -268,6 +309,7 @@ def add_to_sbom(proj_version_url, comp_ver_url):
268
309
parser .add_argument ("--out-file" , dest = 'out_file' , required = True , help = "Unmatched components file" )
269
310
parser .add_argument ("--project" , dest = 'project_name' , required = True , help = "Project that contains the BOM components" )
270
311
parser .add_argument ("--version" , dest = 'version_name' , required = True , help = "Version that contains the BOM components" )
312
+ parser .add_argument ("--license" , dest = 'license_name' , required = False , default = "NOASSERTION" , help = "License name to use for custom components" )
271
313
parser .add_argument ("--no-verify" , dest = 'verify' , action = 'store_false' , help = "Disable TLS certificate verification" )
272
314
parser .add_argument ("--no-spdx-validate" , dest = 'spdx_validate' , action = 'store_false' , help = "Disable SPDX validation" )
273
315
args = parser .parse_args ()
@@ -277,21 +319,38 @@ def add_to_sbom(proj_version_url, comp_ver_url):
277
319
format = "[%(asctime)s] {%(module)s:%(lineno)d} %(levelname)s - %(message)s"
278
320
)
279
321
280
- document = spdx_parse (args .spdx_file )
281
- if (args .spdx_validate ):
282
- spdx_validate (document )
322
+ if (Path (args .spdx_file ).is_file ()):
323
+ document = spdx_parse (args .spdx_file )
324
+ if (args .spdx_validate ):
325
+ spdx_validate (document )
326
+ else :
327
+ logging .error (f"Invalid SPDX file: { args .spdx_file } " )
328
+ sys .exit (1 )
283
329
284
330
with open (args .token_file , 'r' ) as tf :
285
331
access_token = tf .readline ().strip ()
286
332
287
333
bd = Client (base_url = args .base_url , token = access_token , verify = args .verify )
288
334
335
+ # some little debug/test stubs
336
+ # TODO: delete these
337
+ #comp_ver_url = create_cust_comp("MY COMPONENT z", "1", args.license_name)
338
+ #
339
+ #comp_url = "https://purl-validation.saas-staging.blackduck.com/api/components/886c04d4-28ce-4a27-be4c-f083e73a9f69"
340
+ #comp_ver_url = create_cust_comp_ver(comp_url, "701", "NOASSERTION")
341
+ #
342
+ #pv = "https://purl-validation.saas-staging.blackduck.com/api/projects/14b714d0-fa37-4684-86cc-ed4e7cc64b89/versions/b8426ca3-1e27-4045-843b-003eca72f98e"
343
+ #cv = "https://purl-validation.saas-staging.blackduck.com/api/components/886c04d4-28ce-4a27-be4c-f083e73a9f69/versions/56f64b7f-c284-457d-b593-0cf19a272a19"
344
+ #add_to_sbom(pv, cv)
345
+ #quit()
346
+
289
347
# Open unmatched component file
290
348
# Will save name, spdxid, version, and origin/purl for later in json format:
291
349
# "name": "react-bootstrap",
292
350
# "spdx_id": "SPDXRef-Pkg-react-bootstrap-2.1.2-30223",
293
351
# "version": "2.1.2",
294
352
# "origin": null
353
+ # TODO this try/except actually isn't right
295
354
try : outfile = open (args .out_file , 'w' )
296
355
except :
297
356
logging .exception ("Failed to open file for writing: " + args .out_file )
@@ -383,17 +442,18 @@ def add_to_sbom(proj_version_url, comp_ver_url):
383
442
# Check if custom component already exists
384
443
comp_url , comp_ver_url = find_cust_comp (package .name , package .version )
385
444
386
- # TODO make these optional args with defaults
387
- license = "NOASSERTION"
388
- approval = "UNREVIEWED"
389
445
if not comp_url :
446
+ # Custom component did not exist, so create it
390
447
cust_comp_count += 1
391
448
comp_ver_url = create_cust_comp (package .name , package .version ,
392
- license , approval )
449
+ args . license_name , approval )
393
450
elif comp_url and not comp_ver_url :
451
+ # Custom component existed, but not the version we care about
394
452
cust_ver_count += 1
395
453
print (f"Adding version { package .version } to custom component { package .name } " )
396
- comp_ver_url = create_cust_comp_ver (comp_url , package .version , license )
454
+ comp_ver_url = create_cust_comp_ver (comp_url , package .version , args .license_name )
455
+ # DEBUG
456
+ quit ()
397
457
else :
398
458
print ("Custom component already exists, not in SBOM" )
399
459
0 commit comments