eric7/CycloneDXInterface/CycloneDXUtilities.py

Sat, 04 Jun 2022 15:53:41 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 04 Jun 2022 15:53:41 +0200
branch
eric7
changeset 9119
5bcdef5207f6
parent 9117
c6afba2049cf
child 9122
ddf8ed8f7387
permissions
-rw-r--r--

CycloneDX
- added capability to list vulnerabilities in the SBOM file

# -*- 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 LicenseChoice
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) = 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 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)
        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 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

eric ide

mercurial