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.
Files changed (78) hide show
  1. License.md +27 -0
  2. capycli/__init__.py +214 -0
  3. capycli/__main__.py +13 -0
  4. capycli/bom/__init__.py +10 -0
  5. capycli/bom/bom_convert.py +163 -0
  6. capycli/bom/check_bom.py +187 -0
  7. capycli/bom/check_bom_item_status.py +197 -0
  8. capycli/bom/check_granularity.py +244 -0
  9. capycli/bom/create_components.py +644 -0
  10. capycli/bom/csv.py +69 -0
  11. capycli/bom/diff_bom.py +279 -0
  12. capycli/bom/download_sources.py +227 -0
  13. capycli/bom/filter_bom.py +323 -0
  14. capycli/bom/findsources.py +278 -0
  15. capycli/bom/handle_bom.py +134 -0
  16. capycli/bom/html.py +67 -0
  17. capycli/bom/legacy.py +312 -0
  18. capycli/bom/legacy_cx.py +151 -0
  19. capycli/bom/map_bom.py +1039 -0
  20. capycli/bom/merge_bom.py +155 -0
  21. capycli/bom/plaintext.py +69 -0
  22. capycli/bom/show_bom.py +77 -0
  23. capycli/common/__init__.py +9 -0
  24. capycli/common/capycli_bom_support.py +629 -0
  25. capycli/common/comparable_version.py +161 -0
  26. capycli/common/component_cache.py +240 -0
  27. capycli/common/dependencies_base.py +48 -0
  28. capycli/common/file_support.py +28 -0
  29. capycli/common/html_support.py +119 -0
  30. capycli/common/json_support.py +36 -0
  31. capycli/common/map_result.py +116 -0
  32. capycli/common/print.py +55 -0
  33. capycli/common/purl_service.py +169 -0
  34. capycli/common/purl_store.py +100 -0
  35. capycli/common/purl_utils.py +85 -0
  36. capycli/common/script_base.py +165 -0
  37. capycli/common/script_support.py +78 -0
  38. capycli/data/__init__.py +9 -0
  39. capycli/data/granularity_list.csv +1338 -0
  40. capycli/dependencies/__init__.py +9 -0
  41. capycli/dependencies/handle_dependencies.py +70 -0
  42. capycli/dependencies/javascript.py +261 -0
  43. capycli/dependencies/maven_list.py +333 -0
  44. capycli/dependencies/maven_pom.py +150 -0
  45. capycli/dependencies/nuget.py +184 -0
  46. capycli/dependencies/python.py +345 -0
  47. capycli/main/__init__.py +9 -0
  48. capycli/main/application.py +165 -0
  49. capycli/main/argument_parser.py +101 -0
  50. capycli/main/cli.py +28 -0
  51. capycli/main/exceptions.py +14 -0
  52. capycli/main/options.py +424 -0
  53. capycli/main/result_codes.py +41 -0
  54. capycli/mapping/handle_mapping.py +46 -0
  55. capycli/mapping/mapping_to_html.py +182 -0
  56. capycli/mapping/mapping_to_xlsx.py +197 -0
  57. capycli/moverview/handle_moverview.py +46 -0
  58. capycli/moverview/moverview_to_html.py +122 -0
  59. capycli/moverview/moverview_to_xlsx.py +170 -0
  60. capycli/project/__init__.py +9 -0
  61. capycli/project/check_prerequisites.py +304 -0
  62. capycli/project/create_bom.py +190 -0
  63. capycli/project/create_project.py +335 -0
  64. capycli/project/create_readme.py +546 -0
  65. capycli/project/find_project.py +128 -0
  66. capycli/project/get_license_info.py +246 -0
  67. capycli/project/handle_project.py +118 -0
  68. capycli/project/show_ecc.py +200 -0
  69. capycli/project/show_licenses.py +211 -0
  70. capycli/project/show_project.py +215 -0
  71. capycli/project/show_vulnerabilities.py +238 -0
  72. capycli-2.0.0.dev8.dist-info/LICENSES/CC0-1.0.txt +121 -0
  73. capycli-2.0.0.dev8.dist-info/LICENSES/MIT.txt +27 -0
  74. capycli-2.0.0.dev8.dist-info/License.md +27 -0
  75. capycli-2.0.0.dev8.dist-info/METADATA +268 -0
  76. capycli-2.0.0.dev8.dist-info/RECORD +78 -0
  77. capycli-2.0.0.dev8.dist-info/WHEEL +4 -0
  78. 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")