Wed, 13 Jul 2022 14:55:47 +0200
Reformatted the source code using the 'Black' utility.
# -*- 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