capycli 2.8.0__tar.gz → 2.9.0.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. {capycli-2.8.0 → capycli-2.9.0.dev1}/PKG-INFO +1 -1
  2. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/__init__.py +6 -0
  3. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/bom_convert.py +1 -1
  4. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/bom_validate.py +1 -1
  5. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/check_bom.py +1 -1
  6. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/check_bom_item_status.py +1 -1
  7. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/check_granularity.py +1 -1
  8. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/create_components.py +11 -2
  9. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/diff_bom.py +1 -1
  10. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/download_sources.py +1 -1
  11. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/filter_bom.py +1 -1
  12. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/findsources.py +5 -3
  13. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/map_bom.py +59 -69
  14. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/merge_bom.py +1 -1
  15. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/show_bom.py +1 -1
  16. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/map_result.py +33 -22
  17. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/purl_service.py +22 -83
  18. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/dependencies/javascript.py +1 -1
  19. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/dependencies/maven_list.py +1 -1
  20. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/dependencies/maven_pom.py +1 -1
  21. capycli-2.9.0.dev1/capycli/dependencies/nuget.py +714 -0
  22. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/dependencies/python.py +1 -1
  23. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/main/options.py +1 -1
  24. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/mapping/mapping_to_html.py +1 -1
  25. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/mapping/mapping_to_xlsx.py +1 -1
  26. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/moverview/moverview_to_html.py +1 -1
  27. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/moverview/moverview_to_xlsx.py +1 -1
  28. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/project/check_prerequisites.py +1 -1
  29. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/project/create_bom.py +1 -1
  30. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/project/create_project.py +16 -1
  31. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/project/create_readme.py +1 -1
  32. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/project/find_project.py +1 -1
  33. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/project/get_license_info.py +1 -1
  34. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/project/show_ecc.py +1 -1
  35. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/project/show_licenses.py +1 -1
  36. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/project/show_project.py +1 -1
  37. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/project/show_vulnerabilities.py +1 -1
  38. {capycli-2.8.0 → capycli-2.9.0.dev1}/pyproject.toml +1 -1
  39. capycli-2.8.0/capycli/dependencies/nuget.py +0 -185
  40. {capycli-2.8.0 → capycli-2.9.0.dev1}/LICENSES/CC0-1.0.txt +0 -0
  41. {capycli-2.8.0 → capycli-2.9.0.dev1}/LICENSES/MIT.txt +0 -0
  42. {capycli-2.8.0 → capycli-2.9.0.dev1}/License.md +0 -0
  43. {capycli-2.8.0 → capycli-2.9.0.dev1}/Readme.md +0 -0
  44. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/__main__.py +0 -0
  45. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/__init__.py +0 -0
  46. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/csv.py +0 -0
  47. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/handle_bom.py +0 -0
  48. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/html.py +0 -0
  49. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/legacy.py +0 -0
  50. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/legacy_cx.py +0 -0
  51. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/bom/plaintext.py +0 -0
  52. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/__init__.py +0 -0
  53. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/capycli_bom_support.py +0 -0
  54. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/comparable_version.py +0 -0
  55. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/component_cache.py +0 -0
  56. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/dependencies_base.py +0 -0
  57. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/file_support.py +0 -0
  58. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/github_support.py +0 -0
  59. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/html_support.py +0 -0
  60. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/json_support.py +0 -0
  61. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/print.py +0 -0
  62. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/purl_store.py +0 -0
  63. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/purl_utils.py +0 -0
  64. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/script_base.py +0 -0
  65. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/common/script_support.py +0 -0
  66. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/data/__init__.py +0 -0
  67. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/data/granularity_list.csv +0 -0
  68. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/dependencies/__init__.py +0 -0
  69. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/dependencies/handle_dependencies.py +0 -0
  70. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/main/__init__.py +0 -0
  71. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/main/application.py +0 -0
  72. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/main/argument_parser.py +0 -0
  73. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/main/cli.py +0 -0
  74. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/main/exceptions.py +0 -0
  75. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/main/result_codes.py +0 -0
  76. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/mapping/handle_mapping.py +0 -0
  77. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/moverview/handle_moverview.py +0 -0
  78. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/project/__init__.py +0 -0
  79. {capycli-2.8.0 → capycli-2.9.0.dev1}/capycli/project/handle_project.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: capycli
