capycli 2.9.1__tar.gz → 2.10.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 (78) hide show
  1. {capycli-2.9.1 → capycli-2.10.0.dev1}/PKG-INFO +9 -14
  2. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/__init__.py +3 -3
  3. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/create_components.py +4 -1
  4. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/download_sources.py +52 -6
  5. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/filter_bom.py +7 -4
  6. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/findsources.py +42 -0
  7. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/legacy.py +7 -1
  8. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/legacy_cx.py +13 -4
  9. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/map_bom.py +88 -36
  10. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/show_bom.py +10 -2
  11. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/capycli_bom_support.py +18 -7
  12. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/map_result.py +22 -0
  13. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/purl_service.py +16 -4
  14. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/purl_store.py +27 -1
  15. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/purl_utils.py +8 -2
  16. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/javascript.py +22 -9
  17. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/python.py +225 -32
  18. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/options.py +23 -8
  19. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/check_prerequisites.py +25 -8
  20. {capycli-2.9.1 → capycli-2.10.0.dev1}/pyproject.toml +46 -38
  21. capycli-2.9.1/LICENSES/CC0-1.0.txt +0 -121
  22. capycli-2.9.1/LICENSES/MIT.txt +0 -27
  23. {capycli-2.9.1 → capycli-2.10.0.dev1}/License.md +0 -0
  24. {capycli-2.9.1 → capycli-2.10.0.dev1}/Readme.md +0 -0
  25. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/__main__.py +0 -0
  26. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/__init__.py +0 -0
  27. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/bom_convert.py +0 -0
  28. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/bom_validate.py +0 -0
  29. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/check_bom.py +0 -0
  30. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/check_bom_item_status.py +0 -0
  31. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/check_granularity.py +0 -0
  32. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/csv.py +0 -0
  33. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/diff_bom.py +0 -0
  34. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/handle_bom.py +0 -0
  35. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/html.py +0 -0
  36. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/merge_bom.py +0 -0
  37. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/plaintext.py +0 -0
  38. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/__init__.py +0 -0
  39. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/comparable_version.py +0 -0
  40. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/component_cache.py +0 -0
  41. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/dependencies_base.py +0 -0
  42. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/file_support.py +0 -0
  43. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/github_support.py +0 -0
  44. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/html_support.py +0 -0
  45. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/json_support.py +0 -0
  46. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/print.py +0 -0
  47. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/script_base.py +0 -0
  48. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/script_support.py +0 -0
  49. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/data/__init__.py +0 -0
  50. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/data/granularity_list.csv +0 -0
  51. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/__init__.py +0 -0
  52. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/handle_dependencies.py +0 -0
  53. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/maven_list.py +0 -0
  54. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/maven_pom.py +0 -0
  55. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/nuget.py +0 -0
  56. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/__init__.py +0 -0
  57. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/application.py +0 -0
  58. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/argument_parser.py +0 -0
  59. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/cli.py +0 -0
  60. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/exceptions.py +0 -0
  61. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/result_codes.py +0 -0
  62. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/mapping/handle_mapping.py +0 -0
  63. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/mapping/mapping_to_html.py +0 -0
  64. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/mapping/mapping_to_xlsx.py +0 -0
  65. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/moverview/handle_moverview.py +0 -0
  66. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/moverview/moverview_to_html.py +0 -0
  67. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/moverview/moverview_to_xlsx.py +0 -0
  68. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/__init__.py +0 -0
  69. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/create_bom.py +0 -0
  70. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/create_project.py +0 -0
  71. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/create_readme.py +0 -0
  72. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/find_project.py +0 -0
  73. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/get_license_info.py +0 -0
  74. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/handle_project.py +0 -0
  75. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/show_ecc.py +0 -0
  76. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/show_licenses.py +0 -0
  77. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/show_project.py +0 -0
  78. {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/show_vulnerabilities.py +0 -0
@@ -1,30 +1,25 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: capycli
3
- Version: 2.9.1
3
+ Version: 2.10.0.dev1
4
4
  Summary: CaPyCli - Clearing Automation Python Command Line Interface for SW360
5
- Home-page: https://github.com/sw360/capycli
6
- License: MIT
5
+ License-Expression: MIT
6
+ License-File: License.md
7
7
  Keywords: sw360,cli, automation,license,compliance,clearing
8
8
  Author: Thomas Graf
9
9
  Author-email: thomas.graf@siemens.com
10
- Requires-Python: >=3.9,<4.0
10
+ Requires-Python: >=3.11,<3.15
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Intended Audience :: Developers
13
- Classifier: License :: OSI Approved :: MIT License
14
13
  Classifier: Natural Language :: English
15
14
  Classifier: Operating System :: OS Independent
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.9
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.12
21
15
  Classifier: Programming Language :: Python :: 3 :: Only
22
16
  Requires-Dist: beautifulsoup4 (>=4.11.1,<5.0.0)
23
17
  Requires-Dist: chardet (==5.2.0)
24
18
  Requires-Dist: cli-support (==2.0.1)
25
19
  Requires-Dist: colorama (>=0.4.3,<0.5.0)
26
- Requires-Dist: cyclonedx-python-lib (>=8.0.0,<9.0.0)
20
+ Requires-Dist: cyclonedx-python-lib (>=11.4.0,<12.0.0)
27
21
  Requires-Dist: dateparser (>=1.1.8,<2.0.0)
22
+ Requires-Dist: halo (>=0.0.31,<0.0.32)
28
23
  Requires-Dist: importlib-resources (>=5.12.0,<6.0.0)
29
24
  Requires-Dist: jsonschema (>=4.23.0,<5.0.0)
30
25
  Requires-Dist: openpyxl (>=3.0.3,<4.0.0)
@@ -34,10 +29,10 @@ Requires-Dist: requests (>=2.31.0,<3.0.0)
34
29
  Requires-Dist: requirements-parser (==0.11.0)
35
30
  Requires-Dist: semver (==3.0.2)
36
31
  Requires-Dist: sw360 (>=1.8.1,<2.0.0)
37
- Requires-Dist: tomli (>=2.0.2,<3.0.0)
38
- Requires-Dist: urllib3
32
+ Requires-Dist: urllib3 (>=2.5.0,<3.0.0)
39
33
  Requires-Dist: validation (>=0.8.3,<0.9.0)
40
34
  Requires-Dist: wheel (>=0.38.4,<0.39.0)
35
+ Project-URL: Homepage, https://github.com/sw360/capycli
41
36
  Project-URL: Repository, https://github.com/sw360/capycli
42
37
  Project-URL: issues, https://github.com/sw360/capycli/issues
43
38
  Description-Content-Type: text/markdown
@@ -19,9 +19,9 @@ import importlib
19
19
  import logging
20
20
  import os
21
21
  import sys
22
+ import tomllib
22
23
  from typing import Any
23
24
 
24
- import tomli
25
25
  from colorama import Fore, Style, init
26
26
 
27
27
  APP_NAME = "CaPyCli"
@@ -36,7 +36,7 @@ def _get_project_meta() -> Any:
36
36
  """Read version information from poetry configuration file."""
37
37
  try:
38
38
  with open('pyproject.toml', mode='rb') as pyproject:
39
- return tomli.load(pyproject)['tool']['poetry']
39
+ return tomllib.load(pyproject)['tool']['poetry']
40
40
  except Exception:
41
41
  # ignore all errors
42
42
  pass
@@ -123,7 +123,7 @@ class ConsoleHandler(logging.Handler):
123
123
  else:
124
124
  # info, debug, all other
125
125
  # suppress all cyclonedx serialize log output
126
- if record.name == "serializable":
126
+ if (record.name == "serializable") or record.name == "py_serializable":
127
127
  return
128
128
  print(msg)
129
129
  except Exception:
@@ -24,6 +24,7 @@ from sw360 import SW360Error
24
24
 
25
25
  import capycli.common.json_support
26
26
  import capycli.common.script_base
27
+ from capycli.bom.download_sources import BomDownloadSources
27
28
  from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomWriter
28
29
  from capycli.common.print import print_green, print_red, print_text, print_yellow
29
30
  from capycli.common.purl_utils import PurlUtils
@@ -162,6 +163,8 @@ class BomCreateComponents(capycli.common.script_base.ScriptBase):
162
163
  " File with the name '", tail, "' is already attached to release. Skip the upload!")
