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.
- {capycli-2.10.0 → capycli-2.10.0.dev1}/PKG-INFO +2 -2
- {capycli-2.10.0 → capycli-2.10.0.dev1}/Readme.md +1 -1
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/download_sources.py +35 -7
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/handle_bom.py +1 -9
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/handle_dependencies.py +1 -9
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/python.py +9 -10
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/options.py +9 -2
- {capycli-2.10.0 → capycli-2.10.0.dev1}/pyproject.toml +1 -1
- capycli-2.10.0/capycli/bom/bom_package.py +0 -215
- capycli-2.10.0/capycli/dependencies/rust.py +0 -446
- {capycli-2.10.0 → capycli-2.10.0.dev1}/License.md +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/__init__.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/__main__.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/__init__.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/bom_convert.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/bom_validate.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/check_bom.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/check_bom_item_status.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/check_granularity.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/create_components.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/csv.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/diff_bom.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/filter_bom.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/findsources.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/html.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/legacy.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/legacy_cx.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/map_bom.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/merge_bom.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/plaintext.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/bom/show_bom.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/__init__.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/capycli_bom_support.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/comparable_version.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/component_cache.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/dependencies_base.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/file_support.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/github_support.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/html_support.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/json_support.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/map_result.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/print.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/purl_service.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/purl_store.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/purl_utils.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/script_base.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/common/script_support.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/data/__init__.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/data/granularity_list.csv +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/__init__.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/javascript.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/maven_list.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/maven_pom.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/dependencies/nuget.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/__init__.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/application.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/argument_parser.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/cli.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/exceptions.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/main/result_codes.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/mapping/handle_mapping.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/mapping/mapping_to_html.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/mapping/mapping_to_xlsx.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/moverview/handle_moverview.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/moverview/moverview_to_html.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/moverview/moverview_to_xlsx.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/__init__.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/check_prerequisites.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/create_bom.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/create_project.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/create_readme.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/find_project.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/get_license_info.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/handle_project.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/show_ecc.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/show_licenses.py +0 -0
- {capycli-2.10.0 → capycli-2.10.0.dev1}/capycli/project/show_project.py +0 -0
- {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
|
-

|
|
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
|
-

|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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.
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|