3
- Version: 2.8.0
3
+ Version: 2.9.0.dev1
4
4
  Summary: CaPyCli - Clearing Automation Python Command Line Interface for SW360
5
5
  Home-page: https://github.com/sw360/capycli
6
6
  License: MIT
@@ -63,6 +63,12 @@ def get_app_version() -> str:
63
63
  return version
64
64
 
65
65
 
66
+ def get_app_signature() -> str:
67
+ """Get the signature of this application."""
68
+ version = get_app_version()
69
+ return f"{APP_NAME}, {version}"
70
+
71
+
66
72
  # There is nothing lower than logging.DEBUG (10) in the logging library,
67
73
  # but we want an extra level to avoid being too verbose when using -vv.
68
74
  _EXTRA_VERBOSE = 5
@@ -163,7 +163,7 @@ class BomConvert(capycli.common.script_base.ScriptBase):
163
163
 
164
164
  def run(self, args: Any) -> None:
165
165
  """Main method()"""
166
- print("\n" + capycli.APP_NAME + ", " + capycli.get_app_version() + " - Convert SBOM formats\n")
166
+ print("\n" + capycli.get_app_signature() + " - Convert SBOM formats\n")
167
167
 
168
168
  if args.help:
169
169
  self.display_help()
@@ -61,7 +61,7 @@ class BomValidate(capycli.common.script_base.ScriptBase):
61
61
 
62
62
  def run(self, args: Any) -> None:
63
63
  """Main method()"""
64
- print("\n" + capycli.APP_NAME + ", " + capycli.get_app_version() + " - Validate a CaPyCLI/CycloneDX SBOM\n")
64
+ print("\n" + capycli.get_app_signature() + " - Validate a CaPyCLI/CycloneDX SBOM\n")
65
65
 
66
66
  if args.help:
67
67
  self.display_help()
@@ -158,7 +158,7 @@ class CheckBom(capycli.common.script_base.ScriptBase):
158
158
  logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
159
159
 
160
160
  print_text(
161
- "\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
161
+ "\n" + capycli.get_app_signature() +
162
162
  " - Check that all releases in the SBOM exist on target SW360 instance.\n")
163
163
 
164
164
  if args.help:
@@ -176,7 +176,7 @@ class CheckBomItemStatus(capycli.common.script_base.ScriptBase):
176
176
  logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
177
177
 
178
178
  print_text(
179
- "\n" + capycli.APP_NAME + ", " + capycli.get_app_version()
179
+ "\n" + capycli.get_app_signature()
180
180
  + " - check the status of the items on SW360\n")
181
181
 
182
182
  if args.help:
@@ -224,7 +224,7 @@ class CheckGranularity(capycli.common.script_base.ScriptBase):
224
224
  LOG = capycli.get_logger(__name__)
225
225
 
226
226
  print_text(
227
- "\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
227
+ "\n" + capycli.get_app_signature() +
228
228
  " - Check the granularity of all releases in the SBOM.\n")
229
229
 
230
230
  if args.help:
@@ -1,5 +1,5 @@
1
1
  # -------------------------------------------------------------------------------
2
- # Copyright (c) 2019-2024 Siemens
2
+ # Copyright (c) 2019-2025 Siemens
3
3
  # All Rights Reserved.
4
4
  # Author: thomas.graf@siemens.com
5
5
  #
@@ -259,6 +259,10 @@ class BomCreateComponents(capycli.common.script_base.ScriptBase):
259
259
  # ensure that we have the only correct external-id name: package-url
260
260
  data["externalIds"]["package-url"] = cx_comp.purl.to_string()
261
261
 
