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