src/eric7/CycloneDXInterface/CycloneDXUtilities.py

branch
eric7
changeset 11282
dc23e5ac706a
parent 11148
15e30f0c76a8
child 11283
67d5b135cb0f
diff -r a2529214787a -r dc23e5ac706a src/eric7/CycloneDXInterface/CycloneDXUtilities.py
--- a/src/eric7/CycloneDXInterface/CycloneDXUtilities.py	Sat May 10 18:39:45 2025 +0200
+++ b/src/eric7/CycloneDXInterface/CycloneDXUtilities.py	Tue May 13 15:58:12 2025 +0200
@@ -7,72 +7,13 @@
 Module implementing the interface to CycloneDX.
 """
 
-import contextlib
-import json
 import os
-
-from xml.etree import ElementTree  # secok
+import sys
 
-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 packageurl import PackageURL
-from PyQt6.QtCore import QCoreApplication
+from PyQt6.QtCore import QCoreApplication, QProcess
 from PyQt6.QtWidgets import QDialog
 
 from eric7.EricWidgets import EricMessageBox
-from eric7.EricWidgets.EricApplication import ericApp
-from eric7.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, parent=None):
@@ -91,14 +32,12 @@
     if dlg.exec() == QDialog.DialogCode.Accepted:
         (
             inputSource,
-            inputFile,
+            inputPath,
             fileFormat,
-            schemaVersion,
+            specVersion,
             sbomFile,
-            withVulnerabilities,
-            withDependencies,
-            readableOutput,
-            metadataDict,
+            pyprojectFile,
+            mainComponentType,
         ) = dlg.getData()
 
         # check error conditions first
@@ -107,316 +46,67 @@
         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(
+        args = [
+            inputSource,
+            "--mc-type",
+            mainComponentType,
+            "--spec-version",
+            specVersion,
+            "--output-format",
+            fileFormat,
+            "--output-file",
+            sbomFile,
+        ]
+        if pyprojectFile:
+            args.extend(["--pyproject", pyprojectFile])
+
+        args.append(inputPath)
+        prog = os.path.join(os.path.dirname(sys.executable), "cyclonedx-py")
+        process = QProcess()
+        process.start(prog, args)
+        if process.waitForStarted():
+            if process.waitForFinished():
+                if process.exitCode() == 0:
+                    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),
+                    )
+                else:
+                    EricMessageBox.critical(
+                        None,
+                        QCoreApplication.translate(
+                            "CycloneDX", "CycloneDX - SBOM Creation"
+                        ),
+                        QCoreApplication.translate(
+                            "CycloneDX",
+                            "<p>The SBOM file <b>{0}</b> could not be written.</p>"
+                            "<p>Reason: {1}</p>",
+                        ).format(sbomFile, process.errorString()),
+                    )
+            else:
+                EricMessageBox.critical(
                     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),
+                        "<p>The SBOM creation process did not finish within 30s.</p>",
+                    ),
                 )
-                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(".", "_"))
-            ],
-        )
-        outputStr = output.output_as_string()
-        if readableOutput:
-            if fileFormat == "XML":
-                outputStr = _prettifyXML(outputStr)
-            elif fileFormat == "JSON":
-                outputStr = _prettifyJSON(outputStr)
-
-        try:
-            with open(sbomFile, "w", encoding="utf-8") as f:
-                f.write(outputStr)
-            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),
-            )
-        except OSError as err:
+        else:
             EricMessageBox.critical(
                 None,
                 QCoreApplication.translate("CycloneDX", "CycloneDX - SBOM Creation"),
                 QCoreApplication.translate(
                     "CycloneDX",
-                    "<p>The SBOM file <b>{0}</b> could not be written.</p>"
-                    "<p>Reason: {1}</p>",
-                ).format(sbomFile, str(err)),
+                    "<p>The SBOM creation process could not be started.</p>"
+                    "<p>Reason: {0}</p>",
+                ).format(process.errorString()),
             )
-
-
-def _prettifyXML(inputStr):
-    """
-    Function to prettify the SBOM XML output generated by CycloneDX.
-
-    Note: Prettifying an XML tree works only with Python 3.9 and above!
-
-    @param inputStr output generated by CycloneDX
-    @type str
-    @return prettified SBOM string
-    @rtype str
-    """
-    tree = ElementTree.fromstring(inputStr)  # secok
-    with contextlib.suppress(AttributeError):
-        ElementTree.indent(tree)
-        return '<?xml version="1.0" encoding="UTF-8"?>\n' + ElementTree.tostring(
-            tree, encoding="unicode"
-        )
-
-    return inputStr
-
-
-def _prettifyJSON(inputStr):
-    """
-    Function to prettify the SBOM JSON output generated by CycloneDX.
-
-    @param inputStr output generated by CycloneDX
-    @type str
-    @return prettified SBOM string
-    @rtype str
-    """
-    sbom = json.loads(inputStr)
-    return json.dumps(sbom, indent="  ")
-
-
-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  # noqa: I-10
-
-        __EricToolVersion = str(meta_version("eric-ide"))
-    except Exception:
-        from eric7.__version__ import Version  # __IGNORE_WARNING_I-101__
-
-        __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.txt"
-                ),
-            ),
-            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