3434Version History
35351.0 2023-09-26 Initial Release
36361.1 2023-10-13 Updates to improve component matching of BD Component IDs
37+ 1.2 2023-11-03 - Handle BD component with no version
38+ - Fix bug related to extrefs with both purl and BD component data
39+ - Check if project every had a "non-SBOM" scan and exit if so
40+ - Fix some invalid sort parmaeter formatting
41+ - Limit notification checking to last 24 hours
3742
3843Requirements
3944
5055 spdx_tools
5156 re
5257 pathlib
58+ datetime
5359
5460- Blackduck instance
5561- API token with sufficient privileges
5662
5763Install python packages with the following command:
5864
59- pip3 install argparse blackduck sys logging time json pprint pathlib spdx_tools
65+ pip3 install datetime argparse blackduck sys logging time json pprint pathlib spdx_tools
6066
6167usage: parse_spdx.py [-h] --base-url BASE_URL --token-file TOKEN_FILE
6268 --spdx-file SPDX_FILE --out-file OUT_FILE --project
9298import logging
9399import time
94100import json
101+ from datetime import datetime ,timedelta ,timezone
95102import re
96103from pprint import pprint
97104from pathlib import Path
103110# Used when we are polling for successful upload and processing
104111global MAX_RETRIES
105112global SLEEP
106- MAX_RETRIES = 30
113+ MAX_RETRIES = 60
107114SLEEP = 10
108115
109116logging .basicConfig (
@@ -198,9 +205,15 @@ def poll_notifications_for_success(cl_url, proj_version_url, summaries_url):
198205 retries = MAX_RETRIES
199206 sleep_time = SLEEP
200207
208+ # Limit the query to the last 24 hours (very conservative but also
209+ # keeps us from having to walk thousands of notifications every time)
210+ today = datetime .now ().astimezone (timezone .utc )
211+ yesterday = today - timedelta (days = 1 )
212+ start = yesterday .strftime ("%Y-%m-%dT%H:%M:%S.000Z" )
201213 params = {
202214 'filter' : ["notificationType:VERSION_BOM_CODE_LOCATION_BOM_COMPUTED" ],
203- 'sort' : ["createdAt: ASC" ]
215+ 'sort' : ["createdAt DESC" ],
216+ 'startDate' : [start ]
204217 }
205218
206219 while (retries ):
@@ -211,18 +224,33 @@ def poll_notifications_for_success(cl_url, proj_version_url, summaries_url):
211224 continue
212225 # We're checking the entire list of notifications, but ours should
213226 # be near the top.
214- if result ['content' ]['projectVersion' ] == proj_version_url and \
215- result ['content' ]['codeLocation' ] == cl_url and \
216- result ['content' ]['scanSummary' ] == summaries_url :
217- print ("BOM calculation complete" )
218- return
227+ if ( result ['content' ]['projectVersion' ] == proj_version_url and
228+ result ['content' ]['codeLocation' ] == cl_url and
229+ result ['content' ]['scanSummary' ] == summaries_url ) :
230+ print ("BOM calculation complete" )
231+ return
219232
220233 print ("Waiting for BOM calculation to complete" )
234+ # For debugging
235+ #print(f"Searching Notifications for:\n Proj_version: {proj_version_url}\n" +
236+ # f" CodeLocation: {cl_url}\n Summaries: {summaries_url}")
221237 time .sleep (sleep_time )
222238
223239 logging .error (f"Failed to verify successful BOM computed in { MAX_RETRIES * sleep_time } seconds" )
224240 sys .exit (1 )
225241
242+ # Check if this project-version ever had a non-SBOM scan
243+ # If so, we do not want to step on any toes and exit the script before
244+ # attempting any SBOM import.
245+ # Note: Uses an internal API for simplicity
246+ def check_for_existing_scan (projver ):
247+ headers = {'Accept' : 'application/vnd.blackducksoftware.internal-1+json' }
248+ for source in bd .get_items (f"{ projver } /source-trees" , headers = headers ):
249+ if not re .fullmatch (r".+spdx/sbom$" , source ['name' ]):
250+ logging .error (f"Project has a non-SBOM scan. Details:" )
251+ pprint (source )
252+ sys .exit (1 )
253+
226254# Poll for successful scan of SBOM.
227255# Inputs:
228256#
@@ -244,7 +272,7 @@ def poll_for_sbom_complete(sbom_name, proj_version_url):
244272 # Search for the latest scan matching our SBOM name
245273 params = {
246274 'q' : [f"name:{ sbom_name } " ],
247- 'sort' : ["updatedAt: ASC " ]
275+ 'sort' : ["updatedAt DESC " ]
248276 }
249277
250278 while (retries ):
@@ -411,6 +439,14 @@ def find_comp_id_in_kb(comp, ver):
411439 # No component match
412440 return None
413441 kb_match ['componentName' ] = json_data ['name' ]
442+ if ver is None :
443+ # Special case where a component was provided but no version.
444+ # Stick the component URL in the version field which we will later use
445+ # to update the BOM. The name of this field is now overloaded but
446+ # reusing it to stay generic.
447+ kb_match ['version' ] = json_data ['_meta' ]['href' ]
448+ kb_match ['versionName' ] = "UNKNOWN"
449+ return kb_match
414450
415451 try :
416452 json_data = bd .get_json (f"/api/components/{ comp } /versions/{ ver } " )
@@ -656,6 +692,7 @@ def import_sbom(bdobj, projname, vername, spdxfile, outfile=None, \
656692 # Validate project/version details
657693 project , version = get_proj_ver (projname , vername )
658694 proj_version_url = version ['_meta' ]['href' ]
695+ check_for_existing_scan (proj_version_url )
659696
660697 # Upload the provided SBOM
661698 upload_sbom_file (spdxfile , projname , vername )
@@ -727,12 +764,15 @@ def import_sbom(bdobj, projname, vername, spdxfile, outfile=None, \
727764
728765 if "purl" in extrefs :
729766 # purl is the preferred lookup
730- kb_match = find_comp_in_kb (ref . locator )
731- extref = ref . locator
767+ kb_match = find_comp_in_kb (extrefs [ 'purl' ] )
768+ extref = extrefs [ 'purl' ]
732769 elif "BlackDuck-Component" in extrefs :
733770 compid = normalize_id (extrefs ['BlackDuck-Component' ])
734- verid = normalize_id (extrefs ['BlackDuck-ComponentVersion' ])
735-
771+ try :
772+ verid = normalize_id (extrefs ['BlackDuck-ComponentVersion' ])
773+ except :
774+ print (" BD Component specified with no version" )
775+ verid = None
736776 # Lookup by KB ID
737777 kb_match = find_comp_id_in_kb (compid , verid )
738778 extref = extrefs ['BlackDuck-Component' ]
@@ -783,7 +823,7 @@ def import_sbom(bdobj, projname, vername, spdxfile, outfile=None, \
783823 if kb_match :
784824 kb_match_added_to_bom += 1
785825 print (f" WARNING: { matchname } { matchver } found in KB but not in SBOM - adding it" )
786- # kb_match['version'] contains the url of the component-version to add
826+ # kb_match['version'] contains the component url to add
787827 add_to_sbom (proj_version_url , kb_match ['version' ])
788828 # short-circuit the rest
789829 continue
0 commit comments