262
+ # add information that this release was created by CaPyCli
263
+ data["additionalData"] = {}
264
+ data["additionalData"]["createdWith"] = capycli.get_app_signature()
265
+
262
266
  # use project site as fallback for source code download url
263
267
  website = CycloneDxSupport.get_ext_ref_website(cx_comp)
264
268
  repo = CycloneDxSupport.get_ext_ref_repository(cx_comp)
@@ -318,6 +322,11 @@ class BomCreateComponents(capycli.common.script_base.ScriptBase):
318
322
  if cx_comp.purl:
319
323
  purl = PurlUtils.component_purl_from_release_purl(cx_comp.purl)
320
324
  data["externalIds"] = {"package-url": purl}
325
+
326
+ # add information that this component was created by CaPyCli
327
+ data["additionalData"] = {}
328
+ data["additionalData"]["createdWith"] = capycli.get_app_signature()
329
+
321
330
  return data
322
331
 
323
332
  def create_release(self, cx_comp: Component, component_id: str) -> Optional[Dict[str, Any]]:
@@ -733,7 +742,7 @@ class BomCreateComponents(capycli.common.script_base.ScriptBase):
733
742
  logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
734
743
 
735
744
  print_text(
736
- "\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
745
+ "\n" + capycli.get_app_signature() +
737
746
  " - Create new components and releases on SW360\n")
738
747
 
739
748
  if args.help:
@@ -202,7 +202,7 @@ class DiffBom(capycli.common.script_base.ScriptBase):
202
202
  LOG = capycli.get_logger(__name__)
203
203
 
204
204
  print_text(
205
- "\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
205
+ "\n" + capycli.get_app_signature() +
206
206
  " - Compare two SBOM files.\n")
207
207
 
208
208
  if args.help:
@@ -158,7 +158,7 @@ class BomDownloadSources(capycli.common.script_base.ScriptBase):
158
158
  logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
159
159
 
160
160
  print_text(
161
- "\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
161
+ "\n" + capycli.get_app_signature() +
162
162
  " - Download source files from the URL specified in the SBOM\n")
163
163
 
164
164
  if args.help:
@@ -271,7 +271,7 @@ class FilterBom(capycli.common.script_base.ScriptBase):
271
271
  global LOG
272
272
  LOG = capycli.get_logger(__name__)
273
273
 
274
- print_text("\n" + capycli.APP_NAME + ", " + capycli.get_app_version() + " - Apply a filter file to a SBOM\n")
274
+ print_text("\n" + capycli.get_app_signature() + " - Apply a filter file to a SBOM\n")
275
275
 
276
276
  if args.help:
277
277
  print("Usage: CaPyCli bom filter [-h] [-v] -i INPUTFILE -o OUTPUTFILE -filterfile FILTERFILE")
@@ -232,12 +232,14 @@ class FindSources(capycli.common.script_base.ScriptBase):
232
232
  w_prefix = [w_prefix]
233
233
 
234
234
  # ORDER BY tag-name-length DESC
