--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/CycloneDXInterface/CycloneDXUtilities.py Fri Jun 03 19:54:57 2022 +0200 @@ -0,0 +1,152 @@ +# -*- 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.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 + + +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 = ( + dlg.getData() + ) + + 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 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) + )