src/eric7/CycloneDXInterface/CycloneDXUtilities.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9146
409d93549d61
child 9221
bf71ee032bb4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/CycloneDXInterface/CycloneDXUtilities.py	Thu Jul 07 11:23:56 2022 +0200
@@ -0,0 +1,364 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the interface to CycloneDX.
+"""
+
+import os
+
+from PyQt6.QtCore import QCoreApplication
+from PyQt6.QtWidgets import QDialog
+
+from EricWidgets.EricApplication import ericApp
+from EricWidgets import EricMessageBox
+
+from packageurl import PackageURL
+
+from cyclonedx.model import (
+    ExternalReference, ExternalReferenceType, LicenseChoice,
+    OrganizationalContact, OrganizationalEntity, Tool, XsUri
+)
+from cyclonedx.model.bom import Bom
+from cyclonedx.model.component import Component
+from cyclonedx.model.vulnerability import Vulnerability, VulnerabilitySource
+from cyclonedx.output import (
+    OutputFormat, SchemaVersion, get_instance as get_output_instance
+)
+from cyclonedx.parser import BaseParser
+
+from cyclonedx_py.parser.pipenv import PipEnvFileParser
+from cyclonedx_py.parser.poetry import PoetryFileParser
+from cyclonedx_py.parser.requirements import RequirementsFileParser
+
+from PipInterface.PipVulnerabilityChecker import (
+    Package, VulnerabilityCheckError
+)
+
+
+class CycloneDXEnvironmentParser(BaseParser):
+    """
+    Class implementing a parser to get package data for a named environment.
+    """
+    def __init__(self, venvName):
+        """
+        Constructor
+        
+        @param venvName name of the virtual environment
+        @type str
+        """
+        super().__init__()
+        
+        pip = ericApp().getObject("Pip")
+        packages = pip.getLicenses(venvName)
+        for package in packages:
+            comp = Component(
+                name=package["Name"],
+                version=package["Version"],
+                author=package["Author"],
+                description=package["Description"],
+                purl=PackageURL(
+                    type='pypi',
+                    name=package["Name"],
+                    version=package["Version"]
+                )
+            )
+            for lic in package["License"].split(";"):
+                comp.licenses.add(
+                    LicenseChoice(license_expression=lic.strip())
+                )
+            
+            self._components.append(comp)
+
+
+def createCycloneDXFile(venvName):
+    """
+    Function to create a CyccloneDX SBOM file.
+    
+    @param venvName name of the virtual environment
+    @type str
+    @exception RuntimeError raised to indicate illegal creation parameters
+    """
+    from .CycloneDXConfigDialog import CycloneDXConfigDialog
+    dlg = CycloneDXConfigDialog(venvName)
+    if dlg.exec() == QDialog.DialogCode.Accepted:
+        (inputSource, inputFile, fileFormat, schemaVersion, sbomFile,
+         withVulnerabilities, withDependencies, metadataDict) = dlg.getData()
+        
+        # check error conditions first
+        if inputSource not in ("environment", "pipenv", "poetry",
+                               "requirements"):
+            raise RuntimeError("Unsupported input source given.")
+        if fileFormat not in ("XML", "JSON"):
+            raise RuntimeError("Unsupported SBOM file format given.")
+        
+        if inputSource == "environment":
+            parser = CycloneDXEnvironmentParser(venvName)
+        else:
+            # all other parsers need an input file
+            if not os.path.isfile(inputFile):
+                EricMessageBox.warning(
+                    None,
+                    QCoreApplication.translate(
+                        "CycloneDX", "CycloneDX - SBOM Creation"),
+                    QCoreApplication.translate(
+                        "CycloneDX",
+                        "<p>The configured input file <b>{0}</b> does not"
+                        " exist. Aborting...</p>"
+                    ).format(inputFile)
+                )
+                return
+            
+            if inputSource == "pipenv":
+                parser = PipEnvFileParser(pipenv_lock_filename=inputFile)
+            elif inputSource == "poetry":
+                parser = PoetryFileParser(poetry_lock_filename=inputFile)
+            elif inputSource == "requirements":
+                parser = RequirementsFileParser(requirements_file=inputFile)
+        
+        if withVulnerabilities:
+            addCycloneDXVulnerabilities(parser)
+        
+        if withDependencies:
+            addCycloneDXDependencies(parser, venvName)
+        
+        if fileFormat == "XML":
+            outputFormat = OutputFormat.XML
+        elif fileFormat == "JSON":
+            outputFormat = OutputFormat.JSON
+        
+        if parser.has_warnings():
+            excludedList = ["<li>{0}</li>".format(warning.get_item())
+                            for warning in parser.get_warnings()]
+            EricMessageBox.warning(
+                None,
+                QCoreApplication.translate(
+                    "CycloneDX", "CycloneDX - SBOM Creation"),
+                QCoreApplication.translate(
+                    "CycloneDX",
+                    "<p>Some of the dependencies do not have pinned version"
+                    " numbers.<ul>{0}</ul>The above listed packages will NOT"
+                    " be included in the generated CycloneDX SBOM file as"
+                    " version is a mandatory field.</p>"
+                ).format("".join(excludedList))
+            )
+        
+        bom = Bom.from_parser(parser=parser)
+        _amendMetaData(bom.metadata, metadataDict)
+        output = get_output_instance(
+            bom=bom,
+            output_format=outputFormat,
+            schema_version=SchemaVersion['V{0}'.format(
+                schemaVersion.replace('.', '_')
+            )]
+        )
+        output.output_to_file(filename=sbomFile, allow_overwrite=True)
+        
+        EricMessageBox.information(
+            None,
+            QCoreApplication.translate(
+                "CycloneDX", "CycloneDX - SBOM Creation"),
+            QCoreApplication.translate(
+                "CycloneDX",
+                "<p>The SBOM data was written to file <b>{0}</b>.</p>"
+            ).format(sbomFile)
+        )
+
+
+def addCycloneDXVulnerabilities(parser):
+    """
+    Function to add vulnerability data to the list of created components.
+    
+    @param parser reference to the parser object containing the list of
+        components
+    @type BaseParser
+    """
+    components = parser.get_components()
+    
+    packages = [
+        Package(name=component.name, version=component.version)
+        for component in components
+    ]
+    
+    pip = ericApp().getObject("Pip")
+    error, vulnerabilities = pip.getVulnerabilityChecker().check(packages)
+    
+    if error == VulnerabilityCheckError.OK:
+        for package in vulnerabilities:
+            component = findCyccloneDXComponent(components, package)
+            if component:
+                for vuln in vulnerabilities[package]:
+                    component.add_vulnerability(Vulnerability(
+                        id=vuln.cve,
+                        description=vuln.advisory,
+                        recommendation="upgrade required",
+                        source=VulnerabilitySource(name="pyup.io")
+                    ))
+
+
+def addCycloneDXDependencies(parser, venvName):
+    """
+    Function to add dependency data to the list of created components.
+    
+    @param parser reference to the parser object containing the list of
+        components
+    @type BaseParser
+    @param venvName name of the virtual environment
+    @type str
+    """
+    components = parser.get_components()
+    
+    pip = ericApp().getObject("Pip")
+    dependencies = pip.getDependencyTree(venvName)
+    for dependency in dependencies:
+        _addCycloneDXDependency(dependency, components)
+
+
+def _addCycloneDXDependency(dependency, components):
+    """
+    Function to add a dependency to the given list of components.
+    
+    @param dependency dependency to be added
+    @type dict
+    @param components list of components
+    @type list of Component
+    """
+    component = findCyccloneDXComponent(components, dependency["package_name"])
+    if component is not None:
+        bomRefs = component.dependencies
+        for dep in dependency["dependencies"]:
+            depComponent = findCyccloneDXComponent(
+                components, dep["package_name"])
+            if depComponent is not None:
+                bomRefs.add(depComponent.bom_ref)
+                # recursively add sub-dependencies
+                _addCycloneDXDependency(dep, components)
+        component.dependencies = bomRefs
+
+
+def findCyccloneDXComponent(components, name):
+    """
+    Function to find a component in a given list of components.
+    
+    @param components list of components to scan
+    @type list of Component
+    @param name name of the component to search for
+    @type str
+    @return reference to the found component or None
+    @rtype Component or None
+    """
+    for component in components:
+        if component.name == name:
+            return component
+    
+    return None
+
+
+def _amendMetaData(bomMetaData, metadataDict):
+    """
+    Function to amend the SBOM meta data according the given data.
+    
+    The modifications done are:
+    <ul>
+    <li>add eric7 to the tools</li>
+    </ul>
+    
+    @param bomMetaData reference to the SBOM meta data object
+    @type BomMetaData
+    @param metadataDict dictionary containing additional meta data
+    @type dict
+    @return reference to the modified SBOM meta data object
+    @rtype BomMetaData
+    """
+    # add a Tool entry for eric7
+    try:
+        from importlib.metadata import version as meta_version
+        __EricToolVersion = str(meta_version('eric-ide'))
+    except Exception:
+        from UI.Info import Version
+        __EricToolVersion = Version
+    
+    EricTool = Tool(vendor='python-projects.org',
+                    name='eric-ide',
+                    version=__EricToolVersion)
+    EricTool.external_references.update([
+        ExternalReference(
+            reference_type=ExternalReferenceType.DISTRIBUTION,
+            url=XsUri(
+                "https://pypi.org/project/eric-ide/"
+            )
+        ),
+        ExternalReference(
+            reference_type=ExternalReferenceType.DOCUMENTATION,
+            url=XsUri(
+                "https://pypi.org/project/eric-ide/"
+            )
+        ),
+        ExternalReference(
+            reference_type=ExternalReferenceType.ISSUE_TRACKER,
+            url=XsUri(
+                "https://tracker.die-offenbachs.homelinux.org"
+            )
+        ),
+        ExternalReference(
+            reference_type=ExternalReferenceType.LICENSE,
+            url=XsUri(
+                "https://hg.die-offenbachs.homelinux.org/eric/file/tip/docs/"
+                "LICENSE.GPL3"
+            )
+        ),
+        ExternalReference(
+            reference_type=ExternalReferenceType.RELEASE_NOTES,
+            url=XsUri(
+                "https://hg.die-offenbachs.homelinux.org/eric/file/tip/docs/"
+                "changelog"
+            )
+        ),
+        ExternalReference(
+            reference_type=ExternalReferenceType.VCS,
+            url=XsUri(
+                "https://hg.die-offenbachs.homelinux.org/eric"
+            )
+        ),
+        ExternalReference(
+            reference_type=ExternalReferenceType.WEBSITE,
+            url=XsUri(
+                "https://eric-ide.python-projects.org"
+            )
+        )
+    ])
+    bomMetaData.tools.add(EricTool)
+    
+    # add the meta data info entered by the user (if any)
+    if metadataDict is not None:
+        if metadataDict["AuthorName"]:
+            bomMetaData.authors = [OrganizationalContact(
+                name=metadataDict["AuthorName"],
+                email=metadataDict["AuthorEmail"]
+            )]
+        if metadataDict["Manufacturer"]:
+            bomMetaData.manufacture = OrganizationalEntity(
+                name=metadataDict["Manufacturer"]
+            )
+        if metadataDict["Supplier"]:
+            bomMetaData.supplier = OrganizationalEntity(
+                name=metadataDict["Supplier"])
+        if metadataDict["License"]:
+            bomMetaData.licenses = [LicenseChoice(
+                license_expression=metadataDict["License"]
+            )]
+        if metadataDict["Name"]:
+            bomMetaData.component = Component(
+                name=metadataDict["Name"],
+                component_type=metadataDict["Type"],
+                version=metadataDict["Version"],
+                description=metadataDict["Description"],
+                author=metadataDict["AuthorName"],
+                licenses=[LicenseChoice(
+                    license_expression=metadataDict["License"]
+                )],
+            )
+    
+    return bomMetaData

eric ide

mercurial