235
- by_size = sorted([(len(tag['ref']), tag) for tag in w_prefix],
235
+ # Note: it may happen that the GithHubSupport.github_request
236
+ # returns items without 'ref'
237
+ by_size = sorted([(len(tag.get('ref', '')), tag) for tag in w_prefix],
236
238
  key=lambda x: x[0])
237
239
  w_prefix = [itm[1] for itm in reversed(by_size)]
238
240
 
239
241
  transformed_for_get_matching_tags = [
240
- {'name': tag['ref'].replace('refs/tags/', '', 1),
242
+ {'name': tag.get('ref', '').replace('refs/tags/', '', 1),
241
243
  'zipball_url': tag['url'].replace(
242
244
  '/git/refs/tags/', '/zipball/refs/tags/', 1),
243
245
  } for tag in w_prefix]
@@ -692,7 +694,7 @@ class FindSources(capycli.common.script_base.ScriptBase):
692
694
  logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
693
695
 
694
696
  print_text(
695
- "\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
697
+ "\n" + capycli.get_app_signature() +
696
698
  " - Go through the list of SBOM items and try to determine the source code.\n")
697
699
 
698
700
  if args.help:
@@ -14,7 +14,7 @@ import re
14
14
  import sys
15
15
  import urllib
16
16
  from enum import Enum
17
- from typing import Any, Dict, List, Optional, Tuple
17
+ from typing import Any, Dict, List, Optional
18
18
 
19
19
  from cyclonedx.model import ExternalReference, ExternalReferenceType, XsUri
20
20
  from cyclonedx.model.bom import Bom
@@ -183,24 +183,22 @@ class MapBom(capycli.common.script_base.ScriptBase):
183
183
  def map_bom_item(self, component: Component, check_similar: bool, result_required: bool) -> MapResult:
184
184
  """Maps a single SBOM item to the list of SW360 releases"""
185
185
 
186
- result, _, _ = self.map_bom_commons(component)
186
+ result = self.map_bom_commons(component)
187
+ result_release_ids = [r.split("/")[-1] for r in result.release_hrefs]
188
+ result_component_ids = [r.split("/")[-1] for r in result.component_hrefs]
187
189
 
188
190
  for release in self.releases:
189
191
  if ("Id" in release) and ("Sw360Id" not in release):
190
192
  release["Sw360Id"] = release["Id"]
191
193
 
192
194
  # first check: unique id
193
- if result.release_id == release["Sw360Id"] or self.is_id_match(release, component):
195
+ if release["Sw360Id"] in result_release_ids or self.is_id_match(release, component):
194
196
  self.add_match_if_better(result, release, MapResult.FULL_MATCH_BY_ID)
195
197
  break
196
198
 
197
199
  # second check: name AND version
198
- if (component.name
199
- and ("Name" in release)
200
- and release["Name"]):
201
- if (
202
- (result.component_id is not None)
203
- and (result.component_id == release["ComponentId"])):
200
+ if (component.name and release.get("Name")):
201
+ if release["ComponentId"] in result_component_ids:
204
202
  name_match = True
205
203
  else:
206
204
  name_match = component.name.lower() == release["Name"].lower()
@@ -276,17 +274,38 @@ class MapBom(capycli.common.script_base.ScriptBase):
276
274
 
277
275
  def map_bom_item_no_cache(self, component: Component) -> MapResult:
278
276
  """Maps a single SBOM item to SW360 via online checks (no cache!)"""
277
+
278
+ def get_release_details(href: str) -> Optional[Dict[str, Any]]:
279
+ """Get release data from SW360 for match result"""
280
+ if not self.client:
281
+ return None
282
+ real_release = self.client.get_release_by_url(href)
283
+ if not real_release:
284
+ print_red("Error accessing release " + href)
285
+ return None
286
+ release = ComponentCacheManagement.convert_release_details(self.client, real_release)
287
+ return release
288
+
279
289
  if not self.client:
280
290
  print_red(" No client!")
281
291
  sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360)
282
292
 
283
- result, release_url, component_url = self.map_bom_commons(component)
293
+ result = self.map_bom_commons(component)
284
294
  components = []
285
- if component_url:
286
- components.append(component_url)
287
295
 
288
- # if there's no purl match, search for component names
289
- if len(components) == 0:
296
+ # Handle matches of PURL cache
297
+ if result.release_hrefs:
298
+ for href in result.release_hrefs:
299
+ release = get_release_details(href)
300
+ if release:
301
+ 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
304
+
305
+ if result.component_hrefs:
306
+ components += result.component_hrefs
307
+ else:
308
+ # if there's no purl match for components, search by name
290
309
  components2 = self.client.get_component_by_name(component.name)
291
310
  if not components2:
292
311
  return result
@@ -297,45 +316,30 @@ class MapBom(capycli.common.script_base.ScriptBase):
297
316
  ]
298
317
 
299
318
  for compref in components:
