--- a/eric7/PipInterface/PipPackagesWidget.py Sun Mar 13 19:59:03 2022 +0100 +++ b/eric7/PipInterface/PipPackagesWidget.py Mon Mar 14 19:49:48 2022 +0100 @@ -24,6 +24,7 @@ from EricWidgets import EricMessageBox from EricGui.EricOverrideCursor import EricOverrideCursor +from .PipVulnerabilityChecker import Package, VulnerabilityCheckError from .Ui_PipPackagesWidget import Ui_PipPackagesWidget import UI.PixmapCache @@ -305,7 +306,8 @@ Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive ) if len(pipList) > 0: - pipVersionTuple = Globals.versionToTuple(pipList[0].text(1)) + pipVersionTuple = Globals.versionToTuple( + pipList[0].text(PipPackagesWidget.InstalledVersionColumn)) return pipVersionTuple @@ -465,94 +467,121 @@ """ self.__refreshPackagesList() - @pyqtSlot() - def on_packagesList_itemSelectionChanged(self): + def __showPackageInformation(self, packageName): + """ + Private method to show information for a package. + + @param packageName name of the package + @type str """ - Private slot handling the selection of a package. + environment = self.environmentsComboBox.currentText() + interpreter = self.__pip.getVirtualenvInterpreter(environment) + if not interpreter: + return + + args = ["-m", "pip", "show"] + if self.verboseCheckBox.isChecked(): + args.append("--verbose") + if self.installedFilesCheckBox.isChecked(): + args.append("--files") + args.append(packageName) + + with EricOverrideCursor(): + success, output = self.__pip.runProcess(args, interpreter) + + if success and output: + mode = self.ShowProcessGeneralMode + for line in output.splitlines(): + line = line.rstrip() + if line != "---": + if mode != self.ShowProcessGeneralMode: + if line[0] == " ": + QTreeWidgetItem( + self.infoWidget, + [" ", line.strip()]) + else: + mode = self.ShowProcessGeneralMode + if mode == self.ShowProcessGeneralMode: + try: + label, info = line.split(": ", 1) + except ValueError: + label = line[:-1] + info = "" + label = label.lower() + if label in self.__infoLabels: + QTreeWidgetItem( + self.infoWidget, + [self.__infoLabels[label], info]) + if label == "files": + mode = self.ShowProcessFilesListMode + elif label == "classifiers": + mode = self.ShowProcessClassifiersMode + elif label == "entry-points": + mode = self.ShowProcessEntryPointsMode + self.infoWidget.scrollToTop() + + header = self.infoWidget.header() + header.setStretchLastSection(False) + header.resizeSections(QHeaderView.ResizeMode.ResizeToContents) + if ( + header.sectionSize(0) + header.sectionSize(1) < + header.width() + ): + header.setStretchLastSection(True) + + @pyqtSlot(QTreeWidgetItem, int) + def on_packagesList_itemClicked(self, item, column): + """ + Private slot reacting on a package item click. + + @param item reference to the clicked item + @type QTreeWidgetItem + @param column clicked column + @type int """ self.infoWidget.clear() - if len(self.packagesList.selectedItems()) == 1: - itm = self.packagesList.selectedItems()[0] - - environment = self.environmentsComboBox.currentText() - interpreter = self.__pip.getVirtualenvInterpreter(environment) - if not interpreter: - return - - args = ["-m", "pip", "show"] - if self.verboseCheckBox.isChecked(): - args.append("--verbose") - if self.installedFilesCheckBox.isChecked(): - args.append("--files") - args.append(itm.text(0)) - - with EricOverrideCursor(): - success, output = self.__pip.runProcess(args, interpreter) - - if success and output: - mode = self.ShowProcessGeneralMode - for line in output.splitlines(): - line = line.rstrip() - if line != "---": - if mode != self.ShowProcessGeneralMode: - if line[0] == " ": - QTreeWidgetItem( - self.infoWidget, - [" ", line.strip()]) - else: - mode = self.ShowProcessGeneralMode - if mode == self.ShowProcessGeneralMode: - try: - label, info = line.split(": ", 1) - except ValueError: - label = line[:-1] - info = "" - label = label.lower() - if label in self.__infoLabels: - QTreeWidgetItem( - self.infoWidget, - [self.__infoLabels[label], info]) - if label == "files": - mode = self.ShowProcessFilesListMode - elif label == "classifiers": - mode = self.ShowProcessClassifiersMode - elif label == "entry-points": - mode = self.ShowProcessEntryPointsMode - self.infoWidget.scrollToTop() - - header = self.infoWidget.header() - header.setStretchLastSection(False) - header.resizeSections(QHeaderView.ResizeMode.ResizeToContents) - if ( - header.sectionSize(0) + header.sectionSize(1) < - header.width() - ): - header.setStretchLastSection(True) + if ( + column == PipPackagesWidget.VulnerabilityColumn and + bool(item.text(PipPackagesWidget.VulnerabilityColumn)) + ): + self.__showVulnerabilityInformation( + item.text(PipPackagesWidget.PackageColumn), + item.text(PipPackagesWidget.InstalledVersionColumn), + item.data(PipPackagesWidget.VulnerabilityColumn, + PipPackagesWidget.VulnerabilityRole) + ) + else: + self.__showPackageInformation( + item.text(PipPackagesWidget.PackageColumn) + ) self.__updateActionButtons() @pyqtSlot(QTreeWidgetItem, int) - def on_packagesList_itemActivated(self, item, column): + def on_packagesList_itemDoubleClicked(self, item, column): """ - Private slot reacting on a package item activation. + Private slot reacting on a package item double click. - @param item reference to the activated item + @param item reference to the double clicked item @type QTreeWidgetItem - @param column activated column + @param column double clicked column @type int """ - packageName = item.text(0) - upgradable = bool(item.text(2)) - if column == 1: + packageName = item.text(PipPackagesWidget.PackageColumn) + upgradable = bool(item.text(PipPackagesWidget.AvailableVersionColumn)) + if column == PipPackagesWidget.InstalledVersionColumn: # show details for installed version - packageVersion = item.text(1) + packageVersion = item.text( + PipPackagesWidget.InstalledVersionColumn) else: # show details for available version or installed one - if item.text(2): - packageVersion = item.text(2) + if item.text(PipPackagesWidget.AvailableVersionColumn): + packageVersion = item.text( + PipPackagesWidget.AvailableVersionColumn) else: - packageVersion = item.text(1) + packageVersion = item.text( + PipPackagesWidget.InstalledVersionColumn) self.__showPackageDetails(packageName, packageVersion, upgradable=upgradable) @@ -566,7 +595,8 @@ @param checked state of the checkbox @type bool """ - self.on_packagesList_itemSelectionChanged() + self.on_packagesList_itemClicked(self.packagesList.currentItem(), + self.packagesList.currentColumn()) @pyqtSlot(bool) def on_installedFilesCheckBox_clicked(self, checked): @@ -577,7 +607,8 @@ @param checked state of the checkbox @type bool """ - self.on_packagesList_itemSelectionChanged() + self.on_packagesList_itemClicked(self.packagesList.currentItem(), + self.packagesList.currentColumn()) @pyqtSlot() def on_refreshButton_clicked(self): @@ -605,7 +636,8 @@ """ Private slot to upgrade selected packages of the selected environment. """ - packages = [itm.text(0) for itm in self.__selectedUpdateableItems()] + packages = [itm.text(PipPackagesWidget.PackageColumn) + for itm in self.__selectedUpdateableItems()] if packages: self.executeUpgradePackages(packages) @@ -614,7 +646,8 @@ """ Private slot to upgrade all packages of the selected environment. """ - packages = [itm.text(0) for itm in self.__allUpdateableItems()] + packages = [itm.text(PipPackagesWidget.PackageColumn) + for itm in self.__allUpdateableItems()] if packages: self.executeUpgradePackages(packages) @@ -662,12 +695,15 @@ item = self.packagesList.selectedItems()[0] if item: packageName = item.text(PipPackagesWidget.PackageColumn) - upgradable = bool(item.text(2)) + upgradable = bool(item.text( + PipPackagesWidget.AvailableVersionColumn)) # show details for available version or installed one - if item.text(2): - packageVersion = item.text(2) + if item.text(PipPackagesWidget.AvailableVersionColumn): + packageVersion = item.text( + PipPackagesWidget.AvailableVersionColumn) else: - packageVersion = item.text(1) + packageVersion = item.text( + PipPackagesWidget.InstalledVersionColumn) self.__showPackageDetails(packageName, packageVersion, upgradable=upgradable) @@ -1024,6 +1060,14 @@ self.tr("Generate Requirements..."), self.__generateRequirements) self.__pipMenu.addSeparator() + self.__checkVulnerabilityAct = self.__pipMenu.addAction( + self.tr("Check Vulnerabilities"), + self.__updateVulnerabilityData) + # updateVulnerabilityDbAct + self.__pipMenu.addAction( + self.tr("Update Vulnerability Database"), + self.__updateVulnerabilityDbCache) + self.__pipMenu.addSeparator() self.__cacheInfoAct = self.__pipMenu.addAction( self.tr("Show Cache Info..."), self.__showCacheInfo) @@ -1080,6 +1124,8 @@ self.__cachePurgeAct.setEnabled(enablePipCache) self.__editVirtualenvConfigAct.setEnabled(enable) + self.__checkVulnerabilityAct.setEnabled( + enable & self.vulnerabilityCheckBox.isEnabled()) @pyqtSlot() def __installPip(self): @@ -1334,8 +1380,102 @@ if clearFirst: self.__clearVulnerabilityInfo() - packages = [] # TODO: fill this list with real data + packages = [] + for row in range(self.packagesList.topLevelItemCount()): + itm = self.packagesList.topLevelItem(row) + packages.append(Package( + name=itm.text(PipPackagesWidget.PackageColumn), + version=itm.text(PipPackagesWidget.InstalledVersionColumn) + )) error, vulnerabilities = ( self.__pip.getVulnerabilityChecker().check(packages) ) + if error == VulnerabilityCheckError.OK: + for package in vulnerabilities: + items = self.packagesList.findItems( + package, + Qt.MatchFlag.MatchExactly | + Qt.MatchFlag.MatchCaseSensitive + ) + if items: + itm = items[0] + itm.setData( + PipPackagesWidget.VulnerabilityColumn, + PipPackagesWidget.VulnerabilityRole, + vulnerabilities[package] + ) + affected = {v.spec for v in vulnerabilities[package]} + itm.setText( + PipPackagesWidget.VulnerabilityColumn, + ', '.join(affected) + ) + itm.setIcon( + PipPackagesWidget.VulnerabilityColumn, + UI.PixmapCache.getIcon("securityLow") + ) + + elif error in (VulnerabilityCheckError.FullDbUnavailable, + VulnerabilityCheckError.SummaryDbUnavailable): + self.vulnerabilityCheckBox.setChecked(False) + self.vulnerabilityCheckBox.setEnabled(False) + self.packagesList.setColumnHidden( + PipPackagesWidget.VulnerabilityColumn, True) + + @pyqtSlot() + def __updateVulnerabilityDbCache(self): + """ + Private slot to initiate an update of the local cache of the + vulnerability database. + """ + with EricOverrideCursor(): + self.__pip.getVulnerabilityChecker().updateVulnerabilityDb() + + def __showVulnerabilityInformation(self, packageName, packageVersion, + vulnerabilities): + """ + Private method to show the detected vulnerability data. + + @param packageName name of the package + @type str + @param packageVersion installed version number + @type str + @param vulnerabilities list of vulnerabilities + @type list of Vulnerability + """ + header = ( + self.tr("{0} {1}", "package name, package version") + .format(packageName, packageVersion) + ) + topItem = QTreeWidgetItem(self.infoWidget, [header]) + topItem.setFirstColumnSpanned(True) + topItem.setExpanded(True) + font = topItem.font(0) + font.setBold(True) + topItem.setFont(0, font) + + for vulnerability in vulnerabilities: + title = ( + vulnerability.cve + if vulnerability.cve else + vulnerability.vulnerabilityId + ) + titleItem = QTreeWidgetItem(topItem, [title]) + titleItem.setFirstColumnSpanned(True) + titleItem.setExpanded(True) + + QTreeWidgetItem( + titleItem, + [self.tr("Affected Version:"), vulnerability.spec]) + itm = QTreeWidgetItem( + titleItem, + [self.tr("Advisory:"), vulnerability.advisory]) + itm.setToolTip(1, "<p>{0}</p>".format( + vulnerability.advisory.replace("\r\n", "<br/>") + )) + + self.infoWidget.scrollToTop() + self.infoWidget.resizeColumnToContents(0) + + header = self.infoWidget.header() + header.setStretchLastSection(True)