66# SPDX-License-Identifier: MIT
77# -------------------------------------------------------------------------------
88
9+ import copy
910import json
1011import logging
1112import os
@@ -58,6 +59,8 @@ def __init__(self) -> None:
5859 self .mode = MapMode .ALL
5960 self .purl_service : Optional [PurlService ] = None
6061 self .no_match_by_name_only = True
62+ self .full_search = False
63+ self .qualifier_match = False
6164
6265 def is_id_match (self , release : Dict [str , Any ], component : Component ) -> bool :
6366 """Determines whether this release is a match via identifier for the specified SBOM item"""
@@ -194,39 +197,51 @@ def map_bom_item(self, component: Component, check_similar: bool, result_require
194197 # first check: unique id
195198 if release ["Sw360Id" ] in result_release_ids or self .is_id_match (release , component ):
196199 self .add_match_if_better (result , release , MapResult .FULL_MATCH_BY_ID )
197- break
198-
199- # second check: name AND version
200- if (component .name and release .get ("Name" )):
201- if release ["ComponentId" ] in result_component_ids :
202- name_match = True
200+ if self .full_search :
201+ continue
203202 else :
204- name_match = component .name .lower () == release ["Name" ].lower ()
205- version_exists = "Version" in release
206- if (name_match
207- and version_exists and component .version
208- and (component .version .lower () == release ["Version" ].lower ())):
209- self .add_match_if_better (result , release , MapResult .FULL_MATCH_BY_NAME_AND_VERSION )
210203 break
211- else :
212- name_match = False
213204
214- # third check unique(?) file hashes
205+ # second check unique(?) file hashes
215206 cmp_hash = CycloneDxSupport .get_source_file_hash (component )
216207 if (("SourceFileHash" in release )
217208 and cmp_hash
218209 and release ["SourceFileHash" ]):
219210 if (cmp_hash .lower () == release ["SourceFileHash" ].lower ()):
220211 self .add_match_if_better (result , release , MapResult .FULL_MATCH_BY_HASH )
221- break
212+ if self .full_search :
213+ continue
214+ else :
215+ break
222216
223217 cmp_hash = CycloneDxSupport .get_binary_file_hash (component )
224218 if (("BinaryFileHash" in release )
225219 and cmp_hash
226220 and release ["BinaryFileHash" ]):
227221 if (cmp_hash .lower () == release ["BinaryFileHash" ].lower ()):
228222 self .add_match_if_better (result , release , MapResult .FULL_MATCH_BY_HASH )
229- break
223+ if self .full_search :
224+ continue
225+ else :
226+ break
227+
228+ # third check: name AND version
229+ if (component .name and release .get ("Name" )):
230+ if release ["ComponentId" ] in result_component_ids :
231+ name_match = True
232+ else :
233+ name_match = component .name .lower () == release ["Name" ].lower ()
234+ version_exists = "Version" in release
235+ if (name_match
236+ and version_exists and component .version
237+ and (component .version .lower () == release ["Version" ].lower ())):
238+ self .add_match_if_better (result , release , MapResult .FULL_MATCH_BY_NAME_AND_VERSION )
239+ if self .full_search :
240+ continue
241+ else :
242+ break
243+ else :
244+ name_match = False
230245
231246 # fourth check: source filename
232247 cmp_src_file = CycloneDxSupport .get_ext_ref_source_file (component )
@@ -235,7 +250,10 @@ def map_bom_item(self, component: Component, check_similar: bool, result_require
235250 and release ["SourceFile" ]):
236251 if cmp_src_file .lower () == release ["SourceFile" ].lower ():
237252 self .add_match_if_better (result , release , MapResult .MATCH_BY_FILENAME )
238- break
253+ if self .full_search :
254+ continue
255+ else :
256+ break
239257
240258 # fifth check: name and ANY version
241259 if name_match :
@@ -299,8 +317,10 @@ def get_release_details(href: str) -> Optional[Dict[str, Any]]:
299317 release = get_release_details (href )
300318 if release :
301319 self .add_match_if_better (result , release , MapResult .FULL_MATCH_BY_ID )
302- # If we have release matches by PURL, we're done
303- return result
320+ if not self .full_search :
321+ return result
322+ # If we have release matches by PURL, we're done
323+ return result
304324
305325 if result .component_hrefs :
306326 components += result .component_hrefs
@@ -343,29 +363,38 @@ def get_release_details(href: str) -> Optional[Dict[str, Any]]:
343363 self .add_match_if_better (result , release , MapResult .FULL_MATCH_BY_ID )
344364 break
345365
346- # second check: name AND version (we don't need to check the name
347- # again as we checked it when compiling component list)
348- version_exists = "Version" in release
349- if (version_exists
350- and ((component .version or "" ).lower () == release .get ("Version" , "" ).lower ())):
351- self .add_match_if_better (result , release , MapResult .FULL_MATCH_BY_NAME_AND_VERSION )
352- break
353-
354- # third check unique(?) file hashes
366+ # second check unique(?) file hashes
355367 cmp_hash = CycloneDxSupport .get_source_file_hash (component )
356368 if (("SourceFileHash" in release )
357369 and cmp_hash
358370 and release ["SourceFileHash" ]):
359371 if (cmp_hash .lower () == release ["SourceFileHash" ].lower ()):
360372 self .add_match_if_better (result , release , MapResult .FULL_MATCH_BY_HASH )
361- break
373+ if self .full_search :
374+ continue
375+ else :
376+ break
362377
363378 cmp_hash = CycloneDxSupport .get_binary_file_hash (component )
364379 if (("BinaryFileHash" in release )
365380 and cmp_hash
366381 and release ["BinaryFileHash" ]):
367382 if (cmp_hash .lower () == release ["BinaryFileHash" ].lower ()):
368383 self .add_match_if_better (result , release , MapResult .FULL_MATCH_BY_HASH )
384+ if self .full_search :
385+ continue
386+ else :
387+ break
388+
389+ # third check: name AND version (we don't need to check the name
390+ # again as we checked it when compiling component list)
391+ version_exists = "Version" in release
392+ if (version_exists
393+ and ((component .version or "" ).lower () == release .get ("Version" , "" ).lower ())):
394+ self .add_match_if_better (result , release , MapResult .FULL_MATCH_BY_NAME_AND_VERSION )
395+ if self .full_search :
396+ continue
397+ else :
369398 break
370399
371400 # fifth check: name and ANY version
@@ -506,6 +535,9 @@ def update_bom_item(self, component: Optional[Component], match: Dict[str, Any])
506535 name = match .get ("Name" , "" ),
507536 version = match .get ("Version" , "" ))
508537 else :
538+ # copy component so we don't overwrite the input component
539+ component = copy .deepcopy (component )
540+
509541 # always overwrite the following properties
510542 name = match .get ("Name" , "" )
511543 if name :
@@ -730,7 +762,9 @@ def map_bom_commons(self, component: Component) -> MapResult:
730762 # search release and component by purl which is independent of the component cache.
731763 if component .purl :
732764 result .component_hrefs = self .external_id_svc .search_components_by_purl (component .purl )
733- result .release_hrefs = self .external_id_svc .search_releases_by_purl (component .purl )
765+ r = self .external_id_svc .search_releases_by_purl (component .purl , self .qualifier_match )
766+ result .release_hrefs = r ["hrefs" ]
767+ result .release_hrefs_results = r ["results" ]
734768
735769 return result
736770
@@ -815,9 +849,14 @@ def show_help(self) -> None:
815849 print (" all = default, write everything to resulting SBOM" )
816850 print (" found = resulting SBOM shows only components that were found" )
817851 print (" notfound = resulting SBOM shows only components that were not found" )
818- print (" --dbx relaxed Debian version handling: *completely* ignore Debian revision," )
819- print (" so SBOM version 3.1 will match SW360 version 3.1-3.debian" )
820- print (" -all also report matches for name, but different version" )
852+ print (" --matchmode MATCHMODE matching mode, comma separated list of:" )
853+ print (" full-search = report best matches, don't abort on first match (recommended)" )
854+ print (" all-versions = also report matches for name, but different version" )
855+ print (" qualifier-match = consider qualifiers for PURL matching" )
856+ print (" ignore-debian = ignore Debian revision in version comparison, so SBOM" )
857+ print (" version 3.1 will match SW360 version 3.1-3.debian" )
858+ print (" -all deprecated, please use --matchmode all-versions" )
859+ print (" --dbx deprecated, please use --matchmode ignore-debian" )
821860
822861 def run (self , args : Any ) -> None :
823862 """Main method()"""
@@ -849,16 +888,29 @@ def run(self, args: Any) -> None:
849888 if args .verbose :
850889 self .verbosity = 2
851890
852- if args .dbx :
891+ if not args .matchmode :
892+ args .matchmode = ""
893+
894+ if "ignore-debian" in args .matchmode or args .dbx :
895+ if args .dbx :
896+ print_yellow ("bom map --dbx is deprecated, use --matchmode ignore-debian instead" )
853897 print_text ("Using relaxed debian version checks" )
854898 self .relaxed_debian_parsing = True
855899
856900 if args .mode :
857901 self .mode = args .mode
858902
859- if args .all :
903+ if "all-versions" in args .matchmode or args .all :
904+ if args .all :
905+ print_yellow ("bom map -all is deprecated, use --matchmode all-versions instead" )
860906 self .no_match_by_name_only = False
861907
908+ if "full-search" in args .matchmode :
909+ self .full_search = True
910+
911+ if "qualifier-match" in args .matchmode :
912+ self .qualifier_match = True
913+
862914 print_text ("Loading SBOM file" , args .inputfile )
863915 try :
864916 sbom = CaPyCliBom .read_sbom (args .inputfile )
0 commit comments