capycli 2.0.0.dev8__py3-none-any.whl
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.
- License.md +27 -0
- capycli/__init__.py +214 -0
- capycli/__main__.py +13 -0
- capycli/bom/__init__.py +10 -0
- capycli/bom/bom_convert.py +163 -0
- capycli/bom/check_bom.py +187 -0
- capycli/bom/check_bom_item_status.py +197 -0
- capycli/bom/check_granularity.py +244 -0
- capycli/bom/create_components.py +644 -0
- capycli/bom/csv.py +69 -0
- capycli/bom/diff_bom.py +279 -0
- capycli/bom/download_sources.py +227 -0
- capycli/bom/filter_bom.py +323 -0
- capycli/bom/findsources.py +278 -0
- capycli/bom/handle_bom.py +134 -0
- capycli/bom/html.py +67 -0
- capycli/bom/legacy.py +312 -0
- capycli/bom/legacy_cx.py +151 -0
- capycli/bom/map_bom.py +1039 -0
- capycli/bom/merge_bom.py +155 -0
- capycli/bom/plaintext.py +69 -0
- capycli/bom/show_bom.py +77 -0
- capycli/common/__init__.py +9 -0
- capycli/common/capycli_bom_support.py +629 -0
- capycli/common/comparable_version.py +161 -0
- capycli/common/component_cache.py +240 -0
- capycli/common/dependencies_base.py +48 -0
- capycli/common/file_support.py +28 -0
- capycli/common/html_support.py +119 -0
- capycli/common/json_support.py +36 -0
- capycli/common/map_result.py +116 -0
- capycli/common/print.py +55 -0
- capycli/common/purl_service.py +169 -0
- capycli/common/purl_store.py +100 -0
- capycli/common/purl_utils.py +85 -0
- capycli/common/script_base.py +165 -0
- capycli/common/script_support.py +78 -0
- capycli/data/__init__.py +9 -0
- capycli/data/granularity_list.csv +1338 -0
- capycli/dependencies/__init__.py +9 -0
- capycli/dependencies/handle_dependencies.py +70 -0
- capycli/dependencies/javascript.py +261 -0
- capycli/dependencies/maven_list.py +333 -0
- capycli/dependencies/maven_pom.py +150 -0
- capycli/dependencies/nuget.py +184 -0
- capycli/dependencies/python.py +345 -0
- capycli/main/__init__.py +9 -0
- capycli/main/application.py +165 -0
- capycli/main/argument_parser.py +101 -0
- capycli/main/cli.py +28 -0
- capycli/main/exceptions.py +14 -0
- capycli/main/options.py +424 -0
- capycli/main/result_codes.py +41 -0
- capycli/mapping/handle_mapping.py +46 -0
- capycli/mapping/mapping_to_html.py +182 -0
- capycli/mapping/mapping_to_xlsx.py +197 -0
- capycli/moverview/handle_moverview.py +46 -0
- capycli/moverview/moverview_to_html.py +122 -0
- capycli/moverview/moverview_to_xlsx.py +170 -0
- capycli/project/__init__.py +9 -0
- capycli/project/check_prerequisites.py +304 -0
- capycli/project/create_bom.py +190 -0
- capycli/project/create_project.py +335 -0
- capycli/project/create_readme.py +546 -0
- capycli/project/find_project.py +128 -0
- capycli/project/get_license_info.py +246 -0
- capycli/project/handle_project.py +118 -0
- capycli/project/show_ecc.py +200 -0
- capycli/project/show_licenses.py +211 -0
- capycli/project/show_project.py +215 -0
- capycli/project/show_vulnerabilities.py +238 -0
- capycli-2.0.0.dev8.dist-info/LICENSES/CC0-1.0.txt +121 -0
- capycli-2.0.0.dev8.dist-info/LICENSES/MIT.txt +27 -0
- capycli-2.0.0.dev8.dist-info/License.md +27 -0
- capycli-2.0.0.dev8.dist-info/METADATA +268 -0
- capycli-2.0.0.dev8.dist-info/RECORD +78 -0
- capycli-2.0.0.dev8.dist-info/WHEEL +4 -0
- capycli-2.0.0.dev8.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2019-23 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 re
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
from typing import Any
|
|
15
|
+
from urllib.parse import urlparse
|
|
16
|
+
|
|
17
|
+
import packageurl
|
|
18
|
+
import requests
|
|
19
|
+
import sw360.sw360_api
|
|
20
|
+
from colorama import Fore, Style
|
|
21
|
+
from cyclonedx.model.bom import Bom
|
|
22
|
+
from cyclonedx.model.component import Component
|
|
23
|
+
|
|
24
|
+
import capycli.common.json_support
|
|
25
|
+
import capycli.common.script_base
|
|
26
|
+
from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomWriter
|
|
27
|
+
from capycli.common.print import print_green, print_red, print_text, print_yellow
|
|
28
|
+
from capycli.common.purl_utils import PurlUtils
|
|
29
|
+
from capycli.common.script_support import ScriptSupport
|
|
30
|
+
from capycli.main.result_codes import ResultCode
|
|
31
|
+
|
|
32
|
+
LOG = capycli.get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BomCreateComponents(capycli.common.script_base.ScriptBase):
|
|
36
|
+
"""
|
|
37
|
+
Create new components and releases on SW360
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
command_help = [
|
|
41
|
+
"usage: CaPyCLI bom {} -i bom.json -o bom_created.json [-source <folder>]",
|
|
42
|
+
"",
|
|
43
|
+
"optional arguments:",
|
|
44
|
+
" -h, --help show this help message and exit",
|
|
45
|
+
" -i INPUTFILE, input file to read from (JSON)",
|
|
46
|
+
" -o OUTPUTFILE, output file to write to",
|
|
47
|
+
" -t SW360_TOKEN, use this token for access to SW360",
|
|
48
|
+
" -oa, --oauth2 this is an oauth2 token",
|
|
49
|
+
" -url SW360_URL use this URL for access to SW360",
|
|
50
|
+
" -o OUTPUT write updated BOM to a JSON file",
|
|
51
|
+
" -source SOURCE source folder or additional source file",
|
|
52
|
+
" --download enable automatic download of missing sources",
|
|
53
|
+
" --dbx relaxed Debian version handling: when checking for existing releases,",
|
|
54
|
+
" ignore prefixes like \"2:\" (epoch) and suffixes like \".debian\"",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
def __init__(self, onlyCreateReleases=False):
|
|
58
|
+
self.source_folder = None
|
|
59
|
+
self.download = False
|
|
60
|
+
self.relaxed_debian_parsing = False
|
|
61
|
+
self.onlyCreateReleases = onlyCreateReleases
|
|
62
|
+
|
|
63
|
+
def upload_source_file(self, release_id, sourcefile, filetype="SOURCE", comment=""):
|
|
64
|
+
"""Upload source code attachment
|
|
65
|
+
|
|
66
|
+
@params:
|
|
67
|
+
release_id - the id of the release (string)
|
|
68
|
+
sourcefile - name/path of the file to get uploaded (string)
|
|
69
|
+
filetype - SW360 attachment type ("SOURCE" or "SOURCE_SELF")
|
|
70
|
+
comment - upload comment for SW360 attachment
|
|
71
|
+
"""
|
|
72
|
+
print_text(" Uploading source file: " + sourcefile)
|
|
73
|
+
try:
|
|
74
|
+
self.client.upload_release_attachment(
|
|
75
|
+
release_id, sourcefile, upload_type=filetype, upload_comment=comment)
|
|
76
|
+
except sw360.sw360_api.SW360Error as swex:
|
|
77
|
+
errortext = " Error uploading source file: " + self.get_error_message(swex)
|
|
78
|
+
print(Fore.LIGHTRED_EX + errortext + Style.RESET_ALL)
|
|
79
|
+
|
|
80
|
+
def upload_file_from_url(self, release_id, url, filename, filetype="SOURCE", comment="", attached_filenames=[]):
|
|
81
|
+
"""Download a file from a URL if it's not available locally
|
|
82
|
+
and upload the file as attachment to SW360.
|
|
83
|
+
|
|
84
|
+
@params:
|
|
85
|
+
release_id - the id of the release (string)
|
|
86
|
+
url - url of the file to get uploaded (string)
|
|
87
|
+
filename - local file name
|
|
88
|
+
filetype - SW360 attachment type ("SOURCE" or "SOURCE_SELF")
|
|
89
|
+
comment - upload comment for SW360 attachment
|
|
90
|
+
"""
|
|
91
|
+
if os.path.isfile(filename):
|
|
92
|
+
self.upload_source_file(release_id, filename, filetype, comment)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
if self.source_folder:
|
|
96
|
+
fullpath = os.path.join(self.source_folder, filename)
|
|
97
|
+
if os.path.isfile(fullpath):
|
|
98
|
+
self.upload_source_file(release_id, fullpath, filetype, comment)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
if not self.download:
|
|
102
|
+
print_red(" File not found, perhaps you want --download?")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
print_text(" Downloading file", filename)
|
|
106
|
+
|
|
107
|
+
if self.source_folder:
|
|
108
|
+
tmpfolder = None
|
|
109
|
+
else:
|
|
110
|
+
tmpfolder = tempfile.TemporaryDirectory()
|
|
111
|
+
fullpath = os.path.join(tmpfolder.name, filename)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
response = requests.get(url, allow_redirects=True)
|
|
115
|
+
if (response.status_code == requests.codes["ok"]):
|
|
116
|
+
print_text(" Writing file", fullpath)
|
|
117
|
+
try:
|
|
118
|
+
open(fullpath, "wb").write(response.content)
|
|
119
|
+
if response.headers.__contains__("content-disposition"):
|
|
120
|
+
header = response.headers.get("content-disposition")
|
|
121
|
+
if header.__contains__("filename="):
|
|
122
|
+
print_text(" Found header:", header)
|
|
123
|
+
newfilename = header.split("=")[-1]
|
|
124
|
+
newfilename = newfilename.strip('"')
|
|
125
|
+
head, tail = os.path.split(fullpath)
|
|
126
|
+
if newfilename != tail:
|
|
127
|
+
newpath = str(fullpath).replace(tail, newfilename)
|
|
128
|
+
print_text(
|
|
129
|
+
" Rename downloaded file from", fullpath, "to", newpath,
|
|
130
|
+
"because content-disposition defines this files name")
|
|
131
|
+
os.rename(fullpath, newpath)
|
|
132
|
+
fullpath = newpath
|
|
133
|
+
|
|
134
|
+
head, tail = os.path.split(fullpath)
|
|
135
|
+
if tail in attached_filenames:
|
|
136
|
+
# for now, we can never get here as upload_file() will not call us if *any* source
|
|
137
|
+
# attachment exists - but this code might be useful in future if this semantics changes
|
|
138
|
+
print_text(
|
|
139
|
+
" File with the name '", tail, "' is already attached to release. Skip the upload!")
|
|
140
|
+
else:
|
|
141
|
+
self.upload_source_file(release_id, fullpath, filetype, comment)
|
|
142
|
+
except Exception as ex:
|
|
143
|
+
print_red(" Error writing downloaded file: " + repr(ex))
|
|
144
|
+
else:
|
|
145
|
+
print_red(
|
|
146
|
+
" Error downloading file, http response = " +
|
|
147
|
+
str(response.status_code))
|
|
148
|
+
except Exception as ex:
|
|
149
|
+
print_red(" Error downloading file: " + repr(ex))
|
|
150
|
+
|
|
151
|
+
# cleanup
|
|
152
|
+
if tmpfolder:
|
|
153
|
+
tmpfolder.cleanup()
|
|
154
|
+
|
|
155
|
+
def prepare_release_data(self, cx_comp: Component) -> dict:
|
|
156
|
+
"""Create release data structure as expected by SW360 REST API
|
|
157
|
+
|
|
158
|
+
:param item: a single bill of materials item - a release
|
|
159
|
+
:type item: dictionary
|
|
160
|
+
:return: the release
|
|
161
|
+
:rtype: release (dictionary)
|
|
162
|
+
"""
|
|
163
|
+
data = {}
|
|
164
|
+
data["name"] = cx_comp.name
|
|
165
|
+
data["version"] = cx_comp.version
|
|
166
|
+
|
|
167
|
+
# mandatory properties
|
|
168
|
+
src_url = CycloneDxSupport.get_ext_ref_source_url(cx_comp)
|
|
169
|
+
if src_url:
|
|
170
|
+
data["sourceCodeDownloadurl"] = src_url
|
|
171
|
+
|
|
172
|
+
bin_url = CycloneDxSupport.get_ext_ref_binary_url(cx_comp)
|
|
173
|
+
if bin_url:
|
|
174
|
+
data["binaryDownloadurl"] = bin_url
|
|
175
|
+
|
|
176
|
+
# recommended properties
|
|
177
|
+
if cx_comp.purl:
|
|
178
|
+
data["externalIds"] = {}
|
|
179
|
+
# ensure that we have the only correct external-id name: package-url
|
|
180
|
+
data["externalIds"]["package-url"] = cx_comp.purl
|
|
181
|
+
|
|
182
|
+
# use project site as fallback for source code download url
|
|
183
|
+
website = CycloneDxSupport.get_ext_ref_website(cx_comp)
|
|
184
|
+
repo = CycloneDxSupport.get_ext_ref_repository(cx_comp)
|
|
185
|
+
if not src_url:
|
|
186
|
+
if repo:
|
|
187
|
+
print(" Using repository for source code download URL...")
|
|
188
|
+
data["sourceCodeDownloadurl"] = repo
|
|
189
|
+
elif website:
|
|
190
|
+
print(" Using website for source code download URL...")
|
|
191
|
+
data["sourceCodeDownloadurl"] = website
|
|
192
|
+
|
|
193
|
+
language = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_LANGUAGE)
|
|
194
|
+
if language:
|
|
195
|
+
data["languages"] = []
|
|
196
|
+
data["languages"].append(language)
|
|
197
|
+
|
|
198
|
+
return data
|
|
199
|
+
|
|
200
|
+
def prepare_component_data(self, cx_comp: Component) -> dict:
|
|
201
|
+
"""Create component data structure as expected by SW360 REST API
|
|
202
|
+
|
|
203
|
+
:param item: single bill of materials item - a release
|
|
204
|
+
:type item: dictionary
|
|
205
|
+
:return: the release structure
|
|
206
|
+
:rtype: release (dictionary)
|
|
207
|
+
"""
|
|
208
|
+
data = {}
|
|
209
|
+
data["description"] = "n/a"
|
|
210
|
+
if cx_comp.description:
|
|
211
|
+
data["description"] = cx_comp.description
|
|
212
|
+
data["componentType"] = "OSS"
|
|
213
|
+
|
|
214
|
+
language = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_LANGUAGE)
|
|
215
|
+
if language:
|
|
216
|
+
data["languages"] = []
|
|
217
|
+
data["languages"].append(language)
|
|
218
|
+
|
|
219
|
+
# optional properties
|
|
220
|
+
categories = []
|
|
221
|
+
cat = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_CATEGORIES)
|
|
222
|
+
if cat:
|
|
223
|
+
categories.append(cat)
|
|
224
|
+
else:
|
|
225
|
+
# default = library
|
|
226
|
+
categories.append("library")
|
|
227
|
+
|
|
228
|
+
data["categories"] = categories
|
|
229
|
+
|
|
230
|
+
data["homepage"] = "n/a"
|
|
231
|
+
website = CycloneDxSupport.get_ext_ref_website(cx_comp)
|
|
232
|
+
if website:
|
|
233
|
+
data["homepage"] = website
|
|
234
|
+
|
|
235
|
+
if cx_comp.purl:
|
|
236
|
+
purl = PurlUtils.component_purl_from_release_purl(cx_comp.purl)
|
|
237
|
+
data["externalIds"] = {"package-url": purl}
|
|
238
|
+
return data
|
|
239
|
+
|
|
240
|
+
def create_release(self, cx_comp: Component, component_id) -> dict:
|
|
241
|
+
"""Create a new release on SW360
|
|
242
|
+
|
|
243
|
+
:param item: a single bill of materials item - a release
|
|
244
|
+
:type item: dictionary
|
|
245
|
+
:param component_id: the id of the component
|
|
246
|
+
:type component_id: string
|
|
247
|
+
:return: the release
|
|
248
|
+
:rtype: release (dictionary)
|
|
249
|
+
"""
|
|
250
|
+
data = self.prepare_release_data(cx_comp)
|
|
251
|
+
# ensure that the release mainline state is properly set
|
|
252
|
+
data["mainlineState"] = "OPEN"
|
|
253
|
+
try:
|
|
254
|
+
release_new = self.client.create_new_release(
|
|
255
|
+
cx_comp.name, cx_comp.version,
|
|
256
|
+
component_id, release_details=data)
|
|
257
|
+
except sw360.sw360_api.SW360Error as swex:
|
|
258
|
+
errortext = " Error creating component: " + self.get_error_message(swex)
|
|
259
|
+
print_red(errortext)
|
|
260
|
+
sys.exit(ResultCode.RESULT_ERROR_CREATING_COMPONENT)
|
|
261
|
+
return release_new
|
|
262
|
+
|
|
263
|
+
def update_release(self, cx_comp: Component, release_data: dict[str, Any]):
|
|
264
|
+
"""Update an existing release on SW360
|
|
265
|
+
|
|
266
|
+
:param item: a single bill of materials item - a release
|
|
267
|
+
:type item: dictionary
|
|
268
|
+
:param release_data: SW360 release data
|
|
269
|
+
:type release_data: release (dictionary)
|
|
270
|
+
"""
|
|
271
|
+
release_id = self.get_sw360_id(release_data)
|
|
272
|
+
data = self.prepare_release_data(cx_comp)
|
|
273
|
+
|
|
274
|
+
update_data = {}
|
|
275
|
+
if "sourceCodeDownloadurl" in data and data["sourceCodeDownloadurl"]:
|
|
276
|
+
if not release_data.get("sourceCodeDownloadurl", ""):
|
|
277
|
+
update_data["sourceCodeDownloadurl"] = data["sourceCodeDownloadurl"]
|
|
278
|
+
elif release_data["sourceCodeDownloadurl"] != data["sourceCodeDownloadurl"]:
|
|
279
|
+
print_yellow(
|
|
280
|
+
" WARNING: SW360 source URL", release_data["sourceCodeDownloadurl"],
|
|
281
|
+
"differs from BOM URL", data["sourceCodeDownloadurl"])
|
|
282
|
+
|
|
283
|
+
if "binaryDownloadurl" in data and data["binaryDownloadurl"]:
|
|
284
|
+
if not release_data.get("binaryDownloadurl", ""):
|
|
285
|
+
update_data["binaryDownloadurl"] = data["binaryDownloadurl"]
|
|
286
|
+
elif release_data["binaryDownloadurl"] != data["binaryDownloadurl"]:
|
|
287
|
+
print_yellow(
|
|
288
|
+
" WARNING: SW360 binary URL", release_data["binaryDownloadurl"],
|
|
289
|
+
"differs from BOM URL", data["binaryDownloadurl"])
|
|
290
|
+
|
|
291
|
+
if len(data.get("externalIds", {})):
|
|
292
|
+
for repository_type, repository_id in data["externalIds"].items():
|
|
293
|
+
if repository_type not in release_data.get("externalIds", {}):
|
|
294
|
+
update_data.setdefault("externalIds", release_data.get("externalIds", {}))
|
|
295
|
+
update_data["externalIds"][repository_type] = repository_id
|
|
296
|
+
elif release_data["externalIds"][repository_type] != data["externalIds"][repository_type]:
|
|
297
|
+
id_match = False
|
|
298
|
+
try:
|
|
299
|
+
bom_purl = packageurl.PackageURL.from_string(
|
|
300
|
+
data["externalIds"][repository_type])
|
|
301
|
+
sw360_purls = PurlUtils.get_purl_list_from_sw360_object(release_data)
|
|
302
|
+
id_match = PurlUtils.contains(sw360_purls, bom_purl)
|
|
303
|
+
except ValueError:
|
|
304
|
+
pass
|
|
305
|
+
if not id_match:
|
|
306
|
+
print_yellow(
|
|
307
|
+
" WARNING: SW360 external id", repository_type,
|
|
308
|
+
release_data["externalIds"][repository_type],
|
|
309
|
+
"differs from BOM id", data["externalIds"][repository_type])
|
|
310
|
+
|
|
311
|
+
if len(update_data):
|
|
312
|
+
# Some releases return 400 code while updating - to not break the script catch this exception
|
|
313
|
+
try:
|
|
314
|
+
print_text(" Updating release data")
|
|
315
|
+
self.client.update_release(update_data, release_id)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
print_yellow(
|
|
318
|
+
" WARNING: Updating SW360 releaseId: ", release_id,
|
|
319
|
+
"data: ", update_data, "failed! ", e)
|
|
320
|
+
|
|
321
|
+
filetype = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_SRC_FILE_TYPE)
|
|
322
|
+
if not filetype:
|
|
323
|
+
filetype = "SOURCE"
|
|
324
|
+
file_comment = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_SRC_FILE_COMMENT)
|
|
325
|
+
if not file_comment:
|
|
326
|
+
file_comment = "Attached by CaPyCli"
|
|
327
|
+
self.upload_file(cx_comp, release_data, release_id, filetype, file_comment)
|
|
328
|
+
|
|
329
|
+
def upload_file(
|
|
330
|
+
self, cx_comp: Component, release_data: dict,
|
|
331
|
+
release_id: str, filetype: str, comment: str):
|
|
332
|
+
|
|
333
|
+
url = None
|
|
334
|
+
filename = None
|
|
335
|
+
filehash = None
|
|
336
|
+
if filetype in ["SOURCE", "SOURCE_SELF"]:
|
|
337
|
+
url = CycloneDxSupport.get_ext_ref_source_url(cx_comp)
|
|
338
|
+
filename = CycloneDxSupport.get_ext_ref_source_file(cx_comp)
|
|
339
|
+
filehash = CycloneDxSupport.get_source_file_hash(cx_comp)
|
|
340
|
+
|
|
341
|
+
if filetype in ["BINARY", "BINARY_SELF"]:
|
|
342
|
+
url = CycloneDxSupport.get_ext_ref_binary_url(cx_comp)
|
|
343
|
+
filename = CycloneDxSupport.get_ext_ref_binary_file(cx_comp)
|
|
344
|
+
filehash = CycloneDxSupport.get_binary_file_hash(cx_comp)
|
|
345
|
+
|
|
346
|
+
# Note that we retreive the SHA1 has from the CycloneDX data.
|
|
347
|
+
# But there is no guarantee that this *IS* really a SHA1 hash!
|
|
348
|
+
|
|
349
|
+
if (filename is None or filename == '') and url:
|
|
350
|
+
filename = urlparse(url)
|
|
351
|
+
if filename:
|
|
352
|
+
filename = os.path.basename(filename.path)
|
|
353
|
+
|
|
354
|
+
if not filename:
|
|
355
|
+
print_red(" Unable to identify filename from url!")
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
if filetype.endswith("_SELF"):
|
|
359
|
+
filetype_pattern = filetype[:-5]
|
|
360
|
+
else:
|
|
361
|
+
filetype_pattern = filetype
|
|
362
|
+
|
|
363
|
+
attached_filenames = []
|
|
364
|
+
source_attachment_exists = False
|
|
365
|
+
for attachment in release_data.get("_embedded", {}).get("sw360:attachments", []):
|
|
366
|
+
if attachment["attachmentType"].startswith(filetype_pattern):
|
|
367
|
+
at_info = self.client.get_attachment_by_url(attachment['_links']['self']['href'])
|
|
368
|
+
if at_info.get("checkStatus", "") == "REJECTED":
|
|
369
|
+
continue
|
|
370
|
+
source_attachment_exists = True
|
|
371
|
+
attached_filenames.append(attachment["filename"])
|
|
372
|
+
if attachment["filename"] != filename:
|
|
373
|
+
print_yellow(
|
|
374
|
+
" WARNING: different source attachment - BOM:",
|
|
375
|
+
filename, "SW360:", attachment["filename"])
|
|
376
|
+
elif filehash and attachment["sha1"] != filehash:
|
|
377
|
+
print_yellow(
|
|
378
|
+
" WARNING: different hash for source attachment", filename,
|
|
379
|
+
"- BOM:", filehash, "SW360:", attachment["sha1"])
|
|
380
|
+
else:
|
|
381
|
+
print_green(" Attachment", filename, "ok")
|
|
382
|
+
|
|
383
|
+
if not source_attachment_exists:
|
|
384
|
+
self.upload_file_from_url(release_id, url, filename, filetype, comment, attached_filenames)
|
|
385
|
+
|
|
386
|
+
def search_for_release(self, component, cx_comp: Component):
|
|
387
|
+
"""Checks whether the given component already contains
|
|
388
|
+
the requested release
|
|
389
|
+
|
|
390
|
+
:param component: the component (dictionary)
|
|
391
|
+
:type component: dictionary
|
|
392
|
+
:param release: the release (dictionary)
|
|
393
|
+
:type release: dictionary
|
|
394
|
+
:return: the release or None
|
|
395
|
+
:rtype: release (dictionary)
|
|
396
|
+
"""
|
|
397
|
+
if "_embedded" not in component:
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
if "sw360:releases" not in component["_embedded"]:
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
for comprel in component["_embedded"]["sw360:releases"]:
|
|
404
|
+
if comprel.get("version", None) == cx_comp.version:
|
|
405
|
+
return self.client.get_release_by_url(comprel["_links"]["self"]["href"])
|
|
406
|
+
|
|
407
|
+
if self.relaxed_debian_parsing:
|
|
408
|
+
# if there's no exact match, try relaxed search
|
|
409
|
+
for comprel in component["_embedded"]["sw360:releases"]:
|
|
410
|
+
# "2:5.2.1-1.debian" -> "5.2.1-1"
|
|
411
|
+
bom_pattern = re.sub("^[0-9]+:", "", cx_comp.version)
|
|
412
|
+
bom_pattern = re.sub(r"[\. \(]*[dD]ebian[ \)]*$", "", bom_pattern)
|
|
413
|
+
sw360_pattern = re.sub("^[0-9]+:", "", comprel.get("version", ""))
|
|
414
|
+
sw360_pattern = re.sub(r"[\. \(]*[dD]ebian[ \)]*$", "", sw360_pattern)
|
|
415
|
+
|
|
416
|
+
if bom_pattern == sw360_pattern:
|
|
417
|
+
print(
|
|
418
|
+
Fore.LIGHTYELLOW_EX,
|
|
419
|
+
" WARNING: SW360 version", comprel["version"],
|
|
420
|
+
"differs from BOM version", cx_comp.version,
|
|
421
|
+
Style.RESET_ALL)
|
|
422
|
+
return self.client.get_release_by_url(comprel["_links"]["self"]["href"])
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
def get_component(self, cx_comp: Component) -> str:
|
|
426
|
+
"""
|
|
427
|
+
Get component id for related BOM item
|
|
428
|
+
- Default take ComponentId from BOM item
|
|
429
|
+
- Alternative search component in SW360 by name
|
|
430
|
+
:param item: BOM item
|
|
431
|
+
:return: id or None
|
|
432
|
+
"""
|
|
433
|
+
component = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_COMPONENT_ID)
|
|
434
|
+
if not component:
|
|
435
|
+
if self.onlyCreateReleases:
|
|
436
|
+
print_yellow(" No component id in bom, skipping due to createreleases mode!")
|
|
437
|
+
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
components = self.client.get_component_by_name(cx_comp.name)
|
|
441
|
+
if not component and components["_embedded"]["sw360:components"]:
|
|
442
|
+
for compref in components["_embedded"]["sw360:components"]:
|
|
443
|
+
if compref["name"].lower() != cx_comp.name.lower():
|
|
444
|
+
continue
|
|
445
|
+
else:
|
|
446
|
+
component = self.get_sw360_id(compref)
|
|
447
|
+
break
|
|
448
|
+
|
|
449
|
+
return component
|
|
450
|
+
|
|
451
|
+
def create_component(self, cx_comp: Component) -> dict:
|
|
452
|
+
data = self.prepare_component_data(cx_comp)
|
|
453
|
+
try:
|
|
454
|
+
component_new = self.client.create_new_component(
|
|
455
|
+
cx_comp.name,
|
|
456
|
+
data["description"],
|
|
457
|
+
data["componentType"],
|
|
458
|
+
data["homepage"],
|
|
459
|
+
component_details=data)
|
|
460
|
+
print_yellow(" Component created")
|
|
461
|
+
return component_new
|
|
462
|
+
except sw360.sw360_api.SW360Error as swex:
|
|
463
|
+
errortext = " Error creating component: " + self.get_error_message(swex)
|
|
464
|
+
print_red(errortext)
|
|
465
|
+
sys.exit(ResultCode.RESULT_ERROR_CREATING_COMPONENT)
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
def update_component(self, cx_comp: Component, component_id, component_data):
|
|
469
|
+
"""Update an existing component on SW360
|
|
470
|
+
|
|
471
|
+
:param item: a single bill of materials item - a component
|
|
472
|
+
:type item: dictionary
|
|
473
|
+
:param component_id: SW360 component id
|
|
474
|
+
:type component_id: string
|
|
475
|
+
:param component_data: SW360 component data
|
|
476
|
+
:type component_data: component (dictionary)
|
|
477
|
+
"""
|
|
478
|
+
purl = ""
|
|
479
|
+
if cx_comp.purl:
|
|
480
|
+
purl = PurlUtils.component_purl_from_release_purl(cx_comp.purl)
|
|
481
|
+
if component_data.get("externalIds", {}).get("package-url", None) is None:
|
|
482
|
+
print_red(" Updating component purl")
|
|
483
|
+
try:
|
|
484
|
+
self.client.update_component_external_id("package-url", purl, component_id)
|
|
485
|
+
except Exception as e:
|
|
486
|
+
print_yellow(" WARNING: Updating component failed!", e)
|
|
487
|
+
elif component_data["externalIds"]["package-url"] != purl:
|
|
488
|
+
id_match = False
|
|
489
|
+
try:
|
|
490
|
+
bom_purl = packageurl.PackageURL.from_string(purl)
|
|
491
|
+
sw360_purls = PurlUtils.get_purl_list_from_sw360_object(component_data)
|
|
492
|
+
id_match = PurlUtils.contains(sw360_purls, bom_purl)
|
|
493
|
+
except ValueError:
|
|
494
|
+
pass
|
|
495
|
+
if not id_match:
|
|
496
|
+
print_yellow(
|
|
497
|
+
" WARNING: SW360 package-url",
|
|
498
|
+
component_data["externalIds"]["package-url"],
|
|
499
|
+
"differs from BOM id", purl)
|
|
500
|
+
|
|
501
|
+
def get_sw360_id(self, sw360_object: dict):
|
|
502
|
+
return self.client.get_id_from_href(sw360_object["_links"]["self"]["href"])
|
|
503
|
+
|
|
504
|
+
def create_component_and_release(self, cx_comp: Component):
|
|
505
|
+
"""Create new releases and if necessary also new components
|
|
506
|
+
|
|
507
|
+
:param item: a single bill of materials item - a release
|
|
508
|
+
:type item: dictionary
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
release = None
|
|
512
|
+
|
|
513
|
+
# Get or create component related to the BOM item
|
|
514
|
+
component_id = self.get_component(cx_comp)
|
|
515
|
+
if component_id:
|
|
516
|
+
print_text(" Component " + cx_comp.name + " exists.")
|
|
517
|
+
|
|
518
|
+
# get full component info
|
|
519
|
+
component = self.client.get_component(component_id)
|
|
520
|
+
self.update_component(cx_comp, component_id, component)
|
|
521
|
+
release = self.search_for_release(component, cx_comp)
|
|
522
|
+
else:
|
|
523
|
+
if self.onlyCreateReleases:
|
|
524
|
+
print_red(" Component doesn't exist!")
|
|
525
|
+
return
|
|
526
|
+
|
|
527
|
+
# create component
|
|
528
|
+
component = self.create_component(cx_comp)
|
|
529
|
+
component_id = self.get_sw360_id(component)
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
if release:
|
|
533
|
+
item_name = ScriptSupport.get_full_name_from_component(cx_comp)
|
|
534
|
+
print_red(" " + item_name + " already exists")
|
|
535
|
+
else:
|
|
536
|
+
release = self.create_release(
|
|
537
|
+
cx_comp, self.get_sw360_id(component))
|
|
538
|
+
print_text(" Release created")
|
|
539
|
+
|
|
540
|
+
self.update_release(cx_comp, release)
|
|
541
|
+
except sw360.sw360_api.SW360Error as swex:
|
|
542
|
+
errortext = " Error creating release: " + self.get_error_message(swex)
|
|
543
|
+
print_red(errortext)
|
|
544
|
+
sys.exit(ResultCode.RESULT_ERROR_CREATING_RELEASE)
|
|
545
|
+
|
|
546
|
+
if release:
|
|
547
|
+
CycloneDxSupport.update_or_set_property(
|
|
548
|
+
cx_comp, CycloneDxSupport.CDX_PROP_SW360ID, self.get_sw360_id(release))
|
|
549
|
+
cx_comp.version = release["version"]
|
|
550
|
+
|
|
551
|
+
def create_items(self, sbom: Bom) -> None:
|
|
552
|
+
"""Create missing components and releases
|
|
553
|
+
|
|
554
|
+
:param bom: the bill of materials
|
|
555
|
+
:type bom: list of components
|
|
556
|
+
"""
|
|
557
|
+
ok = True
|
|
558
|
+
|
|
559
|
+
for cx_comp in sbom.components:
|
|
560
|
+
item_name = ScriptSupport.get_full_name_from_component(cx_comp)
|
|
561
|
+
id = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_SW360ID)
|
|
562
|
+
if id:
|
|
563
|
+
print_text(" " + item_name + " already exists")
|
|
564
|
+
self.update_release(cx_comp, self.client.get_release(id))
|
|
565
|
+
else:
|
|
566
|
+
print_text(" " + item_name)
|
|
567
|
+
self.create_component_and_release(cx_comp)
|
|
568
|
+
id = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_SW360ID)
|
|
569
|
+
if id:
|
|
570
|
+
print(" Release id = " + id)
|
|
571
|
+
else:
|
|
572
|
+
ok = False
|
|
573
|
+
|
|
574
|
+
# clear map result
|
|
575
|
+
CycloneDxSupport.remove_property(cx_comp, CycloneDxSupport.CDX_PROP_MAPRESULT)
|
|
576
|
+
|
|
577
|
+
if not ok:
|
|
578
|
+
print_red("An error occured during component/release creation!")
|
|
579
|
+
sys.exit(ResultCode.RESULT_ERROR_CREATING_ITEM)
|
|
580
|
+
|
|
581
|
+
def run(self, args):
|
|
582
|
+
"""Main method()"""
|
|
583
|
+
if args.debug:
|
|
584
|
+
global LOG
|
|
585
|
+
LOG = capycli.get_logger(__name__)
|
|
586
|
+
else:
|
|
587
|
+
# suppress (debug) log output from requests and urllib
|
|
588
|
+
logging.getLogger("requests").setLevel(logging.WARNING)
|
|
589
|
+
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
590
|
+
logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
|
|
591
|
+
|
|
592
|
+
print_text(
|
|
593
|
+
"\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
|
|
594
|
+
" - Create new components and releases on SW360\n")
|
|
595
|
+
|
|
596
|
+
if args.help:
|
|
597
|
+
sub_command = "createreleases" if self.onlyCreateReleases else "createcomponents"
|
|
598
|
+
for entry in self.command_help:
|
|
599
|
+
print_text(entry.format(sub_command))
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
if not args.inputfile:
|
|
603
|
+
print_red("No input file specified!")
|
|
604
|
+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
|
|
605
|
+
|
|
606
|
+
if not os.path.isfile(args.inputfile):
|
|
607
|
+
print_red("Input file not found!")
|
|
608
|
+
sys.exit(ResultCode.RESULT_FILE_NOT_FOUND)
|
|
609
|
+
|
|
610
|
+
if args.source:
|
|
611
|
+
self.source_folder = args.source
|
|
612
|
+
|
|
613
|
+
self.download = args.download
|
|
614
|
+
|
|
615
|
+
if args.dbx:
|
|
616
|
+
print_text("Using relaxed debian version checks")
|
|
617
|
+
self.relaxed_debian_parsing = True
|
|
618
|
+
|
|
619
|
+
if not self.login(token=args.sw360_token, url=args.sw360_url, oauth2=args.oauth2):
|
|
620
|
+
print_red("ERROR: login failed!")
|
|
621
|
+
sys.exit(ResultCode.RESULT_AUTH_ERROR)
|
|
622
|
+
|
|
623
|
+
print_text("Loading SBOM file", args.inputfile)
|
|
624
|
+
try:
|
|
625
|
+
sbom = CaPyCliBom.read_sbom(args.inputfile)
|
|
626
|
+
except Exception as ex:
|
|
627
|
+
print_red("Error reading input SBOM file: " + repr(ex))
|
|
628
|
+
sys.exit(ResultCode.RESULT_ERROR_READING_BOM)
|
|
629
|
+
print_text(" ", self.get_comp_count_text(sbom), "read from SBOM")
|
|
630
|
+
|
|
631
|
+
print("Creating items...")
|
|
632
|
+
self.create_items(sbom)
|
|
633
|
+
|
|
634
|
+
if args.outputfile:
|
|
635
|
+
print_text("Writing updated SBOM to " + args.outputfile)
|
|
636
|
+
try:
|
|
637
|
+
SbomWriter.write_to_json(sbom, args.outputfile, True)
|
|
638
|
+
except Exception as ex:
|
|
639
|
+
print_red("Error writing updated SBOM file: " + repr(ex))
|
|
640
|
+
sys.exit(ResultCode.RESULT_ERROR_WRITING_BOM)
|
|
641
|
+
|
|
642
|
+
print_text(" ", self.get_comp_count_text(sbom), "written to SBOM file")
|
|
643
|
+
|
|
644
|
+
print("\n")
|