capycli 2.10.0__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.10.0 → capycli-2.10.0.dev1}/PKG-INFO +2 -2
  2. {capycli-2.10.0 → capycli-2.10.0.dev1}/Readme.md +1 -1
  3. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/download_sources.py +35 -7
  4. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/handle_bom.py +1 -9
  5. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/handle_dependencies.py +1 -9
  6. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/python.py +9 -10
  7. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/options.py +9 -2
  8. {capycli-2.10.0 → capycli-2.10.0.dev1}/pyproject.toml +1 -1
  9. capycli-2.10.0/capycli/bom/bom_package.py +0 -215
  10. capycli-2.10.0/capycli/dependencies/rust.py +0 -446
  11. {capycli-2.10.0 → capycli-2.10.0.dev1}/License.md +0 -0
  12. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/__init__.py +0 -0
  13. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/__main__.py +0 -0
  14. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/__init__.py +0 -0
  15. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/bom_convert.py +0 -0
  16. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/bom_validate.py +0 -0
  17. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/check_bom.py +0 -0
  18. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/check_bom_item_status.py +0 -0
  19. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/check_granularity.py +0 -0
  20. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/create_components.py +0 -0
  21. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/csv.py +0 -0
  22. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/diff_bom.py +0 -0
  23. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/filter_bom.py +0 -0
  24. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/findsources.py +0 -0
  25. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/html.py +0 -0
  26. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/legacy.py +0 -0
  27. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/legacy_cx.py +0 -0
  28. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/map_bom.py +0 -0
  29. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/merge_bom.py +0 -0
  30. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/plaintext.py +0 -0
  31. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/show_bom.py +0 -0
  32. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/__init__.py +0 -0
  33. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/capycli_bom_support.py +0 -0
  34. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/comparable_version.py +0 -0
  35. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/component_cache.py +0 -0
  36. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/dependencies_base.py +0 -0
  37. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/file_support.py +0 -0
  38. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/github_support.py +0 -0
  39. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/html_support.py +0 -0
  40. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/json_support.py +0 -0
  41. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/map_result.py +0 -0
  42. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/print.py +0 -0
  43. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/purl_service.py +0 -0
  44. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/purl_store.py +0 -0
  45. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/purl_utils.py +0 -0
  46. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/script_base.py +0 -0
  47. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/script_support.py +0 -0
  48. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/data/__init__.py +0 -0
  49. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/data/granularity_list.csv +0 -0
  50. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/__init__.py +0 -0
  51. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/javascript.py +0 -0
  52. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/maven_list.py +0 -0
  53. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/maven_pom.py +0 -0
  54. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/nuget.py +0 -0
  55. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/__init__.py +0 -0
  56. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/application.py +0 -0
  57. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/argument_parser.py +0 -0
  58. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/cli.py +0 -0
  59. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/exceptions.py +0 -0
  60. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/result_codes.py +0 -0
  61. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/mapping/handle_mapping.py +0 -0
  62. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/mapping/mapping_to_html.py +0 -0
  63. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/mapping/mapping_to_xlsx.py +0 -0
  64. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/moverview/handle_moverview.py +0 -0
  65. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/moverview/moverview_to_html.py +0 -0
  66. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/moverview/moverview_to_xlsx.py +0 -0
  67. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/__init__.py +0 -0
  68. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/check_prerequisites.py +0 -0
  69. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/create_bom.py +0 -0
  70. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/create_project.py +0 -0
  71. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/create_readme.py +0 -0
  72. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/find_project.py +0 -0
  73. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/get_license_info.py +0 -0
  74. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/handle_project.py +0 -0
  75. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/show_ecc.py +0 -0
  76. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/show_licenses.py +0 -0
  77. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/show_project.py +0 -0
  78. {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/show_vulnerabilities.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capycli
3
- Version: 2.10.0
3
+ Version: 2.10.0.dev1
4
4
  Summary: CaPyCli - Clearing Automation Python Command Line Interface for SW360
5
5
  License-Expression: MIT
6
6
  License-File: License.md
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
42
42
  # SPDX-License-Identifier: MIT
43
43
  -->
44
44
 
45
- ![Header_Image](https://github.com/sw360/capycli/blob/0318f71ba1f9ba177432840717768fccfad1d6da/images/Github-social-capycli.png)
45
+ ![Header_Image](images/Github-social-capycli.png)
46
46
 
47
47
  # CaPyCli - Clearing Automation Python Command Line Tool for SW360
48
48
 
@@ -3,7 +3,7 @@
3
3
  # SPDX-License-Identifier: MIT
4
4
  -->
5
5
 
6
- ![Header_Image](https://github.com/sw360/capycli/blob/0318f71ba1f9ba177432840717768fccfad1d6da/images/Github-social-capycli.png)
6
+ ![Header_Image](images/Github-social-capycli.png)
7
7
 
8
8
  # CaPyCli - Clearing Automation Python Command Line Tool for SW360
9
9
 
@@ -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
 
@@ -34,8 +36,7 @@ class BomDownloadSources(capycli.common.script_base.ScriptBase):
34
36
  Download source files from the URL specified in the SBOM.
35
37
  """
36
38
 
37
- @staticmethod
38
- def get_filename_from_cd(cd: str) -> str:
39
+ def get_filename_from_cd(self, cd: str) -> str:
39
40
  """
40
41
  Get filename from content-disposition.
41
42
  """
@@ -56,20 +57,18 @@ class BomDownloadSources(capycli.common.script_base.ScriptBase):
56
57
  _, extension = os.path.splitext(filename)
57
58
  return extension in good_extensions
58
59
 
59
- @staticmethod
60
- def download_source_file(url: str, source_folder: str, is_binary: bool = False) -> Optional[Tuple[str, str]]:
60
+ def download_source_file(self, url: str, source_folder: str) -> Optional[Tuple[str, str]]:
61
61
  """Download a file from a URL.
62
62
 
63
63
  @params:
64
64
  url - Required : url of the file to get uploaded (string)
65
65
  source_folder - Required : folder to store the source files (string)
66
- is_binary - Optional : whether the file is a binary file (boolean, default: False)
67
66
  """
68
67
  print_text(" URL = " + url)
69
68
 
70
69
  try:
71
70
  response = requests.get(url, allow_redirects=True)
72
- filename = BomDownloadSources.get_filename_from_cd(response.headers.get("content-disposition", ""))
71
+ filename = self.get_filename_from_cd(response.headers.get("content-disposition", ""))
73
72
  if not filename:
74
73
  filename_ps = urlparse(url)
75
74
  if filename_ps:
@@ -83,7 +82,7 @@ class BomDownloadSources(capycli.common.script_base.ScriptBase):
83
82
  path = os.path.join(source_folder, filename)
84
83
  if (response.status_code == requests.codes["ok"]):
85
84
  open(path, "wb").write(response.content)
86
- if not is_binary and not BomDownloadSources.is_good_source_file(path):
85
+ if not BomDownloadSources.is_good_source_file(path):
87
86
  print_yellow(" Downloaded file seems not to be a valid source file!")
88
87
  sha1 = hashlib.sha1(response.content).hexdigest()
89
88
  return (path, sha1)
@@ -188,6 +187,7 @@ class BomDownloadSources(capycli.common.script_base.ScriptBase):
188
187
  print(" -source SOURCE source folder or additional source file")
189
188
  print(" -o OUTPUTFILE output file to write to")
190
189
  print(" -v be verbose")
190
+ print(" -bp, --bom-package create a single zip archive that contains the SBOM and all source files")
191
191
  return
192
192
 
193
193
  if not args.inputfile:
@@ -209,12 +209,23 @@ class BomDownloadSources(capycli.common.script_base.ScriptBase):
209
209
  print_text(" " + str(len(bom.components)) + "components read from SBOM file")
210
210
 
211
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
212
217
  if args.source:
213
218
  source_folder = args.source
214
219
  if (not source_folder) or (not os.path.isdir(source_folder)):
215
220
  print_red("Target source code folder does not exist!")
216
221
  sys.exit(ResultCode.RESULT_COMMAND_ERROR)
217
222
 
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
+
218
229
  print_text("\nDownloading source files to folder " + source_folder + " ...")
219
230
 
220
231
  self.download_sources(bom, source_folder)
@@ -233,4 +244,21 @@ class BomDownloadSources(capycli.common.script_base.ScriptBase):
233
244
  if args.verbose:
234
245
  print_text(" " + str(len(bom.components)) + " components written to SBOM file")
235
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
+
236
264
  print("\n")
@@ -1,5 +1,5 @@
1
1
  # -------------------------------------------------------------------------------
2
- # Copyright (c) 2019-2025 Siemens
2
+ # Copyright (c) 2019-24 Siemens
3
3
  # All Rights Reserved.
4
4
  # Author: thomas.graf@siemens.com
5
5
  #
@@ -10,7 +10,6 @@ import sys
10
10
  from typing import Any
11
11
 
12
12
  import capycli.bom.bom_convert
13
- import capycli.bom.bom_package
14
13
  import capycli.bom.bom_validate
15
14
  import capycli.bom.check_bom
16
15
  import capycli.bom.check_bom_item_status
@@ -52,7 +51,6 @@ def run_bom_command(args: Any) -> None:
52
51
  print(" Merge merge two bills of material")
53
52
  print(" Findsources determine the source code for SBOM items")
54
53
  print(" Validate validate an SBOM")
55
- print(" BomPackage create a single archive that contains the SBOM and all source and binary files")
56
54
  return
57
55
 
58
56
  subcommand = args.command[1].lower()
@@ -141,11 +139,5 @@ def run_bom_command(args: Any) -> None:
141
139
  app14.run(args)
142
140
  return
143
141
 
144
- if subcommand == "bompackage":
145
- """Validate an SBOM."""
146
- app15 = capycli.bom.bom_package.BomPackage()
147
- app15.run(args)
148
- return
149
-
150
142
  print_red("Unknown sub-command: ")
151
143
  sys.exit(ResultCode.RESULT_COMMAND_ERROR)
@@ -1,5 +1,5 @@
1
1
  # -------------------------------------------------------------------------------
2
- # Copyright (c) 2019-2025 Siemens
2
+ # Copyright (c) 2019-23 Siemens
3
3
  # All Rights Reserved.
4
4
  # Author: thomas.graf@siemens.com
5
5
  #
@@ -14,7 +14,6 @@ import capycli.dependencies.maven_list
14
14
  import capycli.dependencies.maven_pom
15
15
  import capycli.dependencies.nuget
16
16
  import capycli.dependencies.python
17
- import capycli.dependencies.rust
18
17
  from capycli.common.print import print_red
19
18
  from capycli.main.result_codes import ResultCode
20
19
 
@@ -35,7 +34,6 @@ def run_dependency_command(args: Any) -> None:
35
34
  print(" Javascript determine dependencies for a JavaScript project")
36
35
  print(" MavenPom determine dependencies for a Java/Maven project using the pom.xml file")
37
36
  print(" MavenList determine dependencies for a Java/Maven project using a Maven command")
38
- print(" Rust determine dependencies for a Rust project")
39
37
  return
40
38
 
41
39
  subcommand = args.command[1].lower()
@@ -69,11 +67,5 @@ def run_dependency_command(args: Any) -> None:
69
67
  app5.run(args)
70
68
  return
71
69
 
72
- if subcommand == "rust":
73
- """Determine Rust components/dependencies for a given project"""
74
- app6 = capycli.dependencies.rust.GetRustDependencies()
75
- app6.run(args)
76
- return
77
-
78
70
  print_red("Unknown sub-command: " + subcommand)
79
71
  sys.exit(ResultCode.RESULT_COMMAND_ERROR)
@@ -779,8 +779,7 @@ class GetPythonDependencies(capycli.common.script_base.ScriptBase):
779
779
 
780
780
  return sbom
781
781
 
782
- @staticmethod
783
- def check_meta_data(sbom: Bom, verbose: bool) -> bool:
782
+ def check_meta_data(self, sbom: Bom) -> bool:
784
783
  """
785
784
  Check whether all required meta-data is available.
786
785
 
@@ -791,37 +790,37 @@ class GetPythonDependencies(capycli.common.script_base.ScriptBase):
791
790
  bool: True if all required meta-data is available; otherwise False.
792
791
  """
793
792
 
794
- if verbose:
793
+ if self.verbose:
795
794
  print_text("\nChecking meta-data:")
796
795
 
797
796
  result = True
798
797
  cxcomp: Component
799
798
  for cxcomp in sbom.components:
800
- if verbose:
799
+ if self.verbose:
801
800
  print_text(f" {cxcomp.name}, {cxcomp.version}")
802
801
 
803
802
  if not cxcomp.purl:
804
803
  result = False
805
- if verbose:
804
+ if self.verbose:
806
805
  print_yellow(" package-url missing")
807
806
 
808
807
  homepage = CycloneDxSupport.get_ext_ref_website(cxcomp)
809
808
  if not homepage:
810
809
  result = False
811
- if verbose:
810
+ if self.verbose:
812
811
  print_yellow(" Homepage missing")
813
812
 
814
813
  if not cxcomp.licenses:
815
- if verbose:
814
+ if self.verbose:
816
815
  LOG.debug(" License missing")
817
816
  elif len(cxcomp.licenses) == 0:
818
- if verbose:
817
+ if self.verbose:
819
818
  LOG.debug(" License missing")
820
819
 
821
820
  src_url = CycloneDxSupport.get_ext_ref_source_url(cxcomp)
822
821
  if not src_url:
823
822
  result = False
824
- if verbose:
823
+ if self.verbose:
825
824
  print_yellow(" Source code URL missing")
826
825
 
827
826
  return result
@@ -885,7 +884,7 @@ class GetPythonDependencies(capycli.common.script_base.ScriptBase):
885
884
  print_text("Formatting package list...")
886
885
  sbom = self.convert_package_list(package_list, args.search_meta_data, args.package_source)
887
886
 
888
- GetPythonDependencies.check_meta_data(sbom, self.verbose)
887
+ self.check_meta_data(sbom)
889
888
 
890
889
  if self.verbose:
891
890
  print()
@@ -1,5 +1,5 @@
1
1
  # -------------------------------------------------------------------------------
2
- # Copyright (c) 2019-2025 Siemens
2
+ # Copyright (c) 2019-25 Siemens
3
3
  # All Rights Reserved.
4
4
  # Author: thomas.graf@siemens.com
5
5
  #
@@ -49,7 +49,6 @@ class CommandlineSupport():
49
49
  Merge merge two bills of material
50
50
  Findsources determine the source code for SBOM items
51
51
  Validate validate an SBOM
52
- BomPackage create a single archive that contains the SBOM and all source and binary files
53
52
 
54
53
  mapping
55
54
  ToHtml create a HTML page showing the mapping result
@@ -426,6 +425,14 @@ class CommandlineSupport():
426
425
  help="copy the project with the given id and the update it",
427
426
  )
428
427
 
428
+ # used by BomDownloadSources
429
+ self.parser.add_argument(
430
+ "-bp",
431
+ "--bom-package",
432
+ dest="bom_package",
433
+ help="create a single zip archive that contains the SBOM and all source files",
434
+ )
435
+
429
436
  def read_config(self, filename: str = "", config_string: str = "") -> Dict[str, Any]:
430
437
  """
431
438
  Read configuration from string or config file.
@@ -3,7 +3,7 @@
3
3
 
4
4
  [project]
5
5
  name = "capycli"
6
- version = "2.10.0"
6
+ version = "2.10.0.dev1"
7
7
  description = "CaPyCli - Clearing Automation Python Command Line Interface for SW360"
8
8
  readme="Readme.md"
9
9
  requires-python = ">=3.11,<3.15"
@@ -1,215 +0,0 @@
1
- # -------------------------------------------------------------------------------
2
- # Copyright (c) 2025 Siemens
3
- # All Rights Reserved.
4
- # Author: thomas.graf@siemens.com
5
- #
6
- # SPDX-License-Identifier: MIT
7
- # -------------------------------------------------------------------------------
8
-
9
- import logging
10
- import os
11
- import pathlib
12
- import posixpath
13
- import shutil
14
- import sys
15
- import tempfile
16
- from typing import Any
17
-
18
- from cyclonedx.model import ExternalReference, ExternalReferenceType, HashAlgorithm, HashType, XsUri
19
- from cyclonedx.model.bom import Bom
20
-
21
- import capycli.common.script_base
22
- from capycli.bom.download_sources import BomDownloadSources
23
- from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomWriter
24
- from capycli.common.print import print_red, print_text, print_yellow
25
- from capycli.common.script_support import ScriptSupport
26
- from capycli.main.result_codes import ResultCode
27
-
28
- LOG = capycli.get_logger(__name__)
29
-
30
-
31
- class BomPackage(capycli.common.script_base.ScriptBase):
32
- """
33
- Create a single zip archive that contains the SBOM and all source and binary files.
34
- The archive shoukd have the following structure:
35
-
36
- sbom.cdx.json
37
- +--- binaries
38
- | +--- 77100a62c2e6f04b53977b9f541044d7d722693d
39
- | | `--- some-binary.jar
40
- | +--- 8031352b2bb0a49e67818bf04c027aa92e645d5c
41
- | | `--- another-binary.jar
42
- | `--- (... more ...)
43
- `--- sources
44
- +--- 6bb10559db88828dac3627de26974035a5dd4ddb
45
- | `--- some-sources.jar
46
- +--- 4d44e4edc4a7fb39f09b95b09f560a15976fa1ba
47
- | `--- another-sources.jar
48
- `--- (... more ...)
49
- """
50
- def download_files(self, sbom: Bom, target_folder: str) -> None:
51
- """Download source and binary files for all items of the SBOM.
52
-
53
- @params:
54
- bom - Required : the bill of materials (BOM) (list)
55
- target_folder - Required : folder to store the source files (string)
56
- """
57
-
58
- for component in sbom.components:
59
- item_name = ScriptSupport.get_full_name_from_component(component)
60
- print_text(" " + item_name)
61
-
62
- # download source file
63
- source_url = CycloneDxSupport.get_ext_ref_source_url(component)
64
- if source_url:
65
- result = BomDownloadSources.download_source_file(source_url._uri, target_folder)
66
- else:
67
- result = None
68
- print_red(" No source URL specified!")
69
-
70
- if result is not None:
71
- (path, sha1) = result
72
- # move file to appropriate location
73
- filename = pathlib.Path(path).name
74
- targetsha = os.path.join(target_folder, "sources", sha1)
75
- os.makedirs(targetsha, exist_ok=True)
76
- target = os.path.join(targetsha, filename)
77
- shutil.move(path, target)
78
-
79
- # update SBOM
80
- new = False
81
- ext_ref = CycloneDxSupport.get_ext_ref(
82
- component, ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT)
83
- file_uri = posixpath.join("sources", sha1, filename)
84
- if not file_uri.startswith("file://"):
85
- file_uri = "file:///" + file_uri
86
- if not ext_ref:
87
- ext_ref = ExternalReference(
88
- type=ExternalReferenceType.DISTRIBUTION,
89
- comment=CaPyCliBom.SOURCE_FILE_COMMENT,
90
- url=XsUri(file_uri))
91
- new = True
92
- else:
93
- ext_ref.url = XsUri(file_uri)
94
- ext_ref.hashes.add(HashType(
95
- alg=HashAlgorithm.SHA_1,
96
- content=sha1))
97
- if new:
98
- component.external_references.add(ext_ref)
99
-
100
- # download binary file
101
- binary_url = CycloneDxSupport.get_ext_ref_binary_url(component)
102
- if binary_url:
103
- result = BomDownloadSources.download_source_file(binary_url._uri, target_folder, is_binary=True)
104
- else:
105
- result = None
106
- print_yellow(" No binary URL specified!")
107
-
108
- if result is not None:
109
- (path, sha1) = result
110
- # move file to appropriate location
111
- filename = pathlib.Path(path).name
112
- targetsha = os.path.join(target_folder, "binaries", sha1)
113
- os.makedirs(targetsha, exist_ok=True)
114
- target = os.path.join(targetsha, filename)
115
- shutil.move(path, target)
116
-
117
- # update SBOM
118
- new = False
119
- ext_ref = CycloneDxSupport.get_ext_ref(
120
- component, ExternalReferenceType.DISTRIBUTION, CaPyCliBom.BINARY_FILE_COMMENT)
121
- file_uri = posixpath.join("binaries", sha1, filename)
122
- if not file_uri.startswith("file://"):
123
- file_uri = "file:///" + file_uri
124
- if not ext_ref:
125
- ext_ref = ExternalReference(
126
- type=ExternalReferenceType.DISTRIBUTION,
127
- comment=CaPyCliBom.BINARY_FILE_COMMENT,
128
- url=XsUri(file_uri))
129
- new = True
130
- else:
131
- ext_ref.url = XsUri(file_uri)
132
- ext_ref.hashes.add(HashType(
133
- alg=HashAlgorithm.SHA_1,
134
- content=sha1))
135
- if new:
136
- component.external_references.add(ext_ref)
137
-
138
- def run(self, args: Any) -> None:
139
- """Main method
140
-
141
- @params:
142
- args - command line arguments
143
- """
144
- if args.debug:
145
- global LOG
146
- LOG = capycli.get_logger(__name__)
147
- else:
148
- # suppress (debug) log output from requests and urllib
149
- logging.getLogger("requests").setLevel(logging.WARNING)
150
- logging.getLogger("urllib3").setLevel(logging.WARNING)
151
- logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
152
-
153
- print_text(
154
- "\n" + capycli.get_app_signature() +
155
- " - create a single zip archive that contains the SBOM and all source and binary files\n")
156
-
157
- if args.help:
158
- print("usage: capycli bom bompackage -i bom.json")
159
- print("")
160
- print("optional arguments:")
161
- print(" -h, --help show this help message and exit")
162
- print(" -i INPUTFILE, input SBOM file to read from (JSON)")
163
- print(" -o OUTPUT ARCHIVE, path of the output zip archive")
164
- print(" -v be verbose")
165
- return
166
-
167
- if not args.inputfile:
168
- print_red("No input file specified!")
169
- sys.exit(ResultCode.RESULT_COMMAND_ERROR)
170
-
171
- if not os.path.isfile(args.inputfile):
172
- print_red("Input file not found!")
173
- sys.exit(ResultCode.RESULT_FILE_NOT_FOUND)
174
-
175
- if not args.outputfile:
176
- print_red("No output file specified!")
177
- sys.exit(ResultCode.RESULT_COMMAND_ERROR)
178
-
179
- print_text("Loading SBOM file " + args.inputfile)
180
- try:
181
- bom = CaPyCliBom.read_sbom(args.inputfile)
182
- except Exception as ex:
183
- print_red("Error reading input SBOM file: " + repr(ex))
184
- sys.exit(ResultCode.RESULT_ERROR_READING_BOM)
185
-
186
- if args.verbose:
187
- print_text(" " + str(len(bom.components)) + "components read from SBOM file")
188
-
189
- temp_dir = tempfile.TemporaryDirectory(prefix="capycli_bom_pkg_")
190
- target_folder = temp_dir.name
191
-
192
- pp = pathlib.Path(args.outputfile)
193
- if pp.suffix.lower() != ".zip":
194
- print_yellow("Warning: bom package file should have .zip extension")
195
- args.bom_package = args.outputfile + ".zip"
196
-
197
- print_text("\nDownloading files to folder " + target_folder + " ...")
198
-
199
- self.download_files(bom, target_folder)
200
-
201
- print_text("\nCreating BOM package " + args.outputfile)
202
- try:
203
- # add SBOM to temp folder
204
- sbom_file = os.path.join(target_folder, "sbom.cdx.json")
205
- SbomWriter.write_to_json(bom, sbom_file, True)
206
- shutil.make_archive(
207
- base_name=args.outputfile.rstrip(".zip"),
208
- format="zip",
209
- root_dir=target_folder)
210
- temp_dir.cleanup()
211
- except Exception as ex:
212
- print_red("Error creating BOM package: " + repr(ex))
213
- sys.exit(ResultCode.RESULT_ERROR_WRITING_BOM)
214
-
215
- print("\n")
@@ -1,446 +0,0 @@
1
- # -------------------------------------------------------------------------------
2
- # Copyright (c) 2025 Siemens
3
- # All Rights Reserved.
4
- # Author: thomas.graf@siemens.com
5
- #
6
- # SPDX-License-Identifier: MIT
7
- # -------------------------------------------------------------------------------
8
-
9
- import logging
10
- import os
11
- import sys
12
- import tomllib
13
- from dataclasses import dataclass
14
- from typing import Any, Dict, List, Optional
15
-
16
- import requests
17
- from cyclonedx.contrib.license.factories import LicenseFactory
18
- from cyclonedx.model import ExternalReference, ExternalReferenceType, Property, XsUri
19
- from cyclonedx.model.bom import Bom
20
- from cyclonedx.model.component import Component
21
- from halo import Halo
22
- from packageurl import PackageURL
23
-
24
- import capycli.common.script_base
25
- from capycli import get_logger
26
- from capycli.bom.findsources import FindSources
27
- from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomCreator, SbomWriter
28
- from capycli.common.print import print_red, print_text, print_yellow
29
- from capycli.dependencies.python import GetPythonDependencies
30
- from capycli.main.result_codes import ResultCode
31
-
32
- LOG = get_logger(__name__)
33
-
34
-
35
- @dataclass
36
- class PackageEntry:
37
- """Represents the relevant data of a cargo.toml or cargo.lock entry."""
38
- name: str
39
- version: str
40
- description: str
41
- source: str
42
- checksum: str
43
- dependencies: List[str]
44
- added: bool
45
-
46
-
47
- class GetRustDependencies(capycli.common.script_base.ScriptBase):
48
- """
49
- Determine Rust components/dependencies for a given project
50
- """
51
-
52
- def __init__(self) -> None:
53
- self.verbose = False
54
- self.github_name: str = ""
55
- self.github_token: str = ""
56
- self.spinner_shape = {
57
- "interval": 80,
58
- "frames": [
59
- "⣾",
60
- "⣽",
61
- "⣻",
62
- "⢿",
63
- "⡿",
64
- "⣟",
65
- "⣯",
66
- "⣷"
67
- ]
68
- }
69
-
70
- def get_package_meta_info(self, name: str, version: str) -> Optional[Dict[str, Any]]:
71
- """
72
- Retrieves meta data of the given package from crates.io.
73
-
74
- :param name: the name of the component.
75
- :param version: the version of the component.
76
- :type name: string.
77
- :type version: string.
78
- :return: the PyPi meta data.
79
- :rtype: JSON dictionary or None.
80
- """
81
- url = "https://crates.io/api/v1/crates/" + name + "/" + version
82
-
83
- if self.verbose:
84
- LOG.debug(" Retrieving meta data for " + name + ", " + version)
85
-
86
- try:
87
- response = requests.get(url)
88
- if not response.ok:
89
- print_yellow(
90
- " WARNING: no meta data available for package " +
91
- name + ", " + version)
92
- return None
93
-
94
- json = response.json()
95
- return json
96
- except Exception as ex:
97
- print_red(
98
- " ERROR: unable to retrieve meta data for package " +
99
- name + ", " + version + ": " + str(ex))
100
-
101
- return None
102
-
103
- def add_meta_data_to_bomitem(self, cxcomp: Component) -> None:
104
- """
105
- Try to lookup meta data for the given item.
106
-
107
- :param bomitem: a single bill of material item (a single component)
108
- :type bomitem: dictionary
109
- """
110
- version = ""
111
- if cxcomp.version:
112
- version = cxcomp.version
113
- metadata = self.get_package_meta_info(cxcomp.name, version)
114
- if not metadata:
115
- LOG.debug(f"No metadata found for {cxcomp.name}, {cxcomp.version}")
116
- return
117
-
118
- if metadata:
119
- data = metadata.get("version", {})
120
-
121
- # "repository": "https://github.com/microsoft/windows-rs"
122
- repository = data.get("repository", "")
123
- if repository:
124
- LOG.debug(" got repository")
125
- ext_ref = ExternalReference(
126
- type=ExternalReferenceType.VCS,
127
- url=XsUri(repository))
128
- cxcomp.external_references.add(ext_ref)
129
-
130
- homepage = data.get("homepage", "")
131
- if homepage == "None":
132
- homepage = ""
133
- if not homepage and repository:
134
- homepage = repository
135
- if homepage:
136
- LOG.debug(" got website/homepage")
137
- ext_ref = ExternalReference(
138
- type=ExternalReferenceType.WEBSITE,
139
- url=XsUri(homepage))
140
- cxcomp.external_references.add(ext_ref)
141
-
142
- # "license": "MIT OR Apache-2.0"
143
- license: str = data.get("license", "")
144
- if license:
145
- license_factory = LicenseFactory()
146
- # most Rust components are dual-licensed, MIT OR Apache-2.0
147
- if (license.lower() == "mit or apache-2.0") or (license.lower() == "apache-2.0 or mit"):
148
- cxcomp.licenses.add(license_factory.make_with_expression(license))
149
- else:
150
- cxcomp.licenses.add(license_factory.make_with_name(license))
151
- LOG.debug(" got license")
152
-
153
- # "checksum": "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
154
-
155
- # "description": "Rust for Windows
156
- description = data.get("description", "")
157
- if description and not cxcomp.description:
158
- cxcomp.description = description
159
-
160
- # before we use the dl_path information, let's see whether we
161
- # have a homepage URL already *and* it is GitHub *and*
162
- # we find the matching source code on GitHub
163
- source_url = ""
164
- if homepage and FindSources.is_github_repo(homepage):
165
- fs = FindSources()
166
- fs.github_name = self.github_name
167
- fs.github_token = self.github_token
168
-
169
- # first try to guess the source code URL.
170
- # this works for GitHub releases and does no require
171
- # rate-limited GitHub API calls
172
- source_url = fs.guess_source_code_url(homepage, version=version)
173
- if source_url:
174
- ext_ref = ExternalReference(
175
- type=ExternalReferenceType.DISTRIBUTION,
176
- comment=CaPyCliBom.SOURCE_URL_COMMENT,
177
- url=XsUri(source_url))
178
- cxcomp.external_references.add(ext_ref)
179
- LOG.debug(" got GitHub source file url")
180
- else:
181
- # ok, guess does not help.
182
- # Lets hope that the GitHub API can help us
183
- # beforer we run into rate-limiting issues
184
- source_url = fs.get_github_source_url(homepage, version=version)
185
- if source_url:
186
- ext_ref = ExternalReference(
187
- type=ExternalReferenceType.DISTRIBUTION,
188
- comment=CaPyCliBom.SOURCE_URL_COMMENT,
189
- url=XsUri(source_url))
190
- cxcomp.external_references.add(ext_ref)
191
- LOG.debug(" got GitHub source file url")
192
-
193
- # "dl_path": "/api/v1/crates/windows-sys/0.61.2/download"
194
- dl_path = data.get("dl_path", "")
195
- dl_path = "https://crates.io" + dl_path
196
- if not source_url and dl_path:
197
- ext_ref = ExternalReference(
198
- type=ExternalReferenceType.DISTRIBUTION,
199
- comment=CaPyCliBom.SOURCE_URL_COMMENT,
200
- url=XsUri(dl_path))
201
- cxcomp.external_references.add(ext_ref)
202
- LOG.debug(" got dl_path")
203
-
204
- def read_toml_file(self, filename: str, err_hint: str = "") -> Dict[str, Any]:
205
- """
206
- Ready a TOML file.
207
-
208
- Args:
209
- filename (str): the filename
210
-
211
- Returns:
212
- dict[str, Any]: dictionary
213
- """
214
- try:
215
- with open(filename, "rb") as f:
216
- toml_data = tomllib.load(f)
217
-
218
- return toml_data
219
- except Exception as ex:
220
- LOG.debug(f"Does not look like a {err_hint} file: " + repr(ex))
221
-
222
- return {}
223
-
224
- def analyze_cargo_toml(self,
225
- filename: str,
226
- packages: list[PackageEntry]) -> None:
227
- """
228
- Analyze a Cargo.toml file.
229
-
230
- Args:
231
- filename (str): the filename
232
- """
233
- manifest = self.read_toml_file(filename, "Cargo.toml")
234
-
235
- # analyze project cargo.toml file(s)
236
- if "package" in manifest:
237
- pkg = PackageEntry(
238
- name=manifest["package"]["name"],
239
- version=manifest["package"].get("version", ""),
240
- description=manifest["package"].get("description", ""),
241
- source="",
242
- checksum="",
243
- dependencies=[],
244
- added=False
245
- )
246
- packages.append(pkg)
247
- print_text(f" Found package: {pkg.name}, version: {pkg.version}")
248
-
249
- def analyze_cargo_lock(self, filename: str) -> list[PackageEntry]:
250
- """
251
- Analyze a Cargo.lock file and return all packages/entries found.
252
- """
253
- cargo_lock = self.read_toml_file(filename, "Cargo.lock")
254
- cargo_lock_version = cargo_lock.get("version", 1)
255
- LOG.debug(f" Cargo.lock version: {cargo_lock_version}")
256
-
257
- entry_list: List[PackageEntry] = []
258
- for package in cargo_lock.get("package", []):
259
- pkg = PackageEntry(
260
- name=package.get("name", "").strip(),
261
- version=package.get("version", "").strip(),
262
- description=package.get("description", "").strip(),
263
- source=package.get("source", "").strip(),
264
- checksum=package.get("checksum", "").strip(),
265
- # dependencies=[dep.split(" ")[0] for dep in package.get("dependencies", [])]
266
- dependencies=[dep for dep in package.get("dependencies", [])],
267
- added=False)
268
-
269
- LOG.debug(f" Processing raw entry: {pkg.name}, {pkg.version}")
270
- entry_list.append(pkg)
271
-
272
- return entry_list
273
-
274
- def find_lock_entry(self, name: str, entries: List[PackageEntry]) -> Optional[PackageEntry]:
275
- for entry in entries:
276
- if name == entry.name:
277
- return entry
278
-
279
- return None
280
-
281
- def add_entry(self,
282
- entry: PackageEntry,
283
- entry_list: List[PackageEntry],
284
- all_entries: List[PackageEntry],
285
- is_package: bool) -> None:
286
- """Adds an entry and all its dependencies to the final list."""
287
- if entry.added:
288
- return
289
-
290
- if not is_package:
291
- entry_list.append(entry)
292
- entry.added = True
293
- else:
294
- print_yellow(f" Ignoring package: {entry.name}, {entry.version}")
295
- for dep2 in entry.dependencies:
296
- dep_entry = self.find_lock_entry(dep2, all_entries)
297
- if dep_entry:
298
- if not dep_entry.source:
299
- print_yellow(f" Ignoring local dependency: {dep_entry.name}, {dep_entry.version}")
300
- continue
301
- self.add_entry(dep_entry, entry_list, all_entries, False)
302
- else:
303
- LOG.warning(f"Dependency {dep2} not found!")
304
-
305
- def get_lock_file_entries_for_sbom(self,
306
- all_entries: List[PackageEntry],
307
- packages: list[PackageEntry]) -> List[PackageEntry]:
308
- """Filter lock file entries to get rid of dev, etc. dependencies."""
309
- entry_list: List[PackageEntry] = []
310
- for package in packages:
311
- entry = self.find_lock_entry(package.name, all_entries)
312
- if entry:
313
- self.add_entry(entry, entry_list, all_entries, True)
314
- else:
315
- LOG.warning(f"Dependency {package} not found!")
316
-
317
- return entry_list
318
-
319
- def sbom_from_cargo_files(self, folder: str, search_meta_data: bool) -> Bom:
320
- manifest = self.read_toml_file(os.path.join(folder, "Cargo.toml"))
321
-
322
- # analyze workspace or single project
323
- project_files: list[str] = []
324
- if "workspace" in manifest:
325
- print_text("Evaluating Cargo workspace...")
326
- projects = manifest["workspace"]["members"]
327
- for proj in projects:
328
- project_files.append(os.path.join(folder, proj, "Cargo.toml"))
329
- else:
330
- project_files.append(os.path.join(folder, "Cargo.toml"))
331
-
332
- # analyze project cargo.toml file(s)
333
- packages: list[PackageEntry] = []
334
- for proj_file in project_files:
335
- print_text(" Analyzing project file: " + proj_file)
336
- self.analyze_cargo_toml(proj_file, packages)
337
-
338
- # analyze lock file
339
- print_text(" Analyzing lock file...")
340
- all_entries = self.analyze_cargo_lock(os.path.join(folder, "Cargo.lock"))
341
- entries = self.get_lock_file_entries_for_sbom(all_entries, packages)
342
-
343
- creator = SbomCreator()
344
- sbom = creator.create([], addlicense=True, addprofile=True, addtools=True)
345
-
346
- if search_meta_data:
347
- print_text("\nRetrieving package meta data")
348
- if self.verbose:
349
- spinner = Halo(text="Retrieving package meta data", spinner=self.spinner_shape)
350
- spinner.start()
351
-
352
- if len(packages) > 0:
353
- # add application/package
354
- app_comp = Component(
355
- name=packages[0].name,
356
- version=packages[0].version,
357
- description=packages[0].description)
358
-
359
- for package in entries:
360
- if search_meta_data and self.verbose:
361
- spinner.text = f"Processing package {package.name}, {package.version}"
362
- purl = PackageURL(type="cargo", name=package.name, version=package.version)
363
- cxcomp = Component(
364
- name=package.name,
365
- version=package.version,
366
- purl=purl,
367
- bom_ref=purl.to_string(),
368
- description=package.description)
369
-
370
- prop = Property(
371
- name=CycloneDxSupport.CDX_PROP_LANGUAGE,
372
- value="Rust")
373
- cxcomp.properties.add(prop)
374
-
375
- if search_meta_data:
376
- self.add_meta_data_to_bomitem(cxcomp)
377
-
378
- sbom.components.add(cxcomp)
379
- sbom.register_dependency(app_comp, [cxcomp])
380
-
381
- sbom.metadata.component = app_comp
382
-
383
- if search_meta_data and self.verbose:
384
- spinner.succeed('Package meta data processing completed.')
385
- spinner.stop()
386
-
387
- return sbom
388
-
389
- def run(self, args: Any) -> None:
390
- """Main method()"""
391
- if args.debug:
392
- global LOG
393
- LOG = capycli.get_logger(__name__)
394
- else:
395
- # suppress (debug) log output from requests and urllib
396
- logging.getLogger("requests").setLevel(logging.WARNING)
397
- logging.getLogger("urllib3").setLevel(logging.WARNING)
398
- logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
399
-
400
- print_text(
401
- " \n" + capycli.get_app_signature() +
402
- " - Determine Rust components/dependencies\n")
403
-
404
- if args.help:
405
- print("usage: capycli getdependencies rust [-i INPUTFILE] [-o OUTFILE] [-v] [--search-meta-data]")
406
- print("")
407
- print("Determine Rust project dependencies")
408
- print("")
409
- print("optional arguments:")
410
- print(" -h, --help show this help message and exit")
411
- print(" -i FOLDER, --inputfile FOLDER folder with the rust cargo project")
412
- print(" -o OUTFILE, --outfile OUTFILE output SBOM file")
413
- print(" -v, --verbose verbose output")
414
- print(" --search-meta-data search for package meta data")
415
- print(" -name NAME (optional) GitHub name for login")
416
- print(" -gt TOKEN (optional) GitHub token for login")
417
- return
418
-
419
- if not args.inputfile:
420
- print_red("No input folder specified!")
421
- sys.exit(ResultCode.RESULT_COMMAND_ERROR)
422
-
423
- if not os.path.isdir(args.inputfile):
424
- print_red("Input folder not found!")
425
- sys.exit(ResultCode.RESULT_FILE_NOT_FOUND)
426
-
427
- if not args.outputfile:
428
- print_red("No output SBOM file specified!")
429
- sys.exit(ResultCode.RESULT_COMMAND_ERROR)
430
-
431
- self.verbose = args.verbose
432
- self.github_name = args.name
433
- self.github_token = args.github_token
434
-
435
- sbom = self.sbom_from_cargo_files(args.inputfile, args.search_meta_data)
436
-
437
- GetPythonDependencies.check_meta_data(sbom, self.verbose)
438
-
439
- if self.verbose:
440
- print()
441
-
442
- print_text("Writing new SBOM to " + args.outputfile)
443
- SbomWriter.write_to_json(sbom, args.outputfile, True)
444
- print_text(" " + self.get_comp_count_text(sbom) + " items written to file.")
445
-
446
- print()
File without changes