src/eric7/CycloneDXInterface/CycloneDXUtilities.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9146
409d93549d61
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the interface to CycloneDX.
8 """
9
10 import os
11
12 from PyQt6.QtCore import QCoreApplication
13 from PyQt6.QtWidgets import QDialog
14
15 from EricWidgets.EricApplication import ericApp
16 from EricWidgets import EricMessageBox
17
18 from packageurl import PackageURL
19
20 from cyclonedx.model import (
21 ExternalReference, ExternalReferenceType, LicenseChoice,
22 OrganizationalContact, OrganizationalEntity, Tool, XsUri
23 )
24 from cyclonedx.model.bom import Bom
25 from cyclonedx.model.component import Component
26 from cyclonedx.model.vulnerability import Vulnerability, VulnerabilitySource
27 from cyclonedx.output import (
28 OutputFormat, SchemaVersion, get_instance as get_output_instance
29 )
30 from cyclonedx.parser import BaseParser
31
32 from cyclonedx_py.parser.pipenv import PipEnvFileParser
33 from cyclonedx_py.parser.poetry import PoetryFileParser
34 from cyclonedx_py.parser.requirements import RequirementsFileParser
35
36 from PipInterface.PipVulnerabilityChecker import (
37 Package, VulnerabilityCheckError
38 )
39
40
41 class CycloneDXEnvironmentParser(BaseParser):
42 """
43 Class implementing a parser to get package data for a named environment.
44 """
45 def __init__(self, venvName):
46 """
47 Constructor
48
49 @param venvName name of the virtual environment
50 @type str
51 """
52 super().__init__()
53
54 pip = ericApp().getObject("Pip")
55 packages = pip.getLicenses(venvName)
56 for package in packages:
57 comp = Component(
58 name=package["Name"],
59 version=package["Version"],
60 author=package["Author"],
61 description=package["Description"],
62 purl=PackageURL(
63 type='pypi',
64 name=package["Name"],
65 version=package["Version"]
66 )
67 )
68 for lic in package["License"].split(";"):
69 comp.licenses.add(
70 LicenseChoice(license_expression=lic.strip())
71 )
72
73 self._components.append(comp)
74
75
76 def createCycloneDXFile(venvName):
77 """
78 Function to create a CyccloneDX SBOM file.
79
80 @param venvName name of the virtual environment
81 @type str
82 @exception RuntimeError raised to indicate illegal creation parameters
83 """
84 from .CycloneDXConfigDialog import CycloneDXConfigDialog
85 dlg = CycloneDXConfigDialog(venvName)
86 if dlg.exec() == QDialog.DialogCode.Accepted:
87 (inputSource, inputFile, fileFormat, schemaVersion, sbomFile,
88 withVulnerabilities, withDependencies, metadataDict) = dlg.getData()
89
90 # check error conditions first
91 if inputSource not in ("environment", "pipenv", "poetry",
92 "requirements"):
93 raise RuntimeError("Unsupported input source given.")
94 if fileFormat not in ("XML", "JSON"):
95 raise RuntimeError("Unsupported SBOM file format given.")
96
97 if inputSource == "environment":
98 parser = CycloneDXEnvironmentParser(venvName)
99 else:
100 # all other parsers need an input file
101 if not os.path.isfile(inputFile):
102 EricMessageBox.warning(
103 None,
104 QCoreApplication.translate(
105 "CycloneDX", "CycloneDX - SBOM Creation"),
106 QCoreApplication.translate(
107 "CycloneDX",
108 "<p>The configured input file <b>{0}</b> does not"
109 " exist. Aborting...</p>"
110 ).format(inputFile)
111 )
112 return
113
114 if inputSource == "pipenv":
115 parser = PipEnvFileParser(pipenv_lock_filename=inputFile)
116 elif inputSource == "poetry":
117 parser = PoetryFileParser(poetry_lock_filename=inputFile)
118 elif inputSource == "requirements":
119 parser = RequirementsFileParser(requirements_file=inputFile)
120
121 if withVulnerabilities:
122 addCycloneDXVulnerabilities(parser)
123
124 if withDependencies:
125 addCycloneDXDependencies(parser, venvName)
126
127 if fileFormat == "XML":
128 outputFormat = OutputFormat.XML
129 elif fileFormat == "JSON":
130 outputFormat = OutputFormat.JSON
131
132 if parser.has_warnings():
133 excludedList = ["<li>{0}</li>".format(warning.get_item())
134 for warning in parser.get_warnings()]
135 EricMessageBox.warning(
136 None,
137 QCoreApplication.translate(
138 "CycloneDX", "CycloneDX - SBOM Creation"),
139 QCoreApplication.translate(
140 "CycloneDX",
141 "<p>Some of the dependencies do not have pinned version"
142 " numbers.<ul>{0}</ul>The above listed packages will NOT"
143 " be included in the generated CycloneDX SBOM file as"
144 " version is a mandatory field.</p>"
145 ).format("".join(excludedList))
146 )
147
148 bom = Bom.from_parser(parser=parser)
149 _amendMetaData(bom.metadata, metadataDict)
150 output = get_output_instance(
151 bom=bom,
152 output_format=outputFormat,
153 schema_version=SchemaVersion['V{0}'.format(
154 schemaVersion.replace('.', '_')
155 )]
156 )
157 output.output_to_file(filename=sbomFile, allow_overwrite=True)
158
159 EricMessageBox.information(
160 None,
161 QCoreApplication.translate(
162 "CycloneDX", "CycloneDX - SBOM Creation"),
163 QCoreApplication.translate(
164 "CycloneDX",
165 "<p>The SBOM data was written to file <b>{0}</b>.</p>"
166 ).format(sbomFile)
167 )
168
169
170 def addCycloneDXVulnerabilities(parser):
171 """
172 Function to add vulnerability data to the list of created components.
173
174 @param parser reference to the parser object containing the list of
175 components
176 @type BaseParser
177 """
178 components = parser.get_components()
179
180 packages = [
181 Package(name=component.name, version=component.version)
182 for component in components
183 ]
184
185 pip = ericApp().getObject("Pip")
186 error, vulnerabilities = pip.getVulnerabilityChecker().check(packages)
187
188 if error == VulnerabilityCheckError.OK:
189 for package in vulnerabilities:
190 component = findCyccloneDXComponent(components, package)
191 if component:
192 for vuln in vulnerabilities[package]:
193 component.add_vulnerability(Vulnerability(
194 id=vuln.cve,
195 description=vuln.advisory,
196 recommendation="upgrade required",
197 source=VulnerabilitySource(name="pyup.io")
198 ))
199
200
201 def addCycloneDXDependencies(parser, venvName):
202 """
203 Function to add dependency data to the list of created components.
204
205 @param parser reference to the parser object containing the list of
206 components
207 @type BaseParser
208 @param venvName name of the virtual environment
209 @type str
210 """
211 components = parser.get_components()
212
213 pip = ericApp().getObject("Pip")
214 dependencies = pip.getDependencyTree(venvName)
215 for dependency in dependencies:
216 _addCycloneDXDependency(dependency, components)
217
218
219 def _addCycloneDXDependency(dependency, components):
220 """
221 Function to add a dependency to the given list of components.
222
223 @param dependency dependency to be added
224 @type dict
225 @param components list of components
226 @type list of Component
227 """
228 component = findCyccloneDXComponent(components, dependency["package_name"])
229 if component is not None:
230 bomRefs = component.dependencies
231 for dep in dependency["dependencies"]:
232 depComponent = findCyccloneDXComponent(
233 components, dep["package_name"])
234 if depComponent is not None:
235 bomRefs.add(depComponent.bom_ref)
236 # recursively add sub-dependencies
237 _addCycloneDXDependency(dep, components)
238 component.dependencies = bomRefs
239
240
241 def findCyccloneDXComponent(components, name):
242 """
243 Function to find a component in a given list of components.
244
245 @param components list of components to scan
246 @type list of Component
247 @param name name of the component to search for
248 @type str
249 @return reference to the found component or None
250 @rtype Component or None
251 """
252 for component in components:
253 if component.name == name:
254 return component
255
256 return None
257
258
259 def _amendMetaData(bomMetaData, metadataDict):
260 """
261 Function to amend the SBOM meta data according the given data.
262
263 The modifications done are:
264 <ul>
265 <li>add eric7 to the tools</li>
266 </ul>
267
268 @param bomMetaData reference to the SBOM meta data object
269 @type BomMetaData
270 @param metadataDict dictionary containing additional meta data
271 @type dict
272 @return reference to the modified SBOM meta data object
273 @rtype BomMetaData
274 """
275 # add a Tool entry for eric7
276 try:
277 from importlib.metadata import version as meta_version
278 __EricToolVersion = str(meta_version('eric-ide'))
279 except Exception:
280 from UI.Info import Version
281 __EricToolVersion = Version
282
283 EricTool = Tool(vendor='python-projects.org',
284 name='eric-ide',
285 version=__EricToolVersion)
286 EricTool.external_references.update([
287 ExternalReference(
288 reference_type=ExternalReferenceType.DISTRIBUTION,
289 url=XsUri(
290 "https://pypi.org/project/eric-ide/"
291 )
292 ),
293 ExternalReference(
294 reference_type=ExternalReferenceType.DOCUMENTATION,
295 url=XsUri(
296 "https://pypi.org/project/eric-ide/"
297 )
298 ),
299 ExternalReference(
300 reference_type=ExternalReferenceType.ISSUE_TRACKER,
301 url=XsUri(
302 "https://tracker.die-offenbachs.homelinux.org"
303 )
304 ),
305 ExternalReference(
306 reference_type=ExternalReferenceType.LICENSE,
307 url=XsUri(
308 "https://hg.die-offenbachs.homelinux.org/eric/file/tip/docs/"
309 "LICENSE.GPL3"
310 )
311 ),
312 ExternalReference(
313 reference_type=ExternalReferenceType.RELEASE_NOTES,
314 url=XsUri(
315 "https://hg.die-offenbachs.homelinux.org/eric/file/tip/docs/"
316 "changelog"
317 )
318 ),
319 ExternalReference(
320 reference_type=ExternalReferenceType.VCS,
321 url=XsUri(
322 "https://hg.die-offenbachs.homelinux.org/eric"
323 )
324 ),
325 ExternalReference(
326 reference_type=ExternalReferenceType.WEBSITE,
327 url=XsUri(
328 "https://eric-ide.python-projects.org"
329 )
330 )
331 ])
332 bomMetaData.tools.add(EricTool)
333
334 # add the meta data info entered by the user (if any)
335 if metadataDict is not None:
336 if metadataDict["AuthorName"]:
337 bomMetaData.authors = [OrganizationalContact(
338 name=metadataDict["AuthorName"],
339 email=metadataDict["AuthorEmail"]
340 )]
341 if metadataDict["Manufacturer"]:
342 bomMetaData.manufacture = OrganizationalEntity(
343 name=metadataDict["Manufacturer"]
344 )
345 if metadataDict["Supplier"]:
346 bomMetaData.supplier = OrganizationalEntity(
347 name=metadataDict["Supplier"])
348 if metadataDict["License"]:
349 bomMetaData.licenses = [LicenseChoice(
350 license_expression=metadataDict["License"]
351 )]
352 if metadataDict["Name"]:
353 bomMetaData.component = Component(
354 name=metadataDict["Name"],
355 component_type=metadataDict["Type"],
356 version=metadataDict["Version"],
357 description=metadataDict["Description"],
358 author=metadataDict["AuthorName"],
359 licenses=[LicenseChoice(
360 license_expression=metadataDict["License"]
361 )],
362 )
363
364 return bomMetaData

eric ide

mercurial