5050 pprint
5151 spdx_tools
5252 re
53+ pathlib
5354
5455- Blackduck instance
5556- API token with sufficient privileges to perform project version phase
8788import json
8889import re
8990from pprint import pprint
91+ from pathlib import Path
9092from spdx_tools .spdx .model .document import Document
9193from spdx_tools .spdx .validation .document_validator import validate_full_spdx_document
9294from spdx_tools .spdx .parser .error import SPDXParsingError
9395from spdx_tools .spdx .parser .parse_anything import parse_file
9496
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
97101def spdx_parse (file ):
98102 print ("Parsing SPDX file..." )
99103 start = time .process_time ()
@@ -121,6 +125,7 @@ def spdx_validate(document):
121125 # sample data.
122126 logging .warning (validation_message .validation_message )
123127
128+ # TODO is it possible to make this a case-insensitive match?
124129# Lookup the given matchname in the KB
125130# Logs a successful match
126131# Return the boolean purlmatch and matchname, which we might change from
@@ -144,6 +149,7 @@ def find_comp_in_kb(matchname, extref):
144149 return (purlmatch , result ['componentName' ])
145150 return (purlmatch , matchname )
146151
152+ # TODO is it possible to make this a case-insensitive match?
147153# Locate component name + version in BOM
148154# Returns True on success, False on failure
149155def find_comp_in_bom (bd , compname , compver , projver ):
@@ -173,6 +179,7 @@ def find_comp_in_bom(bd, compname, compver, projver):
173179 return False
174180
175181
182+ # TODO is it possible to make this a case-insensitive match?
176183# Returns:
177184# CompMatch - Contains matched component url, None for no match
178185# VerMatch - Contains matched component verison url, None for no match
@@ -202,6 +209,7 @@ def find_cust_comp(cust_comp_name, cust_comp_version):
202209
203210# Returns URL of matching license
204211# Exits on failure, we assume it must pre-exist - TODO could probably just create this?
212+ # Note: License name search is case-sensitive
205213def get_license_url (license_name ):
206214 params = {
207215 'q' : [f"name:{ license_name } " ]
@@ -214,8 +222,13 @@ def get_license_url(license_name):
214222 logging .error (f"Failed to find license { license_name } " )
215223 sys .exit (1 )
216224
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
217230# 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 ):
219232 print (f"Adding custom component: { name } { version } " )
220233 license_url = get_license_url (license )
221234 data = {
@@ -225,21 +238,34 @@ def create_cust_comp(name, version, license, approval):
225238 'license' : {
226239 'license' : license_url
227240 },
228- },
229- 'approvalStatus' : approval
241+ }
230242 }
231- # TODO validate response
232- # looks like a 412 if it already existed
233243 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!
236257 for version in bd .get_items (response .links ['versions' ]['url' ]):
237258 return (version ['_meta' ]['href' ])
238259
239- #return(response.links['self']['url'])
240260
241261# 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
243269def create_cust_comp_ver (comp_url , version , license ):
244270 license_url = get_license_url (license )
245271 data = {
@@ -248,18 +274,33 @@ def create_cust_comp_ver(comp_url, version, license):
248274 'license' : license_url
249275 },
250276 }
251- #response = bd.session.post(comp['_meta']['href'] + "/versions", json=data)
252277 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' ])
255290
256291# 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
257295def add_to_sbom (proj_version_url , comp_ver_url ):
258296 data = {
259297 'component' : comp_ver_url
260298 }
261- # TODO validate response
262299 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 )
263304
264305parser = argparse .ArgumentParser (description = "Parse SPDX file and verify if component names are in current SBOM for given project-version" )
265306parser .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):
268309parser .add_argument ("--out-file" , dest = 'out_file' , required = True , help = "Unmatched components file" )
269310parser .add_argument ("--project" , dest = 'project_name' , required = True , help = "Project that contains the BOM components" )
270311parser .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" )
271313parser .add_argument ("--no-verify" , dest = 'verify' , action = 'store_false' , help = "Disable TLS certificate verification" )
272314parser .add_argument ("--no-spdx-validate" , dest = 'spdx_validate' , action = 'store_false' , help = "Disable SPDX validation" )
273315args = parser .parse_args ()
@@ -277,21 +319,38 @@ def add_to_sbom(proj_version_url, comp_ver_url):
277319 format = "[%(asctime)s] {%(module)s:%(lineno)d} %(levelname)s - %(message)s"
278320)
279321
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 )
283329
284330with open (args .token_file , 'r' ) as tf :
285331 access_token = tf .readline ().strip ()
286332
287333bd = Client (base_url = args .base_url , token = access_token , verify = args .verify )
288334
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+
289347# Open unmatched component file
290348# Will save name, spdxid, version, and origin/purl for later in json format:
291349# "name": "react-bootstrap",
292350# "spdx_id": "SPDXRef-Pkg-react-bootstrap-2.1.2-30223",
293351# "version": "2.1.2",
294352# "origin": null
353+ # TODO this try/except actually isn't right
295354try : outfile = open (args .out_file , 'w' )
296355except :
297356 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):
383442 # Check if custom component already exists
384443 comp_url , comp_ver_url = find_cust_comp (package .name , package .version )
385444
386- # TODO make these optional args with defaults
387- license = "NOASSERTION"
388- approval = "UNREVIEWED"
389445 if not comp_url :
446+ # Custom component did not exist, so create it
390447 cust_comp_count += 1
391448 comp_ver_url = create_cust_comp (package .name , package .version ,
392- license , approval )
449+ args . license_name , approval )
393450 elif comp_url and not comp_ver_url :
451+ # Custom component existed, but not the version we care about
394452 cust_ver_count += 1
395453 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 ()
397457 else :
398458 print ("Custom component already exists, not in SBOM" )
399459
0 commit comments