163
164
  else:
164
165
  self.upload_source_file(release_id, fullpath, filetype, comment)
166
+ if not BomDownloadSources.is_good_source_file(fullpath):
167
+ print_yellow(" Downloaded file seems not to be a valid source file!")
165
168
  except Exception as ex:
166
169
  print_red(" Error writing downloaded file: " + repr(ex))
167
170
  else:
@@ -399,7 +402,7 @@ class BomCreateComponents(capycli.common.script_base.ScriptBase):
399
402
  bom_purl = packageurl.PackageURL.from_string(
400
403
  data["externalIds"][repository_type])
401
404
  sw360_purls = PurlUtils.get_purl_list_from_sw360_object(release_data)
402
- id_match = PurlUtils.contains(sw360_purls, bom_purl)
405
+ id_match = PurlUtils.contains(sw360_purls, bom_purl, compare_qualifiers=True)
403
406
  except ValueError:
404
407
  pass
405
408
  if not id_match:
@@ -1,5 +1,5 @@
1
1
  # -------------------------------------------------------------------------------
2
- # Copyright (c) 2020-2023 Siemens
2
+ # Copyright (c) 2020-2025 Siemens
3
3
  # All Rights Reserved.
4
4
  # Author: thomas.graf@siemens.com
5
5
  #