300
- if release_url is not None:
301
- rel_list = [{"_links": {"self": {"href": release_url}}}]
302
- else:
303
- comp = self.client.get_component_by_url(compref)
304
- if not comp:
305
- continue
306
- rel_list = comp["_embedded"].get("sw360:releases", [])
319
+ comp = self.client.get_component_by_url(compref)
320
+ if not comp:
321
+ continue
322
+ rel_list = comp["_embedded"].get("sw360:releases", [])
307
323
 
308
324
  # Sorted alternatives in descending version order
309
325
  # Please note: the release list sometimes contain just the href but no version
310
326
  try:
311
327
  rel_list = sorted(rel_list,
312
328
  key=lambda x: "version" in x and ComparableVersion(
313
- x.get("version", "")), reverse=True) # type: ignore
329
+ x.get("version", "")), reverse=True)
314
330
  except ValueError:
315
331
  pass # we can live with an unsorted list
316
332
 
317
333
  for relref in rel_list:
318
334
  href = relref["_links"]["self"]["href"]
319
- real_release = self.client.get_release_by_url(href)
320
- if not real_release:
321
- print_red("Error accessign release " + href)
322
- continue
323
335
 
324
336
  # generate proper release for result
325
- release = {}
326
- release["Name"] = real_release["name"]
327
- if self.relaxed_debian_parsing:
328
- release["Version"] = self.cut_off_debian_extras(real_release["version"])
329
- else:
330
- release["Version"] = real_release["version"]
331
- release["ExternalIds"] = real_release.get("externalIds", "")
332
- release["Id"] = release["Sw360Id"] = self.client.get_id_from_href(href)
333
- release["ComponentId"] = self.client.get_id_from_href(compref)
334
-
335
- release = ComponentCacheManagement.convert_release_details(self.client, real_release)
337
+ release = get_release_details(href)
338
+ if not release:
339
+ continue
336
340
 
337
341
  # first check: unique id
338
- if href == release_url or self.is_id_match(release, component):
342
+ if self.is_id_match(release, component):
339
343
  self.add_match_if_better(result, release, MapResult.FULL_MATCH_BY_ID)
340
344
  break
341
345
 
@@ -446,8 +450,8 @@ class MapBom(capycli.common.script_base.ScriptBase):
446
450
  continue
447
451
 
448
452
  dataitem: Dict[str, Any] = {}
449
- if item.component:
450
- dataitem["BomItem"] = item.component.name + ", " + (item.component.version or "")
453
+ if item.input_component:
454
+ dataitem["BomItem"] = item.input_component.name + ", " + (item.input_component.version or "")
451
455
  dataitem["ResultCode"] = item.result
452
456
  dataitem["ResultText"] = item.map_code_to_string(item.result)
453
457
  dataitems.append(dataitem)
@@ -632,21 +636,17 @@ class MapBom(capycli.common.script_base.ScriptBase):
632
636
  newbom.register_dependency(newbom.metadata.component, [newitem])
633
637
  elif (item.result == MapResult.NO_MATCH
634
638
  or not self.is_good_match(item.result)):
639
+ # if we have no good match, add the component we're looking for as well
635
640
 
636
641
  if (self.mode == MapMode.FOUND):
637
642
  continue
638
643
 
639
- newitem = item.component
644
+ newitem = item.input_component
640
645
  if newitem:
641
646
  CycloneDxSupport.update_or_set_property(
642
647
  newitem,
643
648
  CycloneDxSupport.CDX_PROP_MAPRESULT,
644
649
  MapResult.NO_MATCH)
645
- if item.component_id is not None:
646
- CycloneDxSupport.update_or_set_property(
647
- newitem,
648
- CycloneDxSupport.CDX_PROP_COMPONENT_ID,
649
- item.component_id)
650
650
  newbom.components.add(newitem)
651
651
 
652
652
  # Sorted alternatives in descending version order
@@ -657,7 +657,8 @@ class MapBom(capycli.common.script_base.ScriptBase):
657
657
 
