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.
- {capycli-2.9.1 → capycli-2.10.0.dev1}/PKG-INFO +9 -14
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/__init__.py +3 -3
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/create_components.py +4 -1
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/download_sources.py +52 -6
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/filter_bom.py +7 -4
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/findsources.py +42 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/legacy.py +7 -1
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/legacy_cx.py +13 -4
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/map_bom.py +88 -36
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/show_bom.py +10 -2
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/capycli_bom_support.py +18 -7
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/map_result.py +22 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/purl_service.py +16 -4
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/purl_store.py +27 -1
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/purl_utils.py +8 -2
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/javascript.py +22 -9
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/python.py +225 -32
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/options.py +23 -8
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/check_prerequisites.py +25 -8
- {capycli-2.9.1 → capycli-2.10.0.dev1}/pyproject.toml +46 -38
- capycli-2.9.1/LICENSES/CC0-1.0.txt +0 -121
- capycli-2.9.1/LICENSES/MIT.txt +0 -27
- {capycli-2.9.1 → capycli-2.10.0.dev1}/License.md +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/Readme.md +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/__main__.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/__init__.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/bom_convert.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/bom_validate.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/check_bom.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/check_bom_item_status.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/check_granularity.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/csv.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/diff_bom.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/handle_bom.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/html.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/merge_bom.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/bom/plaintext.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/__init__.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/comparable_version.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/component_cache.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/dependencies_base.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/file_support.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/github_support.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/html_support.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/json_support.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/print.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/script_base.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/common/script_support.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/data/__init__.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/data/granularity_list.csv +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/__init__.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/handle_dependencies.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/maven_list.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/maven_pom.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/dependencies/nuget.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/__init__.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/application.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/argument_parser.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/cli.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/exceptions.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/main/result_codes.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/mapping/handle_mapping.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/mapping/mapping_to_html.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/mapping/mapping_to_xlsx.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/moverview/handle_moverview.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/moverview/moverview_to_html.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/moverview/moverview_to_xlsx.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/__init__.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/create_bom.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/create_project.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/create_readme.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/find_project.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/get_license_info.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/handle_project.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/show_ecc.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/show_licenses.py +0 -0
- {capycli-2.9.1 → capycli-2.10.0.dev1}/capycli/project/show_project.py +0 -0
- {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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: capycli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.10.0.dev1
|
|
4
4
|
Summary: CaPyCli - Clearing Automation Python Command Line Interface for SW360
|
|
5
|
-
|
|
6
|
-
License:
|
|
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.
|
|
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 (>=
|
|
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:
|
|
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
|
|
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-
|
|
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(
|
|
134
|
+
url=XsUri(file_uri))
|
|
118
135
|
new = True
|
|
119
136
|
else:
|
|
120
|
-
ext_ref.url = XsUri(
|
|
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
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
198
|
+
name = filterentry["component"].get("Name", "")
|
|
199
|
+
if not name:
|
|
200
|
+
name = filterentry["component"].get("name", "")
|
|
201
|
+
if name:
|
|
199
202
|
prefix = ""
|
|
200
|
-
if
|
|
201
|
-
prefix =
|
|
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 ==
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(" --
|
|
819
|
-
print("
|
|
820
|
-
print("
|
|
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.
|
|
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
|
-
|
|
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
|