@@ -11,7 +11,9 @@ import logging
11
11
  import os
12
12
  import pathlib
13
13
  import re
14
+ import shutil
14
15
  import sys
16
+ import tempfile
15
17
  from typing import Any, Optional, Tuple
16
18
  from urllib.parse import urlparse
17
19
 
@@ -45,6 +47,16 @@ class BomDownloadSources(capycli.common.script_base.ScriptBase):
45
47
  return ""
46
48
  return fname[0].rstrip('"').lstrip('"')
47
49
 
50
+ @staticmethod
51
+ def is_good_source_file(filename: str) -> bool:
52
+ """
53
+ Checks whether this seems to be a valid/good source file.
54
+ The only thing we can do it to check the file extension.
55
+ """
56
+ good_extensions = [".zip", ".tar", ".gz", ".tar.gz", ".tgz", ".tar.bz2", ".tbz2", ".tar.xz", ".txz", ".7z"]
57
+ _, extension = os.path.splitext(filename)
58
+ return extension in good_extensions
59
+
48
60
  def download_source_file(self, url: str, source_folder: str) -> Optional[Tuple[str, str]]:
49
61
  """Download a file from a URL.
50
62
 
@@ -70,6 +82,8 @@ class BomDownloadSources(capycli.common.script_base.ScriptBase):
70
82
  path = os.path.join(source_folder, filename)
71
83
  if (response.status_code == requests.codes["ok"]):
72
84
  open(path, "wb").write(response.content)
85
+ if not BomDownloadSources.is_good_source_file(path):
86
+ print_yellow(" Downloaded file seems not to be a valid source file!")
73
87
  sha1 = hashlib.sha1(response.content).hexdigest()
74
88
  return (path, sha1)
75
89
  else:
@@ -110,14 +124,17 @@ class BomDownloadSources(capycli.common.script_base.ScriptBase):
110
124
  new = False
111
125
  ext_ref = CycloneDxSupport.get_ext_ref(
112
126
  component, ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT)
127
+ file_uri = path
128
+ if not file_uri.startswith("file://"):
129
+ file_uri = "file:///" + file_uri
113
130
  if not ext_ref:
114
131
  ext_ref = ExternalReference(
115
132
  type=ExternalReferenceType.DISTRIBUTION,
116
133
  comment=CaPyCliBom.SOURCE_FILE_COMMENT,
117
- url=XsUri(path))
134
+ url=XsUri(file_uri))
118
135
  new = True
119
136
  else:
120
- ext_ref.url = XsUri(path)
137
+ ext_ref.url = XsUri(file_uri)
121
138
  ext_ref.hashes.add(HashType(
122
139
  alg=HashAlgorithm.SHA_1,
123
140
  content=sha1))
@@ -170,6 +187,7 @@ class BomDownloadSources(capycli.common.script_base.ScriptBase):
170
187
  print(" -source SOURCE source folder or additional source file")
171
188
  print(" -o OUTPUTFILE output file to write to")
172
189
  print(" -v be verbose")
190
+ print(" -bp, --bom-package create a single zip archive that contains the SBOM and all source files")
173
191
  return
174
192
 
175
193
  if not args.inputfile:
@@ -188,21 +206,32 @@ class BomDownloadSources(capycli.common.script_base.ScriptBase):
188
206
  sys.exit(ResultCode.RESULT_ERROR_READING_BOM)
189
207
 
190
208
  if args.verbose:
191
- print_text(" " + str(len(bom.components)) + "components written to SBOM file")
209
+ print_text(" " + str(len(bom.components)) + "components read from SBOM file")
192
210
 
193
211
  source_folder = "./"
212
+ cleanup_temp_dir = False
213
+ if args.bom_package and not args.source:
214
+ temp_dir = tempfile.TemporaryDirectory(prefix="capycli_bom_pkg_")
215
+ source_folder = temp_dir.name
216
+ cleanup_temp_dir = True
194
217
  if args.source:
195
218
  source_folder = args.source
196
219
  if (not source_folder) or (not os.path.isdir(source_folder)):
197
220
  print_red("Target source code folder does not exist!")
198
221
  sys.exit(ResultCode.RESULT_COMMAND_ERROR)
199
222
 
200
- print_text("Downloading source files to folder " + source_folder + " ...")
223
+ if args.bom_package:
224
+ pp = pathlib.Path(args.bom_package)
225
+ if pp.suffix.lower() != ".zip":
226
+ print_yellow("Warning: bom package file should have .zip extension")
227
+ args.bom_package = args.bom_package + ".zip"
228
+
229
+ print_text("\nDownloading source files to folder " + source_folder + " ...")
201
230
 
202
231
  self.download_sources(bom, source_folder)
203
232
 
204
233
  if args.outputfile:
205
- print_text("Updating path information")
234
+ print_text("\nUpdating path information")
206
235
  self.update_local_path(bom, args.outputfile)
207
236
 
208
237
  print_text("Writing updated SBOM to " + args.outputfile)
@@ -215,4 +244,21 @@ class BomDownloadSources(capycli.common.script_base.ScriptBase):
215
244
  if args.verbose:
216
245
  print_text(" " + str(len(bom.components)) + " components written to SBOM file")
217
246
 
247
+ if args.bom_package:
248
+ print_text("\nCreating BOM package " + args.bom_package)
249
+ try:
250
+ # add SBOM to temp folder
251
+ sbom_file = os.path.join(source_folder, "sbom.cdx.json")
252
+ self.update_local_path(bom, sbom_file)
253
+ SbomWriter.write_to_json(bom, sbom_file, True)
254
+ shutil.make_archive(
255
+ base_name=args.bom_package.rstrip(".zip"),
256
+ format="zip",
257
+ root_dir=source_folder)
258
+ if cleanup_temp_dir:
259
+ temp_dir.cleanup()
260
+ except Exception as ex:
261
+ print_red("Error creating BOM package: " + repr(ex))
262
+ sys.exit(ResultCode.RESULT_ERROR_WRITING_BOM)
263
+
218
264
  print("\n")
@@ -195,15 +195,18 @@ class FilterBom(capycli.common.script_base.ScriptBase):
195
195
  continue
196
196
 
197
197
  match = False
198
- if "Name" in filterentry["component"]:
198
+ name = filterentry["component"].get("Name", "")
199
+ if not name:
200
+ name = filterentry["component"].get("name", "")
201
+ if name:
199
202
  prefix = ""
200
- if filterentry["component"]["Name"].endswith("*"):
201
- prefix = filterentry["component"]["Name"][:-1]
203
+ if name.endswith("*"):
204
+ prefix = name[:-1]
202
205
 
203
206
  if prefix:
204
207
  match = component.name.startswith(prefix)
205
208
  else:
206
- match = component.name == filterentry["component"]["Name"]
209
+ match = component.name == name
207
210
  elif "RepositoryId" in filterentry["component"]:
208
211
  prefix = ""
209
212
  if filterentry["component"]["RepositoryId"].endswith("*"):
@@ -93,6 +93,48 @@ class FindSources(capycli.common.script_base.ScriptBase):
93
93
  self.sw360_url: str = os.environ.get("SW360ServerUrl", "")
94
94
  self.tag_cache = self.TagCache()
95
95
 
96
+ @staticmethod
97
+ def does_url_exist(url: str) -> bool:
98
+ """Check if a URL exists"""
99
+ try:
100
+ response = requests.head(url, allow_redirects=True)
101
+ return response.ok
102
+
103
+ except requests.ConnectionError:
104
+ return False
105
+
106
+ @staticmethod
107
+ def is_github_repo(url: str) -> bool:
108
+ """Check if URL is a GitHub repository URL"""
109
+ # do *NOT* use r"^(https?://)?(www\.)?github\.com/([a-zA-Z0-9-]+)(/[a-zA-Z0-9-]+)+/?$")
110
+ return "github.com" in url.lower()
111
+
112
+ def guess_source_code_url(self, github_project: str, version: str) -> str:
113
+ """Try to guess the source code URL from project url and version"""
114
+ github_project = github_project.replace(".git", "").replace("#readme", "")
115
+ if not github_project:
116
+ return ""
117
+
118
+ if not FindSources.is_github_repo(github_project):
119
+ # only works for GitHub repos
120
+ return ""
121
+
122
+ source_url = github_project + "/archive/tags/" + version + ".zip"
123
+ LOG.debug(f" Guessing source code url {source_url}")
124
+ valid = self.is_sourcefile_accessible(source_url)
125
+ if valid:
126
+ return source_url
127
+
128
+ # try leading 'v'
129
+ source_url = github_project + "/archive/tags/v" + version + ".zip"
130
+ LOG.debug(f" Guessing source code url {source_url}")
131
+ valid = self.is_sourcefile_accessible(source_url)
132
+ if valid:
133
+ return source_url
134
+
135
+ LOG.debug(" Url does not exist")
136
+ return ""
137
+
96
138
  def is_sourcefile_accessible(self, sourcefile_url: str) -> bool:
97
139
  """Check if the URL is accessible."""
98
140
  try:
@@ -1,5 +1,5 @@
1
1
  # -------------------------------------------------------------------------------
2
- # Copyright (c) 2023 Siemens
2
+ # Copyright (c) 2023-2025 Siemens
3
3
  # All Rights Reserved.
4
4
  # Author: thomas.graf@siemens.com
5
5
  #
@@ -70,6 +70,10 @@ class LegacySupport():
70
70
 
71
71
  @staticmethod
72
72
  def get_purl_from_legacy(item: Dict[str, Any]) -> PackageURL:
73
+ if "purl" in item:
74
+ id = item.get("purl", "")
75
+ if id:
76
+ return PackageURL.from_string(id)
73
77
  if "RepositoryType" in item:
74
78
  if (item["RepositoryType"] == "package-url") or (item["RepositoryType"] == "purl"):
75
79
  id = item.get("RepositoryId", "")
@@ -201,6 +205,8 @@ class LegacySupport():
201
205
 
202
206
  binaryFile = item.get("BinaryFile", "")
203
207
  if binaryFile:
208
+ if not binaryFile.startswith("file://"):
209
+ binaryFile = "file:///" + binaryFile
204
210
  ext_ref = ExternalReference(
205
211
  type=ExternalReferenceType.DISTRIBUTION,
206
212
  comment=CaPyCliBom.BINARY_FILE_COMMENT,
@@ -1,5 +1,5 @@
1
1
  # -------------------------------------------------------------------------------
2
- # Copyright (c) 2023 Siemens
2
+ # Copyright (c) 2023-2025 Siemens
3
3
  # All Rights Reserved.
4
4
  # Author: thomas.graf@siemens.com
5
5
  #
@@ -66,10 +66,13 @@ class LegacyCx(CaPyCliBom):
66
66
  # extra handling
67
67
  prop = CycloneDxSupport.get_property(component, "source-file")
68
68
  if prop:
69
+ file_uri = prop.value
70
+ if not file_uri.startswith("file://"):
71
+ file_uri = "file:///" + file_uri
69
72
  ext_ref = ExternalReference(
70
73
  type=ExternalReferenceType.DISTRIBUTION,
71
74
  comment=CaPyCliBom.SOURCE_FILE_COMMENT,
72
- url=XsUri(prop.value))
75
+ url=XsUri(file_uri))
73
76
  prop2 = CycloneDxSupport.get_property(component, "source-file-hash")
74
77
  if prop2:
75
78
  ext_ref.hashes.add(HashType(
@@ -80,10 +83,13 @@ class LegacyCx(CaPyCliBom):
80
83
 
81
84
  prop = CycloneDxSupport.get_property(component, "source-file-url")
82
85
  if prop:
86
+ file_uri = prop.value
87
+ if not file_uri.startswith("file://"):
88
+ file_uri = "file:///" + file_uri
83
89
  ext_ref = ExternalReference(
84
90
  type=ExternalReferenceType.DISTRIBUTION,
85
91
  comment=CaPyCliBom.SOURCE_URL_COMMENT,
86
- url=XsUri(prop.value))
92
+ url=XsUri(file_uri))
87
93
  prop2 = CycloneDxSupport.get_property(component, "source-file-hash")
88
94
  if prop2:
89
95
  ext_ref.hashes.add(HashType(
@@ -112,10 +118,13 @@ class LegacyCx(CaPyCliBom):
112
118
 
113
119
  prop = CycloneDxSupport.get_property(component, "binary-file")
114
120
  if prop:
121
+ file_uri = prop.value
122
+ if not file_uri.startswith("file://"):
123
+ file_uri = "file:///" + file_uri
115
124
  ext_ref = ExternalReference(
116
125
  type=ExternalReferenceType.DISTRIBUTION,
117
126
  comment=CaPyCliBom.BINARY_FILE_COMMENT,
118
- url=XsUri(prop.value))
127
+ url=XsUri(file_uri))
119
128
  prop2 = CycloneDxSupport.get_property(component, "binary-file-hash")
120
129
  if prop2:
121
130
  ext_ref.hashes.add(HashType(
@@ -6,6 +6,7 @@
6
6
  # SPDX-License-Identifier: MIT
7
7
  # -------------------------------------------------------------------------------
8
8
 
9
+ import copy
9
10
  import json
10
11
  import logging
11
12
  import os
@@ -58,6 +59,8 @@ class MapBom(capycli.common.script_base.ScriptBase):
58
59
  self.mode = MapMode.ALL
59
60
  self.purl_service: Optional[PurlService] = None
60
61
  self.no_match_by_name_only = True
62
+ self.full_search = False
63
+ self.qualifier_match = False
61
64
 
62
65
  def is_id_match(self, release: Dict[str, Any], component: Component) -> bool:
63
66
  """Determines whether this release is a match via identifier for the specified SBOM item"""
@@ -194,31 +197,22 @@ class MapBom(capycli.common.script_base.ScriptBase):
194
197
  # first check: unique id
195
198
  if release["Sw360Id"] in result_release_ids or self.is_id_match(release, component):
196
199
  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
203
202
  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)
210
203
  break
211
- else:
212
- name_match = False
213
204
 
214
- # third check unique(?) file hashes
205
+ # second check unique(?) file hashes
215
206
  cmp_hash = CycloneDxSupport.get_source_file_hash(component)
216
207
  if (("SourceFileHash" in release)
217
208
  and cmp_hash
218
209
  and release["SourceFileHash"]):
219
210
  if (cmp_hash.lower() == release["SourceFileHash"].lower()):
220
211
  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
222
216
 
223
217
  cmp_hash = CycloneDxSupport.get_binary_file_hash(component)
224
218
  if (("BinaryFileHash" in release)
@@ -226,7 +220,28 @@ class MapBom(capycli.common.script_base.ScriptBase):
226
220
  and release["BinaryFileHash"]):
227
221
  if (cmp_hash.lower() == release["BinaryFileHash"].lower()):
228
222
  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
230
245
 
231
246
  # fourth check: source filename
232
247
  cmp_src_file = CycloneDxSupport.get_ext_ref_source_file(component)
@@ -235,7 +250,10 @@ class MapBom(capycli.common.script_base.ScriptBase):
235
250
  and release["SourceFile"]):
236
251
  if cmp_src_file.lower() == release["SourceFile"].lower():
237
252
  self.add_match_if_better(result, release, MapResult.MATCH_BY_FILENAME)
238
- break
253
+ if self.full_search:
254
+ continue
255
+ else:
256
+ break
239
257
 
240
258
  # fifth check: name and ANY version
241
259
  if name_match:
@@ -299,8 +317,10 @@ class MapBom(capycli.common.script_base.ScriptBase):
299
317
  release = get_release_details(href)
300
318
  if release:
301
319
  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
304
324
 
305
325
  if result.component_hrefs:
306
326
  components += result.component_hrefs
@@ -343,22 +363,17 @@ class MapBom(capycli.common.script_base.ScriptBase):
343
363
  self.add_match_if_better(result, release, MapResult.FULL_MATCH_BY_ID)
344
364
  break
345
365
 
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
355
367
  cmp_hash = CycloneDxSupport.get_source_file_hash(component)
356
368
  if (("SourceFileHash" in release)
357
369
  and cmp_hash
358
370
  and release["SourceFileHash"]):
359
371
  if (cmp_hash.lower() == release["SourceFileHash"].lower()):
360
372
  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
362
377
 
363
378
  cmp_hash = CycloneDxSupport.get_binary_file_hash(component)
364
379
  if (("BinaryFileHash" in release)
@@ -366,6 +381,20 @@ class MapBom(capycli.common.script_base.ScriptBase):
366
381
  and release["BinaryFileHash"]):
367
382
  if (cmp_hash.lower() == release["BinaryFileHash"].lower()):
368
383
  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:
369
398
  break
370
399
 
371
400
  # fifth check: name and ANY version
@@ -506,6 +535,9 @@ class MapBom(capycli.common.script_base.ScriptBase):
506
535
  name=match.get("Name", ""),
507
536
  version=match.get("Version", ""))
508
537
  else:
538
+ # copy component so we don't overwrite the input component
539
+ component = copy.deepcopy(component)
540
+
509
541
  # always overwrite the following properties
510
542
  name = match.get("Name", "")
511
543
  if name:
@@ -730,7 +762,9 @@ class MapBom(capycli.common.script_base.ScriptBase):
730
762
  # search release and component by purl which is independent of the component cache.
731
763
  if component.purl:
732
764
  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"]
734
768
 
735
769
  return result
736
770
 
@@ -815,9 +849,14 @@ class MapBom(capycli.common.script_base.ScriptBase):
815
849
  print(" all = default, write everything to resulting SBOM")
816
850
  print(" found = resulting SBOM shows only components that were found")
817
851
  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")
821
860
 
822
861
  def run(self, args: Any) -> None:
823
862
  """Main method()"""
@@ -849,16 +888,29 @@ class MapBom(capycli.common.script_base.ScriptBase):
849
888
  if args.verbose:
850
889
  self.verbosity = 2
851
890
 
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")
853
897
  print_text("Using relaxed debian version checks")
854
898
  self.relaxed_debian_parsing = True
855
899
 
856
900
  if args.mode:
857
901
  self.mode = args.mode
858
902
 
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")
860
906
  self.no_match_by_name_only = False
861
907
 
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
+
862
914
  print_text("Loading SBOM file", args.inputfile)
863
915
  try:
864
916
  sbom = CaPyCliBom.read_sbom(args.inputfile)
@@ -14,12 +14,13 @@ import os
14
14
  import sys
15
15
  from typing import Any
16
16
 
17
- from cyclonedx.factory.license import DisjunctiveLicense, LicenseExpression # type: ignore
18
17
  from cyclonedx.model import XsUri
19
18
  from cyclonedx.model.bom import Bom
20
19
  from cyclonedx.model.component import Component
20
+ from cyclonedx.model.license import DisjunctiveLicense, LicenseExpression
21
21
 
22
22
  import capycli.common.script_base
23
+ from capycli.bom.download_sources import BomDownloadSources
23
24
  from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport
24
25
  from capycli.common.print import print_red, print_text, print_yellow
25
26
  from capycli.main.result_codes import ResultCode
@@ -63,7 +64,11 @@ class ShowBom(capycli.common.script_base.ScriptBase):
63
64
  return
64
65
 
65
66
  for bomitem in bom.components:
66
- print_text(" " + bomitem.name + ", " + bomitem.version)
67
+ if not bomitem.version:
68
+ print_text(" " + bomitem.name)
69
+ print_yellow(" component version is missing!")
70
+ else:
71
+ print_text(" " + bomitem.name + ", " + bomitem.version)
67
72
 
68
73
  if self.verbose:
69
74
  if bomitem.purl:
@@ -80,6 +85,9 @@ class ShowBom(capycli.common.script_base.ScriptBase):
80
85
  download_url = CycloneDxSupport.get_ext_ref_source_url(bomitem)
81
86
  if download_url:
82
87
  print_text(" download URL: " + download_url.uri)
88
+ if not BomDownloadSources.is_good_source_file(download_url.uri):
89
+ print_yellow(" Download file seems not to be a valid source file!")
90
+ self.has_error = True
83
91
  else:
84
92
  print_yellow(" No download URL given!")
85
93
  self.has_error = True