658
658
  for match_item in item.releases:
659
659
  if self.is_good_match(match_item["MapResult"]):
660
- newitem = self.update_bom_item(item.component, match_item)
660
+ # For good matches, merge the input component with the match item
661
+ newitem = self.update_bom_item(item.input_component, match_item)
661
662
  else:
662
663
  # newitem = match_item
663
664
  newitem = self.update_bom_item(None, match_item)
@@ -680,15 +681,15 @@ class MapBom(capycli.common.script_base.ScriptBase):
680
681
 
681
682
  for item in result:
682
683
  single_result: Dict[str, Any] = {}
683
- if not item.component:
684
+ if not item.input_component:
684
685
  continue
685
686
 
686
- for prop in item.component.properties:
687
+ for prop in item.input_component.properties:
687
688
  if prop.name == CycloneDxSupport.CDX_PROP_MAPRESULT:
688
- item.component.properties.remove(prop)
689
+ item.input_component.properties.remove(prop)
689
690
  break
690
691
 
691
- single_result["BomItem"] = LegacySupport.cdx_component_to_legacy(item.component)
692
+ single_result["BomItem"] = LegacySupport.cdx_component_to_legacy(item.input_component)
692
693
  single_result["Result"] = item.result
693
694
  single_result["Matches"] = []
694
695
  for item_match in item.releases:
@@ -716,34 +717,23 @@ class MapBom(capycli.common.script_base.ScriptBase):
716
717
  cachefile, True, token, oauth2=oauth2, url=sw360_url)
717
718
  return rel_data
718
719
 
719
- def map_bom_commons(self, component: Component) -> Tuple[MapResult, Optional[str], Optional[str]]:
720
+ def map_bom_commons(self, component: Component) -> MapResult:
720
721
  """
721
722
  Common parts to map from a SBOM component to the SW360 component/release.
722
723
  :param bomitem: SBOM component
723
724
  :return: MapResult instance, (Optional) release url, (Optional) component url
724
725
  """
725
- if not self.client:
726
- print_red(" No client!")
727
- sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360)
728
-
729
726
  if self.relaxed_debian_parsing and component.version:
730
727
  component.version = self.cut_off_debian_extras(component.version)
731
728
 
732
- result = capycli.common.map_result.MapResult(component)
729
+ result = MapResult(component)
733
730
 
734
731
  # search release and component by purl which is independent of the component cache.
735
- if type(component.purl) is PackageURL:
736
- component_href, release_href = self.external_id_svc.search_component_and_release(
737
- component.purl.to_string())
738
- else:
739
- component_href, release_href = self.external_id_svc.search_component_and_release(
740
- component.purl) # type: ignore
741
- if component_href:
742
- result.component_id = self.client.get_id_from_href(component_href)
743
- if release_href:
744
- result.release_id = self.client.get_id_from_href(release_href)
732
+ if component.purl:
733
+ result.component_hrefs = self.external_id_svc.search_components_by_purl(component.purl)
734
+ result.release_hrefs = self.external_id_svc.search_releases_by_purl(component.purl)
745
735
 
746
- return result, release_href, component_href
736
+ return result
747
737
 
748
738
  @property
749
739
  def external_id_svc(self) -> PurlService:
@@ -842,7 +832,7 @@ class MapBom(capycli.common.script_base.ScriptBase):
842
832
  logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
843
833
 
844
834
  print_text(
845
- "\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
835
+ "\n" + capycli.get_app_signature() +
846
836
  " - Map a given SBOM to data on SW360\n")
847
837
 
848
838
  if args.help:
@@ -122,7 +122,7 @@ class MergeBom(capycli.common.script_base.ScriptBase):
122
122
  LOG = capycli.get_logger(__name__)
123
123
 
124
124
  print_text(
125
- "\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
125
+ "\n" + capycli.get_app_signature() +
126
126
  " - Merge two SBOM files.\n")
127
127
 
128
128
  if args.help:
