Sat, 04 Jun 2022 16:57:02 +0200
CycloneDX
- added capability to list dependencies 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, withDependencies) = 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) 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