@@ -98,7 +98,7 @@ class ShowBom(capycli.common.script_base.ScriptBase):
98
98
  global LOG
99
99
  LOG = capycli.get_logger(__name__)
100
100
 
101
- print_text("\n" + capycli.APP_NAME + ", " + capycli.get_app_version() + " - Print SBOM contents to stdout\n")
101
+ print_text("\n" + capycli.get_app_signature() + " - Print SBOM contents to stdout\n")
102
102
 
103
103
  if args.help:
104
104
  print("usage: capycli bom show [-h] -i bomfile")
@@ -46,31 +46,42 @@ class MapResult:
46
46
  NO_MATCH = "9-no-match"
47
47
 
48
48
  def __init__(self, component: Optional[Component] = None) -> None:
49
- self.component: Optional[Component] = component
49
+ self.input_component: Optional[Component] = component
50
50
  self.result: str = MapResult.NO_MATCH
51
- self._component_id: str = ""
52
- self._release_id: str = ""
51
+ self._component_hrefs: List[str] = []
52
+ self._release_hrefs: List[str] = []
53
53
  self.releases: List[Any] = []
54
54
 
55
55
  @property
56
- def component_id(self) -> str:
57
- return self._component_id
58
-
59
- @component_id.setter
60
- def component_id(self, value: str) -> None:
61
- self._component_id = value
62
- if self.component:
63
- CycloneDxSupport.update_or_set_property(self.component, CycloneDxSupport.CDX_PROP_COMPONENT_ID, value)
56
+ def component_hrefs(self) -> List[str]:
57
+ return self._component_hrefs
58
+
59
+ @component_hrefs.setter
60
+ def component_hrefs(self, value: List[str]) -> None:
61
+ self._component_hrefs = value
62
+ if not self.input_component:
63
+ return
64
+ if len(value) == 1:
65
+ CycloneDxSupport.update_or_set_property(self.input_component, CycloneDxSupport.CDX_PROP_COMPONENT_ID,
66
+ value[0].split("/")[-1])
67
+ else:
68
+ CycloneDxSupport.remove_property(self.input_component,
69
+ CycloneDxSupport.CDX_PROP_COMPONENT_ID)
64
70
 
65
71
  @property
66
- def release_id(self) -> str:
67
- return self._release_id
68
-
69
- @release_id.setter
70
- def release_id(self, value: str) -> None:
71
- self._release_id = value
72
- if self.component:
73
- CycloneDxSupport.update_or_set_property(self.component, CycloneDxSupport.CDX_PROP_SW360ID, value)
72
+ def release_hrefs(self) -> List[str]:
73
+ return self._release_hrefs
74
+
75
+ @release_hrefs.setter
76
+ def release_hrefs(self, value: List[str]) -> None:
77
+ self._release_hrefs = value
78
+ if not self.input_component:
79
+ return
80
+ if len(value) == 1:
81
+ CycloneDxSupport.update_or_set_property(self.input_component, CycloneDxSupport.CDX_PROP_SW360ID,
82
+ value[0].split("/")[-1])
83
+ else:
84
+ CycloneDxSupport.remove_property(self.input_component, CycloneDxSupport.CDX_PROP_SW360ID)
74
85
 
75
86
  @classmethod
76
87
  def map_code_to_string(cls, map_code: str) -> str:
@@ -109,7 +120,7 @@ class MapResult:
109
120
  + more
110
121
  )
111
122
 
112
- if not self.component:
123
+ if not self.input_component:
113
124
  return (
114
125
  self.map_code_to_string(self.result)
115
126
  + ", (no component), "
@@ -119,9 +130,9 @@ class MapResult:
119
130
  return (
120
131
  self.map_code_to_string(self.result)
121
132
  + ", "
122
- + str(self.component.name)
133
+ + str(self.input_component.name)
123
134
  + ", "
124
- + str(self.component.version or "")
135
+ + str(self.input_component.version or "")
125
136
  + " "
126
137
  + str(rel)
127
138
  )