From 58266b6c1a2566578a7e8e6e08589e3c540bea60 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 30 Aug 2024 12:45:30 +0200 Subject: [PATCH 01/18] chore: cleanup old OSS functionality [IDE-609] (#586) * chore: cleanup after release * fix: compile errors * fix: more cleanup * fix: more compile error fixing * fix: SnykControllerImplTest * fix: remove old annotators from plugin.xml --- .../io/snyk/plugin/SnykPostStartupActivity.kt | 2 +- src/main/kotlin/io/snyk/plugin/Utils.kt | 12 - .../plugin/analytics/AnalyticsScanListener.kt | 17 -- .../io/snyk/plugin/events/SnykScanListener.kt | 5 - .../plugin/services/SnykTaskQueueService.kt | 87 ++---- .../SnykTreeScanTypeFilterActionGroup.kt | 6 +- .../plugin/ui/toolwindow/SnykToolWindow.kt | 28 +- .../ui/toolwindow/SnykToolWindowPanel.kt | 171 +----------- .../ui/toolwindow/SnykTreeCellRenderer.kt | 44 ---- .../nodes/leaf/VulnerabilityTreeNode.kt | 19 -- .../nodes/secondlevel/FileTreeNode.kt | 10 - .../panels/VulnerabilityDescriptionPanel.kt | 247 ------------------ .../snyk/code/annotator/SnykOSSAnnotatorLS.kt | 4 +- .../kotlin/snyk/common/SnykCachedResults.kt | 24 +- .../snyk/common/lsp/LanguageServerWrapper.kt | 14 +- .../snyk/common/lsp/SnykLanguageClient.kt | 4 - .../container/ContainerBulkFileListener.kt | 41 ++- .../kotlin/snyk/oss/OssBulkFileListener.kt | 67 +---- src/main/kotlin/snyk/oss/OssGroupedResult.kt | 7 - src/main/kotlin/snyk/oss/OssResult.kt | 22 -- src/main/kotlin/snyk/oss/OssService.kt | 59 ----- .../kotlin/snyk/oss/OssTextRangeFinder.kt | 18 -- .../snyk/oss/OssVulnerabilitiesForFile.kt | 29 -- src/main/kotlin/snyk/oss/Vulnerability.kt | 41 --- .../snyk/oss/annotator/AnnotatorHelper.kt | 34 --- .../snyk/oss/annotator/OSSBaseAnnotator.kt | 174 ------------ .../snyk/oss/annotator/OSSGoModAnnotator.kt | 36 --- .../snyk/oss/annotator/OSSGradleAnnotator.kt | 75 ------ .../snyk/oss/annotator/OSSMavenAnnotator.kt | 70 ----- .../snyk/oss/annotator/OSSNpmAnnotator.kt | 90 ------- .../resources/META-INF/optional/withGo.xml | 2 - .../resources/META-INF/optional/withJSON.xml | 1 - .../resources/META-INF/optional/withJava.xml | 2 - .../META-INF/optional/withKotlin.xml | 2 +- .../resources/META-INF/optional/withXML.xml | 3 +- .../analytics/AnalyticsScanListenerTest.kt | 7 - .../plugin/cli/ConsoleCommandRunnerTest.kt | 65 +---- .../extensions/SnykControllerImplTest.kt | 41 +-- .../services/SnykTaskQueueServiceTest.kt | 20 +- .../SnykToolWindowPanelIntegTest.kt | 106 ++++---- .../ui/toolwindow/SnykToolWindowPanelTest.kt | 39 --- .../VulnerabilityDescriptionPanelTest.kt | 52 ---- .../snyk/oss/OssBulkFileListenerTest.kt | 76 ------ 43 files changed, 164 insertions(+), 1709 deletions(-) delete mode 100644 src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/leaf/VulnerabilityTreeNode.kt delete mode 100644 src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/secondlevel/FileTreeNode.kt delete mode 100644 src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/VulnerabilityDescriptionPanel.kt delete mode 100644 src/main/kotlin/snyk/oss/OssGroupedResult.kt delete mode 100644 src/main/kotlin/snyk/oss/OssResult.kt delete mode 100644 src/main/kotlin/snyk/oss/OssService.kt delete mode 100644 src/main/kotlin/snyk/oss/OssTextRangeFinder.kt delete mode 100644 src/main/kotlin/snyk/oss/OssVulnerabilitiesForFile.kt delete mode 100644 src/main/kotlin/snyk/oss/Vulnerability.kt delete mode 100644 src/main/kotlin/snyk/oss/annotator/AnnotatorHelper.kt delete mode 100644 src/main/kotlin/snyk/oss/annotator/OSSBaseAnnotator.kt delete mode 100644 src/main/kotlin/snyk/oss/annotator/OSSGoModAnnotator.kt delete mode 100644 src/main/kotlin/snyk/oss/annotator/OSSGradleAnnotator.kt delete mode 100644 src/main/kotlin/snyk/oss/annotator/OSSMavenAnnotator.kt delete mode 100644 src/main/kotlin/snyk/oss/annotator/OSSNpmAnnotator.kt delete mode 100644 src/test/kotlin/io/snyk/plugin/ui/toolwindow/VulnerabilityDescriptionPanelTest.kt delete mode 100644 src/test/kotlin/snyk/oss/OssBulkFileListenerTest.kt diff --git a/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt b/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt index 63625c891..07538f7cf 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt @@ -81,7 +81,7 @@ class SnykPostStartupActivity : ProjectActivity { settings.lastTimeFeedbackRequestShown = Date.from(Instant.now()) } - if (isContainerEnabled()) { + if (settings.containerScanEnabled) { getKubernetesImageCache(project)?.cacheKubernetesFileFromProject() } diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index f7cada5e8..4cbdc2e06 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -54,8 +54,6 @@ import snyk.container.ContainerService import snyk.container.KubernetesImageCache import snyk.errorHandler.SentryErrorReporter import snyk.iac.IacScanService -import snyk.oss.OssService -import snyk.oss.OssTextRangeFinder import java.io.File import java.io.FileNotFoundException import java.net.URI @@ -72,8 +70,6 @@ import javax.swing.JComponent private val logger = Logger.getInstance("#io.snyk.plugin.UtilsKt") -fun getOssService(project: Project): OssService? = project.serviceIfNotDisposed() - fun getIacService(project: Project): IacScanService? = project.serviceIfNotDisposed() fun getKubernetesImageCache(project: Project): KubernetesImageCache? = project.serviceIfNotDisposed() @@ -110,8 +106,6 @@ fun isCliInstalled(): Boolean = ApplicationManager.getApplication().isUnitTestMo fun pluginSettings(): SnykApplicationSettingsStateService = getApplicationService() -fun getOssTextRangeFinderService(): OssTextRangeFinder = getApplicationService() - fun getPluginPath() = PathManager.getPluginsPath() + "/snyk-intellij-plugin" fun isProjectSettingsAvailable(project: Project?) = nonNull(project) && !project!!.isDefault @@ -224,14 +218,8 @@ fun controlExternalProcessWithProgressIndicator( checkCancelled.invoke() } -fun isIacEnabled(): Boolean = true - -fun isContainerEnabled(): Boolean = true - fun isFileListenerEnabled(): Boolean = pluginSettings().fileListenerEnabled -fun isSnykOSSLSEnabled(): Boolean = true // TODO: cleanup usage - fun isSnykIaCLSEnabled(): Boolean = false diff --git a/src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt b/src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt index 965cd489d..383c0b86f 100644 --- a/src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt +++ b/src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt @@ -9,7 +9,6 @@ import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.commands.ScanDoneEvent import snyk.container.ContainerResult import snyk.iac.IacResult -import snyk.oss.OssResult @Service(Service.Level.PROJECT) class AnalyticsScanListener(val project: Project) { @@ -45,18 +44,6 @@ class AnalyticsScanListener(val project: Project) { start = System.currentTimeMillis() } - override fun scanningOssFinished(ossResult: OssResult) { - val scanDoneEvent = getScanDoneEvent( - System.currentTimeMillis() - start, - "Snyk Open Source", - ossResult.criticalSeveritiesCount(), - ossResult.highSeveritiesCount(), - ossResult.mediumSeveritiesCount(), - ossResult.lowSeveritiesCount() - ) - LanguageServerWrapper.getInstance().sendReportAnalyticsCommand(scanDoneEvent) - } - override fun scanningIacFinished(iacResult: IacResult) { val scanDoneEvent = getScanDoneEvent( System.currentTimeMillis() - start, @@ -81,10 +68,6 @@ class AnalyticsScanListener(val project: Project) { LanguageServerWrapper.getInstance().sendReportAnalyticsCommand(scanDoneEvent) } - override fun scanningOssError(snykError: SnykError) { - // do nothing - } - override fun scanningIacError(snykError: SnykError) { // do nothing } diff --git a/src/main/kotlin/io/snyk/plugin/events/SnykScanListener.kt b/src/main/kotlin/io/snyk/plugin/events/SnykScanListener.kt index bd3c5d12b..51c37e191 100644 --- a/src/main/kotlin/io/snyk/plugin/events/SnykScanListener.kt +++ b/src/main/kotlin/io/snyk/plugin/events/SnykScanListener.kt @@ -4,7 +4,6 @@ import com.intellij.util.messages.Topic import snyk.common.SnykError import snyk.container.ContainerResult import snyk.iac.IacResult -import snyk.oss.OssResult interface SnykScanListener { companion object { @@ -14,12 +13,8 @@ interface SnykScanListener { fun scanningStarted() - fun scanningOssFinished(ossResult: OssResult) - fun scanningIacFinished(iacResult: IacResult) - fun scanningOssError(snykError: SnykError) - fun scanningIacError(snykError: SnykError) fun scanningContainerFinished(containerResult: ContainerResult) diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt index 5875faf55..f8b0e1766 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt @@ -16,19 +16,15 @@ import io.snyk.plugin.events.SnykSettingsListener import io.snyk.plugin.events.SnykTaskQueueListener import io.snyk.plugin.getContainerService import io.snyk.plugin.getIacService -import io.snyk.plugin.getOssService import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.getSnykCliDownloaderService import io.snyk.plugin.getSnykToolWindowPanel import io.snyk.plugin.getSyncPublisher import io.snyk.plugin.isCliDownloading import io.snyk.plugin.isCliInstalled -import io.snyk.plugin.isContainerEnabled -import io.snyk.plugin.isIacEnabled import io.snyk.plugin.isOssRunning import io.snyk.plugin.isSnykCodeRunning import io.snyk.plugin.isSnykIaCLSEnabled -import io.snyk.plugin.isSnykOSSLSEnabled import io.snyk.plugin.pluginSettings import io.snyk.plugin.refreshAnnotationsForOpenFiles import io.snyk.plugin.ui.SnykBalloonNotificationHelper @@ -69,29 +65,29 @@ class SnykTaskQueueService(val project: Project) { fun getTaskQueue() = taskQueue fun connectProjectToLanguageServer(project: Project) { - // subscribe to the settings changed topic + // subscribe to the settings changed topic val languageServerWrapper = LanguageServerWrapper.getInstance() getSnykToolWindowPanel(project)?.let { - project.messageBus.connect(it) - .subscribe( - SnykSettingsListener.SNYK_SETTINGS_TOPIC, - object : SnykSettingsListener { - override fun settingsChanged() { - languageServerWrapper.updateConfiguration() - } + project.messageBus.connect(it) + .subscribe( + SnykSettingsListener.SNYK_SETTINGS_TOPIC, + object : SnykSettingsListener { + override fun settingsChanged() { + languageServerWrapper.updateConfiguration() } - ) + } + ) + } + // Try to connect project for up to 30s + for (tries in 1..300) { + if (!languageServerWrapper.isInitialized) { + Thread.sleep(100) + continue } - // Try to connect project for up to 30s - for (tries in 1..300) { - if (!languageServerWrapper.isInitialized) { - Thread.sleep(100) - continue - } - languageServerWrapper.addContentRoots(project) - break - } + languageServerWrapper.addContentRoots(project) + break + } } fun scan(isStartup: Boolean) { @@ -106,22 +102,16 @@ class SnykTaskQueueService(val project: Project) { waitUntilCliDownloadedIfNeeded() indicator.checkCanceled() - if (settings.snykCodeSecurityIssuesScanEnable || settings.snykCodeQualityIssuesScanEnable || isSnykOSSLSEnabled()) { - if (!isStartup) { - LanguageServerWrapper.getInstance().sendScanCommand(project) - } - } - if (settings.ossScanEnable) { - if (!isSnykOSSLSEnabled()) { - scheduleOssScan() - } + if (!isStartup) { + LanguageServerWrapper.getInstance().sendScanCommand(project) } - if (isIacEnabled() && settings.iacScanEnabled) { + + if (settings.iacScanEnabled) { if (!isSnykIaCLSEnabled()) { scheduleIacScan() } } - if (isContainerEnabled() && settings.containerScanEnabled) { + if (settings.containerScanEnabled) { scheduleContainerScan() } } @@ -173,37 +163,6 @@ class SnykTaskQueueService(val project: Project) { }) } - private fun scheduleOssScan() { - taskQueue.run(object : Task.Backgroundable(project, "Snyk Open Source is scanning", true) { - override fun run(indicator: ProgressIndicator) { - if (!isCliInstalled()) return - val snykCachedResults = getSnykCachedResults(project) ?: return - if (snykCachedResults.currentOssResults != null) return - - ossScanProgressIndicator = indicator - scanPublisher?.scanningStarted() - - val ossResult = try { - getOssService(project)?.scan() - } finally { - ossScanProgressIndicator = null - } - if (ossResult == null || project.isDisposed) return - - if (indicator.isCanceled) { - taskQueuePublisher?.stopped(wasOssRunning = true) - } else { - if (ossResult.isSuccessful()) { - scanPublisher?.scanningOssFinished(ossResult) - } else { - ossResult.getFirstError()?.let { scanPublisher?.scanningOssError(it) } - } - } - invokeLater { refreshAnnotationsForOpenFiles(project) } - } - }) - } - private fun scheduleIacScan() { taskQueueIac.run(object : Task.Backgroundable(project, "Snyk Infrastructure as Code is scanning", true) { override fun run(indicator: ProgressIndicator) { diff --git a/src/main/kotlin/io/snyk/plugin/ui/actions/SnykTreeScanTypeFilterActionGroup.kt b/src/main/kotlin/io/snyk/plugin/ui/actions/SnykTreeScanTypeFilterActionGroup.kt index f5a8cb526..4020382f1 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/actions/SnykTreeScanTypeFilterActionGroup.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/actions/SnykTreeScanTypeFilterActionGroup.kt @@ -7,8 +7,6 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.ToggleAction import io.snyk.plugin.events.SnykResultsFilteringListener import io.snyk.plugin.getSyncPublisher -import io.snyk.plugin.isContainerEnabled -import io.snyk.plugin.isIacEnabled import io.snyk.plugin.pluginSettings import io.snyk.plugin.showSettings import io.snyk.plugin.ui.SnykBalloonNotificationHelper @@ -36,8 +34,8 @@ class SnykTreeScanTypeFilterActionGroup : ActionGroup() { createOssScanAction(), createSecurityIssuesScanAction(), createQualityIssuesScanAction(), - if (isIacEnabled()) createIacScanAction() else null, - if (isContainerEnabled()) createContainerScanAction() else null + createIacScanAction(), + createContainerScanAction() ).toTypedArray() private fun createScanFilteringAction( diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt index a5fc763cd..0b346ad39 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt @@ -13,14 +13,17 @@ import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.ui.SimpleToolWindowPanel import com.intellij.ui.PopupHandler +import io.snyk.plugin.SnykFile import io.snyk.plugin.events.SnykScanListener +import io.snyk.plugin.events.SnykScanListenerLS import io.snyk.plugin.events.SnykTaskQueueListener import io.snyk.plugin.getSnykToolWindowPanel import io.snyk.plugin.ui.expandTreeNodeRecursively import snyk.common.SnykError +import snyk.common.lsp.ScanIssue +import snyk.common.lsp.SnykScanParams import snyk.container.ContainerResult import snyk.iac.IacResult -import snyk.oss.OssResult import javax.swing.JTree import javax.swing.tree.DefaultMutableTreeNode @@ -69,12 +72,8 @@ class SnykToolWindow(private val project: Project) : SimpleToolWindowPanel(false override fun scanningStarted() = updateActionsPresentation() - override fun scanningOssFinished(ossResult: OssResult) = updateActionsPresentation() - override fun scanningIacFinished(iacResult: IacResult) = updateActionsPresentation() - override fun scanningOssError(snykError: SnykError) = updateActionsPresentation() - override fun scanningIacError(snykError: SnykError) = updateActionsPresentation() override fun scanningContainerFinished(containerResult: ContainerResult) = updateActionsPresentation() @@ -82,6 +81,25 @@ class SnykToolWindow(private val project: Project) : SimpleToolWindowPanel(false override fun scanningContainerError(snykError: SnykError) = updateActionsPresentation() }) + project.messageBus.connect(this) + .subscribe(SnykScanListenerLS.SNYK_SCAN_TOPIC, object : SnykScanListenerLS { + override fun scanningSnykCodeFinished() { + updateActionsPresentation() + } + + override fun scanningOssFinished() { + updateActionsPresentation() + } + + override fun scanningError(snykScan: SnykScanParams) { + updateActionsPresentation() + } + + override fun onPublishDiagnostics(product: String, snykFile: SnykFile, issueList: List) = + Unit + }) + + project.messageBus.connect(this) .subscribe(SnykTaskQueueListener.TASK_QUEUE_TOPIC, object : SnykTaskQueueListener { override fun stopped( diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt index b72f055e4..c82ac0c9e 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -3,15 +3,12 @@ package io.snyk.plugin.ui.toolwindow import com.intellij.notification.NotificationAction import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.ReadAction import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.openapi.ui.SimpleToolWindowPanel import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.psi.PsiManager import com.intellij.ui.OnePixelSplitter import com.intellij.ui.TreeSpeedSearch import com.intellij.ui.TreeUIHelper @@ -27,21 +24,17 @@ import io.snyk.plugin.events.SnykScanListenerLS import io.snyk.plugin.events.SnykSettingsListener import io.snyk.plugin.events.SnykTaskQueueListener import io.snyk.plugin.getKubernetesImageCache -import io.snyk.plugin.getOssTextRangeFinderService import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.getSnykCachedResultsForProduct import io.snyk.plugin.getSnykCliDownloaderService import io.snyk.plugin.getSnykTaskQueueService import io.snyk.plugin.head import io.snyk.plugin.isCliDownloading -import io.snyk.plugin.isContainerEnabled import io.snyk.plugin.isContainerRunning -import io.snyk.plugin.isIacEnabled import io.snyk.plugin.isIacRunning import io.snyk.plugin.isOssRunning import io.snyk.plugin.isScanRunning import io.snyk.plugin.isSnykCodeRunning -import io.snyk.plugin.isSnykOSSLSEnabled import io.snyk.plugin.navigateToSource import io.snyk.plugin.pluginSettings import io.snyk.plugin.refreshAnnotationsForOpenFiles @@ -53,7 +46,6 @@ import io.snyk.plugin.ui.toolwindow.nodes.DescriptionHolderTreeNode import io.snyk.plugin.ui.toolwindow.nodes.ErrorHolderTreeNode import io.snyk.plugin.ui.toolwindow.nodes.NavigatableToSourceTreeNode import io.snyk.plugin.ui.toolwindow.nodes.leaf.SuggestionTreeNode -import io.snyk.plugin.ui.toolwindow.nodes.leaf.VulnerabilityTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootContainerIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootIacIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode @@ -61,7 +53,6 @@ import io.snyk.plugin.ui.toolwindow.nodes.root.RootQualityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootTreeNodeBase import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ErrorTreeNode -import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.FileTreeNode import io.snyk.plugin.ui.toolwindow.panels.IssueDescriptionPanel import io.snyk.plugin.ui.toolwindow.panels.SnykAuthPanel import io.snyk.plugin.ui.toolwindow.panels.SnykErrorPanel @@ -69,7 +60,6 @@ import io.snyk.plugin.ui.toolwindow.panels.StatePanel import io.snyk.plugin.ui.toolwindow.panels.TreePanel import io.snyk.plugin.ui.wrapWithScrollPane import org.jetbrains.annotations.TestOnly -import org.jetbrains.concurrency.runAsync import snyk.common.ProductType import snyk.common.SnykError import snyk.common.lsp.LanguageServerWrapper @@ -84,12 +74,7 @@ import snyk.iac.IacResult import snyk.iac.ignorableErrorCodes import snyk.iac.ui.toolwindow.IacFileTreeNode import snyk.iac.ui.toolwindow.IacIssueTreeNode -import snyk.oss.OssResult -import snyk.oss.OssVulnerabilitiesForFile -import snyk.oss.Vulnerability import java.awt.BorderLayout -import java.nio.file.InvalidPathException -import java.nio.file.Paths import java.util.Objects.nonNull import javax.swing.JPanel import javax.swing.JScrollPane @@ -118,8 +103,8 @@ class SnykToolWindowPanel( rootTreeNode.add(rootOssTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) - if (isIacEnabled()) rootTreeNode.add(rootIacIssuesTreeNode) - if (isContainerEnabled()) rootTreeNode.add(rootContainerIssuesTreeNode) + rootTreeNode.add(rootIacIssuesTreeNode) + rootTreeNode.add(rootContainerIssuesTreeNode) Tree(rootTreeNode).apply { this.isRootVisible = false } @@ -187,14 +172,6 @@ class SnykToolWindowPanel( } } - override fun scanningOssFinished(ossResult: OssResult) { - ApplicationManager.getApplication().invokeLater { - displayOssResults(ossResult) - notifyAboutErrorsIfNeeded(ProductType.OSS, ossResult) - refreshAnnotationsForOpenFiles(project) - } - } - override fun scanningIacFinished(iacResult: IacResult) { ApplicationManager.getApplication().invokeLater { displayIacResults(iacResult) @@ -231,26 +208,6 @@ class SnykToolWindowPanel( } } - override fun scanningOssError(snykError: SnykError) { - var ossResultsCount: Int? = null - ApplicationManager.getApplication().invokeLater { - if (snykError.message.contains(NO_OSS_FILES)) { - rootOssTreeNode.originalCliErrorMessage = snykError.message - ossResultsCount = NODE_NOT_SUPPORTED_STATE - } else { - rootOssTreeNode.originalCliErrorMessage = null - SnykBalloonNotificationHelper.showError(snykError.message, project) - if (snykError.message.startsWith(AUTH_FAILED_TEXT)) { - pluginSettings().token = null - } - } - removeAllChildren(listOf(rootOssTreeNode)) - updateTreeRootNodesPresentation(ossResultsCount = ossResultsCount) - chooseMainPanelToDisplay() - refreshAnnotationsForOpenFiles(project) - } - } - override fun scanningIacError(snykError: SnykError) { var iacResultsCount: Int? = null ApplicationManager.getApplication().invokeLater { @@ -300,18 +257,13 @@ class SnykToolWindowPanel( getSnykCachedResultsForProduct(project, ProductType.CODE_SECURITY) ?: return@invokeLater scanListenerLS.displaySnykCodeResults(codeResultsLS) } - if (!isSnykOSSLSEnabled()) { - ApplicationManager.getApplication().invokeLater { - val snykCachedResults = getSnykCachedResults(project) ?: return@invokeLater - snykCachedResults.currentOssResults?.let { displayOssResults(it) } - } - } else { - ApplicationManager.getApplication().invokeLater { - val ossResultsLS = - getSnykCachedResultsForProduct(project, ProductType.OSS) ?: return@invokeLater - scanListenerLS.displayOssResults(ossResultsLS) - } + + ApplicationManager.getApplication().invokeLater { + val ossResultsLS = + getSnykCachedResultsForProduct(project, ProductType.OSS) ?: return@invokeLater + scanListenerLS.displayOssResults(ossResultsLS) } + ApplicationManager.getApplication().invokeLater { val snykCachedResults = getSnykCachedResults(project) ?: return@invokeLater snykCachedResults.currentIacResult?.let { displayIacResults(it) } @@ -423,11 +375,9 @@ class SnykToolWindowPanel( getSnykCachedResults(project)?.cleanCaches() rootOssTreeNode.originalCliErrorMessage = null - if (isContainerEnabled()) { - getKubernetesImageCache(project)?.let { - it.clear() - it.cacheKubernetesFileFromProject() - } + getKubernetesImageCache(project)?.let { + it.clear() + it.cacheKubernetesFileFromProject() } ApplicationManager.getApplication().invokeLater { @@ -684,6 +634,7 @@ class SnykToolWindowPanel( count == 0 -> { NO_ISSUES_FOUND_TEXT } + count > 0 -> ProductType.OSS.getCountText(count, isUniqueCount = true) + addHMLPostfix count == NODE_NOT_SUPPORTED_STATE -> NO_SUPPORTED_PACKAGE_MANAGER_FOUND else -> throw IllegalStateException("ResultsCount is not meaningful") @@ -743,95 +694,6 @@ class SnykToolWindowPanel( revalidate() } - private fun displayOssResults(ossResult: OssResult) { - val userObjectsForExpandedChildren = userObjectsForExpandedNodes(rootOssTreeNode) - val selectedNodeUserObject = TreeUtil.findObjectInPath(vulnerabilitiesTree.selectionPath, Any::class.java) - - rootOssTreeNode.removeAllChildren() - - fun navigateToOssVulnerability( - filePath: String, - vulnerability: Vulnerability?, - ): () -> Unit = - { - runAsync { - var virtualFile: VirtualFile? = null - ReadAction.run { - virtualFile = VirtualFileManager.getInstance().findFileByNioPath(Paths.get(filePath)) - } - val vf = virtualFile - if (vf == null || !vf.isValid) { - return@runAsync - } - - if (vulnerability == null) { - navigateToSource(project, vf, 0) - } else { - ReadAction.run { - val psiFile = PsiManager.getInstance(project).findFile(vf) - val textRange = - psiFile?.let { getOssTextRangeFinderService().findTextRange(it, vulnerability) } - navigateToSource( - project = project, - virtualFile = vf, - selectionStartOffset = textRange?.startOffset ?: 0, - selectionEndOffset = textRange?.endOffset, - ) - } - } - } - } - - val settings = pluginSettings() - if (settings.ossScanEnable && settings.treeFiltering.ossResults) { - ossResult.allCliIssues?.forEach { vulnsForFile -> - if (vulnsForFile.vulnerabilities.isNotEmpty()) { - val ossGroupedResult = vulnsForFile.toGroupedResult() - val fileTreeNode = FileTreeNode(vulnsForFile, project) - rootOssTreeNode.add(fileTreeNode) - - ossGroupedResult.id2vulnerabilities.values - .filter { settings.hasSeverityEnabledAndFiltered(it.head.getSeverity()) } - .sortedByDescending { it.head.getSeverity() } - .forEach { - val navigateToSource = - try { - val filePath = sanitizeNavigationalFilePath(vulnsForFile) - navigateToOssVulnerability(filePath, it.head) - } catch (ignore: InvalidPathException) { - // empty navigation function for invalid path - {} - } - fileTreeNode.add(VulnerabilityTreeNode(it, project, navigateToSource)) - } - } - } - ossResult.errors.forEach { snykError -> - rootOssTreeNode.add( - ErrorTreeNode(snykError, project, navigateToOssVulnerability(snykError.path, null)), - ) - } - } - updateTreeRootNodesPresentation( - ossResultsCount = ossResult.issuesCount, - addHMLPostfix = buildHMLpostfix(ossResult), - ) - - smartReloadRootNode(rootOssTreeNode, userObjectsForExpandedChildren, selectedNodeUserObject) - } - - fun sanitizeNavigationalFilePath(vulnsForFile: OssVulnerabilitiesForFile): String { - val dirPath = vulnsForFile.path - val targetFilePath = vulnsForFile.sanitizedTargetFile - val filePath = - if (Paths.get(targetFilePath).isAbsolute) { - targetFilePath - } else { - Paths.get(dirPath, targetFilePath).toString() - } - return filePath - } - fun displayIacResults(iacResult: IacResult) { val userObjectsForExpandedChildren = userObjectsForExpandedNodes(rootIacIssuesTreeNode) val selectedNodeUserObject = TreeUtil.findObjectInPath(vulnerabilitiesTree.selectionPath, Any::class.java) @@ -1058,15 +920,6 @@ class SnykToolWindowPanel( } } - @Suppress("UNCHECKED_CAST") - fun selectNodeAndDisplayDescription(vulnerability: Vulnerability) = - selectAndDisplayNodeWithIssueDescription { treeNode -> - treeNode is VulnerabilityTreeNode && - (treeNode.userObject as Collection).any { - it == vulnerability - } - } - fun selectNodeAndDisplayDescription(iacIssue: IacIssue) = selectAndDisplayNodeWithIssueDescription { treeNode -> treeNode is IacIssueTreeNode && diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt index d2730a1cc..1faf59d85 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt @@ -13,14 +13,12 @@ import io.snyk.plugin.ui.PackageManagerIconProvider.Companion.getIcon import io.snyk.plugin.ui.getDisabledIcon import io.snyk.plugin.ui.snykCodeAvailabilityPostfix import io.snyk.plugin.ui.toolwindow.nodes.leaf.SuggestionTreeNode -import io.snyk.plugin.ui.toolwindow.nodes.leaf.VulnerabilityTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootContainerIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootIacIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootQualityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ErrorTreeNode -import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.FileTreeNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.InfoTreeNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.SnykFileTreeNode import snyk.common.ProductType @@ -34,8 +32,6 @@ import snyk.iac.IacIssue import snyk.iac.IacIssuesForFile import snyk.iac.ui.toolwindow.IacFileTreeNode import snyk.iac.ui.toolwindow.IacIssueTreeNode -import snyk.oss.OssVulnerabilitiesForFile -import snyk.oss.Vulnerability import java.util.Locale import javax.swing.Icon import javax.swing.JTree @@ -58,46 +54,6 @@ class SnykTreeCellRenderer : ColoredTreeCellRenderer() { var text: String? = null var attributes = SimpleTextAttributes.REGULAR_ATTRIBUTES when (value) { - is VulnerabilityTreeNode -> { - val vulnerability = (value.userObject as Collection).first() - nodeIcon = SnykIcons.getSeverityIcon(vulnerability.getSeverity()) - text = vulnerability.getPackageNameTitle() - - val snykCachedResults = getSnykCachedResults(value.project) - if (snykCachedResults?.currentOssResults == null) { - attributes = SimpleTextAttributes.GRAYED_ATTRIBUTES - nodeIcon = getDisabledIcon(nodeIcon) - } - } - - is FileTreeNode -> { - val fileVulns = value.userObject as OssVulnerabilitiesForFile - nodeIcon = getIcon(fileVulns.packageManager.lowercase(Locale.getDefault())) - toolTipText = - buildString { - append(fileVulns.relativePath ?: "") - append(fileVulns.sanitizedTargetFile) - append(ProductType.OSS.getCountText(value.childCount)) - } - text = - toolTipText.apply { - if (toolTipText.length > MAX_FILE_TREE_NODE_LENGTH) { - "..." + - this.substring( - this.length - MAX_FILE_TREE_NODE_LENGTH, - this.length, - ) - } - } - - val snykCachedResults = getSnykCachedResults(value.project) - if (snykCachedResults?.currentOssResults == null) { - attributes = SimpleTextAttributes.GRAYED_ATTRIBUTES - text += OBSOLETE_SUFFIX - nodeIcon = getDisabledIcon(nodeIcon) - } - } - is SuggestionTreeNode -> { val issue = value.userObject as ScanIssue nodeIcon = SnykIcons.getSeverityIcon(issue.getSeverityAsEnum()) diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/leaf/VulnerabilityTreeNode.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/leaf/VulnerabilityTreeNode.kt deleted file mode 100644 index 6e7891dd1..000000000 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/leaf/VulnerabilityTreeNode.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.snyk.plugin.ui.toolwindow.nodes.leaf - -import com.intellij.openapi.project.Project -import io.snyk.plugin.ui.toolwindow.nodes.DescriptionHolderTreeNode -import io.snyk.plugin.ui.toolwindow.nodes.NavigatableToSourceTreeNode -import io.snyk.plugin.ui.toolwindow.panels.VulnerabilityDescriptionPanel -import snyk.oss.Vulnerability -import javax.swing.tree.DefaultMutableTreeNode - -class VulnerabilityTreeNode( - private val groupedVulns: Collection, - val project: Project, - override val navigateToSource: () -> Unit -) : DefaultMutableTreeNode(groupedVulns), NavigatableToSourceTreeNode, DescriptionHolderTreeNode { - - override fun getDescriptionPanel(): VulnerabilityDescriptionPanel { - return VulnerabilityDescriptionPanel(groupedVulns) - } -} diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/secondlevel/FileTreeNode.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/secondlevel/FileTreeNode.kt deleted file mode 100644 index b84f4c002..000000000 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/secondlevel/FileTreeNode.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.snyk.plugin.ui.toolwindow.nodes.secondlevel - -import com.intellij.openapi.project.Project -import snyk.oss.OssVulnerabilitiesForFile -import javax.swing.tree.DefaultMutableTreeNode - -class FileTreeNode( - ossVulnerabilitiesForFile: OssVulnerabilitiesForFile, - val project: Project -) : DefaultMutableTreeNode(ossVulnerabilitiesForFile) diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/VulnerabilityDescriptionPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/VulnerabilityDescriptionPanel.kt deleted file mode 100644 index b2d49a7cb..000000000 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/VulnerabilityDescriptionPanel.kt +++ /dev/null @@ -1,247 +0,0 @@ -package io.snyk.plugin.ui.toolwindow.panels - -import com.intellij.ui.components.ActionLink -import com.intellij.uiDesigner.core.GridLayoutManager -import com.intellij.util.ui.JBUI -import io.snyk.plugin.ui.DescriptionHeaderPanel -import io.snyk.plugin.ui.addRowOfItemsToPanel -import io.snyk.plugin.ui.baseGridConstraintsAnchorWest -import io.snyk.plugin.ui.boldLabel -import io.snyk.plugin.ui.descriptionHeaderPanel -import io.snyk.plugin.ui.getReadOnlyClickableHtmlJEditorPane -import io.snyk.plugin.ui.insertTitleAndResizableTextIntoPanelColumns -import io.snyk.plugin.ui.panelGridConstraints -import io.snyk.plugin.ui.toolwindow.LabelProvider -import org.commonmark.parser.Parser -import org.commonmark.renderer.html.HtmlRenderer -import snyk.oss.Vulnerability -import javax.swing.JLabel -import javax.swing.JPanel - -class VulnerabilityDescriptionPanel( - private val groupedVulns: Collection -) : IssueDescriptionPanelBase( - title = groupedVulns.first().title, - severity = groupedVulns.first().getSeverity() -) { - - private val labelProvider: LabelProvider = LabelProvider() - private val vulnerability = groupedVulns.first() - - init { - createUI() - } - - override fun createMainBodyPanel(): Pair { - val lastRowToAddSpacer = 4 - val panel = JPanel( - GridLayoutManager(lastRowToAddSpacer + 1, 1, JBUI.insets(0, 10, 20, 20), -1, 10) - ).apply { - this.add( - getMainBodyPanel(), - baseGridConstraintsAnchorWest(1, indent = 0) - ) - this.add( - getDetailedPathsPanel(), - panelGridConstraints(2) - ) - this.add( - getOverviewPanel(), - panelGridConstraints(3) - ) - } - return Pair(panel, lastRowToAddSpacer) - } - - private fun getMainBodyPanel(): JPanel { - val panel = JPanel() - panel.layout = GridLayoutManager(11, 2, JBUI.insets(10, 0, 20, 0), 30, -1) - - panel.add( - boldLabel("Vulnerable module:"), - baseGridConstraintsAnchorWest(2, 0) - ) - panel.add( - JLabel(vulnerability.name)/*.apply { this.horizontalAlignment = SwingConstants.LEFT }*/, - baseGridConstraintsAnchorWest(2, 1, indent = 0) - ) - - val introducedThroughListPanel = getIntroducedThroughListPanel() - if (introducedThroughListPanel != null) { - panel.add( - boldLabel("Introduced through:"), - baseGridConstraintsAnchorWest(3, 0) - ) - panel.add( - introducedThroughListPanel, - baseGridConstraintsAnchorWest(3, 1, indent = 0) - ) - } - - val fixedInText = vulnerability.fixedIn?.let { - if (it.isNotEmpty()) { - it.joinToString(prefix = "${vulnerability.name}@", separator = ", @") - } else "Not fixed" - } - if (fixedInText != null) { - panel.add( - boldLabel("Fixed in:"), - baseGridConstraintsAnchorWest(4, 0) - ) - panel.add( - JLabel(fixedInText), - baseGridConstraintsAnchorWest(4, 1, indent = 0) - ) - } - - val exploit = vulnerability.exploit - if (exploit != null) { - panel.add( - boldLabel("Exploit maturity:"), - baseGridConstraintsAnchorWest(5, 0) - ) - panel.add( - JLabel(exploit), - baseGridConstraintsAnchorWest(5, 1, indent = 0) - ) - } - - return panel - } - - private fun getIntroducedThroughListPanel(): JPanel? { - val intros = groupedVulns - .mapNotNull { vulnerability -> - vulnerability.from.let { if (it.size > 1) it[1] else null } - } - .distinct() - - if (intros.isEmpty()) return null - - val panel = JPanel() - val packageManager = groupedVulns.first().packageManager - - panel.layout = GridLayoutManager(1, intros.size * 2, JBUI.emptyInsets(), 0, 0) - - addRowOfItemsToPanel( - panel = panel, - startingColumn = 0, - items = intros.map { item -> labelProvider.getDependencyLabel(packageManager, item) }, - separator = ", ", - firstSeparator = false, - opaqueSeparator = false - ) - return panel - } - - private fun getDetailedPathsPanel(): JPanel { - val detailsPanel = JPanel() - detailsPanel.layout = GridLayoutManager(2, 2, JBUI.emptyInsets(), -1, -1) - - detailsPanel.add( - boldLabel("Detailed paths").apply { - font = font.deriveFont(14f) - }, - baseGridConstraintsAnchorWest( - row = 0 - ) - ) - - detailsPanel.add( - getInnerDetailedPathsPanel(3), - panelGridConstraints( - row = 1 - ) - ) - - return detailsPanel - } - - private fun getInnerDetailedPathsPanel(itemsToShow: Int? = null): JPanel { - val detailsPanel = JPanel() - detailsPanel.layout = GridLayoutManager(groupedVulns.size + 2, 2, JBUI.emptyInsets(), -1, -1) - - groupedVulns - .take(itemsToShow ?: groupedVulns.size) - .forEachIndexed { index, vuln -> - val detailPanel = JPanel() - detailPanel.layout = GridLayoutManager(2, 2, JBUI.emptyInsets(), 30, 0) - - insertTitleAndResizableTextIntoPanelColumns( - panel = detailPanel, - row = 0, - title = "Introduced through:", - htmlText = vuln.from.joinToString(separator = " > ") - ) - - val remediationText = when { - vuln.upgradePath.isEmpty() || vuln.upgradePath.size < 2 -> "none" - else -> "Upgrade to " + vuln.upgradePath[1] - } - insertTitleAndResizableTextIntoPanelColumns( - panel = detailPanel, - row = 1, - title = "Fix:", - htmlText = remediationText - ) - - detailsPanel.add( - detailPanel, - panelGridConstraints( - row = index + 1 - ) - ) - } - - if (itemsToShow != null && itemsToShow < groupedVulns.size) { - val showMoreLabel = ActionLink("...and ${groupedVulns.size - itemsToShow} more") { - detailsPanel.removeAll() - detailsPanel.add( - getInnerDetailedPathsPanel(), - panelGridConstraints( - row = 0 - ) - ) - } - detailsPanel.add( - showMoreLabel, - baseGridConstraintsAnchorWest(groupedVulns.size + 1) - ) - } - - return detailsPanel - } - - private fun getOverviewPanel(): JPanel { - val overviewPanel = JPanel() - overviewPanel.layout = GridLayoutManager(2, 1, JBUI.insetsLeft(5), -1, 0) - - val descriptionPane = getReadOnlyClickableHtmlJEditorPane(getDescriptionAsHtml()) - - overviewPanel.add( - descriptionPane, - panelGridConstraints(1) - ) - return overviewPanel - } - - private fun getDescriptionAsHtml(): String { - val overviewMarkdownStr = vulnerability.description - - val parser = Parser.builder().build() - val document = parser.parse(overviewMarkdownStr) - - val renderer = HtmlRenderer.builder().escapeHtml(true).build() - - return renderer.render(document) - } - - override fun secondRowTitlePanel(): DescriptionHeaderPanel = descriptionHeaderPanel( - issueNaming = if (vulnerability.license == null) "Vulnerability" else "License", - cwes = vulnerability.identifiers?.cwe ?: emptyList(), - cves = vulnerability.identifiers?.cve ?: emptyList(), - cvssScore = vulnerability.cvssScore, - cvssV3 = vulnerability.cvssV3, - id = vulnerability.id - ) -} diff --git a/src/main/kotlin/snyk/code/annotator/SnykOSSAnnotatorLS.kt b/src/main/kotlin/snyk/code/annotator/SnykOSSAnnotatorLS.kt index 02156896b..fe6054190 100644 --- a/src/main/kotlin/snyk/code/annotator/SnykOSSAnnotatorLS.kt +++ b/src/main/kotlin/snyk/code/annotator/SnykOSSAnnotatorLS.kt @@ -4,7 +4,7 @@ import com.intellij.lang.annotation.AnnotationHolder import com.intellij.openapi.util.Disposer import com.intellij.psi.PsiFile import io.snyk.plugin.isOssRunning -import io.snyk.plugin.isSnykOSSLSEnabled +import io.snyk.plugin.pluginSettings import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import snyk.common.ProductType @@ -19,7 +19,7 @@ class SnykOSSAnnotatorLS : SnykAnnotator(product = ProductType.OSS) { holder: AnnotationHolder, ) { if (disposed) return - if (!isSnykOSSLSEnabled()) return + if (!pluginSettings().ossScanEnable) return if (isOssRunning(psiFile.project)) return super.apply(psiFile, annotationResult, holder) diff --git a/src/main/kotlin/snyk/common/SnykCachedResults.kt b/src/main/kotlin/snyk/common/SnykCachedResults.kt index 181173a06..3f1ceb591 100644 --- a/src/main/kotlin/snyk/common/SnykCachedResults.kt +++ b/src/main/kotlin/snyk/common/SnykCachedResults.kt @@ -19,7 +19,6 @@ import snyk.common.lsp.SnykScanParams import snyk.container.ContainerResult import snyk.container.ContainerService import snyk.iac.IacResult -import snyk.oss.OssResult @Service(Service.Level.PROJECT) class SnykCachedResults( @@ -43,8 +42,6 @@ class SnykCachedResults( val currentSnykCodeResultsLS: MutableMap> = mutableMapOf() val currentOSSResultsLS: MutableMap> = mutableMapOf() - var currentOssResults: OssResult? = null - get() = if (field?.isExpired() == false) field else null val currentContainerResultsLS: MutableMap> = mutableMapOf() var currentContainerResult: ContainerResult? = null @@ -60,7 +57,6 @@ class SnykCachedResults( var currentSnykCodeError: SnykError? = null fun cleanCaches() { - currentOssResults = null currentContainerResult = null currentIacResult = null currentOssError = null @@ -85,10 +81,6 @@ class SnykCachedResults( currentContainerError = null } - override fun scanningOssFinished(ossResult: OssResult) { - currentOssResults = ossResult - } - override fun scanningIacFinished(iacResult: IacResult) { currentIacResult = iacResult } @@ -97,16 +89,6 @@ class SnykCachedResults( currentContainerResult = containerResult } - override fun scanningOssError(snykError: SnykError) { - currentOssResults = null - currentOssError = - when { - snykError.message.startsWith(SnykToolWindowPanel.NO_OSS_FILES) -> null - snykError.message.startsWith(SnykToolWindowPanel.AUTH_FAILED_TEXT) -> null - else -> snykError - } - } - override fun scanningIacError(snykError: SnykError) { currentIacResult = null currentIacError = @@ -142,11 +124,9 @@ class SnykCachedResults( currentContainerError = null } - override fun scanningSnykCodeFinished() { - } + override fun scanningSnykCodeFinished() = Unit - override fun scanningOssFinished() { - } + override fun scanningOssFinished() = Unit override fun scanningError(snykScan: SnykScanParams) { when (snykScan.product) { diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 39a723749..c766b4069 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -16,10 +16,8 @@ import io.snyk.plugin.getContentRootVirtualFiles import io.snyk.plugin.getSnykTaskQueueService import io.snyk.plugin.getWaitForResultsTimeout import io.snyk.plugin.isSnykIaCLSEnabled -import io.snyk.plugin.isSnykOSSLSEnabled import io.snyk.plugin.pluginSettings import io.snyk.plugin.toLanguageServerURL -import io.snyk.plugin.toVirtualFile import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -163,7 +161,7 @@ class LanguageServerWrapper( fun shutdown(): Future<*> = executorService.submit { - if (process.isAlive) { + if (::process.isInitialized && process.isAlive) { languageServer.shutdown().get(1, TimeUnit.SECONDS) languageServer.exit() if (process.isAlive) { @@ -196,7 +194,13 @@ class LanguageServerWrapper( val normalizedRoots = mutableSetOf() for (root in contentRoots) { - val pathTrusted = trustService.isPathTrusted(root.toNioPath()) + val pathTrusted = try { + trustService.isPathTrusted(root.toNioPath()) + } catch (e: UnsupportedOperationException) { + // this must be temp filesystem so the path mapping doesn't work + continue + } + if (!pathTrusted) { logger.debug("Path not trusted: ${root.path}") continue @@ -366,7 +370,7 @@ class LanguageServerWrapper( "oauth" } return LanguageServerSettings( - activateSnykOpenSource = (isSnykOSSLSEnabled() && ps.ossScanEnable).toString(), + activateSnykOpenSource = ps.ossScanEnable.toString(), activateSnykCodeSecurity = ps.snykCodeSecurityIssuesScanEnable.toString(), activateSnykCodeQuality = ps.snykCodeQualityIssuesScanEnable.toString(), activateSnykIac = isSnykIaCLSEnabled().toString(), diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index 646885e80..04832ef75 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -27,7 +27,6 @@ import io.snyk.plugin.SnykFile import io.snyk.plugin.events.SnykScanListenerLS import io.snyk.plugin.getContentRootVirtualFiles import io.snyk.plugin.getSyncPublisher -import io.snyk.plugin.isSnykOSSLSEnabled import io.snyk.plugin.pluginSettings import io.snyk.plugin.refreshAnnotationsForOpenFiles import io.snyk.plugin.toVirtualFile @@ -206,9 +205,6 @@ class SnykLanguageClient : @JsonNotification(value = "$/snyk.scan") fun snykScan(snykScan: SnykScanParams) { if (disposed) return - if (snykScan.product == "oss" && !isSnykOSSLSEnabled()) { - return - } try { getScanPublishersFor(snykScan.folderPath).forEach { (_, scanPublisher) -> processSnykScan(snykScan, scanPublisher) diff --git a/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt b/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt index 032edf68f..ac28a3f7e 100644 --- a/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt +++ b/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt @@ -9,7 +9,6 @@ import io.snyk.plugin.findPsiFileIgnoringExceptions import io.snyk.plugin.getKubernetesImageCache import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.getSnykToolWindowPanel -import io.snyk.plugin.isContainerEnabled import io.snyk.plugin.refreshAnnotationsForOpenFiles class ContainerBulkFileListener : SnykBulkFileListener() { @@ -19,36 +18,32 @@ class ContainerBulkFileListener : SnykBulkFileListener() { override fun before(project: Project, virtualFilesAffected: Set) { if (virtualFilesAffected.isEmpty()) return // clean Container cached results for Container related deleted/moved/renamed files - if (isContainerEnabled()) { - val imageCache = getKubernetesImageCache(project) - val kubernetesWorkloadFilesFromCache = imageCache?.getKubernetesWorkloadFilesFromCache() ?: emptySet() - val containerRelatedVirtualFilesAffected = virtualFilesAffected.filter { - kubernetesWorkloadFilesFromCache.contains(it) - } - imageCache?.cleanCache(virtualFilesAffected) - updateContainerCache(containerRelatedVirtualFilesAffected, project) + val imageCache = getKubernetesImageCache(project) + val kubernetesWorkloadFilesFromCache = imageCache?.getKubernetesWorkloadFilesFromCache() ?: emptySet() + val containerRelatedVirtualFilesAffected = virtualFilesAffected.filter { + kubernetesWorkloadFilesFromCache.contains(it) } + imageCache?.cleanCache(virtualFilesAffected) + updateContainerCache(containerRelatedVirtualFilesAffected, project) } override fun after(project: Project, virtualFilesAffected: Set) { if (virtualFilesAffected.isEmpty()) return // update Container cached results for Container related files - if (isContainerEnabled()) { - getKubernetesImageCache(project)?.updateCache(virtualFilesAffected) - val containerRelatedVirtualFilesAffected = virtualFilesAffected.filter { virtualFile -> - val knownContainerIssues: List = - getSnykCachedResults(project)?.currentContainerResult?.allCliIssues ?: emptyList() - val containerFilesCached = knownContainerIssues - .flatMap { it.workloadImages } - .map { it.virtualFile } - // if file was cached before - we should update cache even if it's none k8s file anymore - if (containerFilesCached.contains(virtualFile) || isDotSnykFile(virtualFile)) return@filter true + getKubernetesImageCache(project)?.updateCache(virtualFilesAffected) + val containerRelatedVirtualFilesAffected = virtualFilesAffected.filter { virtualFile -> + val knownContainerIssues: List = + getSnykCachedResults(project)?.currentContainerResult?.allCliIssues ?: emptyList() + val containerFilesCached = knownContainerIssues + .flatMap { it.workloadImages } + .map { it.virtualFile } + // if file was cached before - we should update cache even if it's none k8s file anymore + if (containerFilesCached.contains(virtualFile) || isDotSnykFile(virtualFile)) return@filter true - val psiFile = findPsiFileIgnoringExceptions(virtualFile, project) ?: return@filter false - YAMLImageExtractor.isKubernetes(psiFile) - } - updateContainerCache(containerRelatedVirtualFilesAffected, project) + val psiFile = findPsiFileIgnoringExceptions(virtualFile, project) ?: return@filter false + YAMLImageExtractor.isKubernetes(psiFile) } + updateContainerCache(containerRelatedVirtualFilesAffected, project) } private fun isDotSnykFile(virtualFile: VirtualFile) = virtualFile.name.endsWith(".snyk") diff --git a/src/main/kotlin/snyk/oss/OssBulkFileListener.kt b/src/main/kotlin/snyk/oss/OssBulkFileListener.kt index 8fe9b6544..f6ef8d233 100644 --- a/src/main/kotlin/snyk/oss/OssBulkFileListener.kt +++ b/src/main/kotlin/snyk/oss/OssBulkFileListener.kt @@ -2,12 +2,10 @@ package snyk.oss import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.ide.impl.ProjectUtil -import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project -import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.vfs.newvfs.events.VFileEvent @@ -15,7 +13,6 @@ import com.intellij.openapi.vfs.readText import io.snyk.plugin.SnykBulkFileListener import io.snyk.plugin.SnykFile import io.snyk.plugin.getSnykCachedResults -import io.snyk.plugin.isSnykOSSLSEnabled import io.snyk.plugin.toLanguageServerURL import io.snyk.plugin.toSnykFileSet import org.eclipse.lsp4j.DidSaveTextDocumentParams @@ -23,33 +20,23 @@ import org.eclipse.lsp4j.TextDocumentIdentifier import snyk.common.lsp.LanguageServerWrapper class OssBulkFileListener : SnykBulkFileListener() { - private val log = logger() override fun before( project: Project, virtualFilesAffected: Set, - ) { - if (isSnykOSSLSEnabled()) { - return - } - dropOssCacheIfNeeded(project, virtualFilesAffected) - } + ) = Unit override fun after( project: Project, virtualFilesAffected: Set, ) { - if (isSnykOSSLSEnabled()) { - val filesAffected = toSnykFileSet(project, virtualFilesAffected) - updateCacheAndUI(filesAffected, project) - } - dropOssCacheIfNeeded(project, virtualFilesAffected) + val filesAffected = toSnykFileSet(project, virtualFilesAffected) + updateCacheAndUI(filesAffected, project) } override fun forwardEvents(events: MutableList) { val languageServerWrapper = LanguageServerWrapper.getInstance() - if (!isSnykOSSLSEnabled()) return if (!languageServerWrapper.isInitialized) return val languageServer = languageServerWrapper.languageServer @@ -75,23 +62,6 @@ class OssBulkFileListener : SnykBulkFileListener() { } } - private fun dropOssCacheIfNeeded( - project: Project, - virtualFilesAffected: Set, - ) { - val snykCachedResults = getSnykCachedResults(project) - if (snykCachedResults?.currentOssResults != null) { - val buildFileChanged = - virtualFilesAffected - .filter { scanInvalidatingFiles.contains(it.name) } - .find { ProjectRootManager.getInstance(project).fileIndex.isInContent(it) } - if (buildFileChanged != null) { - snykCachedResults.currentOssResults = null - log.debug("OSS cached results dropped due to changes in: $buildFileChanged") - } - } - } - private fun updateCacheAndUI( filesAffected: Set, project: Project, @@ -103,35 +73,4 @@ class OssBulkFileListener : SnykBulkFileListener() { VirtualFileManager.getInstance().asyncRefresh() DaemonCodeAnalyzer.getInstance(project).restart() } - - companion object { - // see https://github.com/snyk/snyk/blob/master/src/lib/detect.ts#L10 - private val scanInvalidatingFiles = - listOf( - "yarn.lock", - "package-lock.json", - "package.json", - "Gemfile", - "Gemfile.lock", - "pom.xml", - "build.gradle", - "build.gradle.kts", - "build.sbt", - "Pipfile", - "requirements.txt", - "Gopkg.lock", - "go.mod", - "vendor.json", - "project.assets.json", - "project.assets.json", - "packages.config", - "paket.dependencies", - "composer.lock", - "Podfile", - "Podfile.lock", - "pyproject.toml", - "poetry.lock", - ".snyk", - ) - } } diff --git a/src/main/kotlin/snyk/oss/OssGroupedResult.kt b/src/main/kotlin/snyk/oss/OssGroupedResult.kt deleted file mode 100644 index 0fd1b203d..000000000 --- a/src/main/kotlin/snyk/oss/OssGroupedResult.kt +++ /dev/null @@ -1,7 +0,0 @@ -package snyk.oss - -data class OssGroupedResult( - val id2vulnerabilities: Map>, - val uniqueCount: Int, - val pathsCount: Int -) diff --git a/src/main/kotlin/snyk/oss/OssResult.kt b/src/main/kotlin/snyk/oss/OssResult.kt deleted file mode 100644 index 15fcd4c54..000000000 --- a/src/main/kotlin/snyk/oss/OssResult.kt +++ /dev/null @@ -1,22 +0,0 @@ -package snyk.oss - -import io.snyk.plugin.Severity -import io.snyk.plugin.cli.CliResult -import snyk.common.SnykError - -class OssResult( - allOssVulnerabilities: List?, - errors: List = emptyList() -) : CliResult(allOssVulnerabilities, errors) { - - override val issuesCount = allOssVulnerabilities?.sumOf { it.uniqueCount } - - override fun countBySeverity(severity: Severity): Int? { - return allCliIssues?.sumOf { vulnerabilitiesForFile -> - vulnerabilitiesForFile.vulnerabilities - .filter { it.getSeverity() == severity } - .distinctBy { it.id } - .size - } - } -} diff --git a/src/main/kotlin/snyk/oss/OssService.kt b/src/main/kotlin/snyk/oss/OssService.kt deleted file mode 100644 index cb2ba2610..000000000 --- a/src/main/kotlin/snyk/oss/OssService.kt +++ /dev/null @@ -1,59 +0,0 @@ -package snyk.oss - -import com.intellij.openapi.components.Service -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.LocalFileSystem -import io.snyk.plugin.pluginSettings -import io.snyk.plugin.services.CliAdapter -import snyk.common.RelativePathHelper -import snyk.common.SnykError - -/** - * Wrap work with Snyk CLI for OSS (`test` command). - */ -@Service(Service.Level.PROJECT) -class OssService(project: Project) : CliAdapter(project) { - - fun scan(): OssResult = execute(listOf("test")) - - override fun getProductResult(cliIssues: List?, snykErrors: List): OssResult { - return OssResult(cliIssues, snykErrors) - } - - override fun sanitizeCliIssues(cliIssues: OssVulnerabilitiesForFile): OssVulnerabilitiesForFile { - // .copy() will check nullability of fields - val virtualFile = cliIssues.virtualFile ?: LocalFileSystem.getInstance().findFileByPath(cliIssues.path) - // determine relative path for each issue at scan time - return cliIssues.copy( - vulnerabilities = cliIssues.vulnerabilities.map { it.copy() }, - project = project, - virtualFile = virtualFile, - relativePath = virtualFile?.let { RelativePathHelper().getRelativePath(virtualFile, project) } - ) - } - - override fun getCliIIssuesClass(): Class = OssVulnerabilitiesForFile::class.java - - override fun buildExtraOptions(): List { - val settings = pluginSettings() - val options: MutableList = mutableListOf() - - options.add("--json") - - val additionalParameters = settings.getAdditionalParameters(project) - val hasAllProjectsParam = additionalParameters != null && additionalParameters.contains(ALL_PROJECTS_PARAM) - - if (!hasAllProjectsParam) { - options.add(ALL_PROJECTS_PARAM) - } - - if (additionalParameters != null && additionalParameters.trim().isNotEmpty()) { - options.addAll(additionalParameters.trim().split(" ")) - } - return options - } - - companion object { - const val ALL_PROJECTS_PARAM = "--all-projects" - } -} diff --git a/src/main/kotlin/snyk/oss/OssTextRangeFinder.kt b/src/main/kotlin/snyk/oss/OssTextRangeFinder.kt deleted file mode 100644 index 7cbdf16ec..000000000 --- a/src/main/kotlin/snyk/oss/OssTextRangeFinder.kt +++ /dev/null @@ -1,18 +0,0 @@ -package snyk.oss - -import com.intellij.openapi.components.Service -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiFile - -@Service -class OssTextRangeFinder { - private val availableFinders: MutableList<(psiFile: PsiFile, vulnerability: Vulnerability) -> TextRange> = mutableListOf() - - fun registerFinder(finder: (psiFile: PsiFile, vulnerability: Vulnerability) -> TextRange) { - availableFinders.add(finder) - } - - fun findTextRange(psiFile: PsiFile, vulnerability: Vulnerability): TextRange? = availableFinders.asSequence() - .map { it(psiFile, vulnerability) } - .firstOrNull { it != TextRange.EMPTY_RANGE } -} diff --git a/src/main/kotlin/snyk/oss/OssVulnerabilitiesForFile.kt b/src/main/kotlin/snyk/oss/OssVulnerabilitiesForFile.kt deleted file mode 100644 index 182189517..000000000 --- a/src/main/kotlin/snyk/oss/OssVulnerabilitiesForFile.kt +++ /dev/null @@ -1,29 +0,0 @@ -package snyk.oss - -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile - -data class OssVulnerabilitiesForFile( - val vulnerabilities: List, - private val displayTargetFile: String, - val packageManager: String, - val path: String, - val remediation: Remediation? = null, - val virtualFile: VirtualFile? = null, - val relativePath: String? = null, - val project: Project? = null -) { - val uniqueCount: Int get() = vulnerabilities.groupBy { it.id }.size - val sanitizedTargetFile: String get() = displayTargetFile.replace("-lock", "") - - fun toGroupedResult(): OssGroupedResult { - val id2vulnerabilities = vulnerabilities.groupBy({ it.id }, { it }) - val uniqueCount = id2vulnerabilities.keys.size - val pathsCount = id2vulnerabilities.values.flatten().size - - return OssGroupedResult(id2vulnerabilities, uniqueCount, pathsCount) - } - - data class Upgrade(val upgradeTo: String) - data class Remediation(val upgrade: Map) -} diff --git a/src/main/kotlin/snyk/oss/Vulnerability.kt b/src/main/kotlin/snyk/oss/Vulnerability.kt deleted file mode 100644 index e6a593e57..000000000 --- a/src/main/kotlin/snyk/oss/Vulnerability.kt +++ /dev/null @@ -1,41 +0,0 @@ -package snyk.oss - -import com.google.gson.annotations.SerializedName -import io.snyk.plugin.Severity - -data class Vulnerability( - val id: String, - val license: String? = null, - val identifiers: Identifiers? = null, - val title: String, - val description: String, - val language: String, - val packageManager: String, - val packageName: String, - private val severity: String, - val name: String, - val version: String, - val exploit: String? = null, - - @SerializedName("CVSSv3") - val cvssV3: String? = null, - val cvssScore: String? = null, - - val fixedIn: List? = null, - val from: List, - val upgradePath: List -) { - val obsolete: Boolean = false - val ignored: Boolean = false - - fun getPackageNameTitle(): String = "$packageName@$version: $title" - - fun getSeverity(): Severity = Severity.getFromName(severity) -} - -data class Identifiers ( - @SerializedName("CWE") - val cwe: List, - @SerializedName("CVE") - val cve: List -) diff --git a/src/main/kotlin/snyk/oss/annotator/AnnotatorHelper.kt b/src/main/kotlin/snyk/oss/annotator/AnnotatorHelper.kt deleted file mode 100644 index 00c01bfcc..000000000 --- a/src/main/kotlin/snyk/oss/annotator/AnnotatorHelper.kt +++ /dev/null @@ -1,34 +0,0 @@ -package snyk.oss.annotator - -import java.util.Locale - -object AnnotatorHelper { - - fun isFileSupported(filePath: String): Boolean = - listOf( - "arn.lock", - "package-lock.json", - "package.json", - "Gemfile", - "Gemfile.lock", - "pom.xml", - "build.gradle", - "build.gradle.kts", - "build.sbt", - "Pipfile", - "requirements.txt", - "Gopkg.lock", - "go.mod", - "vendor/vendor.json", - "obj/project.assets.json", - "project.assets.json", - "packages.config", - "paket.dependencies", - "composer.lock", - "Podfile", - "Podfile.lock", - "poetry.lock", - "mix.exs", - "mix.lock" - ).any { filePath.lowercase(Locale.getDefault()).endsWith(it) } -} diff --git a/src/main/kotlin/snyk/oss/annotator/OSSBaseAnnotator.kt b/src/main/kotlin/snyk/oss/annotator/OSSBaseAnnotator.kt deleted file mode 100644 index 0b8165592..000000000 --- a/src/main/kotlin/snyk/oss/annotator/OSSBaseAnnotator.kt +++ /dev/null @@ -1,174 +0,0 @@ -package snyk.oss.annotator - -import com.intellij.lang.annotation.AnnotationBuilder -import com.intellij.lang.annotation.AnnotationHolder -import com.intellij.lang.annotation.ExternalAnnotator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiFile -import io.snyk.plugin.Severity -import io.snyk.plugin.getSnykCachedResults -import io.snyk.plugin.isSnykOSSLSEnabled -import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel -import snyk.common.AnnotatorCommon -import snyk.common.intentionactions.AlwaysAvailableReplacementIntentionAction -import snyk.common.intentionactions.ShowDetailsIntentionActionBase -import snyk.oss.OssVulnerabilitiesForFile -import snyk.oss.Vulnerability -import kotlin.math.max - -abstract class OSSBaseAnnotator : ExternalAnnotator() { - // overrides needed for the Annotator to invoke apply(). We don't do anything here - override fun collectInformation(file: PsiFile): PsiFile = file - override fun doAnnotate(psiFile: PsiFile?) { - val filePath = psiFile?.virtualFile?.path ?: return - - if (AnnotatorHelper.isFileSupported(filePath)) { - AnnotatorCommon.prepareAnnotate(psiFile) - } - } - - override fun apply(psiFile: PsiFile, annotationResult: Unit, holder: AnnotationHolder) { - if (isSnykOSSLSEnabled()) return - - val issues = getIssuesForFile(psiFile) ?: return - - val filteredVulns = issues.vulnerabilities - .filter { AnnotatorCommon.isSeverityToShow(it.getSeverity()) } - .distinctBy { getIntroducingPackage(it) + it.id } - - filteredVulns.forEach { vulnerability -> - if (vulnerability.ignored || vulnerability.obsolete) return@forEach - val textRange = textRange(psiFile, vulnerability) - val highlightSeverity = vulnerability.getSeverity().getHighlightSeverity() - if (textRange != TextRange.EMPTY_RANGE) { - val annotationMessage = annotationMessage(vulnerability) - val annotationBuilder = - holder.newAnnotation(highlightSeverity, "Snyk: $annotationMessage").range(textRange) - val fixRange = fixRange(psiFile, vulnerability) - val fixVersion = getFixVersion(issues.remediation, vulnerability) - if (fixRange != TextRange.EMPTY_RANGE && fixVersion.isNotBlank()) { - addQuickFix(psiFile, vulnerability, annotationBuilder, fixRange, fixVersion) - } - annotationBuilder.withFix( - ShowDetailsIntentionAction(annotationMessage, vulnerability) - ) - annotationBuilder.create() - } - } - } - - fun getIssuesForFile(psiFile: PsiFile): OssVulnerabilitiesForFile? { - val ossResult = getSnykCachedResults(psiFile.project)?.currentOssResults ?: return null - val filePath = psiFile.virtualFile?.path ?: return null - if (!AnnotatorHelper.isFileSupported(filePath)) return null - - ProgressManager.checkCanceled() - - return ossResult.allCliIssues - ?.firstOrNull { filePath.endsWith(it.sanitizedTargetFile) } - } - - fun annotationMessage(vulnerability: Vulnerability): String = - "${vulnerability.title} in '${vulnerability.name}' id: ${vulnerability.id}" - - open fun addQuickFix( - psiFile: PsiFile, - vulnerability: Vulnerability, - annotationBuilder: AnnotationBuilder, - textRange: TextRange, - fixVersion: String - ) { - if (fixVersion.isNotBlank()) { - annotationBuilder.withFix( - AlwaysAvailableReplacementIntentionAction( - textRange, - fixVersion - ) - ) - } - } - - open fun getFixVersion(remediation: OssVulnerabilitiesForFile.Remediation?, vulnerability: Vulnerability): String { - val upgrade = getUpgradeProposal(vulnerability, remediation) - return upgrade?.upgradeTo?.split("@")?.get(1) ?: "" - } - - open fun getUpgradeProposal( - vulnerability: Vulnerability, - remediation: OssVulnerabilitiesForFile.Remediation? - ): OssVulnerabilitiesForFile.Upgrade? { - val upgradeKey = getIntroducingPackage(vulnerability) + "@" + getIntroducingPackageVersion(vulnerability) - return remediation?.upgrade?.get(upgradeKey) - } - - open fun textRange(psiFile: PsiFile, vulnerability: Vulnerability): TextRange { - val document = psiFile.viewProvider.document ?: return TextRange.EMPTY_RANGE - val lines = document.text.lines() - var lineStart = 0 - var lineEnd = 0 - var colStart = 0 - var colEnd = 0 - for (i in lines.indices) { - val line = lines[i] - if (lineMatches(psiFile, line, vulnerability)) { - lineStart = i - lineEnd = i - colStart = colStart(line, vulnerability) - colEnd = colEnd(line, vulnerability) - break - } - } - val lineOffSet = document.getLineStartOffset(lineStart) + colStart - val lineOffSetEnd = document.getLineStartOffset(lineEnd) + colEnd - return TextRange.create(lineOffSet, lineOffSetEnd) - } - - open fun fixRange(psiFile: PsiFile, vulnerability: Vulnerability): TextRange = textRange(psiFile, vulnerability) - - open fun colEnd(line: String, vulnerability: Vulnerability): Int { - val versionIndex = line.indexOf(getIntroducingPackageVersion(vulnerability)) - return if (versionIndex == -1) { - line.length - } else { - versionIndex + getIntroducingPackageVersion(vulnerability).length - } - } - - open fun colStart(line: String, vulnerability: Vulnerability) = - max(0, line.indexOf(getIntroducingPackage(vulnerability))) - - open fun getIntroducingPackage(vulnerability: Vulnerability): String { - return if (hasNoIntroducingPackage(vulnerability)) { - vulnerability.packageName - } else { - vulnerability.from[1].split("@")[0] - } - } - - open fun getIntroducingPackageVersion(vulnerability: Vulnerability): String { - return if (hasNoIntroducingPackage(vulnerability)) { - vulnerability.version - } else { - vulnerability.from[1].split("@")[1] - } - } - - private fun hasNoIntroducingPackage(vulnerability: Vulnerability) = - vulnerability.from.isEmpty() || vulnerability.from.size < 2 - - open fun lineMatches(psiFile: PsiFile, line: String, vulnerability: Vulnerability): Boolean = - line.contains(getIntroducingPackage(vulnerability)) - - inner class ShowDetailsIntentionAction( - override val annotationMessage: String, - private val vulnerability: Vulnerability - ) : ShowDetailsIntentionActionBase() { - - override fun selectNodeAndDisplayDescription(toolWindowPanel: SnykToolWindowPanel) { - toolWindowPanel.selectNodeAndDisplayDescription(vulnerability) - } - - override fun getSeverity(): Severity = vulnerability.getSeverity() - } -} diff --git a/src/main/kotlin/snyk/oss/annotator/OSSGoModAnnotator.kt b/src/main/kotlin/snyk/oss/annotator/OSSGoModAnnotator.kt deleted file mode 100644 index 3a2413012..000000000 --- a/src/main/kotlin/snyk/oss/annotator/OSSGoModAnnotator.kt +++ /dev/null @@ -1,36 +0,0 @@ -package snyk.oss.annotator - -import com.intellij.psi.PsiFile -import io.snyk.plugin.getOssTextRangeFinderService -import snyk.oss.OssVulnerabilitiesForFile -import snyk.oss.Vulnerability - -class OSSGoModAnnotator : OSSBaseAnnotator() { - - init { - getOssTextRangeFinderService().registerFinder(this::textRange) - } - - override fun lineMatches(psiFile: PsiFile, line: String, vulnerability: Vulnerability): Boolean { - val fileName = psiFile.virtualFile.path - return fileName.endsWith("go.mod") && !line.endsWith("// indirect") && - super.lineMatches(psiFile, line, vulnerability) - } - - override fun getFixVersion( - remediation: OssVulnerabilitiesForFile.Remediation?, - vulnerability: Vulnerability - ): String { - return super.getFixVersion(remediation, vulnerability).replace("@", " v") - } - - override fun getIntroducingPackage(vulnerability: Vulnerability): String { - return sanitize(super.getIntroducingPackage(vulnerability)) - } - - private fun sanitize(string: String): String { - return string.replace("/pkg/tool", "") - } - - override fun colEnd(line: String, vulnerability: Vulnerability): Int = line.length -} diff --git a/src/main/kotlin/snyk/oss/annotator/OSSGradleAnnotator.kt b/src/main/kotlin/snyk/oss/annotator/OSSGradleAnnotator.kt deleted file mode 100644 index 29845b492..000000000 --- a/src/main/kotlin/snyk/oss/annotator/OSSGradleAnnotator.kt +++ /dev/null @@ -1,75 +0,0 @@ -package snyk.oss.annotator - -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiComment -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiRecursiveElementVisitor -import com.intellij.psi.PsiWhiteSpace -import io.snyk.plugin.getOssTextRangeFinderService -import snyk.oss.Vulnerability - -class OSSGradleAnnotator : OSSBaseAnnotator() { - - init { - getOssTextRangeFinderService().registerFinder(this::textRange) - } - - override fun getIntroducingPackage(vulnerability: Vulnerability): String { - return super.getIntroducingPackage(vulnerability) - .replace("'", "").replace("\"", "") - } - - override fun textRange(psiFile: PsiFile, vulnerability: Vulnerability) = - findTextRanges(psiFile, vulnerability).first - - private fun findTextRanges( - psiFile: PsiFile, - vulnerability: Vulnerability - ): Pair { - if (!psiFile.name.startsWith("build.gradle")) return Pair( - TextRange.EMPTY_RANGE, - TextRange.EMPTY_RANGE - ) - val currentVersion = getIntroducingPackageVersion(vulnerability) - val packageName = getIntroducingPackage(vulnerability) - val visitor = GradleRecursiveVisitor(packageName, currentVersion) - psiFile.accept(visitor) - return Pair(visitor.artifactTextRange, visitor.versionTextRange) - } - - override fun fixRange(psiFile: PsiFile, vulnerability: Vulnerability): TextRange = - findTextRanges(psiFile, vulnerability).second - - internal class GradleRecursiveVisitor( - private val packageName: String, private val version: String - ) : PsiRecursiveElementVisitor() { - - var artifactTextRange: TextRange = TextRange.EMPTY_RANGE - var versionTextRange: TextRange = TextRange.EMPTY_RANGE - - override fun visitElement(element: PsiElement) { - if (isSearchedDependency(element)) { - val endOffset = element.textRange.endOffset - this.artifactTextRange = TextRange(element.textRange.startOffset, endOffset) - val indexOf = element.text.indexOf(version) - if (indexOf > 0) { - versionTextRange = TextRange(element.textRange.startOffset + indexOf, endOffset) - } - return - } - super.visitElement(element) - } - - private fun isSearchedDependency(element: PsiElement): Boolean { - if (element is PsiComment || element is PsiWhiteSpace || element is PsiFile) return false - // no version given - val depGroups = element.text.split(":") - return if (depGroups.size > 2) { - element.textMatches("$packageName:$version") || element.textMatches("'$packageName:$version'") - } else { - element.textMatches(packageName) || element.textMatches("'$packageName'") - } - } - } -} diff --git a/src/main/kotlin/snyk/oss/annotator/OSSMavenAnnotator.kt b/src/main/kotlin/snyk/oss/annotator/OSSMavenAnnotator.kt deleted file mode 100644 index d015487a3..000000000 --- a/src/main/kotlin/snyk/oss/annotator/OSSMavenAnnotator.kt +++ /dev/null @@ -1,70 +0,0 @@ -package snyk.oss.annotator - -import com.intellij.ide.highlighter.XmlFileType -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.XmlRecursiveElementVisitor -import com.intellij.psi.util.siblings -import com.intellij.psi.xml.XmlTag -import io.snyk.plugin.getOssTextRangeFinderService -import snyk.oss.OssVulnerabilitiesForFile -import snyk.oss.Vulnerability - -class OSSMavenAnnotator : OSSBaseAnnotator() { - - init { - getOssTextRangeFinderService().registerFinder(this::textRange) - } - - override fun getIntroducingPackage(vulnerability: Vulnerability): String { - return super.getIntroducingPackage(vulnerability).split(":")[1].replace("\"", "") - } - - override fun textRange(psiFile: PsiFile, vulnerability: Vulnerability): TextRange { - return fixRange(psiFile, vulnerability) - } - - override fun getFixVersion( - remediation: OssVulnerabilitiesForFile.Remediation?, - vulnerability: Vulnerability - ): String { - // we need to use the super class, as we need the group for finding the upgrade - val key = super.getIntroducingPackage(vulnerability) + "@" + super.getIntroducingPackageVersion(vulnerability) - return remediation?.upgrade?.get(key)?.upgradeTo?.split("@")?.get(1) ?: "" - } - - override fun fixRange(psiFile: PsiFile, vulnerability: Vulnerability): TextRange { - if (psiFile.fileType !is XmlFileType || psiFile.name != "pom.xml") return TextRange.EMPTY_RANGE - val currentVersion = getIntroducingPackageVersion(vulnerability) - val artifactName = getIntroducingPackage(vulnerability) - val visitor = MavenRecursiveVisitor(artifactName, currentVersion) - psiFile.accept(visitor) - return visitor.foundTextRange - } - - internal class MavenRecursiveVisitor( - private val artifactName: String, private val artifactVersion: String - ) : XmlRecursiveElementVisitor() { - - var foundTextRange: TextRange = TextRange.EMPTY_RANGE - - override fun visitElement(element: PsiElement) { - if (isSearchedDependency(element)) { - val siblings = element.siblings() - siblings.forEach { - if (it is XmlTag && it.name == "version" && it.value.text == artifactVersion) { - this.foundTextRange = TextRange(it.value.textRange.startOffset, it.value.textRange.endOffset) - return - } - } - } - super.visitElement(element) - } - - private fun isSearchedDependency(element: PsiElement): Boolean { - return element is XmlTag && element.name == "artifactId" && element.value.text == artifactName - && element.parent is XmlTag && (element.parent as XmlTag).name == "dependency" - } - } -} diff --git a/src/main/kotlin/snyk/oss/annotator/OSSNpmAnnotator.kt b/src/main/kotlin/snyk/oss/annotator/OSSNpmAnnotator.kt deleted file mode 100644 index 8445e3cd8..000000000 --- a/src/main/kotlin/snyk/oss/annotator/OSSNpmAnnotator.kt +++ /dev/null @@ -1,90 +0,0 @@ -package snyk.oss.annotator - -import com.intellij.json.JsonFileType -import com.intellij.json.psi.JsonStringLiteral -import com.intellij.json.psi.impl.JsonRecursiveElementVisitor -import com.intellij.lang.annotation.AnnotationBuilder -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiComment -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiWhiteSpace -import io.snyk.plugin.getOssTextRangeFinderService -import snyk.common.intentionactions.AlwaysAvailableReplacementIntentionAction -import snyk.oss.OssVulnerabilitiesForFile -import snyk.oss.Vulnerability - -class OSSNpmAnnotator : OSSBaseAnnotator() { - - init { - getOssTextRangeFinderService().registerFinder(this::textRange) - } - - override fun textRange(psiFile: PsiFile, vulnerability: Vulnerability): TextRange { - if (psiFile.fileType !is JsonFileType || psiFile.name != "package.json") return TextRange.EMPTY_RANGE - val packageName = getIntroducingPackage(vulnerability) - val visitor = NpmRecursiveVisitor(packageName) - psiFile.accept(visitor) - return visitor.foundTextRange - } - - override fun addQuickFix( - psiFile: PsiFile, - vulnerability: Vulnerability, - annotationBuilder: AnnotationBuilder, - textRange: TextRange, - fixVersion: String - ) { - if (fixVersion.isNotBlank()) { - val msg = if (psiFile.parent?.findFile("package-lock.json") != null) { - "Please update your package-lock.json to finish fixing the vulnerability." - } else { - "" - } - annotationBuilder.withFix( - AlwaysAvailableReplacementIntentionAction( - textRange, - fixVersion, - message = msg - ) - ) - } - } - - override fun getFixVersion( - remediation: OssVulnerabilitiesForFile.Remediation?, - vulnerability: Vulnerability - ): String { - val upgrade = getUpgradeProposal(vulnerability, remediation) - val split = upgrade?.upgradeTo?.split("@") ?: return "" - return "\"${split[0]}\": \"${split[1]}\"" - } - - internal class NpmRecursiveVisitor(private val artifactName: String) : JsonRecursiveElementVisitor() { - - var foundTextRange: TextRange = TextRange.EMPTY_RANGE - - override fun visitElement(element: PsiElement) { - if (isSearchedDependency(element)) { - val value = element.getNextSiblingIgnoringWhitespace()?.getNextSiblingIgnoringWhitespace() ?: return - this.foundTextRange = TextRange(element.textRange.startOffset, value.textRange.endOffset) - return - } - super.visitElement(element) - } - - private fun PsiElement.getNextSiblingIgnoringWhitespace(): PsiElement? { - var candidate = this.nextSibling - while (candidate is PsiWhiteSpace) { - candidate = candidate.nextSibling - } - return candidate - } - - private fun isSearchedDependency(element: PsiElement): Boolean { - return element !is PsiComment && element !is PsiWhiteSpace && - element is JsonStringLiteral && - element.value == artifactName - } - } -} diff --git a/src/main/resources/META-INF/optional/withGo.xml b/src/main/resources/META-INF/optional/withGo.xml index 142bd28e2..418f6d0aa 100644 --- a/src/main/resources/META-INF/optional/withGo.xml +++ b/src/main/resources/META-INF/optional/withGo.xml @@ -1,6 +1,4 @@ - - diff --git a/src/main/resources/META-INF/optional/withJSON.xml b/src/main/resources/META-INF/optional/withJSON.xml index 9b36b70dd..4a893eae0 100644 --- a/src/main/resources/META-INF/optional/withJSON.xml +++ b/src/main/resources/META-INF/optional/withJSON.xml @@ -1,6 +1,5 @@ - diff --git a/src/main/resources/META-INF/optional/withJava.xml b/src/main/resources/META-INF/optional/withJava.xml index 57c5d7cae..418f6d0aa 100644 --- a/src/main/resources/META-INF/optional/withJava.xml +++ b/src/main/resources/META-INF/optional/withJava.xml @@ -1,6 +1,4 @@ - - diff --git a/src/main/resources/META-INF/optional/withKotlin.xml b/src/main/resources/META-INF/optional/withKotlin.xml index 8995548a6..16f4bd5a1 100644 --- a/src/main/resources/META-INF/optional/withKotlin.xml +++ b/src/main/resources/META-INF/optional/withKotlin.xml @@ -1,5 +1,5 @@ - + diff --git a/src/main/resources/META-INF/optional/withXML.xml b/src/main/resources/META-INF/optional/withXML.xml index 96238d8ea..16f4bd5a1 100644 --- a/src/main/resources/META-INF/optional/withXML.xml +++ b/src/main/resources/META-INF/optional/withXML.xml @@ -1,6 +1,5 @@ - - + diff --git a/src/test/kotlin/io/snyk/plugin/analytics/AnalyticsScanListenerTest.kt b/src/test/kotlin/io/snyk/plugin/analytics/AnalyticsScanListenerTest.kt index 4e0a34d53..883e74831 100644 --- a/src/test/kotlin/io/snyk/plugin/analytics/AnalyticsScanListenerTest.kt +++ b/src/test/kotlin/io/snyk/plugin/analytics/AnalyticsScanListenerTest.kt @@ -107,13 +107,6 @@ class AnalyticsScanListenerTest { verify { languageServerWrapper.sendReportAnalyticsCommand(any()) } } - @Test - fun `testScanListener scanningOssFinished should call language server to report analytics`() { - cut.snykScanListener.scanningOssFinished(mockk(relaxed = true)) - - verify { languageServerWrapper.sendReportAnalyticsCommand(any()) } - } - @Test fun `testScanListener scanningContainerFinished should call language server to report analytics`() { cut.snykScanListener.scanningContainerFinished(mockk(relaxed = true)) diff --git a/src/test/kotlin/io/snyk/plugin/cli/ConsoleCommandRunnerTest.kt b/src/test/kotlin/io/snyk/plugin/cli/ConsoleCommandRunnerTest.kt index 91dae7a6d..f5c28ee82 100644 --- a/src/test/kotlin/io/snyk/plugin/cli/ConsoleCommandRunnerTest.kt +++ b/src/test/kotlin/io/snyk/plugin/cli/ConsoleCommandRunnerTest.kt @@ -20,7 +20,6 @@ import io.mockk.verify import io.sentry.protocol.SentryId import io.snyk.plugin.DEFAULT_TIMEOUT_FOR_SCAN_WAITING_MS import io.snyk.plugin.getCliFile -import io.snyk.plugin.getOssService import io.snyk.plugin.getPluginPath import io.snyk.plugin.isCliInstalled import io.snyk.plugin.pluginSettings @@ -28,9 +27,9 @@ import io.snyk.plugin.removeDummyCliFile import io.snyk.plugin.resetSettings import io.snyk.plugin.services.download.SnykCliDownloaderService import io.snyk.plugin.setupDummyCliFile +import org.junit.Ignore import snyk.PLUGIN_ID import snyk.errorHandler.SentryErrorReporter -import snyk.oss.OssService import java.net.URLEncoder import java.util.Locale import java.util.UUID @@ -38,9 +37,6 @@ import java.util.concurrent.TimeUnit class ConsoleCommandRunnerTest : LightPlatformTestCase() { - private val ossService: OssService - get() = getOssService(project) ?: throw IllegalStateException("OSS service should be available") - override fun setUp() { super.setUp() unmockkAll() @@ -266,65 +262,6 @@ class ConsoleCommandRunnerTest : LightPlatformTestCase() { assertEquals("2023.1".length, generalCommandLine.environment["SNYK_INTEGRATION_ENVIRONMENT_VERSION"]?.length) } - @Suppress("SwallowedException") - fun testCommandExecutionRequestWhileCliIsDownloading() { - val cliFile = getCliFile() - cliFile.delete() - mockkStatic("io.snyk.plugin.UtilsKt") - every { isCliInstalled() } returns false - - val progressManager = ProgressManager.getInstance() as CoreProgressManager - val snykCliDownloaderService = service() - var downloadIndicator: ProgressIndicator? = null - - assertFalse("CLI binary should NOT exist at this stage", cliFile.exists()) - progressManager.runProcessWithProgressAsynchronously( - object : Task.Backgroundable(project, "Test CLI download", true) { - override fun run(indicator: ProgressIndicator) { - assertFalse("CLI binary should NOT exist at this stage", cliFile.exists()) - downloadIndicator = indicator - snykCliDownloaderService.downloadLatestRelease(indicator, project) - } - }, - EmptyProgressIndicator() - ) - - assertFalse("CLI binary should NOT exist at this stage", cliFile.exists()) - val testRunFuture = progressManager.runProcessWithProgressAsynchronously( - object : Task.Backgroundable(project, "Test CLI command invocation", true) { - override fun run(indicator: ProgressIndicator) { - while (!snykCliDownloaderService.isCliDownloading()) { - Thread.sleep(10) // lets wait till actual download begin - } - assertTrue( - "Downloading of CLI should be in progress at this stage.", - snykCliDownloaderService.isCliDownloading() - ) - // CLINotExistsException should happen while CLI is not there, - // but downloading and any CLI command is invoked - try { - val commands = ossService.buildCliCommandsList_TEST_ONLY(listOf("test")) - ConsoleCommandRunner().execute(commands, getPluginPath(), "", project) - fail("Should have thrown CliNotExistsException, as the CLI is still downloading.") - } catch (e: CliNotExistsException) { - // this is expected and actually desired - } - } - }, - EmptyProgressIndicator(), - null - ) - - testRunFuture.get(30000, TimeUnit.MILLISECONDS) - // we have to stop CLI download process otherwise partially downloaded CLI file will be visible in other tests - downloadIndicator?.cancel() - while (snykCliDownloaderService.isCliDownloading()) { - Thread.sleep(10) // lets wait till download actually stopped - } - assertFalse(cliFile.exists()) - verify(exactly = 0) { SentryErrorReporter.captureException(any()) } - } - fun testErrorReportedWhenExecutionTimeoutExpire() { val registryValue = Registry.get("snyk.timeout.results.waiting") val defaultValue = registryValue.asInteger() diff --git a/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt b/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt index a840c1f05..83c5e605b 100644 --- a/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt +++ b/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt @@ -2,42 +2,27 @@ package io.snyk.plugin.extensions import com.intellij.testFramework.LightPlatformTestCase import com.intellij.testFramework.PlatformTestUtil -import com.intellij.testFramework.replaceService import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.mockkObject import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.unmockkAll -import io.snyk.plugin.getOssService -import io.snyk.plugin.getSnykCachedResults +import io.mockk.verify import io.snyk.plugin.getSnykCliDownloaderService -import io.snyk.plugin.isCliInstalled import io.snyk.plugin.pluginSettings -import io.snyk.plugin.removeDummyCliFile -import io.snyk.plugin.resetSettings import io.snyk.plugin.services.download.SnykCliDownloaderService -import org.awaitility.Awaitility.await -import org.junit.Ignore import snyk.common.lsp.LanguageServerWrapper -import snyk.oss.OssResult -import snyk.oss.OssService import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded -import java.util.concurrent.TimeUnit -//TODO rewrite -@Ignore("change to language server") class SnykControllerImplTest : LightPlatformTestCase() { - - private lateinit var ossServiceMock: OssService + private val languageServerWrapper = mockk() private lateinit var downloaderServiceMock: SnykCliDownloaderService override fun setUp() { super.setUp() unmockkAll() - resetSettings(project) - ossServiceMock = mockk(relaxed = true) - project.replaceService(OssService::class.java, ossServiceMock, project) mockkStatic("io.snyk.plugin.UtilsKt") mockkStatic("snyk.trust.TrustedProjectsKt") downloaderServiceMock = spyk(SnykCliDownloaderService()) @@ -45,27 +30,19 @@ class SnykControllerImplTest : LightPlatformTestCase() { every { getSnykCliDownloaderService() } returns downloaderServiceMock every { downloaderServiceMock.isFourDaysPassedSinceLastCheck() } returns false every { confirmScanningAndSetWorkspaceTrustedStateIfNeeded(any()) } returns true - - val languageServerWrapper = mockk(relaxed = true) mockkObject(LanguageServerWrapper.Companion) every { LanguageServerWrapper.getInstance() } returns languageServerWrapper - every { languageServerWrapper.getFeatureFlagStatus(any()) } returns true - + every { languageServerWrapper.isInitialized } returns true + justRun { languageServerWrapper.sendReportAnalyticsCommand(any()) } + justRun { languageServerWrapper.sendScanCommand(any()) } } override fun tearDown() { unmockkAll() - resetSettings(project) - removeDummyCliFile() super.tearDown() } fun testControllerCanTriggerScan() { - mockkStatic("io.snyk.plugin.UtilsKt") - every { isCliInstalled() } returns true - val fakeResult = OssResult(emptyList()) - every { getOssService(project)?.scan() } returns fakeResult - val settings = pluginSettings() settings.ossScanEnable = true settings.snykCodeSecurityIssuesScanEnable = false @@ -73,15 +50,11 @@ class SnykControllerImplTest : LightPlatformTestCase() { settings.iacScanEnabled = false settings.containerScanEnabled = false - getSnykCachedResults(project)?.currentContainerResult = null - val controller = SnykControllerImpl(project) controller.scan() PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue() - await().atMost(2, TimeUnit.SECONDS).until { - getSnykCachedResults(project)?.currentOssResults != null - } + verify { languageServerWrapper.sendScanCommand(project) } } } diff --git a/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt b/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt index 0305c6c67..935321bfd 100644 --- a/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt +++ b/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt @@ -14,12 +14,9 @@ import io.mockk.verify import io.snyk.plugin.getCliFile import io.snyk.plugin.getContainerService import io.snyk.plugin.getIacService -import io.snyk.plugin.getOssService import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.getSnykCliDownloaderService import io.snyk.plugin.isCliInstalled -import io.snyk.plugin.isContainerEnabled -import io.snyk.plugin.isIacEnabled import io.snyk.plugin.pluginSettings import io.snyk.plugin.removeDummyCliFile import io.snyk.plugin.resetSettings @@ -27,27 +24,23 @@ import io.snyk.plugin.services.download.CliDownloader import io.snyk.plugin.services.download.SnykCliDownloaderService import io.snyk.plugin.setupDummyCliFile import org.awaitility.Awaitility.await +import org.eclipse.lsp4j.services.LanguageServer import snyk.common.lsp.LanguageServerWrapper import snyk.container.ContainerResult import snyk.iac.IacResult -import snyk.oss.OssResult -import snyk.oss.OssService import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import java.util.concurrent.TimeUnit class SnykTaskQueueServiceTest : LightPlatformTestCase() { - private lateinit var ossServiceMock: OssService private lateinit var downloaderServiceMock: SnykCliDownloaderService + private val lsMock = mockk() override fun setUp() { super.setUp() unmockkAll() resetSettings(project) - ossServiceMock = mockk(relaxed = true) - project.replaceService(OssService::class.java, ossServiceMock, project) - mockkStatic("io.snyk.plugin.UtilsKt") mockkStatic("snyk.trust.TrustedProjectsKt") @@ -59,7 +52,10 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { every { confirmScanningAndSetWorkspaceTrustedStateIfNeeded(any()) } returns true mockkObject(LanguageServerWrapper.Companion) - every { LanguageServerWrapper.getInstance() } returns mockk(relaxed = true) + val lswMock = mockk(relaxed = true) + every { LanguageServerWrapper.getInstance() } returns lswMock + every { lswMock.languageServer } returns lsMock + every { lswMock.isInitialized } returns true } override fun tearDown() { @@ -111,8 +107,6 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { } private fun setupAppSettingsForDownloadTests(): SnykApplicationSettingsStateService { - every { getOssService(project)?.scan() } returns OssResult(null) - val settings = pluginSettings() settings.ossScanEnable = true settings.snykCodeSecurityIssuesScanEnable = false @@ -159,7 +153,6 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { val fakeIacResult = IacResult(emptyList()) mockkStatic("io.snyk.plugin.UtilsKt") - every { isIacEnabled() } returns true every { isCliInstalled() } returns true every { getIacService(project)?.scan() } returns fakeIacResult @@ -171,7 +164,6 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { fun testContainerScanTriggeredAndProduceResults() { mockkStatic("io.snyk.plugin.UtilsKt") - every { isContainerEnabled() } returns true every { isCliInstalled() } returns true val fakeContainerResult = ContainerResult(emptyList()) every { getContainerService(project)?.scan() } returns fakeContainerResult diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt index eeaa92f2f..9ff2b1e19 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt @@ -19,10 +19,10 @@ import io.mockk.verify import io.snyk.plugin.Severity import io.snyk.plugin.events.SnykResultsFilteringListener import io.snyk.plugin.events.SnykScanListener +import io.snyk.plugin.events.SnykScanListenerLS import io.snyk.plugin.getContainerService import io.snyk.plugin.getIacService import io.snyk.plugin.getKubernetesImageCache -import io.snyk.plugin.getOssService import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.getSyncPublisher import io.snyk.plugin.isOssRunning @@ -33,18 +33,15 @@ import io.snyk.plugin.services.SnykTaskQueueService import io.snyk.plugin.setupDummyCliFile import io.snyk.plugin.ui.SnykBalloonNotificationHelper import io.snyk.plugin.ui.actions.SnykTreeMediumSeverityFilterAction -import io.snyk.plugin.ui.toolwindow.nodes.leaf.VulnerabilityTreeNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ErrorTreeNode -import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.FileTreeNode import io.snyk.plugin.ui.toolwindow.panels.SnykErrorPanel -import io.snyk.plugin.ui.toolwindow.panels.VulnerabilityDescriptionPanel import org.eclipse.lsp4j.ExecuteCommandParams import org.eclipse.lsp4j.services.LanguageServer import org.junit.Ignore -import org.junit.Test import snyk.common.SnykError import snyk.common.UIComponentFinder import snyk.common.lsp.LanguageServerWrapper +import snyk.common.lsp.SnykScanParams import snyk.common.lsp.commands.COMMAND_EXECUTE_CLI import snyk.container.ContainerIssue import snyk.container.ContainerIssuesForImage @@ -65,7 +62,6 @@ import snyk.iac.IacSuggestionDescriptionPanel import snyk.iac.IgnoreButtonActionListener import snyk.iac.ui.toolwindow.IacFileTreeNode import snyk.iac.ui.toolwindow.IacIssueTreeNode -import snyk.oss.Vulnerability import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import java.util.concurrent.CompletableFuture import javax.swing.JButton @@ -91,6 +87,9 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { private val scanPublisher get() = getSyncPublisher(project, SnykScanListener.SNYK_SCAN_TOPIC)!! + private val scanPublisherLS + get() = getSyncPublisher(project, SnykScanListenerLS.SNYK_SCAN_TOPIC)!! + private val fakeApiToken = "fake_token" private val lsMock = mockk(relaxed = true) @@ -158,8 +157,8 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { every { lsMock.workspaceService.executeCommand(param) } returns CompletableFuture.completedFuture(mapOf(Pair("stdOut", ossGoofJson))) - val ossResult = getOssService(project)?.scan()!! - scanPublisher.scanningOssFinished(ossResult) + LanguageServerWrapper.getInstance().sendScanCommand(project) + PlatformTestUtil.waitWhileBusy(toolWindowPanel.getTree()) } @@ -252,11 +251,12 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { fun `test when no OSS supported file found should display special text (not error) in node and description`() { mockkObject(SnykBalloonNotificationHelper) - val snykError = SnykError(SnykToolWindowPanel.NO_OSS_FILES, project.basePath.toString()) - val snykErrorControl = SnykError("control", project.basePath.toString()) + val snykError = + SnykScanParams("failed", "oss", project.basePath!!, emptyList(), SnykToolWindowPanel.NO_OSS_FILES) + val snykErrorControl = SnykScanParams("failed", "oss", project.basePath!!, emptyList(), "control") - scanPublisher.scanningOssError(snykErrorControl) - scanPublisher.scanningOssError(snykError) + scanPublisherLS.scanningError(snykErrorControl) + scanPublisherLS.scanningError(snykError) PlatformTestUtil.dispatchAllEventsInIdeEventQueue() val rootOssTreeNode = toolWindowPanel.getRootOssIssuesTreeNode() @@ -265,7 +265,7 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { SnykBalloonNotificationHelper.showError(any(), project) } assertTrue(getSnykCachedResults(project)?.currentOssError == null) - assertTrue(getSnykCachedResults(project)?.currentOssResults == null) + assertTrue(getSnykCachedResults(project)?.currentOSSResultsLS?.isEmpty() ?: false) val cliErrorMessage = rootOssTreeNode.originalCliErrorMessage assertTrue(cliErrorMessage != null && cliErrorMessage.startsWith(SnykToolWindowPanel.NO_OSS_FILES)) // node check @@ -285,7 +285,6 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { assertTrue(jEditorPane.text.contains(SnykToolWindowPanel.NO_OSS_FILES)) } - @Test fun `test when no IAC supported file found should display special text (not error) in node and description`() { mockkObject(SnykBalloonNotificationHelper) @@ -322,7 +321,6 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { assertTrue(jEditorPane.text.contains(SnykToolWindowPanel.NO_IAC_FILES)) } - @Test fun `test should ignore IaC failures in IaC scan results (no issues found)`() { mockkObject(SnykBalloonNotificationHelper) val jsonError = SnykError("Failed to parse JSON file", project.basePath.toString(), 1021) @@ -439,18 +437,27 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { fun `test OSS scan should redirect to Auth panel if token is invalid`() { mockkObject(SnykBalloonNotificationHelper) - val snykErrorControl = SnykError("control", project.basePath.toString()) - val snykError = SnykError("Authentication failed. Please check the API token on ", project.basePath.toString()) - scanPublisher.scanningOssError(snykErrorControl) - scanPublisher.scanningOssError(snykError) + val snykError = + SnykScanParams( + "failed", + "oss", + project.basePath!!, + emptyList(), + "Authentication failed. Please check the API token on " + ) + val snykErrorControl = SnykScanParams("failed", "oss", project.basePath!!, emptyList(), "control") + + scanPublisherLS.scanningError(snykErrorControl) + scanPublisherLS.scanningError(snykError) + PlatformTestUtil.dispatchAllEventsInIdeEventQueue() verify(exactly = 1, timeout = 2000) { - SnykBalloonNotificationHelper.showError(snykErrorControl.message, project) + SnykBalloonNotificationHelper.showError(snykErrorControl.errorMessage!!, project) } verify(exactly = 1, timeout = 2000) { - SnykBalloonNotificationHelper.showError(snykError.message, project) + SnykBalloonNotificationHelper.showError(snykError.errorMessage!!, project) } assertTrue(getSnykCachedResults(project)?.currentOssError == null) assertEquals( @@ -505,12 +512,20 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { } fun `test should display '(error)' in OSS root tree node when result is empty and error occurs`() { - val snykError = SnykError("an error", project.basePath.toString()) - scanPublisher.scanningOssError(snykError) + val snykError = + SnykScanParams("failed", "oss", project.basePath!!, emptyList(), "an error") + + scanPublisherLS.scanningError(snykError) + PlatformTestUtil.dispatchAllEventsInIdeEventQueue() - assertTrue(getSnykCachedResults(project)?.currentOssError == snykError) - assertTrue(getSnykCachedResults(project)?.currentOssResults == null) + assertTrue( + getSnykCachedResults(project)?.currentOssError == SnykError( + snykError.errorMessage!!, + project.basePath!! + ) + ) + assertTrue(getSnykCachedResults(project)?.currentOSSResultsLS?.isEmpty() ?: false) assertEquals( SnykToolWindowPanel.OSS_ROOT_TEXT + " (error)", toolWindowPanel.getRootOssIssuesTreeNode().userObject @@ -523,7 +538,7 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { PlatformTestUtil.dispatchAllEventsInIdeEventQueue() toolWindowPanel.updateTreeRootNodesPresentation(null, 0, 0, 0) - assertTrue(getSnykCachedResults(project)?.currentOssResults == null) + assertTrue(getSnykCachedResults(project)?.currentOSSResultsLS?.isEmpty() ?: false) assertEquals( SnykToolWindowPanel.OSS_ROOT_TEXT + " (scanning...)", toolWindowPanel.getRootOssIssuesTreeNode().userObject @@ -843,9 +858,19 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { setOf("ignored_image_name") val containerService = ContainerService(project) - val param = ExecuteCommandParams(COMMAND_EXECUTE_CLI, listOf(project.basePath, "container", "test", "ignored_image_name", "--json")) + val param = ExecuteCommandParams( + COMMAND_EXECUTE_CLI, + listOf(project.basePath, "container", "test", "ignored_image_name", "--json") + ) - every { lsMock.workspaceService.executeCommand(param) } returns CompletableFuture.completedFuture(mapOf(Pair("stdOut", containerResultJson))) + every { lsMock.workspaceService.executeCommand(param) } returns CompletableFuture.completedFuture( + mapOf( + Pair( + "stdOut", + containerResultJson + ) + ) + ) val containerResult = containerService.scan() setUpContainerTest(containerResult) @@ -901,31 +926,6 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { assertNotNull("IacSuggestionDescriptionPanel should not be null", iacDescriptionPanel) } - fun `test OSS node selected and Description shown on external request`() { - prepareTreeWithFakeOssResults() - - val rootOssIssuesTreeNode = toolWindowPanel.getRootOssIssuesTreeNode() - val firstOssFileNode = rootOssIssuesTreeNode.firstChild as FileTreeNode - val firstOssIssueNode = firstOssFileNode.firstChild as VulnerabilityTreeNode - val groupedVulns = firstOssIssueNode.userObject as Collection - val vulnerability = groupedVulns.first() - - // actual test run - toolWindowPanel.selectNodeAndDisplayDescription(vulnerability) - waitWhileTreeBusy() - - // Assertions - val selectedNodeUserObject = TreeUtil.findObjectInPath(toolWindowPanel.getTree().selectionPath, Any::class.java) - assertEquals(groupedVulns, selectedNodeUserObject) - - val vulnerabilityDescriptionPanel = - UIComponentFinder.getComponentByName( - toolWindowPanel.getDescriptionPanel(), - VulnerabilityDescriptionPanel::class - ) - assertNotNull("VulnerabilityDescriptionPanel should not be null", vulnerabilityDescriptionPanel) - } - fun `test Container node selected and Description shown on external request`() { // prepare Tree with fake Container results setUpContainerTest(null) diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelTest.kt index 1976aaa1a..b920408f0 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelTest.kt @@ -20,7 +20,6 @@ import org.junit.Test import snyk.UIComponentFinder import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.SnykLanguageClient -import snyk.oss.OssVulnerabilitiesForFile import java.awt.Container import java.io.File import java.util.concurrent.CompletableFuture @@ -99,44 +98,6 @@ class SnykToolWindowPanelTest : LightPlatform4TestCase() { assertNotNull(treePanel) } - @Test - fun `sanitizeNavigationalFilePath should return a valid filepath when given abs path in displayTargetFile`() { - val tempFile = File.createTempFile("package", ".json") - tempFile.deleteOnExit() - val absPath = tempFile.toPath().toAbsolutePath() - val vulnsForFile = - OssVulnerabilitiesForFile( - displayTargetFile = absPath.toString(), - path = absPath.parent.toString(), - packageManager = "npm", - vulnerabilities = emptyList() - ) - cut = SnykToolWindowPanel(project) - - val filePath = cut.sanitizeNavigationalFilePath(vulnsForFile) - - assertEquals(absPath.toString(), filePath) - } - - @Test - fun `sanitizeNavigationalFilePath should return a valid filepath when given rel path in displayTargetFile`() { - val tempFile = File.createTempFile("package", ".json") - tempFile.deleteOnExit() - val absPath = tempFile.toPath().toAbsolutePath() - val vulnsForFile = - OssVulnerabilitiesForFile( - displayTargetFile = tempFile.name, - path = absPath.parent.toString(), - packageManager = "npm", - vulnerabilities = emptyList() - ) - cut = SnykToolWindowPanel(project) - - val filePath = cut.sanitizeNavigationalFilePath(vulnsForFile) - - assertEquals(absPath.toString(), filePath) - } - @Test fun `should not display onboarding panel and run scan directly`() { every { settings.token } returns "test-token" diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/VulnerabilityDescriptionPanelTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/VulnerabilityDescriptionPanelTest.kt deleted file mode 100644 index 2f7cda0a4..000000000 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/VulnerabilityDescriptionPanelTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -package io.snyk.plugin.ui.toolwindow - -import com.google.gson.Gson -import com.intellij.ui.components.ActionLink -import io.snyk.plugin.ui.toolwindow.panels.VulnerabilityDescriptionPanel -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertNotNull -import org.junit.Before -import org.junit.Test -import snyk.UIComponentFinder.getJButtonByText -import snyk.oss.Vulnerability -import java.io.FileReader - -class VulnerabilityDescriptionPanelTest { - private lateinit var cut: VulnerabilityDescriptionPanel - private lateinit var vulnerability: Vulnerability - - @Before - fun setup() { - val gson = Gson() - val json = FileReader("src/test/resources/npm-test-vulnerability.json") - vulnerability = gson.fromJson(json, Vulnerability::class.java) - cut = VulnerabilityDescriptionPanel(listOf(vulnerability)) - } - - @Test - fun `constructor should build panel with npm introduced-from info as link label`() { - val introducingDependency = vulnerability.from[1] - val actual = getJButtonByText(cut, introducingDependency) - assertNotNull(actual) - assertEquals(ActionLink::class, actual!!::class) - } - - @Test - fun `constructor should build panel with non-npm introduced-from info as plain text label`() { - val introducingDependency = vulnerability.from[1] - vulnerability = vulnerability.copy(packageManager = "not npm!") - cut = VulnerabilityDescriptionPanel(listOf(vulnerability)) - val actual = getJButtonByText(cut, introducingDependency) - assertNotNull(actual) - assertEquals(ActionLink::class, actual!!::class) - } - - @Test - fun `constructor should build panel with all CWEs as link labels`() { - val cwes = vulnerability.identifiers!!.cwe - cwes.forEach { cwe -> - val actual = getJButtonByText(cut, cwe) - assertNotNull("Expected to find label for $cwe, but was null", actual) - } - } -} diff --git a/src/test/kotlin/snyk/oss/OssBulkFileListenerTest.kt b/src/test/kotlin/snyk/oss/OssBulkFileListenerTest.kt deleted file mode 100644 index bef9afa32..000000000 --- a/src/test/kotlin/snyk/oss/OssBulkFileListenerTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -package snyk.oss - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.roots.ProjectRootManager -import com.intellij.psi.PsiDocumentManager -import com.intellij.testFramework.PsiTestUtil -import com.intellij.testFramework.fixtures.BasePlatformTestCase -import io.mockk.mockk -import io.mockk.unmockkAll -import io.snyk.plugin.getSnykCachedResults -import io.snyk.plugin.resetSettings -import org.eclipse.lsp4j.services.LanguageServer -import org.junit.Test -import snyk.common.lsp.LanguageServerWrapper - -class OssBulkFileListenerTest : BasePlatformTestCase() { - private val lsMock = mockk(relaxed = true) - override fun setUp() { - super.setUp() - unmockkAll() - resetSettings(project) - val languageServerWrapper = LanguageServerWrapper.getInstance() - languageServerWrapper.languageServer = lsMock - languageServerWrapper.isInitialized = true - } - - override fun tearDown() { - resetSettings(project) - unmockkAll() - try { - super.tearDown() - } catch (ignore: Exception) { - // nothing to do - } - } - - @Test - fun `test currentOssResults should be dropped when build file changed`() { - val fakeOssResult = OssResult(null) - getSnykCachedResults(project)?.currentOssResults = fakeOssResult - - myFixture.configureByText("package.json", "main project file") - - assertNull( - "cached OssResult should be dropped after project build file changed", - getSnykCachedResults(project)?.currentOssResults - ) - } - - @Test - fun `test keep currentOssResults when out-of-project build file content changed`() { - val fakeOssResult = OssResult(null) - - val file = myFixture.addFileToProject("exclude/package.json", "") - val module = ProjectRootManager.getInstance(project).fileIndex.getModuleForFile(file.virtualFile)!! - PsiTestUtil.addExcludedRoot(module, file.virtualFile.parent) - - getSnykCachedResults(project)?.currentOssResults = fakeOssResult - - // change and save excluded file to trigger BulkFileListener to proceed events - ApplicationManager.getApplication().runWriteAction { - PsiDocumentManager.getInstance(project).getDocument(file)?.setText("updated content") - } - FileDocumentManager.getInstance().saveAllDocuments() - - // dispose virtual pointer manually created before - PsiTestUtil.removeExcludedRoot(module, file.virtualFile.parent) - - assertEquals( - "cached OssResult should NOT be dropped after NON project build file changed", - fakeOssResult, - getSnykCachedResults(project)?.currentOssResults - ) - } -} From e8a36aeeba4be494c23e200d8952d256d3e4b257 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 30 Aug 2024 14:18:52 +0200 Subject: [PATCH 02/18] feat: store folder config in IDE settings [IDE-584] (#588) * fix: ensureInitialized * fix: add default cli path, if cli path is empty * feat: add setting & display base branch in settings * fix: settings storage --- .../SnykApplicationSettingsStateService.kt | 1 + .../SnykProjectSettingsConfigurable.kt | 4 ++ .../io/snyk/plugin/ui/SnykSettingsDialog.kt | 39 +++++++++++++++++- .../snyk/common/lsp/FolderConfigSettings.kt | 41 +++++++++++++++++++ .../snyk/common/lsp/LanguageServerWrapper.kt | 37 +++++++++++------ .../snyk/common/lsp/SnykLanguageClient.kt | 9 +++- src/main/kotlin/snyk/common/lsp/Types.kt | 10 +++++ .../snyk/common/lsp/SnykLanguageClientTest.kt | 2 + 8 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt index 39224663c..819762bba 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt @@ -12,6 +12,7 @@ import io.snyk.plugin.cli.Platform import io.snyk.plugin.getPluginPath import io.snyk.plugin.getSnykProjectSettingsService import io.snyk.plugin.isProjectSettingsAvailable +import snyk.common.lsp.FolderConfig import java.io.File.separator import java.time.Instant import java.time.LocalDate diff --git a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt index b9cd8ca61..c9e9b09f7 100644 --- a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt +++ b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt @@ -67,6 +67,10 @@ class SnykProjectSettingsConfigurable( override fun apply() { val customEndpoint = snykSettingsDialog.getCustomEndpoint() + if (snykSettingsDialog.getCliPath().isEmpty()) { + snykSettingsDialog.setDefaultCliPath() + } + if (!isUrlValid(customEndpoint)) { SnykBalloonNotificationHelper.showError("Invalid URL, Settings changes ignored.", project) return diff --git a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt index e187aa582..05739d09f 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt @@ -3,6 +3,7 @@ package io.snyk.plugin.ui import com.intellij.ide.BrowserUtil import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.fileChooser.FileChooserDescriptor import com.intellij.openapi.progress.runBackgroundableTask @@ -17,6 +18,7 @@ import com.intellij.ui.ContextHelpLabel import com.intellij.ui.DocumentAdapter import com.intellij.ui.HyperlinkLabel import com.intellij.ui.IdeBorderFactory +import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBPasswordField import com.intellij.ui.components.JBTextField import com.intellij.ui.components.fields.ExpandableTextField @@ -30,8 +32,10 @@ import com.intellij.util.FontUtil import com.intellij.util.containers.toArray import com.intellij.util.ui.GridBag import com.intellij.util.ui.JBUI +import io.snyk.plugin.cli.Platform import io.snyk.plugin.events.SnykCliDownloadListener import io.snyk.plugin.getCliFile +import io.snyk.plugin.getPluginPath import io.snyk.plugin.getSnykCliAuthenticationService import io.snyk.plugin.getSnykCliDownloaderService import io.snyk.plugin.isAdditionalParametersValid @@ -45,8 +49,10 @@ import io.snyk.plugin.ui.settings.ScanTypesPanel import io.snyk.plugin.ui.settings.SeveritiesEnablementPanel import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import snyk.SnykBundle +import snyk.common.lsp.FolderConfigSettings import java.awt.GridBagConstraints import java.awt.GridBagLayout +import java.io.File.separator import java.util.Objects.nonNull import java.util.function.Supplier import javax.swing.JButton @@ -113,6 +119,7 @@ class SnykSettingsDialog( private val channels = listOf("stable", "rc", "preview").toArray(emptyArray()) private val cliReleaseChannelDropDown = ComboBox(channels).apply { this.isEditable = true } private val cliBaseDownloadUrlTextField = JBTextField() + val baseBranchInfoLabel = JBLabel("Base branch: ") private val logger = Logger.getInstance(this::class.java) @@ -174,6 +181,9 @@ class SnykSettingsDialog( additionalParametersTextField.text = applicationSettings.getAdditionalParameters(project) scanOnSaveCheckbox.isSelected = applicationSettings.scanOnSave cliReleaseChannelDropDown.selectedItem = applicationSettings.cliReleaseChannel + + baseBranchInfoLabel.text = service().getAll() + .values.joinToString("\n") { "Base branch for ${it.folderPath}: ${it.baseBranch}" } } } @@ -492,11 +502,24 @@ class SnykSettingsDialog( additionalParametersLabel.labelFor = additionalParametersTextField + projectSettingsPanel.add( + baseBranchInfoLabel, + baseGridConstraints( + 1, + 0, + anchor = UIGridConstraints.ANCHOR_WEST, + fill = UIGridConstraints.FILL_HORIZONTAL, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_GROW, + colSpan = 2, + indent = 0 + ) + ) + val projectSettingsSpacer = Spacer() projectSettingsPanel.add( projectSettingsSpacer, baseGridConstraints( - row = 1, + row = 2, fill = UIGridConstraints.FILL_VERTICAL, hSizePolicy = 1, vSizePolicy = UIGridConstraints.SIZEPOLICY_WANT_GROW, @@ -703,11 +726,22 @@ class SnykSettingsDialog( "Invalid custom endpoint URL, please use https://api.xxx.snyk[gov].io", ::isUrlValid, ) + setupValidation( additionalParametersTextField, "The -d option is not supported by the Snyk IntelliJ plugin", ::isAdditionalParametersValid, ) + + setupValidation( + cliBaseDownloadUrlTextField, + "The base URL cannot be empty", + ::isCliBaseDownloadUrlTextFieldValid, + ) + } + + private fun isCliBaseDownloadUrlTextFieldValid(params: String?): Boolean { + return !params.isNullOrEmpty() } private fun setupValidation( @@ -740,6 +774,9 @@ class SnykSettingsDialog( } fun getCliPath(): String = cliPathTextBoxWithFileBrowser.text + fun setDefaultCliPath() { + cliPathTextBoxWithFileBrowser.text = getPluginPath() + separator + Platform.current().snykWrapperFileName + } fun manageBinariesAutomatically() = manageBinariesAutomatically.isSelected diff --git a/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt b/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt new file mode 100644 index 000000000..fc6621b26 --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt @@ -0,0 +1,41 @@ +package snyk.common.lsp + +import com.google.gson.Gson +import com.intellij.openapi.components.BaseState +import com.intellij.openapi.components.RoamingType +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.SimplePersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.util.xmlb.annotations.MapAnnotation + +@Service +@State( + name = "FolderConfig.Settings", storages = [Storage("snyk.settings.xml", roamingType = RoamingType.DISABLED)] +) +class FolderConfigSettings : SimplePersistentStateComponent(State()) { + val gson = Gson() + + class State : BaseState() { + @get:MapAnnotation(keyAttributeName = "folderPath", entryTagName = "folderConfig") + var configs by map() + } + + private fun addFolderConfig(folderConfig: FolderConfig) { + state.configs[folderConfig.folderPath] = gson.toJson(folderConfig) + } + + fun getFolderConfig(folderPath: String): FolderConfig? { + return gson.fromJson(state.configs[folderPath], FolderConfig::class.java) + } + + fun getAll() : Map { + return state.configs.map { + it.key to gson.fromJson(it.value, FolderConfig::class.java) + }.toMap() + } + + fun clear() = state.configs.clear() + + fun addAll(folderConfigs: List) = folderConfigs.forEach { addFolderConfig(it) } +} diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index c766b4069..9dbe962e7 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -154,9 +154,6 @@ class LanguageServerWrapper( process.destroy() isInitialized = false } - - // update feature flags - runAsync { pluginSettings().isGlobalIgnoresFeatureEnabled = isGlobalIgnoresFeatureEnabled() } } fun shutdown(): Future<*> = @@ -276,17 +273,26 @@ class LanguageServerWrapper( fun ensureLanguageServerInitialized(): Boolean { if (disposed) return false - isInitializing.lock() - assert(isInitializing.holdCount == 1) + try { + isInitializing.lock() + if (isInitializing.holdCount > 1) { + val message = + "Snyk failed to initialize. This is an unexpected loop error, please contact " + + "Snyk support with the error message.\n\n" + RuntimeException().stackTraceToString() + logger.error(message) + return false + } - if (!isInitialized) { - try { - initialize() - } catch (e: RuntimeException) { - throw (e) + if (!isInitialized) { + try { + initialize() + } catch (e: RuntimeException) { + throw (e) + } } + } finally { + isInitializing.unlock() } - isInitializing.unlock() return isInitialized } @@ -307,13 +313,20 @@ class LanguageServerWrapper( if (!ensureLanguageServerInitialized()) return DumbService.getInstance(project).runWhenSmart { getTrustedContentRoots(project).forEach { + refreshFeatureFlags() sendFolderScanCommand(it.path, project) } } } + fun refreshFeatureFlags() { + runAsync { + pluginSettings().isGlobalIgnoresFeatureEnabled = isGlobalIgnoresFeatureEnabled() + } + } + fun getFeatureFlagStatus(featureFlag: String): Boolean { - if (!ensureLanguageServerInitialized()) return false + if (!isInitialized) return false return getFeatureFlagStatusInternal(featureFlag) } diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index 04832ef75..1dc15542f 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -202,6 +202,13 @@ class SnykLanguageClient : return completedFuture } + @JsonNotification(value = "$/snyk.folderConfigs") + fun folderConfig(folderConfigParam: FolderConfigsParam) { + runAsync { + service().addAll(folderConfigParam.folderConfigs) + } + } + @JsonNotification(value = "$/snyk.scan") fun snykScan(snykScan: SnykScanParams) { if (disposed) return @@ -300,7 +307,7 @@ class SnykLanguageClient : if (pluginSettings().token?.isNotEmpty() == true && pluginSettings().scanOnSave) { val wrapper = LanguageServerWrapper.getInstance() // retrieve global ignores feature flag status after auth - pluginSettings().isGlobalIgnoresFeatureEnabled = wrapper.isGlobalIgnoresFeatureEnabled() + LanguageServerWrapper.getInstance().refreshFeatureFlags() ProjectManager.getInstance().openProjects.forEach { wrapper.sendScanCommand(it) diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index 4140c3958..648396750 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -614,3 +614,13 @@ data class OssIdentifiers( return result } } + +data class FolderConfigsParam( + @SerializedName("folderConfigs") val folderConfigs: List, +) + +data class FolderConfig( + @SerializedName("folderPath") val folderPath: String, + @SerializedName("baseBranch") val baseBranch: String, + @SerializedName("localBranches") val localBranches: List = emptyList() +) diff --git a/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt b/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt index 969fd0e5a..3e55376ce 100644 --- a/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt +++ b/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt @@ -130,4 +130,6 @@ class SnykLanguageClientTest { verify { trustServiceMock.addTrustedPath(eq(Path(path))) } } + + } From 4d819c15dd559cfeee8edf95f8d00dead40779be Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Mon, 2 Sep 2024 12:56:40 +0200 Subject: [PATCH 03/18] feat: use intellij environment in ls startup (#590) --- CHANGELOG.md | 16 ++++++++++++++++ src/main/kotlin/io/snyk/plugin/SnykFile.kt | 7 ------- .../SnykApplicationSettingsStateService.kt | 3 +-- .../io/snyk/plugin/ui/SnykSettingsDialog.kt | 10 +++++----- .../plugin/ui/toolwindow/SnykTreeCellRenderer.kt | 4 ++-- ...SnykOSSAnnotatorLS.kt => SnykOSSAnnotator.kt} | 2 +- src/main/kotlin/snyk/common/EnvironmentHelper.kt | 3 +++ .../snyk/common/lsp/LSCodeVisionProvider.kt | 10 ++++++---- src/main/kotlin/snyk/common/lsp/Types.kt | 5 +++-- src/main/resources/META-INF/plugin.xml | 5 +++-- .../services/download/CliDownloaderTest.kt | 2 +- 11 files changed, 41 insertions(+), 26 deletions(-) rename src/main/kotlin/snyk/code/annotator/{SnykOSSAnnotatorLS.kt => SnykOSSAnnotator.kt} (91%) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb4dc9815..974f6b2f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Snyk Security Changelog +## [2.9.1] + +### Changed +- save git folder config in settings +- propagate Jetbrains determined runtime environment to language server +- automatically propagate standard file path for CLI if empty on apply in settings +- guard base branch setting against being empty +- better error messaging when unexpected loop occurs during initialization +- switch downloads to downloads.snyk.io + +### Fixes +- add name to code vision provider +- add flashes for auto-fixable Open Source Issues +- show code vision for Open Source also, when Snyk Code is still analysing +- clean-up old open source scan functionality + ## [2.9.0] ### Changed - Updated the language server protocol version to 14 to support new communication model. diff --git a/src/main/kotlin/io/snyk/plugin/SnykFile.kt b/src/main/kotlin/io/snyk/plugin/SnykFile.kt index add0434fd..826a0aa3a 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykFile.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykFile.kt @@ -1,6 +1,5 @@ package io.snyk.plugin -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project import com.intellij.openapi.util.Iconable import com.intellij.openapi.vfs.VirtualFile @@ -8,13 +7,7 @@ import snyk.common.RelativePathHelper import javax.swing.Icon data class SnykFile(val project: Project, val virtualFile: VirtualFile) { - var icon: Icon? = null val relativePath = RelativePathHelper().getRelativePath(virtualFile, project) - init { - ApplicationManager.getApplication().runReadAction { - virtualFile.getPsiFile(project)?.getIcon(Iconable.ICON_FLAG_READ_STATUS) - } - } } fun toSnykFileSet(project: Project, virtualFiles: Set) = diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt index 819762bba..fa3a2ba69 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt @@ -12,7 +12,6 @@ import io.snyk.plugin.cli.Platform import io.snyk.plugin.getPluginPath import io.snyk.plugin.getSnykProjectSettingsService import io.snyk.plugin.isProjectSettingsAvailable -import snyk.common.lsp.FolderConfig import java.io.File.separator import java.time.Instant import java.time.LocalDate @@ -33,7 +32,7 @@ class SnykApplicationSettingsStateService : PersistentStateComponent().getAll() - .values.joinToString("\n") { "Base branch for ${it.folderPath}: ${it.baseBranch}" } + baseBranchInfoLabel.text = ""+service().getAll() + .values.joinToString("
") { "Base branch for ${it.folderPath}: ${it.baseBranch}" }+"" } } @@ -618,8 +618,8 @@ class SnykSettingsDialog( gb.nextLine(), ) - cliBaseDownloadUrlTextField.toolTipText = "The default URL is https://static.snyk.io. " + - "for FIPS-enabled CLIs (only available for Windows and Linux), please use https://static.snyk.io/fips" + cliBaseDownloadUrlTextField.toolTipText = "The default URL is https://downloads.snyk.io. " + + "for FIPS-enabled CLIs (only available for Windows and Linux), please use https://downloads.snyk.io/fips" val cliBaseDownloadPanel = panel { row { diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt index 1faf59d85..386c0a634 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt @@ -76,7 +76,7 @@ class SnykTreeCellRenderer : ColoredTreeCellRenderer() { val file = entry.key val pair = updateTextTooltipAndIcon(file, productType, value, entry.value.first()) - nodeIcon = pair.first + pair.first?.let { nodeIcon = pair.first } text = pair.second val cachedIssues = getSnykCachedResultsForProduct(entry.key.project, productType) @@ -295,7 +295,7 @@ class SnykTreeCellRenderer : ColoredTreeCellRenderer() { } } - val nodeIcon = firstIssue?.icon() ?: file.icon + val nodeIcon = firstIssue?.icon() return Pair(nodeIcon, text) } diff --git a/src/main/kotlin/snyk/code/annotator/SnykOSSAnnotatorLS.kt b/src/main/kotlin/snyk/code/annotator/SnykOSSAnnotator.kt similarity index 91% rename from src/main/kotlin/snyk/code/annotator/SnykOSSAnnotatorLS.kt rename to src/main/kotlin/snyk/code/annotator/SnykOSSAnnotator.kt index fe6054190..8e9653183 100644 --- a/src/main/kotlin/snyk/code/annotator/SnykOSSAnnotatorLS.kt +++ b/src/main/kotlin/snyk/code/annotator/SnykOSSAnnotator.kt @@ -8,7 +8,7 @@ import io.snyk.plugin.pluginSettings import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import snyk.common.ProductType -class SnykOSSAnnotatorLS : SnykAnnotator(product = ProductType.OSS) { +class SnykOSSAnnotator : SnykAnnotator(product = ProductType.OSS) { init { Disposer.register(SnykPluginDisposable.getInstance(), this) } diff --git a/src/main/kotlin/snyk/common/EnvironmentHelper.kt b/src/main/kotlin/snyk/common/EnvironmentHelper.kt index 3b763e98a..1f66c2b28 100644 --- a/src/main/kotlin/snyk/common/EnvironmentHelper.kt +++ b/src/main/kotlin/snyk/common/EnvironmentHelper.kt @@ -1,5 +1,6 @@ package snyk.common +import com.intellij.util.EnvironmentUtil import com.intellij.util.net.HttpConfigurable import io.snyk.plugin.pluginSettings import snyk.pluginInfo @@ -11,6 +12,8 @@ object EnvironmentHelper { environment: MutableMap, apiToken: String, ) { + // first of all, use IntelliJ environment tool, to spice up env + environment.putAll(EnvironmentUtil.getEnvironmentMap()) val endpoint = getEndpointUrl() val oauthEnabledEnvVar = "INTERNAL_SNYK_OAUTH_ENABLED" diff --git a/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt b/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt index b90d7d3f2..16518ecfd 100644 --- a/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt +++ b/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt @@ -5,6 +5,7 @@ import com.intellij.codeInsight.codeVision.CodeVisionEntry import com.intellij.codeInsight.codeVision.CodeVisionProvider import com.intellij.codeInsight.codeVision.CodeVisionRelativeOrdering import com.intellij.codeInsight.codeVision.CodeVisionState +import com.intellij.codeInsight.codeVision.settings.CodeVisionGroupSettingProvider import com.intellij.codeInsight.codeVision.ui.model.ClickableTextCodeVisionEntry import com.intellij.openapi.application.ReadAction import com.intellij.openapi.diagnostic.logger @@ -16,7 +17,6 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiDocumentManager import icons.SnykIcons -import io.snyk.plugin.isSnykCodeRunning import io.snyk.plugin.toLanguageServerURL import org.eclipse.lsp4j.CodeLens import org.eclipse.lsp4j.CodeLensParams @@ -29,11 +29,14 @@ import java.util.concurrent.TimeoutException private const val CODELENS_FETCH_TIMEOUT = 2L @Suppress("UnstableApiUsage") -class LSCodeVisionProvider : CodeVisionProvider { +class LSCodeVisionProvider : CodeVisionProvider, CodeVisionGroupSettingProvider { private val logger = logger() override val defaultAnchor: CodeVisionAnchorKind = CodeVisionAnchorKind.Default override val id = "snyk.common.lsp.LSCodeVisionProvider" - override val name = "Snyk Language Server Code Vision Provider" + override val name = "Snyk Security Language Server Code Vision Provider" + override val groupId: String = "Snyk Security" + override val groupName: String = groupId + override val relativeOrderings: List get() = emptyList() @@ -46,7 +49,6 @@ class LSCodeVisionProvider : CodeVisionProvider { override fun computeCodeVision(editor: Editor, uiData: Unit): CodeVisionState { if (editor.project == null) return CodeVisionState.READY_EMPTY if (!LanguageServerWrapper.getInstance().isInitialized) return CodeVisionState.READY_EMPTY - if (isSnykCodeRunning(editor.project!!)) return CodeVisionState.READY_EMPTY return ReadAction.compute { val project = editor.project ?: return@compute CodeVisionState.READY_EMPTY diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index 648396750..958a6d8ac 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -285,7 +285,8 @@ data class ScanIssue( fun hasAIFix(): Boolean { return when (this.additionalData.getProductType()) { - ProductType.OSS -> false + ProductType.OSS -> + return this.additionalData.isUpgradable == true ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> { return this.additionalData.hasAIFix } @@ -443,7 +444,7 @@ data class IssueData( @SerializedName("from") val from: List, @SerializedName("upgradePath") val upgradePath: List, @SerializedName("isPatchable") val isPatchable: Boolean, - @SerializedName("isUpgradable") val isUpgradable: Boolean?, + @SerializedName("isUpgradable") val isUpgradable: Boolean, @SerializedName("projectName") val projectName: String, @SerializedName("displayTargetFile") val displayTargetFile: String?, @SerializedName("matchingIssues") val matchingIssues: List, diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index cbf59b7f5..e72125af0 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -41,10 +41,11 @@ - + + - + diff --git a/src/test/kotlin/io/snyk/plugin/services/download/CliDownloaderTest.kt b/src/test/kotlin/io/snyk/plugin/services/download/CliDownloaderTest.kt index 46278f693..1b89bda11 100644 --- a/src/test/kotlin/io/snyk/plugin/services/download/CliDownloaderTest.kt +++ b/src/test/kotlin/io/snyk/plugin/services/download/CliDownloaderTest.kt @@ -41,7 +41,7 @@ class CliDownloaderTest { @Test fun `should refer to snyk static website as base url`() { - assertEquals("https://static.snyk.io", CliDownloader.BASE_URL) + assertEquals("https://downloads.snyk.io", CliDownloader.BASE_URL) } @Test From 554a70a128eaf2aef066040ba55bc32c97f52ebb Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Tue, 3 Sep 2024 07:59:42 +0200 Subject: [PATCH 04/18] fix/IDA-116_NullPointerException-and-error-reporting (#591) * fix: convert line breaks to HTML in balloon notifications * fix: NPE in folder config --- .../io/snyk/plugin/ui/SnykBalloonNotificationHelper.kt | 5 +++-- src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt | 5 +++-- src/main/kotlin/snyk/common/lsp/Types.kt | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/SnykBalloonNotificationHelper.kt b/src/main/kotlin/io/snyk/plugin/ui/SnykBalloonNotificationHelper.kt index a67e53b78..108de1870 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/SnykBalloonNotificationHelper.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/SnykBalloonNotificationHelper.kt @@ -46,10 +46,11 @@ object SnykBalloonNotificationHelper { NotificationType.ERROR, NotificationType.WARNING -> logger.warn(message) else -> logger.info(message) } + val msg = message.replace("\n","
") val notification = if (actions.isEmpty()) { - Notification(groupAutoHide, title, message, type) + Notification(groupAutoHide, title, msg, type) } else { - notificationGroup.createNotification(title, message, type).apply { + notificationGroup.createNotification(title, msg, type).apply { actions.forEach { this.addAction(it) } } } diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index 1dc15542f..03e609dc7 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -203,9 +203,10 @@ class SnykLanguageClient : } @JsonNotification(value = "$/snyk.folderConfigs") - fun folderConfig(folderConfigParam: FolderConfigsParam) { + fun folderConfig(folderConfigParam: FolderConfigsParam?) { + val folderConfigs = folderConfigParam?.folderConfigs ?: emptyList() runAsync { - service().addAll(folderConfigParam.folderConfigs) + service().addAll(folderConfigs) } } diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index 958a6d8ac..5d1b99310 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -617,7 +617,7 @@ data class OssIdentifiers( } data class FolderConfigsParam( - @SerializedName("folderConfigs") val folderConfigs: List, + @SerializedName("folderConfigs") val folderConfigs: List?, ) data class FolderConfig( From d6d3a41b633832045cc5bdbf716d4187a288db19 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 4 Sep 2024 09:44:32 +0200 Subject: [PATCH 05/18] feat: add gutter icons for found issues (#594) * fix: mark our annotators as dumbaware * docs: update CHANGELOG.md * feat: add gutter icons for snyk findings * feat: add gutter icons for snyk findings * feat: sort annotations before applying --- CHANGELOG.md | 2 + .../annotator/ShowDetailsIntentionAction.kt | 2 +- .../snyk/code/annotator/SnykAnnotator.kt | 96 +++++++++++++++---- ...waysAvailableReplacementIntentionAction.kt | 45 --------- ...AvailableReplacementIntentionActionTest.kt | 75 --------------- 5 files changed, 79 insertions(+), 141 deletions(-) delete mode 100644 src/main/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionAction.kt delete mode 100644 src/test/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionActionTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 974f6b2f0..5633ad345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - guard base branch setting against being empty - better error messaging when unexpected loop occurs during initialization - switch downloads to downloads.snyk.io +- allow annotations during IntelliJ indexing +- add gutter icons for Snyk issues ### Fixes - add name to code vision provider diff --git a/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt b/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt index 3f88f90df..97ba5cf61 100644 --- a/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt +++ b/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt @@ -7,7 +7,7 @@ import snyk.common.lsp.ScanIssue class ShowDetailsIntentionAction( override val annotationMessage: String, - private val issue: ScanIssue + val issue: ScanIssue ) : ShowDetailsIntentionActionBase() { override fun selectNodeAndDisplayDescription(toolWindowPanel: SnykToolWindowPanel) { toolWindowPanel.selectNodeAndDisplayDescription(issue) diff --git a/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt index 322bb70ed..673b05ef8 100644 --- a/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt @@ -1,15 +1,24 @@ package snyk.code.annotator +import com.intellij.codeInsight.inline.completion.InlineCompletionEventType import com.intellij.codeInsight.intention.IntentionAction import com.intellij.lang.annotation.AnnotationHolder import com.intellij.lang.annotation.ExternalAnnotator import com.intellij.lang.annotation.HighlightSeverity import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.invokeLater import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.guessProjectForFile import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile +import icons.SnykIcons import io.snyk.plugin.getSnykCachedResultsForProduct +import io.snyk.plugin.getSnykToolWindowPanel import io.snyk.plugin.toLanguageServerURL import org.eclipse.lsp4j.CodeActionContext import org.eclipse.lsp4j.CodeActionParams @@ -22,11 +31,12 @@ import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.ScanIssue import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException +import javax.swing.Icon private const val CODEACTION_TIMEOUT = 5000L abstract class SnykAnnotator(private val product: ProductType) : - ExternalAnnotator>, List>(), Disposable { + ExternalAnnotator>, List>(), Disposable, DumbAware { val logger = logger() protected var disposed = false get() { @@ -43,7 +53,8 @@ abstract class SnykAnnotator(private val product: ProductType) : val annotationSeverity: HighlightSeverity, val annotationMessage: String, val range: TextRange, - val intention: IntentionAction + val intention: IntentionAction, + val renderGutterIcon: Boolean = false ) // overrides needed for the Annotator to invoke apply(). We don't do anything here @@ -60,7 +71,8 @@ abstract class SnykAnnotator(private val product: ProductType) : if (!LanguageServerWrapper.getInstance().isInitialized) return emptyList() val annotations = mutableListOf() - initial.second.forEach { issue -> + val gutterIcons : MutableSet = mutableSetOf() + initial.second.sortedBy { it.getSeverityAsEnum().getHighlightSeverity() }.forEach { issue -> val textRange = textRange(initial.first, issue.range) val highlightSeverity = issue.getSeverityAsEnum().getHighlightSeverity() val annotationMessage = issue.annotationMessage() @@ -69,14 +81,16 @@ abstract class SnykAnnotator(private val product: ProductType) : return@forEach } if (!textRange.isEmpty) { - annotations.add( - SnykAnnotation( - highlightSeverity, - annotationMessage, - textRange, - ShowDetailsIntentionAction(annotationMessage, issue) - ) + val detailAnnotation = SnykAnnotation( + highlightSeverity, + annotationMessage, + textRange, + ShowDetailsIntentionAction(annotationMessage, issue), + renderGutterIcon = !gutterIcons.contains(textRange) ) + annotations.add(detailAnnotation) + gutterIcons.add(textRange) + val params = CodeActionParams( TextDocumentIdentifier(initial.first.virtualFile.toLanguageServerURL()), @@ -102,14 +116,13 @@ abstract class SnykAnnotator(private val product: ProductType) : .sortedBy { it.right.title }.forEach { action -> val codeAction = action.right val title = codeAction.title - annotations.add( - SnykAnnotation( - highlightSeverity, - title, - textRange, - CodeActionIntention(issue, codeAction, product) - ) + val codeActionAnnotation = SnykAnnotation( + highlightSeverity, + title, + textRange, + CodeActionIntention(issue, codeAction, product) ) + annotations.add(codeActionAnnotation) } } @@ -124,12 +137,15 @@ abstract class SnykAnnotator(private val product: ProductType) : ) { if (disposed) return if (!LanguageServerWrapper.getInstance().isInitialized) return - annotationResult.forEach { annotation -> + annotationResult.sortedBy { it.annotationSeverity }.forEach { annotation -> if (!annotation.range.isEmpty) { - holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) + val annoBuilder = holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) .range(annotation.range) .withFix(annotation.intention) - .create() + if (annotation.renderGutterIcon) { + annoBuilder.gutterIconRenderer(SnykShowDetailsGutterRenderer(annotation)) + } + annoBuilder.create() } } } @@ -179,3 +195,43 @@ abstract class SnykAnnotator(private val product: ProductType) : } } } + +class SnykShowDetailsGutterRenderer(val annotation: SnykAnnotation) : GutterIconRenderer() { + override fun equals(other: Any?): Boolean { + return annotation == other + } + + override fun hashCode(): Int { + return annotation.hashCode() + } + + override fun getIcon(): Icon { + return SnykIcons.TOOL_WINDOW + } + + override fun getClickAction(): AnAction? { + if (annotation.intention !is ShowDetailsIntentionAction) return null + return object: AnAction() { + override fun actionPerformed(e: AnActionEvent) { + invokeLater { + val virtualFile = annotation.intention.issue.virtualFile ?: return@invokeLater + val project = guessProjectForFile(virtualFile) ?: return@invokeLater + val toolWindowPanel = getSnykToolWindowPanel(project) ?: return@invokeLater + + annotation.intention.selectNodeAndDisplayDescription(toolWindowPanel) + } + + } + } + } + + override fun isNavigateAction(): Boolean { + return true + } + + override fun isDumbAware(): Boolean { + return true + } + + +} diff --git a/src/main/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionAction.kt b/src/main/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionAction.kt deleted file mode 100644 index 3ff1fa7e1..000000000 --- a/src/main/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionAction.kt +++ /dev/null @@ -1,45 +0,0 @@ -package snyk.common.intentionactions - -import com.intellij.codeInsight.intention.PriorityAction -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Iconable.IconFlags -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiFile -import com.intellij.util.DocumentUtil -import icons.SnykIcons -import io.snyk.plugin.refreshAnnotationsForOpenFiles -import io.snyk.plugin.ui.SnykBalloonNotificationHelper -import javax.swing.Icon - -class AlwaysAvailableReplacementIntentionAction( - val range: TextRange, - val replacementText: String, - val message: String = "" -) : SnykIntentionActionBase() { - - override fun getIcon(@IconFlags flags: Int): Icon = SnykIcons.CHECKMARK_GREEN - - override fun getPriority(): PriorityAction.Priority = PriorityAction.Priority.TOP - - override fun getText(): String = intentionTextPrefix + replacementText + intentionTextPostfix - - override fun invoke(project: Project, editor: Editor?, file: PsiFile) { - val doc = editor?.document ?: return - DocumentUtil.writeInRunUndoTransparentAction { - doc.replaceString(range.startOffset, range.endOffset, replacementText) - // save all changes on disk to update caches through SnykBulkFileListener - FileDocumentManager.getInstance().saveDocument(doc) - refreshAnnotationsForOpenFiles(project) - if (message.isNotBlank()) { - SnykBalloonNotificationHelper.showWarn(message, project) - } - } - } - - companion object { - private const val intentionTextPrefix = "Upgrade to " - private const val intentionTextPostfix = " (Snyk)" - } -} diff --git a/src/test/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionActionTest.kt b/src/test/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionActionTest.kt deleted file mode 100644 index 333363e8b..000000000 --- a/src/test/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionActionTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package snyk.common.intentionactions - -import com.intellij.openapi.application.WriteAction -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.util.TextRange -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiFile -import com.intellij.testFramework.fixtures.BasePlatformTestCase -import io.mockk.every -import io.mockk.mockk -import io.mockk.unmockkAll -import io.snyk.plugin.pluginSettings -import org.junit.Test -import java.nio.file.Paths - -class AlwaysAvailableReplacementIntentionActionTest : BasePlatformTestCase() { - private lateinit var file: VirtualFile - private val replacementText = "b" - private val range = TextRange(/* startOffset = */ 0,/* endOffset = */ 4) - private val familyName = "Snyk" - private lateinit var cut: AlwaysAvailableReplacementIntentionAction - - private val fileName = "package.json" - - private lateinit var psiFile: PsiFile - - override fun getTestDataPath(): String { - val resource = AlwaysAvailableReplacementIntentionAction::class.java - .getResource("/test-fixtures/oss/annotator") - requireNotNull(resource) { "Make sure that the resource $resource exists!" } - return Paths.get(resource.toURI()).toString() - } - - override fun isWriteActionRequired(): Boolean = true - - override fun setUp() { - super.setUp() - unmockkAll() - pluginSettings().fileListenerEnabled = false - file = myFixture.copyFileToProject(fileName) - psiFile = WriteAction.computeAndWait { psiManager.findFile(file)!! } - cut = AlwaysAvailableReplacementIntentionAction( - range = range, - replacementText = replacementText - ) - } - - override fun tearDown() { - unmockkAll() - pluginSettings().fileListenerEnabled = true - super.tearDown() - } - - @Test - fun `test getText`() { - assertTrue(cut.text.contains(replacementText)) - } - - @Test - fun `test familyName`() { - assertEquals(familyName, cut.familyName) - } - - @Test - fun `test invoke`() { - val editor = mockk() - val doc = psiFile.viewProvider.document - doc!!.setText("abcd") - every { editor.document } returns doc - - cut.invoke(project, editor, psiFile) - - assertEquals("b", doc.text) - } -} From dd08eefb087d70db393347b9a0b8b8f7b2dd6311 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 4 Sep 2024 16:25:22 +0200 Subject: [PATCH 06/18] feat: transmit project specific additional params [IDE-567] (#592) * feat: transmit project specific additional params * chore: remove analytics & error reporting settings * fix: ensure sorting descended * feat: switch gutter icons to severity icons, have tooltip * feat: add color settings for annotations * chore: fix code smells * fix: tests * fix: default to theme annotation defaults * chore: remove todo --- CHANGELOG.md | 1 + .../SnykProjectSettingsConfigurable.kt | 22 +++--- .../io/snyk/plugin/ui/SnykSettingsDialog.kt | 30 +------- .../annotator/CodeActionIntention.kt | 9 ++- .../common/annotator/ColorSettingsPage.kt | 55 +++++++++++++++ .../annotator/ShowDetailsIntentionAction.kt | 2 +- .../annotator/SnykAnnotator.kt | 68 ++++++++++++++----- .../annotator/SnykCodeAnnotator.kt | 2 +- .../annotator/SnykOSSAnnotator.kt | 2 +- .../snyk/common/lsp/FolderConfigSettings.kt | 2 +- .../snyk/common/lsp/LanguageServerSettings.kt | 4 +- .../snyk/common/lsp/LanguageServerWrapper.kt | 2 +- src/main/kotlin/snyk/common/lsp/Types.kt | 3 +- src/main/resources/META-INF/plugin.xml | 5 +- .../jcef/OpenFileLoadHandlerGeneratorTest.kt | 4 +- .../SnykToolWindowSnykScanListenerLSTest.kt | 2 +- ...uggestionDescriptionPanelFromLSCodeTest.kt | 8 +-- ...SuggestionDescriptionPanelFromLSOSSTest.kt | 6 +- .../common/lsp/LanguageServerWrapperTest.kt | 3 + 19 files changed, 142 insertions(+), 88 deletions(-) rename src/main/kotlin/snyk/{code => common}/annotator/CodeActionIntention.kt (95%) create mode 100644 src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt rename src/main/kotlin/snyk/{code => common}/annotator/ShowDetailsIntentionAction.kt (94%) rename src/main/kotlin/snyk/{code => common}/annotator/SnykAnnotator.kt (77%) rename src/main/kotlin/snyk/{code => common}/annotator/SnykCodeAnnotator.kt (95%) rename src/main/kotlin/snyk/{code => common}/annotator/SnykOSSAnnotator.kt (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5633ad345..756b0b069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - switch downloads to downloads.snyk.io - allow annotations during IntelliJ indexing - add gutter icons for Snyk issues +- add color and highlighting setting for Snyk issues ### Fixes - add name to code vision provider diff --git a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt index c9e9b09f7..b461d3bc4 100644 --- a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt +++ b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt @@ -3,12 +3,14 @@ package io.snyk.plugin.settings import com.intellij.notification.Notification import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service import com.intellij.openapi.options.SearchableConfigurable import com.intellij.openapi.progress.runBackgroundableTask import com.intellij.openapi.project.Project import io.snyk.plugin.events.SnykProductsOrSeverityListener import io.snyk.plugin.events.SnykResultsFilteringListener import io.snyk.plugin.events.SnykSettingsListener +import io.snyk.plugin.getContentRootPaths import io.snyk.plugin.getSnykProjectSettingsService import io.snyk.plugin.getSnykTaskQueueService import io.snyk.plugin.getSnykToolWindowPanel @@ -18,6 +20,7 @@ import io.snyk.plugin.isUrlValid import io.snyk.plugin.pluginSettings import io.snyk.plugin.ui.SnykBalloonNotificationHelper import io.snyk.plugin.ui.SnykSettingsDialog +import snyk.common.lsp.FolderConfigSettings import snyk.common.lsp.LanguageServerWrapper import javax.swing.JComponent @@ -39,8 +42,6 @@ class SnykProjectSettingsConfigurable( override fun isModified(): Boolean = isCoreParamsModified() || isIgnoreUnknownCAModified() || - isSendUsageAnalyticsModified() || - isCrashReportingModified() || snykSettingsDialog.isScanTypeChanged() || snykSettingsDialog.isSeverityEnablementChanged() || snykSettingsDialog.isIssueOptionChanged() || @@ -89,9 +90,6 @@ class SnykProjectSettingsConfigurable( settingsStateService.organization = snykSettingsDialog.getOrganization() settingsStateService.ignoreUnknownCA = snykSettingsDialog.isIgnoreUnknownCA() - settingsStateService.usageAnalyticsEnabled = snykSettingsDialog.isUsageAnalyticsEnabled() - settingsStateService.crashReportingEnabled = snykSettingsDialog.isCrashReportingEnabled() - settingsStateService.manageBinariesAutomatically = snykSettingsDialog.manageBinariesAutomatically() settingsStateService.cliPath = snykSettingsDialog.getCliPath().trim() settingsStateService.cliBaseDownloadURL = snykSettingsDialog.getCliBaseDownloadURL().trim() @@ -105,6 +103,14 @@ class SnykProjectSettingsConfigurable( if (isProjectSettingsAvailable(project)) { val snykProjectSettingsService = getSnykProjectSettingsService(project) snykProjectSettingsService?.additionalParameters = snykSettingsDialog.getAdditionalParameters() + val fcs = service() + project.getContentRootPaths().forEach { + val fc = fcs.getFolderConfig(it.toAbsolutePath().toString()) + if (fc != null) { + val newFC = fc.copy(additionalParameters = snykSettingsDialog.getAdditionalParameters().split(" ")) + fcs.addFolderConfig(newFC) + } + } } runBackgroundableTask("processing config changes", project, true) { @@ -169,12 +175,6 @@ class SnykProjectSettingsConfigurable( private fun isIgnoreUnknownCAModified(): Boolean = snykSettingsDialog.isIgnoreUnknownCA() != settingsStateService.ignoreUnknownCA - private fun isSendUsageAnalyticsModified(): Boolean = - snykSettingsDialog.isUsageAnalyticsEnabled() != settingsStateService.usageAnalyticsEnabled - - private fun isCrashReportingModified(): Boolean = - snykSettingsDialog.isCrashReportingEnabled() != settingsStateService.crashReportingEnabled - private fun isAdditionalParametersModified(): Boolean = isProjectSettingsAvailable(project) && snykSettingsDialog.getAdditionalParameters() != getSnykProjectSettingsService(project)?.additionalParameters diff --git a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt index b63979de8..d444007f3 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt @@ -99,9 +99,7 @@ class SnykSettingsDialog( } private val ignoreUnknownCACheckBox: JCheckBox = JCheckBox().apply { toolTipText = "Enabling this causes SSL certificate validation to be disabled" } - private val usageAnalyticsCheckBox: JCheckBox = - JCheckBox().apply { toolTipText = "If enabled, send analytics to Amplitude" } - private val crashReportingCheckBox = JCheckBox().apply { toolTipText = "If enabled, send error reports to Sentry" } + private val scanOnSaveCheckbox = JCheckBox().apply { toolTipText = "If enabled, automatically scan on save, start-up and configuration change" } private val additionalParametersTextField: JTextField = @@ -172,8 +170,6 @@ class SnykSettingsDialog( customEndpointTextField.text = applicationSettings.customEndpointUrl organizationTextField.text = applicationSettings.organization ignoreUnknownCACheckBox.isSelected = applicationSettings.ignoreUnknownCA - usageAnalyticsCheckBox.isSelected = applicationSettings.usageAnalyticsEnabled - crashReportingCheckBox.isSelected = applicationSettings.crashReportingEnabled manageBinariesAutomatically.isSelected = applicationSettings.manageBinariesAutomatically cliPathTextBoxWithFileBrowser.text = applicationSettings.cliPath @@ -556,26 +552,6 @@ class SnykSettingsDialog( ), ) - usageAnalyticsCheckBox.text = "Send usage statistics to Snyk" - userExperiencePanel.add( - usageAnalyticsCheckBox, - baseGridConstraints( - row = 1, - anchor = UIGridConstraints.ANCHOR_NORTHWEST, - indent = 0, - ), - ) - - crashReportingCheckBox.text = "Send error reports to Snyk" - userExperiencePanel.add( - crashReportingCheckBox, - baseGridConstraints( - row = 2, - anchor = UIGridConstraints.ANCHOR_NORTHWEST, - indent = 0, - ), - ) - /** Spacer ------------------ */ val generalSettingsSpacer = Spacer() @@ -700,12 +676,8 @@ class SnykSettingsDialog( fun isIgnoreUnknownCA(): Boolean = ignoreUnknownCACheckBox.isSelected - fun isUsageAnalyticsEnabled(): Boolean = usageAnalyticsCheckBox.isSelected - fun isScanOnSaveEnabled(): Boolean = scanOnSaveCheckbox.isSelected - fun isCrashReportingEnabled(): Boolean = crashReportingCheckBox.isSelected - fun isScanTypeChanged(): Boolean = scanTypesPanel.isModified() fun saveScanTypeChanges() = scanTypesPanel.apply() diff --git a/src/main/kotlin/snyk/code/annotator/CodeActionIntention.kt b/src/main/kotlin/snyk/common/annotator/CodeActionIntention.kt similarity index 95% rename from src/main/kotlin/snyk/code/annotator/CodeActionIntention.kt rename to src/main/kotlin/snyk/common/annotator/CodeActionIntention.kt index 9a1f8f9a2..e230e8155 100644 --- a/src/main/kotlin/snyk/code/annotator/CodeActionIntention.kt +++ b/src/main/kotlin/snyk/common/annotator/CodeActionIntention.kt @@ -1,4 +1,4 @@ -package snyk.code.annotator +package snyk.common.annotator import com.intellij.codeInsight.intention.PriorityAction import com.intellij.openapi.command.WriteCommandAction @@ -20,14 +20,13 @@ import snyk.common.lsp.ScanIssue import java.util.concurrent.TimeUnit import javax.swing.Icon -private const val TIMEOUT = 120L - class CodeActionIntention( private val issue: ScanIssue, private val codeAction: CodeAction, private val product: ProductType, ) : SnykIntentionActionBase() { private var changes: Map>? = null + private val timeout = 120L override fun getText(): String = codeAction.title @@ -39,7 +38,7 @@ class CodeActionIntention( if (codeAction.command == null && codeAction.edit == null) { resolvedCodeAction = languageServer.textDocumentService - .resolveCodeAction(codeAction).get(TIMEOUT, TimeUnit.SECONDS) + .resolveCodeAction(codeAction).get(timeout, TimeUnit.SECONDS) val edit = resolvedCodeAction.edit if (edit == null || edit.changes == null) return @@ -49,7 +48,7 @@ class CodeActionIntention( val executeCommandParams = ExecuteCommandParams(codeActionCommand.command, codeActionCommand.arguments) languageServer.workspaceService - .executeCommand(executeCommandParams).get(TIMEOUT, TimeUnit.SECONDS) + .executeCommand(executeCommandParams).get(timeout, TimeUnit.SECONDS) } } diff --git a/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt b/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt new file mode 100644 index 000000000..0abc985af --- /dev/null +++ b/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt @@ -0,0 +1,55 @@ +package snyk.common.annotator + +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.fileTypes.PlainSyntaxHighlighter +import com.intellij.openapi.fileTypes.SyntaxHighlighter +import com.intellij.openapi.options.colors.AttributesDescriptor +import com.intellij.openapi.options.colors.ColorDescriptor +import com.intellij.openapi.options.colors.ColorSettingsPage +import icons.SnykIcons +import javax.swing.Icon + +object SnykAnnotationAttributeKey { + val unknown: TextAttributesKey = TextAttributesKey.createTextAttributesKey("Unknown Severity", TextAttributesKey.find("INFO_ATTRIBUTES")) + val low: TextAttributesKey = TextAttributesKey.createTextAttributesKey("Low Severity", TextAttributesKey.find("INFO_ATTRIBUTES")) + val medium: TextAttributesKey = TextAttributesKey.createTextAttributesKey("Medium Severity", TextAttributesKey.find("WARNING_ATTRIBUTES")) + val high: TextAttributesKey = TextAttributesKey.createTextAttributesKey("High Severity", TextAttributesKey.find("ERRORS_ATTRIBUTES")) + val critical: TextAttributesKey = TextAttributesKey.createTextAttributesKey("Critical Severity", TextAttributesKey.find("ERRORS_ATTRIBUTES")) +} + +class SnykAnnotationColorSettingsPage : ColorSettingsPage { + + private val attributesDescriptors = arrayOf( + AttributesDescriptor("Snyk Critical Issue", SnykAnnotationAttributeKey.critical), + AttributesDescriptor("Snyk High Issue", SnykAnnotationAttributeKey.high), + AttributesDescriptor("Snyk Medium Issue", SnykAnnotationAttributeKey.medium), + AttributesDescriptor("Snyk Low Issue", SnykAnnotationAttributeKey.low), + AttributesDescriptor("Snyk Unknown Issue", SnykAnnotationAttributeKey.unknown), + ) + + override fun getIcon(): Icon = SnykIcons.TOOL_WINDOW + + override fun getHighlighter(): SyntaxHighlighter = PlainSyntaxHighlighter() + + override fun getDemoText(): String = + "This is a demo of a Snyk Critical Issue\n" + + "This is a demo of a Snyk High Issue\n" + + "This is a demo of a Snyk Medium Issue\n" + + "This is a demo of a Snyk Low Issue\n" + + "This is a demo of a Unknown High Issue\n" + + override fun getAdditionalHighlightingTagToDescriptorMap(): Map = + mapOf( + "snyk_unknown_issue" to SnykAnnotationAttributeKey.unknown, + "snyk_low_issue" to SnykAnnotationAttributeKey.low, + "snyk_medium_issue" to SnykAnnotationAttributeKey.medium, + "snyk_high_issue" to SnykAnnotationAttributeKey.high, + "snyk_critical_issue" to SnykAnnotationAttributeKey.critical + ) + + override fun getAttributeDescriptors(): Array = attributesDescriptors + + override fun getColorDescriptors(): Array = ColorDescriptor.EMPTY_ARRAY + + override fun getDisplayName(): String = "Snyk Colors" +} diff --git a/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt b/src/main/kotlin/snyk/common/annotator/ShowDetailsIntentionAction.kt similarity index 94% rename from src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt rename to src/main/kotlin/snyk/common/annotator/ShowDetailsIntentionAction.kt index 97ba5cf61..5fce33046 100644 --- a/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt +++ b/src/main/kotlin/snyk/common/annotator/ShowDetailsIntentionAction.kt @@ -1,4 +1,4 @@ -package snyk.code.annotator +package snyk.common.annotator import io.snyk.plugin.Severity import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel diff --git a/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt similarity index 77% rename from src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt rename to src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt index 673b05ef8..9a141e761 100644 --- a/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -1,6 +1,5 @@ -package snyk.code.annotator +package snyk.common.annotator -import com.intellij.codeInsight.inline.completion.InlineCompletionEventType import com.intellij.codeInsight.intention.IntentionAction import com.intellij.lang.annotation.AnnotationHolder import com.intellij.lang.annotation.ExternalAnnotator @@ -11,12 +10,14 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.invokeLater import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.editor.colors.TextAttributesKey import com.intellij.openapi.editor.markup.GutterIconRenderer import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.guessProjectForFile import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile import icons.SnykIcons +import io.snyk.plugin.Severity import io.snyk.plugin.getSnykCachedResultsForProduct import io.snyk.plugin.getSnykToolWindowPanel import io.snyk.plugin.toLanguageServerURL @@ -24,9 +25,14 @@ import org.eclipse.lsp4j.CodeActionContext import org.eclipse.lsp4j.CodeActionParams import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.TextDocumentIdentifier -import snyk.code.annotator.SnykAnnotator.SnykAnnotation import snyk.common.AnnotatorCommon import snyk.common.ProductType +import snyk.common.annotator.SnykAnnotationAttributeKey.critical +import snyk.common.annotator.SnykAnnotationAttributeKey.high +import snyk.common.annotator.SnykAnnotationAttributeKey.low +import snyk.common.annotator.SnykAnnotationAttributeKey.medium +import snyk.common.annotator.SnykAnnotationAttributeKey.unknown +import snyk.common.annotator.SnykAnnotator.SnykAnnotation import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.ScanIssue import java.util.concurrent.TimeUnit @@ -37,6 +43,7 @@ private const val CODEACTION_TIMEOUT = 5000L abstract class SnykAnnotator(private val product: ProductType) : ExternalAnnotator>, List>(), Disposable, DumbAware { + val logger = logger() protected var disposed = false get() { @@ -50,6 +57,7 @@ abstract class SnykAnnotator(private val product: ProductType) : } inner class SnykAnnotation( + val issue: ScanIssue, val annotationSeverity: HighlightSeverity, val annotationMessage: String, val range: TextRange, @@ -71,8 +79,8 @@ abstract class SnykAnnotator(private val product: ProductType) : if (!LanguageServerWrapper.getInstance().isInitialized) return emptyList() val annotations = mutableListOf() - val gutterIcons : MutableSet = mutableSetOf() - initial.second.sortedBy { it.getSeverityAsEnum().getHighlightSeverity() }.forEach { issue -> + val gutterIcons: MutableSet = mutableSetOf() + initial.second.sortedByDescending { it.getSeverityAsEnum() }.forEach { issue -> val textRange = textRange(initial.first, issue.range) val highlightSeverity = issue.getSeverityAsEnum().getHighlightSeverity() val annotationMessage = issue.annotationMessage() @@ -82,6 +90,7 @@ abstract class SnykAnnotator(private val product: ProductType) : } if (!textRange.isEmpty) { val detailAnnotation = SnykAnnotation( + issue, highlightSeverity, annotationMessage, textRange, @@ -117,6 +126,7 @@ abstract class SnykAnnotator(private val product: ProductType) : val codeAction = action.right val title = codeAction.title val codeActionAnnotation = SnykAnnotation( + issue, highlightSeverity, title, textRange, @@ -137,16 +147,28 @@ abstract class SnykAnnotator(private val product: ProductType) : ) { if (disposed) return if (!LanguageServerWrapper.getInstance().isInitialized) return - annotationResult.sortedBy { it.annotationSeverity }.forEach { annotation -> - if (!annotation.range.isEmpty) { - val annoBuilder = holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) - .range(annotation.range) - .withFix(annotation.intention) - if (annotation.renderGutterIcon) { - annoBuilder.gutterIconRenderer(SnykShowDetailsGutterRenderer(annotation)) + annotationResult.sortedByDescending { it.issue.getSeverityAsEnum() } + .forEach { annotation -> + if (!annotation.range.isEmpty) { + val annoBuilder = holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) + .range(annotation.range) + .textAttributes(getTextAttributeKeyBySeverity(annotation.issue.getSeverityAsEnum())) + .withFix(annotation.intention) + if (annotation.renderGutterIcon) { + annoBuilder.gutterIconRenderer(SnykShowDetailsGutterRenderer(annotation)) + } + annoBuilder.create() } - annoBuilder.create() } + } + + private fun getTextAttributeKeyBySeverity(severity: Severity): TextAttributesKey { + return when(severity) { + Severity.UNKNOWN -> unknown + Severity.LOW -> low + Severity.MEDIUM -> medium + Severity.HIGH -> high + Severity.CRITICAL -> critical } } @@ -206,23 +228,33 @@ class SnykShowDetailsGutterRenderer(val annotation: SnykAnnotation) : GutterIcon } override fun getIcon(): Icon { - return SnykIcons.TOOL_WINDOW + return SnykIcons.getSeverityIcon(annotation.issue.getSeverityAsEnum()) } override fun getClickAction(): AnAction? { if (annotation.intention !is ShowDetailsIntentionAction) return null - return object: AnAction() { + return getShowDetailsNavigationAction(annotation.intention) + } + + private fun getShowDetailsNavigationAction(intention: ShowDetailsIntentionAction) = + object : AnAction() { override fun actionPerformed(e: AnActionEvent) { invokeLater { - val virtualFile = annotation.intention.issue.virtualFile ?: return@invokeLater + val virtualFile = intention.issue.virtualFile ?: return@invokeLater val project = guessProjectForFile(virtualFile) ?: return@invokeLater val toolWindowPanel = getSnykToolWindowPanel(project) ?: return@invokeLater - annotation.intention.selectNodeAndDisplayDescription(toolWindowPanel) + intention.selectNodeAndDisplayDescription(toolWindowPanel) } - } } + + override fun getTooltipText(): String { + return annotation.annotationMessage + } + + override fun getAccessibleName(): String { + return annotation.annotationMessage } override fun isNavigateAction(): Boolean { diff --git a/src/main/kotlin/snyk/code/annotator/SnykCodeAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykCodeAnnotator.kt similarity index 95% rename from src/main/kotlin/snyk/code/annotator/SnykCodeAnnotator.kt rename to src/main/kotlin/snyk/common/annotator/SnykCodeAnnotator.kt index 2efd7d999..d99df7262 100644 --- a/src/main/kotlin/snyk/code/annotator/SnykCodeAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykCodeAnnotator.kt @@ -1,4 +1,4 @@ -package snyk.code.annotator +package snyk.common.annotator import com.intellij.lang.annotation.AnnotationHolder import com.intellij.openapi.util.Disposer diff --git a/src/main/kotlin/snyk/code/annotator/SnykOSSAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykOSSAnnotator.kt similarity index 96% rename from src/main/kotlin/snyk/code/annotator/SnykOSSAnnotator.kt rename to src/main/kotlin/snyk/common/annotator/SnykOSSAnnotator.kt index 8e9653183..a16cf220c 100644 --- a/src/main/kotlin/snyk/code/annotator/SnykOSSAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykOSSAnnotator.kt @@ -1,4 +1,4 @@ -package snyk.code.annotator +package snyk.common.annotator import com.intellij.lang.annotation.AnnotationHolder import com.intellij.openapi.util.Disposer diff --git a/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt b/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt index fc6621b26..8fa59dfee 100644 --- a/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt @@ -21,7 +21,7 @@ class FolderConfigSettings : SimplePersistentStateComponent() } - private fun addFolderConfig(folderConfig: FolderConfig) { + fun addFolderConfig(folderConfig: FolderConfig) { state.configs[folderConfig.folderPath] = gson.toJson(folderConfig) } diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt index 4445a112c..f48549abc 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt @@ -3,6 +3,7 @@ package snyk.common.lsp import com.google.gson.annotations.SerializedName +import com.intellij.openapi.components.service import io.snyk.plugin.pluginSettings import org.apache.commons.lang3.SystemUtils import snyk.pluginInfo @@ -16,7 +17,7 @@ data class LanguageServerSettings( @SerializedName("additionalParams") val additionalParams: String? = null, @SerializedName("additionalEnv") val additionalEnv: String? = null, @SerializedName("path") val path: String? = null, - @SerializedName("sendErrorReports") val sendErrorReports: String? = "false", + @SerializedName("sendErrorReports") val sendErrorReports: String? = "true", @SerializedName("organization") val organization: String? = null, @SerializedName("enableTelemetry") val enableTelemetry: String? = "false", @SerializedName("manageBinariesAutomatically") val manageBinariesAutomatically: String? = "false", @@ -42,6 +43,7 @@ data class LanguageServerSettings( @SerializedName("enableSnykOSSQuickFixCodeActions") val enableSnykOSSQuickFixCodeActions: String? = null, @SerializedName("requiredProtocolVersion") val requiredProtocolVersion: String = pluginSettings().requiredLsProtocolVersion.toString(), + @SerializedName("folderConfigs") val folderConfigs: List = service().getAll().values.toList() ) data class SeverityFilter( diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 9dbe962e7..339b344b3 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -140,7 +140,7 @@ class LanguageServerWrapper( if (!disposed) { try { process.errorStream.bufferedReader().forEachLine { println(it) } - } catch (ignored: RuntimeException) { + } catch (ignored: Exception) { // ignore } } diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index 5d1b99310..a30700a1d 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -623,5 +623,6 @@ data class FolderConfigsParam( data class FolderConfig( @SerializedName("folderPath") val folderPath: String, @SerializedName("baseBranch") val baseBranch: String, - @SerializedName("localBranches") val localBranches: List = emptyList() + @SerializedName("localBranches") val localBranches: List = emptyList(), + @SerializedName("additionalParameters") val additionalParameters: List = emptyList() ) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e72125af0..e1cbb3b6d 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -19,6 +19,7 @@ com.intellij.modules.xml + - - + + diff --git a/src/test/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGeneratorTest.kt b/src/test/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGeneratorTest.kt index 393151542..3ff6990f0 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGeneratorTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGeneratorTest.kt @@ -8,8 +8,7 @@ import com.intellij.testFramework.PlatformTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase import io.mockk.unmockkAll import io.snyk.plugin.resetSettings -import org.junit.Test -import snyk.code.annotator.SnykCodeAnnotator +import snyk.common.annotator.SnykCodeAnnotator import java.nio.file.Paths import java.util.function.BooleanSupplier @@ -39,7 +38,6 @@ class OpenFileLoadHandlerGeneratorTest : BasePlatformTestCase() { generator = OpenFileLoadHandlerGenerator(psiFile.project, virtualFiles) } - @Test fun `test openFile should navigate to source`() { generator.openFile("$fileName:1:2:3:4") val matcher = BooleanSupplier { FileEditorManager.getInstance(project).isFileOpen(psiFile.virtualFile) } diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt index 9e8515a0c..d2216379d 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -15,7 +15,7 @@ import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode import junit.framework.TestCase import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.Range -import snyk.code.annotator.SnykCodeAnnotator +import snyk.common.annotator.SnykCodeAnnotator import snyk.common.lsp.DataFlow import snyk.common.lsp.IssueData import snyk.common.lsp.ScanIssue diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt index a717a650b..7ad4b5661 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt @@ -17,12 +17,11 @@ import io.snyk.plugin.ui.jcef.JCEFUtils import io.snyk.plugin.ui.toolwindow.panels.SuggestionDescriptionPanelFromLS import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.Range -import org.junit.Test import snyk.UIComponentFinder.getJBCEFBrowser import snyk.UIComponentFinder.getJLabelByText import snyk.UIComponentFinder.getJPanelByName -import snyk.code.annotator.SnykCodeAnnotator import snyk.common.ProductType +import snyk.common.annotator.SnykCodeAnnotator import snyk.common.lsp.CommitChangeLine import snyk.common.lsp.DataFlow import snyk.common.lsp.ExampleCommitFix @@ -80,7 +79,6 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { } returns listOf(DataFlow(0, getTestDataPath(), Range(Position(1, 1), Position(1, 1)), "")) } - @Test fun `test createUI should build the right panels for Snyk Code if HTML is not allowed`() { every { issue.canLoadSuggestionPanelFromHTML() } returns false @@ -108,7 +106,6 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { assertNull(ossOverviewPanel) } - @Test fun `test createUI should build panel with issue message as overview label if HTML is not allowed`() { every { issue.canLoadSuggestionPanelFromHTML() } returns false @@ -121,7 +118,6 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { assertNull(actualBrowser) } - @Test fun `test createUI should show nothing if HTML is allowed but JCEF is not supported`() { mockkObject(JCEFUtils) every { JCEFUtils.getJBCefBrowserComponentIfSupported(eq("HTML message"), any()) } returns null @@ -137,7 +133,6 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { assertNull(actualBrowser) } - @Test fun `test createUI should build panel with HTML from details if allowed`() { val mockJBCefBrowserComponent = JLabel("HTML message") mockkObject(JCEFUtils) @@ -156,7 +151,6 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { assertNotNull(actualBrowser) } - @Test fun `test getStyledHTML should inject CSS into the HTML if allowed`() { every { issue.details() } returns "\${ideStyle}HTML message" every { issue.canLoadSuggestionPanelFromHTML() } returns true diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt index 418009594..e80feed1c 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt @@ -15,12 +15,11 @@ import io.snyk.plugin.SnykFile import io.snyk.plugin.resetSettings import io.snyk.plugin.ui.jcef.JCEFUtils import io.snyk.plugin.ui.toolwindow.panels.SuggestionDescriptionPanelFromLS -import org.junit.Test import snyk.UIComponentFinder.getActionLinkByText import snyk.UIComponentFinder.getJLabelByText import snyk.UIComponentFinder.getJPanelByName -import snyk.code.annotator.SnykCodeAnnotator import snyk.common.ProductType +import snyk.common.annotator.SnykCodeAnnotator import snyk.common.lsp.IssueData import snyk.common.lsp.ScanIssue import java.nio.file.Paths @@ -79,7 +78,6 @@ class SuggestionDescriptionPanelFromLSOSSTest : BasePlatformTestCase() { } returns emptyList() } - @Test fun `test createUI should build the right panels for Snyk OSS if HTML not allowed`() { every { issue.canLoadSuggestionPanelFromHTML() } returns false @@ -113,7 +111,6 @@ class SuggestionDescriptionPanelFromLSOSSTest : BasePlatformTestCase() { assertNotNull(ossOverviewPanel) } - @Test fun `test createUI should build panel with HTML from details if allowed`() { val mockJBCefBrowserComponent = JLabel("HTML message") mockkObject(JCEFUtils) @@ -132,7 +129,6 @@ class SuggestionDescriptionPanelFromLSOSSTest : BasePlatformTestCase() { assertNotNull(actualBrowser) } - @Test fun `test getStyledHTML should inject CSS into the HTML if allowed`() { every { issue.details() } returns "HTML message" every { issue.canLoadSuggestionPanelFromHTML() } returns true diff --git a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt index 4b92d2594..338ae9734 100644 --- a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt +++ b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt @@ -34,6 +34,7 @@ import snyk.trust.WorkspaceTrustService import java.util.concurrent.CompletableFuture class LanguageServerWrapperTest { + private val folderConfigSettingsMock: FolderConfigSettings = mockk(relaxed = true) private val applicationMock: Application = mockk() private val projectMock: Project = mockk() private val lsMock: LanguageServer = mockk() @@ -56,6 +57,7 @@ class LanguageServerWrapperTest { val projectManagerMock = mockk() every { applicationMock.getService(ProjectManager::class.java) } returns projectManagerMock every { applicationMock.getService(SnykPluginDisposable::class.java) } returns snykPluginDisposable + every { applicationMock.getService(FolderConfigSettings::class.java) } returns folderConfigSettingsMock every { applicationMock.isDisposed } returns false every { projectManagerMock.openProjects } returns arrayOf(projectMock) @@ -95,6 +97,7 @@ class LanguageServerWrapperTest { verify { lsMock.initialize(any()) } verify { lsMock.initialized(any()) } + verify { folderConfigSettingsMock.getAll() } } @Test From 337af1a7df8d06db8977f000ae7d460c69aa8b22 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 4 Sep 2024 16:43:51 +0200 Subject: [PATCH 07/18] chore: rephrase color setting examples (#597) --- .../kotlin/snyk/common/annotator/ColorSettingsPage.kt | 10 +++++----- src/main/kotlin/snyk/common/lsp/Types.kt | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt b/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt index 0abc985af..bd44321b5 100644 --- a/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt +++ b/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt @@ -32,11 +32,11 @@ class SnykAnnotationColorSettingsPage : ColorSettingsPage { override fun getHighlighter(): SyntaxHighlighter = PlainSyntaxHighlighter() override fun getDemoText(): String = - "This is a demo of a Snyk Critical Issue\n" + - "This is a demo of a Snyk High Issue\n" + - "This is a demo of a Snyk Medium Issue\n" + - "This is a demo of a Snyk Low Issue\n" + - "This is a demo of a Unknown High Issue\n" + "This is a demo of a Snyk Critical Severity Issue\n" + + "This is a demo of a Snyk High Severity Issue\n" + + "This is a demo of a Snyk Medium Severity Issue\n" + + "This is a demo of a Snyk Low Severity Issue\n" + + "This is a demo of a Snyk Unknown Severity Issue\n" override fun getAdditionalHighlightingTagToDescriptorMap(): Map = mapOf( diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index a30700a1d..5f60e5204 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -286,11 +286,11 @@ data class ScanIssue( fun hasAIFix(): Boolean { return when (this.additionalData.getProductType()) { ProductType.OSS -> - return this.additionalData.isUpgradable == true + this.additionalData.isUpgradable + ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> { - return this.additionalData.hasAIFix + this.additionalData.hasAIFix } - else -> TODO() } } From 10962dd82b30a8ab622c9d15339bf64e3e18958e Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Thu, 5 Sep 2024 16:12:57 +0200 Subject: [PATCH 08/18] feat: add dialog to choose reference branch [IDE-601] (#600) * fix: UI thread usage during node searching /selection * chore: enable info nodes for OSS and Code always * fix: save settings when token is received * feat: add dialog to choose a reference branch * docs: update CHANGELOG.md * fix: tests * chore: add test for branch choosing tree node * chore: optimize import * fix: tests * feat: add test for branch chooser --- CHANGELOG.md | 2 + .../SnykApplicationSettingsStateService.kt | 1 + .../SnykProjectSettingsConfigurable.kt | 11 +- .../plugin/ui/BranchChooserComboboxDialog.kt | 83 ++++++ .../ui/toolwindow/SnykToolWindowPanel.kt | 27 +- .../SnykToolWindowSnykScanListenerLS.kt | 78 ++--- .../ui/toolwindow/SnykTreeCellRenderer.kt | 6 + .../nodes/secondlevel/InfoTreeNode.kt | 11 +- .../snyk/common/annotator/SnykAnnotator.kt | 12 +- .../snyk/common/lsp/FolderConfigSettings.kt | 28 +- .../snyk/common/lsp/SnykLanguageClient.kt | 2 + .../ui/BranchChooserComboBoxDialogTest.kt | 109 +++++++ .../SnykToolWindowSnykScanListenerLSTest.kt | 277 +++++++++--------- .../ContainerBulkFileListenerTest.kt | 12 +- 14 files changed, 447 insertions(+), 212 deletions(-) create mode 100644 src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt create mode 100644 src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 756b0b069..d350c3fa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - allow annotations during IntelliJ indexing - add gutter icons for Snyk issues - add color and highlighting setting for Snyk issues +- add dialog to choose reference branch when delta scanning +- always display info nodes ### Fixes - add name to code vision provider diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt index fa3a2ba69..a813b380f 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt @@ -37,6 +37,7 @@ class SnykApplicationSettingsStateService : PersistentStateComponent() - project.getContentRootPaths().forEach { - val fc = fcs.getFolderConfig(it.toAbsolutePath().toString()) - if (fc != null) { - val newFC = fc.copy(additionalParameters = snykSettingsDialog.getAdditionalParameters().split(" ")) - fcs.addFolderConfig(newFC) - } - } + fcs.getAllForProject(project) + .map { it.copy(additionalParameters = snykSettingsDialog.getAdditionalParameters().split(" ")) } + .forEach { fcs.addFolderConfig(it) } } runBackgroundableTask("processing config changes", project, true) { diff --git a/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt new file mode 100644 index 000000000..547bc2401 --- /dev/null +++ b/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt @@ -0,0 +1,83 @@ +package io.snyk.plugin.ui + +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.util.ui.GridBag +import com.intellij.util.ui.JBUI +import org.jetbrains.concurrency.runAsync +import snyk.common.lsp.FolderConfig +import snyk.common.lsp.FolderConfigSettings +import snyk.common.lsp.LanguageServerWrapper +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import javax.swing.JComponent +import javax.swing.JLabel +import javax.swing.JPanel + + +class BranchChooserComboBoxDialog(val project: Project) : DialogWrapper(true) { + var comboBoxes: MutableList> = mutableListOf() + + init { + init() + title = "Choose base branch for net-new issue scanning" + } + + override fun createCenterPanel(): JComponent { + val folderConfigs = service().getAllForProject(project) + folderConfigs.forEach { + val comboBox = ComboBox(it.localBranches.sorted().toTypedArray()) + comboBox.selectedItem = it.baseBranch + comboBox.name = it.folderPath + comboBoxes.add(comboBox) + } + val gridBagLayout = GridBagLayout() + val dialogPanel = JPanel(gridBagLayout) + val gridBag = GridBag() + gridBag.defaultFill = GridBagConstraints.HORIZONTAL + gridBag.insets = JBUI.insets(20) + comboBoxes.forEach { + dialogPanel.add(JLabel("Base Branch for ${it.name}: ")) + dialogPanel.add(it, gridBag.nextLine()) + } + return dialogPanel + } + + override fun doOKAction() { + execute() + super.doOKAction() + } + + fun execute() { + val folderConfigSettings = service() + comboBoxes.forEach { + val folderConfig: FolderConfig? = folderConfigSettings.getFolderConfig(it.name) + if (folderConfig == null) { + SnykBalloonNotificationHelper.showError( + "Unexpectedly cannot retrieve folder config for ${it.name} for base branch updating.", + project + ) + return@forEach + } + + val baseBranch = it.selectedItem!!.toString() // validation makes sure it is not null and not empty + folderConfigSettings.addFolderConfig(folderConfig.copy(baseBranch = baseBranch)) + } + runAsync { + LanguageServerWrapper.getInstance().updateConfiguration() + LanguageServerWrapper.getInstance().sendScanCommand(project) + } + } + + override fun doValidate(): ValidationInfo? { + comboBoxes.forEach { + if (it.selectedItem == null || it.selectedItem?.toString()?.isEmpty() == true) { + return ValidationInfo("Please select a base branch for ${it.name}", it) + } + } + return null + } +} diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt index c82ac0c9e..41beb5c27 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -3,6 +3,7 @@ package io.snyk.plugin.ui.toolwindow import com.intellij.notification.NotificationAction import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.invokeLater import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project @@ -40,6 +41,7 @@ import io.snyk.plugin.pluginSettings import io.snyk.plugin.refreshAnnotationsForOpenFiles import io.snyk.plugin.services.SnykApplicationSettingsStateService import io.snyk.plugin.snykToolWindow +import io.snyk.plugin.ui.BranchChooserComboBoxDialog import io.snyk.plugin.ui.SnykBalloonNotificationHelper import io.snyk.plugin.ui.expandTreeNodeRecursively import io.snyk.plugin.ui.toolwindow.nodes.DescriptionHolderTreeNode @@ -52,7 +54,9 @@ import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootQualityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootTreeNodeBase +import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ChooseBranchNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ErrorTreeNode +import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.InfoTreeNode import io.snyk.plugin.ui.toolwindow.panels.IssueDescriptionPanel import io.snyk.plugin.ui.toolwindow.panels.SnykAuthPanel import io.snyk.plugin.ui.toolwindow.panels.SnykErrorPanel @@ -116,7 +120,7 @@ class SnykToolWindowPanel( * */ private var smartReloadMode = false - var navigateToSourceEnabled = true + var triggerSelectionListeners = true private val treeNodeStub = object : RootTreeNodeBase("", project) { @@ -328,13 +332,18 @@ class SnykToolWindowPanel( private fun updateDescriptionPanelBySelectedTreeNode() { val capturedSmartReloadMode = smartReloadMode - val capturedNavigateToSourceEnabled = navigateToSourceEnabled + val capturedNavigateToSourceEnabled = triggerSelectionListeners ApplicationManager.getApplication().invokeLater { descriptionPanel.removeAll() val selectionPath = vulnerabilitiesTree.selectionPath if (nonNull(selectionPath)) { val lastPathComponent = selectionPath!!.lastPathComponent + + if (lastPathComponent is ChooseBranchNode && capturedNavigateToSourceEnabled && !capturedSmartReloadMode) { + BranchChooserComboBoxDialog(project).show() + } + if (!capturedSmartReloadMode && capturedNavigateToSourceEnabled && lastPathComponent is NavigatableToSourceTreeNode @@ -883,6 +892,7 @@ class SnykToolWindowPanel( selectedNodeUserObject: Any?, ) { val selectedNode = TreeUtil.findNodeWithObject(rootTreeNode, selectedNodeUserObject) + if (selectedNode is InfoTreeNode) return displayEmptyDescription() (vulnerabilitiesTree.model as DefaultTreeModel).reload(nodeToReload) @@ -910,12 +920,13 @@ class SnykToolWindowPanel( private fun selectAndDisplayNodeWithIssueDescription(selectCondition: (DefaultMutableTreeNode) -> Boolean) { val node = TreeUtil.findNode(rootTreeNode) { selectCondition(it) } if (node != null) { - navigateToSourceEnabled = false - try { - TreeUtil.selectNode(vulnerabilitiesTree, node) - // here TreeSelectionListener is invoked, so no needs for explicit updateDescriptionPanelBySelectedTreeNode() - } finally { - navigateToSourceEnabled = true + invokeLater { + try { + triggerSelectionListeners = false + TreeUtil.selectNode(vulnerabilitiesTree, node) + } finally { + triggerSelectionListeners = true + } } } } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt index 73cf445d0..450485a99 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -2,6 +2,7 @@ package io.snyk.plugin.ui.toolwindow import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.TextRange @@ -19,16 +20,19 @@ import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.CODE_SECURITY_ import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.NODE_NOT_SUPPORTED_STATE import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.NO_OSS_FILES import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.OSS_ROOT_TEXT +import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.SCANNING_TEXT import io.snyk.plugin.ui.toolwindow.nodes.leaf.SuggestionTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootContainerIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootIacIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootQualityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode +import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ChooseBranchNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.InfoTreeNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.SnykFileTreeNode import snyk.common.ProductType import snyk.common.SnykFileIssueComparator +import snyk.common.lsp.FolderConfigSettings import snyk.common.lsp.ScanIssue import snyk.common.lsp.SnykScanParams import javax.swing.JTree @@ -70,11 +74,11 @@ class SnykToolWindowSnykScanListenerLS( ApplicationManager.getApplication().invokeLater { this.rootSecurityIssuesTreeNode.userObject = "$CODE_SECURITY_ROOT_TEXT (scanning finished)" this.rootQualityIssuesTreeNode.userObject = "$CODE_QUALITY_ROOT_TEXT (scanning finished)" - this.snykToolWindowPanel.navigateToSourceEnabled = false + this.snykToolWindowPanel.triggerSelectionListeners = false val snykCachedResults = getSnykCachedResults(project) displaySnykCodeResults(snykCachedResults?.currentSnykCodeResultsLS ?: emptyMap()) refreshAnnotationsForOpenFiles(project) - this.snykToolWindowPanel.navigateToSourceEnabled = true + this.snykToolWindowPanel.triggerSelectionListeners = true } } @@ -83,11 +87,11 @@ class SnykToolWindowSnykScanListenerLS( ApplicationManager.getApplication().invokeLater { cancelOssIndicator(project) this.rootOssIssuesTreeNode.userObject = "$OSS_ROOT_TEXT (scanning finished)" - this.snykToolWindowPanel.navigateToSourceEnabled = false + this.snykToolWindowPanel.triggerSelectionListeners = false val snykCachedResults = getSnykCachedResults(project) displayOssResults(snykCachedResults?.currentOSSResultsLS ?: emptyMap()) refreshAnnotationsForOpenFiles(project) - this.snykToolWindowPanel.navigateToSourceEnabled = true + this.snykToolWindowPanel.triggerSelectionListeners = true } } @@ -167,6 +171,7 @@ class SnykToolWindowSnykScanListenerLS( snykResults = snykResults, rootNode = this.rootOssIssuesTreeNode, ossResultsCount = snykResults.values.flatten().distinct().size, + fixableIssuesCount = snykResults.values.flatten().count { it.additionalData.isUpgradable } ) } @@ -204,7 +209,6 @@ class SnykToolWindowSnykScanListenerLS( addInfoTreeNodes( rootNode = rootNode, issues = snykResults.values.flatten().distinct(), - securityIssuesCount = securityIssuesCount, fixableIssuesCount = fixableIssuesCount, ) @@ -259,32 +263,39 @@ class SnykToolWindowSnykScanListenerLS( fun addInfoTreeNodes( rootNode: DefaultMutableTreeNode, issues: List, - securityIssuesCount: Int? = null, fixableIssuesCount: Int? = null, ) { if (disposed) return - - // only add these info tree nodes to Snyk Code security vulnerabilities for now - if (securityIssuesCount == null) { + if (rootNode.userObject == SCANNING_TEXT) { return } val settings = pluginSettings() - // only add these when we enable the consistent ignores flow for now - if (!settings.isGlobalIgnoresFeatureEnabled) { - return + // TODO: check for delta findings + val deltaFindingsEnabled = true + if (deltaFindingsEnabled) { + // we need one choose branch node for each content root. sigh. + service().getAllForProject(project).forEach { + val branchChooserTreeNode = ChooseBranchNode( + project = project, + info = "Click to choose base branch for ${it.folderPath} [ current: ${it.baseBranch} ]" + ) + rootNode.add(branchChooserTreeNode) + } } var text = "✅ Congrats! No vulnerabilities found!" val issuesCount = issues.size val ignoredIssuesCount = issues.count { it.isIgnored() } if (issuesCount != 0) { - if (issuesCount == 1) { - text = "$issuesCount vulnerability found by Snyk" + text = if (issuesCount == 1) { + "$issuesCount vulnerability found by Snyk" } else { - text = "✋ $issuesCount vulnerabilities found by Snyk" + "✋ $issuesCount vulnerabilities found by Snyk" + } + if (pluginSettings().isGlobalIgnoresFeatureEnabled) { + text += ", $ignoredIssuesCount ignored" } - text += ", $ignoredIssuesCount ignored" } rootNode.add( InfoTreeNode( @@ -297,31 +308,32 @@ class SnykToolWindowSnykScanListenerLS( if (fixableIssuesCount > 0) { rootNode.add( InfoTreeNode( - "⚡ $fixableIssuesCount vulnerabilities can be fixed by Snyk DeepCode AI", + "⚡ $fixableIssuesCount vulnerabilities can be fixed automatically", project, ), ) } else { rootNode.add( - InfoTreeNode("There are no vulnerabilities fixable by Snyk DeepCode AI", project), + InfoTreeNode("There are no vulnerabilities automatically fixable", project), ) } } - - if (ignoredIssuesCount == issuesCount && !settings.ignoredIssuesEnabled) { - rootNode.add( - InfoTreeNode( - "Adjust your Issue View Options to see ignored issues.", - project, - ), - ) - } else if (ignoredIssuesCount == 0 && !settings.openIssuesEnabled) { - rootNode.add( - InfoTreeNode( - "Adjust your Issue View Options to open issues.", - project, - ), - ) + if (pluginSettings().isGlobalIgnoresFeatureEnabled) { + if (ignoredIssuesCount == issuesCount && !settings.ignoredIssuesEnabled) { + rootNode.add( + InfoTreeNode( + "Adjust your Issue View Options to see ignored issues.", + project, + ), + ) + } else if (ignoredIssuesCount == 0 && !settings.openIssuesEnabled) { + rootNode.add( + InfoTreeNode( + "Adjust your Issue View Options to open issues.", + project, + ), + ) + } } } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt index 386c0a634..05dd3acd9 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt @@ -18,6 +18,7 @@ import io.snyk.plugin.ui.toolwindow.nodes.root.RootIacIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootQualityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode +import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ChooseBranchNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ErrorTreeNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.InfoTreeNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.SnykFileTreeNode @@ -139,6 +140,11 @@ class SnykTreeCellRenderer : ColoredTreeCellRenderer() { nodeIcon = AllIcons.General.Error } + is ChooseBranchNode -> { + text = value.info + nodeIcon = value.icon + } + is InfoTreeNode -> { val info = value.userObject as String text = info diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/secondlevel/InfoTreeNode.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/secondlevel/InfoTreeNode.kt index c9fadd6a7..4a0516029 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/secondlevel/InfoTreeNode.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/secondlevel/InfoTreeNode.kt @@ -1,9 +1,14 @@ package io.snyk.plugin.ui.toolwindow.nodes.secondlevel +import com.intellij.icons.AllIcons import com.intellij.openapi.project.Project import javax.swing.tree.DefaultMutableTreeNode -class InfoTreeNode( - private val info: String, - val project: Project, +open class InfoTreeNode( + open val info: String, + open val project: Project, ) : DefaultMutableTreeNode(info) + +class ChooseBranchNode(override val info: String = "Choose base branch", override val project: Project) : InfoTreeNode(info, project) { + val icon = AllIcons.Vcs.BranchNode +} diff --git a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt index 9a141e761..f621f1967 100644 --- a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -25,6 +25,7 @@ import org.eclipse.lsp4j.CodeActionContext import org.eclipse.lsp4j.CodeActionParams import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.TextDocumentIdentifier +import org.jetbrains.concurrency.runAsync import snyk.common.AnnotatorCommon import snyk.common.ProductType import snyk.common.annotator.SnykAnnotationAttributeKey.critical @@ -163,7 +164,7 @@ abstract class SnykAnnotator(private val product: ProductType) : } private fun getTextAttributeKeyBySeverity(severity: Severity): TextAttributesKey { - return when(severity) { + return when (severity) { Severity.UNKNOWN -> unknown Severity.LOW -> low Severity.MEDIUM -> medium @@ -239,11 +240,10 @@ class SnykShowDetailsGutterRenderer(val annotation: SnykAnnotation) : GutterIcon private fun getShowDetailsNavigationAction(intention: ShowDetailsIntentionAction) = object : AnAction() { override fun actionPerformed(e: AnActionEvent) { - invokeLater { - val virtualFile = intention.issue.virtualFile ?: return@invokeLater - val project = guessProjectForFile(virtualFile) ?: return@invokeLater - val toolWindowPanel = getSnykToolWindowPanel(project) ?: return@invokeLater - + runAsync { + val virtualFile = intention.issue.virtualFile ?: return@runAsync + val project = guessProjectForFile(virtualFile) ?: return@runAsync + val toolWindowPanel = getSnykToolWindowPanel(project) ?: return@runAsync intention.selectNodeAndDisplayDescription(toolWindowPanel) } } diff --git a/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt b/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt index 8fa59dfee..bd9d56e1f 100644 --- a/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt @@ -7,7 +7,10 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.components.SimplePersistentStateComponent import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage +import com.intellij.openapi.project.Project import com.intellij.util.xmlb.annotations.MapAnnotation +import io.snyk.plugin.getContentRootPaths +import java.util.stream.Collectors @Service @State( @@ -25,11 +28,23 @@ class FolderConfigSettings : SimplePersistentStateComponent { + fun getAll(): Map { return state.configs.map { it.key to gson.fromJson(it.value, FolderConfig::class.java) }.toMap() @@ -38,4 +53,11 @@ class FolderConfigSettings : SimplePersistentStateComponent) = folderConfigs.forEach { addFolderConfig(it) } + + fun getAllForProject(project: Project): List = + project.getContentRootPaths() + .mapNotNull { getFolderConfig(it.toAbsolutePath().toString()) } + .stream() + .sorted() + .collect(Collectors.toList()).toList() } diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index 03e609dc7..29f192460 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -19,6 +19,7 @@ import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectLocator import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.VfsUtilCore @@ -304,6 +305,7 @@ class SnykLanguageClient : if (disposed) return if (pluginSettings().token == param.token) return pluginSettings().token = param.token + ApplicationManager.getApplication().saveSettings() if (pluginSettings().token?.isNotEmpty() == true && pluginSettings().scanOnSave) { val wrapper = LanguageServerWrapper.getInstance() diff --git a/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt new file mode 100644 index 000000000..17c14e897 --- /dev/null +++ b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt @@ -0,0 +1,109 @@ +package io.snyk.plugin.ui + +import com.intellij.openapi.components.service +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.testFramework.LightPlatform4TestCase +import com.intellij.testFramework.PlatformTestUtil +import io.mockk.CapturingSlot +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import okio.Path.Companion.toPath +import org.eclipse.lsp4j.DidChangeConfigurationParams +import org.eclipse.lsp4j.services.LanguageServer +import org.junit.Test +import snyk.common.lsp.FolderConfig +import snyk.common.lsp.FolderConfigSettings +import snyk.common.lsp.LanguageServerSettings +import snyk.common.lsp.LanguageServerWrapper +import snyk.trust.WorkspaceTrustService + + +class BranchChooserComboBoxDialogTest : LightPlatform4TestCase() { + private val lsMock: LanguageServer = mockk(relaxed = true) + lateinit var folderConfig: FolderConfig + lateinit var cut: BranchChooserComboBoxDialog + + override fun setUp(): Unit { + super.setUp() + unmockkAll() + folderConfig = FolderConfig(project.basePath.toString(), "testBranch") + service().addFolderConfig(folderConfig) + project.basePath?.let { service().addTrustedPath(it.toNioPathOrNull()!!) } + val languageServerWrapper = LanguageServerWrapper.getInstance() + languageServerWrapper.isInitialized = true + languageServerWrapper.languageServer = lsMock + cut = BranchChooserComboBoxDialog(project) + } + + override fun tearDown() { + super.tearDown() + unmockkAll() + } + + @Test + fun `test execute transmits the folder config to language server`() { + // setup selected item to main + val comboBox = ComboBox(arrayOf("main", "master")).apply { + name = folderConfig.folderPath + selectedItem = "main" + } + + cut.comboBoxes = mutableListOf(comboBox) + + cut.execute() + PlatformTestUtil.dispatchAllEventsInIdeEventQueue() + + + val capturedParam = CapturingSlot() + verify { lsMock.workspaceService.didChangeConfiguration(capture(capturedParam)) } + val transmittedSettings = capturedParam.captured.settings as LanguageServerSettings + // we expect the selected item + assertEquals("main", transmittedSettings.folderConfigs[0].baseBranch) + } + + @Test + fun `test execute does not transmit the folder config to language server`() { + // setup selected item to main + val comboBox = ComboBox(arrayOf("main", "master")).apply { + name = folderConfig.folderPath + selectedItem = "main" + } + cut.comboBoxes = mutableListOf(comboBox) + + cut.execute() + PlatformTestUtil.dispatchAllEventsInIdeEventQueue() + + val capturedParam = CapturingSlot() + // we need the config update before the scan + verify(exactly = 1, timeout = 2000) { + lsMock.workspaceService.didChangeConfiguration(capture(capturedParam)) + } + + val transmittedSettings = capturedParam.captured.settings as LanguageServerSettings + + // we expect the selected item + assertEquals("main", transmittedSettings.folderConfigs[0].baseBranch) + } + + @Test + fun `test doCancelAction does not transmit the folder config to language server`() { + // setup selected item to main + val comboBox = ComboBox(arrayOf("main", "master")).apply { + name = folderConfig.folderPath + selectedItem = "main" + } + + cut.comboBoxes = mutableListOf(comboBox) + + cut.doCancelAction() + + val capturedParam = CapturingSlot() + + // we need the config update before the scan + verify(exactly = 0) { + lsMock.workspaceService.didChangeConfiguration(capture(capturedParam)) + } + } +} diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt index d2216379d..55c72350c 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -1,12 +1,14 @@ package io.snyk.plugin.ui.toolwindow import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.components.service import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiFile import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.ui.treeStructure.Tree import io.mockk.unmockkAll import io.snyk.plugin.Severity +import io.snyk.plugin.getContentRootPaths import io.snyk.plugin.pluginSettings import io.snyk.plugin.resetSettings import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode @@ -17,6 +19,8 @@ import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.Range import snyk.common.annotator.SnykCodeAnnotator import snyk.common.lsp.DataFlow +import snyk.common.lsp.FolderConfig +import snyk.common.lsp.FolderConfigSettings import snyk.common.lsp.IssueData import snyk.common.lsp.ScanIssue import java.nio.file.Paths @@ -49,7 +53,12 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { file = myFixture.copyFileToProject(fileName) psiFile = WriteAction.computeAndWait { psiManager.findFile(file)!! } - + service() + .addFolderConfig( + FolderConfig( + project.getContentRootPaths().first().toAbsolutePath().toString(), "main" + ) + ) snykToolWindowPanel = SnykToolWindowPanel(project) rootOssIssuesTreeNode = RootOssTreeNode(project) rootSecurityIssuesTreeNode = RootSecurityIssuesTreeNode(project) @@ -60,59 +69,57 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { isIgnored: Boolean? = false, hasAIFix: Boolean? = false, ): List { - val issue = - ScanIssue( - id = "id", - title = "title", - severity = Severity.CRITICAL.toString(), - filePath = getTestDataPath(), - range = Range(), - additionalData = - IssueData( - message = "Test message", - leadURL = "", - rule = "", - repoDatasetSize = 1, - exampleCommitFixes = listOf(), - cwe = emptyList(), - text = "", - markers = null, - cols = null, - rows = null, - isSecurityType = true, - priorityScore = 0, - hasAIFix = hasAIFix!!, - dataFlow = listOf(DataFlow(0, getTestDataPath(), Range(Position(1, 1), Position(1, 1)), "")), - license = null, - identifiers = null, - description = "", - language = "", - packageManager = "", - packageName = "", - name = "", - version = "", - exploit = null, - CVSSv3 = null, - cvssScore = null, - fixedIn = null, - from = listOf(), - upgradePath = listOf(), - isPatchable = false, - isUpgradable = false, - projectName = "", - displayTargetFile = null, - matchingIssues = listOf(), - lesson = null, - details = "", - ruleId = "", - ), - isIgnored = isIgnored, - ignoreDetails = null, - ) + val issue = ScanIssue( + id = "id", + title = "title", + severity = Severity.CRITICAL.toString(), + filePath = getTestDataPath(), + range = Range(), + additionalData = IssueData( + message = "Test message", + leadURL = "", + rule = "", + repoDatasetSize = 1, + exampleCommitFixes = listOf(), + cwe = emptyList(), + text = "", + markers = null, + cols = null, + rows = null, + isSecurityType = true, + priorityScore = 0, + hasAIFix = hasAIFix!!, + dataFlow = listOf(DataFlow(0, getTestDataPath(), Range(Position(1, 1), Position(1, 1)), "")), + license = null, + identifiers = null, + description = "", + language = "", + packageManager = "", + packageName = "", + name = "", + version = "", + exploit = null, + CVSSv3 = null, + cvssScore = null, + fixedIn = null, + from = listOf(), + upgradePath = listOf(), + isPatchable = false, + isUpgradable = false, + projectName = "", + displayTargetFile = null, + matchingIssues = listOf(), + lesson = null, + details = "", + ruleId = "", + ), + isIgnored = isIgnored, + ignoreDetails = null, + ) return listOf(issue) } - fun `testAddInfoTreeNodes does not add new tree nodes for non-code security`() { + fun `testAddInfoTreeNodes adds new tree nodes`() { pluginSettings().isGlobalIgnoresFeatureEnabled = true // setup the rootTreeNode from scratch @@ -120,90 +127,72 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) - vulnerabilitiesTree = - Tree(rootTreeNode).apply { - this.isRootVisible = false - } - - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) + vulnerabilitiesTree = Tree(rootTreeNode).apply { + this.isRootVisible = false + } + + cut = SnykToolWindowSnykScanListenerLS( + project, + snykToolWindowPanel, + vulnerabilitiesTree, + rootSecurityIssuesTreeNode, + rootQualityIssuesTreeNode, + rootOssIssuesTreeNode, + ) TestCase.assertEquals(3, rootTreeNode.childCount) - cut.addInfoTreeNodes(rootTreeNode, mockScanIssues()) - TestCase.assertEquals(3, rootTreeNode.childCount) + cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(), 1) + TestCase.assertEquals(6, rootTreeNode.childCount) + TestCase.assertEquals(rootTreeNode.children().toList()[0].toString(), " Open Source") + TestCase.assertEquals(rootTreeNode.children().toList()[1].toString(), " Code Security") + TestCase.assertEquals(rootTreeNode.children().toList()[2].toString(), " Code Quality") + TestCase.assertEquals( + rootTreeNode.children().toList()[4].toString(), + "1 vulnerability found by Snyk, 0 ignored", + ) + TestCase.assertEquals( + rootTreeNode.children().toList()[5].toString(), + "⚡ 1 vulnerabilities can be fixed automatically", + ) } - fun `testAddInfoTreeNodes does not add new tree nodes for code security if ignores are not enabled`() { - pluginSettings().isGlobalIgnoresFeatureEnabled = false + fun `testAddInfoTreeNodes adds new branch selection tree nodes`() { + pluginSettings().isGlobalIgnoresFeatureEnabled = true // setup the rootTreeNode from scratch rootTreeNode = DefaultMutableTreeNode("") rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) - vulnerabilitiesTree = - Tree(rootTreeNode).apply { - this.isRootVisible = false - } - - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) + vulnerabilitiesTree = Tree(rootTreeNode).apply { + this.isRootVisible = false + } + + cut = SnykToolWindowSnykScanListenerLS( + project, + snykToolWindowPanel, + vulnerabilitiesTree, + rootSecurityIssuesTreeNode, + rootQualityIssuesTreeNode, + rootOssIssuesTreeNode, + ) TestCase.assertEquals(3, rootTreeNode.childCount) - cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(), 1, 1) - TestCase.assertEquals(3, rootTreeNode.childCount) - } - fun `testAddInfoTreeNodes adds new tree nodes for code security if ignores are enabled`() { - pluginSettings().isGlobalIgnoresFeatureEnabled = true + cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(), 1) - // setup the rootTreeNode from scratch - rootTreeNode = DefaultMutableTreeNode("") - rootTreeNode.add(rootOssIssuesTreeNode) - rootTreeNode.add(rootSecurityIssuesTreeNode) - rootTreeNode.add(rootQualityIssuesTreeNode) - vulnerabilitiesTree = - Tree(rootTreeNode).apply { - this.isRootVisible = false - } - - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) - - TestCase.assertEquals(3, rootTreeNode.childCount) - cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(), 1, 1) - TestCase.assertEquals(5, rootTreeNode.childCount) + TestCase.assertEquals(6, rootTreeNode.childCount) TestCase.assertEquals(rootTreeNode.children().toList()[0].toString(), " Open Source") TestCase.assertEquals(rootTreeNode.children().toList()[1].toString(), " Code Security") TestCase.assertEquals(rootTreeNode.children().toList()[2].toString(), " Code Quality") + TestCase.assertTrue(rootTreeNode.children().toList()[3].toString().contains("Click to choose base branch for")) TestCase.assertEquals( - rootTreeNode.children().toList()[3].toString(), + rootTreeNode.children().toList()[4].toString(), "1 vulnerability found by Snyk, 0 ignored", ) TestCase.assertEquals( - rootTreeNode.children().toList()[4].toString(), - "⚡ 1 vulnerabilities can be fixed by Snyk DeepCode AI", + rootTreeNode.children().toList()[5].toString(), + "⚡ 1 vulnerabilities can be fixed automatically", ) } @@ -216,24 +205,22 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) - vulnerabilitiesTree = - Tree(rootTreeNode).apply { - this.isRootVisible = false - } - - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) + vulnerabilitiesTree = Tree(rootTreeNode).apply { + this.isRootVisible = false + } + + cut = SnykToolWindowSnykScanListenerLS( + project, + snykToolWindowPanel, + vulnerabilitiesTree, + rootSecurityIssuesTreeNode, + rootQualityIssuesTreeNode, + rootOssIssuesTreeNode, + ) TestCase.assertEquals(3, rootTreeNode.childCount) - cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(true), 1, 1) - TestCase.assertEquals(6, rootTreeNode.childCount) + cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(true), 1) + TestCase.assertEquals(7, rootTreeNode.childCount) } fun `testAddInfoTreeNodes adds new tree nodes for code security if all open issues are hidden`() { @@ -245,23 +232,21 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) - vulnerabilitiesTree = - Tree(rootTreeNode).apply { - this.isRootVisible = false - } - - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) + vulnerabilitiesTree = Tree(rootTreeNode).apply { + this.isRootVisible = false + } + + cut = SnykToolWindowSnykScanListenerLS( + project, + snykToolWindowPanel, + vulnerabilitiesTree, + rootSecurityIssuesTreeNode, + rootQualityIssuesTreeNode, + rootOssIssuesTreeNode, + ) TestCase.assertEquals(3, rootTreeNode.childCount) - cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(false), 1, 1) - TestCase.assertEquals(6, rootTreeNode.childCount) + cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(false), 1) + TestCase.assertEquals(7, rootTreeNode.childCount) } } diff --git a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt index 183f1d1a8..6c05e2797 100644 --- a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt +++ b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt @@ -14,7 +14,6 @@ import com.intellij.testFramework.PlatformTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.util.io.createDirectories import com.intellij.util.io.delete -import io.mockk.justRun import io.mockk.mockk import io.mockk.unmockkAll import io.snyk.plugin.getKubernetesImageCache @@ -72,11 +71,14 @@ class ContainerBulkFileListenerTest : BasePlatformTestCase() { val path = createNewFileInProjectRoot().toPath() Files.write(path, "\n".toByteArray(Charsets.UTF_8)) VirtualFileManager.getInstance().syncRefresh() - val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(path) - require(virtualFile != null) + var virtualFile: VirtualFile? = null + await().atMost(2, TimeUnit.SECONDS).until { + virtualFile = VirtualFileManager.getInstance().findFileByNioPath(path) + virtualFile?.isValid ?: false + } ApplicationManager.getApplication().runWriteAction { - val file = PsiManager.getInstance(project).findFile(virtualFile) + val file = PsiManager.getInstance(project).findFile(virtualFile!!) require(file != null) PsiDocumentManager.getInstance(project).getDocument(file) ?.setText(TestYamls.podYaml()) @@ -93,7 +95,7 @@ class ContainerBulkFileListenerTest : BasePlatformTestCase() { assertEquals(1, kubernetesWorkloadImages.size) assertEquals(path, kubernetesWorkloadImages.first().virtualFile.toNioPath()) assertEquals("nginx:1.16.0", kubernetesWorkloadImages.first().image) - virtualFile.toNioPath().delete(true) + virtualFile!!.toNioPath().delete(true) } fun `test Container should delete images from cache when yaml file is deleted`() { From 6d669d1d5f7ee28c878b1f9ba83c2dc73d94f6d7 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 6 Sep 2024 09:21:33 +0200 Subject: [PATCH 09/18] feat: add option to enable/disable gutter icons (#601) * feat: add option to enable/disable gutter icons * chore: small performance fix --- CHANGELOG.md | 1 + .../snyk/common/annotator/SnykAnnotator.kt | 135 +++++++++++------- .../annotator/SnykLineMarkerProvider.kt | 23 +++ src/main/resources/META-INF/plugin.xml | 3 + 4 files changed, 114 insertions(+), 48 deletions(-) create mode 100644 src/main/kotlin/snyk/common/annotator/SnykLineMarkerProvider.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index d350c3fa1..cca560442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - switch downloads to downloads.snyk.io - allow annotations during IntelliJ indexing - add gutter icons for Snyk issues +- add option to switch gutter icons on/off in Snyk Settings (the IntelliJ setting only works for parsed languages) - add color and highlighting setting for Snyk issues - add dialog to choose reference branch when delta scanning - always display info nodes diff --git a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt index f621f1967..fa7a94879 100644 --- a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -1,6 +1,9 @@ package snyk.common.annotator +import com.intellij.codeInsight.daemon.LineMarkerProviders +import com.intellij.codeInsight.daemon.LineMarkerSettings import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.lang.Language.ANY import com.intellij.lang.annotation.AnnotationHolder import com.intellij.lang.annotation.ExternalAnnotator import com.intellij.lang.annotation.HighlightSeverity @@ -8,7 +11,6 @@ import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.invokeLater import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.editor.colors.TextAttributesKey import com.intellij.openapi.editor.markup.GutterIconRenderer @@ -40,6 +42,7 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import javax.swing.Icon + private const val CODEACTION_TIMEOUT = 5000L abstract class SnykAnnotator(private val product: ProductType) : @@ -63,26 +66,29 @@ abstract class SnykAnnotator(private val product: ProductType) : val annotationMessage: String, val range: TextRange, val intention: IntentionAction, - val renderGutterIcon: Boolean = false + var gutterIconRenderer: GutterIconRenderer? = null ) // overrides needed for the Annotator to invoke apply(). We don't do anything here override fun collectInformation(file: PsiFile): Pair> { return Pair(file, getIssuesForFile(file) .filter { AnnotatorCommon.isSeverityToShow(it.getSeverityAsEnum()) } - .distinctBy { it.id } - .sortedBy { it.title }) + .distinctBy { it.id }) } - override fun doAnnotate(initial: Pair>): List { + override fun doAnnotate(initialInfo: Pair>): List { if (disposed) return emptyList() - AnnotatorCommon.prepareAnnotate(initial.first) + AnnotatorCommon.prepareAnnotate(initialInfo.first) if (!LanguageServerWrapper.getInstance().isInitialized) return emptyList() + val lineMarkerProviderDescriptor: SnykLineMarkerProvider = getLineMarkerProvider() + val gutterIconEnabled = LineMarkerSettings.getSettings().isEnabled(lineMarkerProviderDescriptor) + val annotations = mutableListOf() val gutterIcons: MutableSet = mutableSetOf() - initial.second.sortedByDescending { it.getSeverityAsEnum() }.forEach { issue -> - val textRange = textRange(initial.first, issue.range) + + initialInfo.second.sortedByDescending { it.getSeverityAsEnum() }.forEach { issue -> + val textRange = textRange(initialInfo.first, issue.range) val highlightSeverity = issue.getSeverityAsEnum().getHighlightSeverity() val annotationMessage = issue.annotationMessage() if (textRange == null) { @@ -95,47 +101,28 @@ abstract class SnykAnnotator(private val product: ProductType) : highlightSeverity, annotationMessage, textRange, - ShowDetailsIntentionAction(annotationMessage, issue), - renderGutterIcon = !gutterIcons.contains(textRange) + ShowDetailsIntentionAction(annotationMessage, issue) ) - annotations.add(detailAnnotation) - gutterIcons.add(textRange) - val params = - CodeActionParams( - TextDocumentIdentifier(initial.first.virtualFile.toLanguageServerURL()), - issue.range, - CodeActionContext(emptyList()), - ) - val languageServer = LanguageServerWrapper.getInstance().languageServer - val codeActions = - try { - languageServer.textDocumentService - .codeAction(params).get(CODEACTION_TIMEOUT, TimeUnit.MILLISECONDS) ?: emptyList() - } catch (ignored: TimeoutException) { - logger.info("Timeout fetching code actions for issue: $issue") - emptyList() + val gutterIconRenderer = + if (gutterIconEnabled && !gutterIcons.contains(textRange)) { + gutterIcons.add(textRange) + SnykShowDetailsGutterRenderer(detailAnnotation) + } else { + null } - codeActions - .filter { a -> - val diagnosticCode = a.right.diagnostics?.get(0)?.code?.left - val ruleId = issue.ruleId() - diagnosticCode == ruleId - } - .sortedBy { it.right.title }.forEach { action -> - val codeAction = action.right - val title = codeAction.title - val codeActionAnnotation = SnykAnnotation( - issue, - highlightSeverity, - title, - textRange, - CodeActionIntention(issue, codeAction, product) - ) - annotations.add(codeActionAnnotation) - } + detailAnnotation.gutterIconRenderer = gutterIconRenderer + annotations.add(detailAnnotation) + annotations.addAll( + getAnnotationsForCodeActions( + initialInfo, + issue, + highlightSeverity, + textRange + ) + ) } } return annotations @@ -148,14 +135,14 @@ abstract class SnykAnnotator(private val product: ProductType) : ) { if (disposed) return if (!LanguageServerWrapper.getInstance().isInitialized) return - annotationResult.sortedByDescending { it.issue.getSeverityAsEnum() } + annotationResult .forEach { annotation -> if (!annotation.range.isEmpty) { val annoBuilder = holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) .range(annotation.range) .textAttributes(getTextAttributeKeyBySeverity(annotation.issue.getSeverityAsEnum())) .withFix(annotation.intention) - if (annotation.renderGutterIcon) { + if (annotation.gutterIconRenderer != null) { annoBuilder.gutterIconRenderer(SnykShowDetailsGutterRenderer(annotation)) } annoBuilder.create() @@ -163,6 +150,60 @@ abstract class SnykAnnotator(private val product: ProductType) : } } + private fun getAnnotationsForCodeActions( + initial: Pair>, + issue: ScanIssue, + highlightSeverity: HighlightSeverity, + textRange: TextRange, + ): MutableList { + val addedAnnotationsList = mutableListOf() + val params = + CodeActionParams( + TextDocumentIdentifier(initial.first.virtualFile.toLanguageServerURL()), + issue.range, + CodeActionContext(emptyList()), + ) + val languageServer = LanguageServerWrapper.getInstance().languageServer + val codeActions = + try { + languageServer.textDocumentService + .codeAction(params).get(CODEACTION_TIMEOUT, TimeUnit.MILLISECONDS) ?: emptyList() + } catch (ignored: TimeoutException) { + logger.info("Timeout fetching code actions for issue: $issue") + emptyList() + } + + codeActions + .filter { a -> + val diagnosticCode = a.right.diagnostics?.get(0)?.code?.left + val ruleId = issue.ruleId() + diagnosticCode == ruleId + } + .sortedBy { it.right.title }.forEach { action -> + val codeAction = action.right + val title = codeAction.title + val codeActionAnnotation = SnykAnnotation( + issue, + highlightSeverity, + title, + textRange, + CodeActionIntention(issue, codeAction, product) + ) + addedAnnotationsList.add(codeActionAnnotation) + } + return addedAnnotationsList + } + + private fun getLineMarkerProvider(): SnykLineMarkerProvider { + val lineMarkerProviderDescriptor: SnykLineMarkerProvider = + LineMarkerProviders.getInstance().allForLanguage(ANY) + .stream() + .filter { p -> p is SnykLineMarkerProvider } + .findFirst() + .orElse(null) as SnykLineMarkerProvider + return lineMarkerProviderDescriptor + } + private fun getTextAttributeKeyBySeverity(severity: Severity): TextAttributesKey { return when (severity) { Severity.UNKNOWN -> unknown @@ -264,6 +305,4 @@ class SnykShowDetailsGutterRenderer(val annotation: SnykAnnotation) : GutterIcon override fun isDumbAware(): Boolean { return true } - - } diff --git a/src/main/kotlin/snyk/common/annotator/SnykLineMarkerProvider.kt b/src/main/kotlin/snyk/common/annotator/SnykLineMarkerProvider.kt new file mode 100644 index 000000000..37ee1b6d7 --- /dev/null +++ b/src/main/kotlin/snyk/common/annotator/SnykLineMarkerProvider.kt @@ -0,0 +1,23 @@ +package snyk.common.annotator + +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.codeInsight.daemon.LineMarkerProviderDescriptor +import com.intellij.psi.PsiElement +import icons.SnykIcons +import javax.swing.Icon + +// we only define a line marker provider so we can use the gutter icon settings to switch +// rendering of gutter icons on and off +class SnykLineMarkerProvider : LineMarkerProviderDescriptor() { + override fun getName(): String { + return "Snyk Security" + } + + override fun getIcon(): Icon { + return SnykIcons.TOOL_WINDOW + } + + override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? { + return null + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e1cbb3b6d..d2fc3bd25 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -19,6 +19,9 @@ com.intellij.modules.xml + Date: Fri, 6 Sep 2024 09:39:15 +0200 Subject: [PATCH 10/18] fix: changelog (#602) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cca560442..ebc157020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - switch downloads to downloads.snyk.io - allow annotations during IntelliJ indexing - add gutter icons for Snyk issues -- add option to switch gutter icons on/off in Snyk Settings (the IntelliJ setting only works for parsed languages) +- add option to switch gutter icons on/off - add color and highlighting setting for Snyk issues - add dialog to choose reference branch when delta scanning - always display info nodes From a530ebb8569f8e574b3de8b5b570e9a03ad04d1d Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Fri, 6 Sep 2024 10:25:04 +0200 Subject: [PATCH 11/18] feat: add net new issues setting to settings page [IDE-598] (#589) * feat: add net new issues setting to settings page * fix: set the language server setting for enableDeltaFindings * wip: adding setting for delta scans * chore: update changelog * fix: update the Net new settings text * chore: refactor delta finding settings * fix: refactor isNetNewIssuesSelected -> getNetNewIssuesSelected --- CHANGELOG.md | 2 +- .../SnykApplicationSettingsStateService.kt | 4 ++ .../SnykProjectSettingsConfigurable.kt | 51 +++++++------ .../io/snyk/plugin/ui/SnykSettingsDialog.kt | 71 +++++++++++++++++-- .../snyk/common/lsp/LanguageServerSettings.kt | 1 + .../snyk/common/lsp/LanguageServerWrapper.kt | 2 + 6 files changed, 102 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc157020..1ca913e35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,6 @@ # Snyk Security Changelog ## [2.9.1] - ### Changed - save git folder config in settings - propagate Jetbrains determined runtime environment to language server @@ -9,6 +8,7 @@ - guard base branch setting against being empty - better error messaging when unexpected loop occurs during initialization - switch downloads to downloads.snyk.io +- Added support for Net new scans in settings - allow annotations during IntelliJ indexing - add gutter icons for Snyk issues - add option to switch gutter icons on/off diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt index a813b380f..3ae24a2e8 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt @@ -35,6 +35,7 @@ class SnykApplicationSettingsStateService : PersistentStateComponent().getAll() - .values.joinToString("
") { "Base branch for ${it.folderPath}: ${it.baseBranch}" }+"" + baseBranchInfoLabel.text = service().getAll() + .values.joinToString("\n") { "Base branch for ${it.folderPath}: ${it.baseBranch}" } + netNewIssuesDropDown.selectedItem = applicationSettings.netNewIssues } } @@ -408,7 +411,8 @@ class SnykSettingsDialog( ), ) } - val productAndSeveritiesPanel = JPanel(UIGridLayoutManager(1, 2, JBUI.emptyInsets(), 30, -1)) + + val productAndSeveritiesPanel = JPanel(UIGridLayoutManager(2, 2, JBUI.emptyInsets(), 30, -1)) rootPanel.add( productAndSeveritiesPanel, @@ -459,6 +463,60 @@ class SnykSettingsDialog( ), ) + val netNewIssuesPanel = JPanel(UIGridLayoutManager(2, 4, JBUI.emptyInsets(), -1, -1)) + + productAndSeveritiesPanel.add( + netNewIssuesPanel, + baseGridConstraints( + row = 1, + column = 0, + anchor = UIGridConstraints.ANCHOR_NORTHWEST, + fill = UIGridConstraints.FILL_NONE, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, + indent = 0, + ), + ) + + val newNewIssuesLabel = JLabel("All Issues Vs Net New Issues:") + newNewIssuesLabel.labelFor = netNewIssuesDropDown + netNewIssuesPanel.add( + newNewIssuesLabel, + baseGridConstraintsAnchorWest( + row = 0, + indent = 0, + ), + ) + + netNewIssuesPanel.add( + netNewIssuesDropDown, + baseGridConstraints( + row = 0, + column = 1, + anchor = UIGridConstraints.ANCHOR_WEST, + fill = UIGridConstraints.FILL_HORIZONTAL, + hSizePolicy = UIGridConstraints.SIZEPOLICY_WANT_GROW, + indent = 0, + ), + ) + + val netNewIssuesText = + JLabel( + "Specifies whether to see only net new issues or all issues. " + + "Only applies to Code Security and Code Quality." + ).apply { font = FontUtil.minusOne(this.font) } + + netNewIssuesPanel.add( + netNewIssuesText, + baseGridConstraints( + row = 1, + column = 0, + anchor = UIGridConstraints.ANCHOR_WEST, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, + colSpan = 2, + indent = 0, + ), + ) + /** Project settings ------------------ */ if (isProjectSettingsAvailable(project)) { @@ -519,6 +577,7 @@ class SnykSettingsDialog( fill = UIGridConstraints.FILL_VERTICAL, hSizePolicy = 1, vSizePolicy = UIGridConstraints.SIZEPOLICY_WANT_GROW, + colSpan = 2, indent = 0, ), ) @@ -534,7 +593,7 @@ class SnykSettingsDialog( rootPanel.add( userExperiencePanel, baseGridConstraints( - row = 5, + row = 6, anchor = UIGridConstraints.ANCHOR_NORTHWEST, fill = UIGridConstraints.FILL_HORIZONTAL, hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, @@ -575,7 +634,7 @@ class SnykSettingsDialog( rootPanel.add( executableSettingsPanel, baseGridConstraints( - row = 4, + row = 5, anchor = UIGridConstraints.ANCHOR_NORTHWEST, fill = UIGridConstraints.FILL_HORIZONTAL, hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, @@ -756,5 +815,7 @@ class SnykSettingsDialog( fun getCliReleaseChannel(): String = cliReleaseChannelDropDown.selectedItem as String + fun getNetNewIssuesSelected(): String = netNewIssuesDropDown.selectedItem as String + fun getUseTokenAuthentication(): Boolean = useTokenAuthentication.selectedIndex == 1 } diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt index f48549abc..d94b3db21 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt @@ -43,6 +43,7 @@ data class LanguageServerSettings( @SerializedName("enableSnykOSSQuickFixCodeActions") val enableSnykOSSQuickFixCodeActions: String? = null, @SerializedName("requiredProtocolVersion") val requiredProtocolVersion: String = pluginSettings().requiredLsProtocolVersion.toString(), + @SerializedName("enableDeltaFindings") val enableDeltaFindings: String = pluginSettings().isDeltaFindingsEnabled(), @SerializedName("folderConfigs") val folderConfigs: List = service().getAll().values.toList() ) diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 339b344b3..62c89f875 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -17,6 +17,7 @@ import io.snyk.plugin.getSnykTaskQueueService import io.snyk.plugin.getWaitForResultsTimeout import io.snyk.plugin.isSnykIaCLSEnabled import io.snyk.plugin.pluginSettings +import io.snyk.plugin.services.SnykApplicationSettingsStateService import io.snyk.plugin.toLanguageServerURL import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import kotlinx.coroutines.DelicateCoroutinesApi @@ -405,6 +406,7 @@ class LanguageServerWrapper( integrationVersion = pluginInfo.integrationVersion, authenticationMethod = authMethod, enableSnykOSSQuickFixCodeActions = "true", + enableDeltaFindings = ps.isDeltaFindingsEnabled(), ) } From 9fc19764cab599d411b3e60e5879b4bde43f7f4c Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 6 Sep 2024 14:09:21 +0200 Subject: [PATCH 12/18] fix: settings layout (#603) * fix: settings layout * fix settings layout * fix: tests * fix: insets of net new * fix: clean ls caches on setting change between all/net-new issue scanning --- CHANGELOG.md | 6 +- src/main/kotlin/io/snyk/plugin/Utils.kt | 35 ++-- .../SnykApplicationSettingsStateService.kt | 8 +- .../SnykProjectSettingsConfigurable.kt | 5 + .../io/snyk/plugin/ui/SnykSettingsDialog.kt | 190 +++++++++--------- .../SnykToolWindowSnykScanListenerLS.kt | 4 +- .../snyk/common/lsp/LanguageServerSettings.kt | 2 +- .../snyk/common/lsp/LanguageServerWrapper.kt | 1 - .../SnykToolWindowSnykScanListenerLSTest.kt | 1 + 9 files changed, 134 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ca913e35..1de8ad615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Snyk Security Changelog -## [2.9.1] +## [2.10.0] ### Changed - save git folder config in settings - propagate Jetbrains determined runtime environment to language server @@ -8,12 +8,12 @@ - guard base branch setting against being empty - better error messaging when unexpected loop occurs during initialization - switch downloads to downloads.snyk.io -- Added support for Net new scans in settings +- added support for net new scans - allow annotations during IntelliJ indexing - add gutter icons for Snyk issues - add option to switch gutter icons on/off - add color and highlighting setting for Snyk issues -- add dialog to choose reference branch when delta scanning +- add dialog to choose reference branch when net new scanning - always display info nodes ### Fixes diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 4cbdc2e06..18881c6a1 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -252,32 +252,41 @@ fun getX509TrustManager(): X509TrustManager { return trustManagers[0] as X509TrustManager } -fun findPsiFileIgnoringExceptions(virtualFile: VirtualFile, project: Project): PsiFile? = - if (!virtualFile.isValid || project.isDisposed) { +fun findPsiFileIgnoringExceptions(virtualFile: VirtualFile, project: Project): PsiFile? { + return if (!virtualFile.isValid || project.isDisposed) { null } else { try { - PsiManager.getInstance(project).findFile(virtualFile) + var psiFile : PsiFile? = null + ReadAction.run { + psiFile = PsiManager.getInstance(project).findFile(virtualFile) + } + return psiFile } catch (ignored: Throwable) { null } } +} fun refreshAnnotationsForOpenFiles(project: Project) { if (project.isDisposed || ApplicationManager.getApplication().isDisposed) return - VirtualFileManager.getInstance().asyncRefresh() + runAsync { + VirtualFileManager.getInstance().asyncRefresh() - val openFiles = FileEditorManager.getInstance(project).openFiles + val openFiles = FileEditorManager.getInstance(project).openFiles - ApplicationManager.getApplication().invokeLater { - if (!project.isDisposed) { - project.service().invalidateProvider(CodeVisionHost.LensInvalidateSignal(null)) + ApplicationManager.getApplication().invokeLater { + if (!project.isDisposed) { + project.service().invalidateProvider(CodeVisionHost.LensInvalidateSignal(null)) + } } - } - openFiles.forEach { - val psiFile = findPsiFileIgnoringExceptions(it, project) - if (psiFile != null) { - DaemonCodeAnalyzer.getInstance(project).restart(psiFile) + openFiles.forEach { + val psiFile = findPsiFileIgnoringExceptions(it, project) + if (psiFile != null) { + invokeLater { + DaemonCodeAnalyzer.getInstance(project).restart(psiFile) + } + } } } } diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt index 3ae24a2e8..b2db93677 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt @@ -101,8 +101,8 @@ class SnykApplicationSettingsStateService : PersistentStateComponent().getAllForProject(project).forEach { val branchChooserTreeNode = ChooseBranchNode( diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt index d94b3db21..a48a5223c 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt @@ -43,7 +43,7 @@ data class LanguageServerSettings( @SerializedName("enableSnykOSSQuickFixCodeActions") val enableSnykOSSQuickFixCodeActions: String? = null, @SerializedName("requiredProtocolVersion") val requiredProtocolVersion: String = pluginSettings().requiredLsProtocolVersion.toString(), - @SerializedName("enableDeltaFindings") val enableDeltaFindings: String = pluginSettings().isDeltaFindingsEnabled(), + @SerializedName("enableDeltaFindings") val enableDeltaFindings: String = pluginSettings().isDeltaFindingsEnabled().toString(), @SerializedName("folderConfigs") val folderConfigs: List = service().getAll().values.toList() ) diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 62c89f875..42a898eff 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -406,7 +406,6 @@ class LanguageServerWrapper( integrationVersion = pluginInfo.integrationVersion, authenticationMethod = authMethod, enableSnykOSSQuickFixCodeActions = "true", - enableDeltaFindings = ps.isDeltaFindingsEnabled(), ) } diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt index 55c72350c..0a876e8e6 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -63,6 +63,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootOssIssuesTreeNode = RootOssTreeNode(project) rootSecurityIssuesTreeNode = RootSecurityIssuesTreeNode(project) rootQualityIssuesTreeNode = RootQualityIssuesTreeNode(project) + pluginSettings().setDeltaEnabled() } private fun mockScanIssues( From 7bda3b10eb59bfef382cc3a697a4917ac2188962 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Mon, 9 Sep 2024 10:05:31 +0200 Subject: [PATCH 13/18] fix: only create one annotation per finding, not one per fix (#604) * fix: only create one annotations and associate several fixes with it * chore: improve annotator performance call language server for code actions only once per range, do pre-sort before UI thread * refactoring: optimize usage of invokeLater * chore: cleanup unused code --- src/main/kotlin/io/snyk/plugin/Severity.kt | 9 - .../plugin/services/SnykTaskQueueService.kt | 5 +- .../ui/toolwindow/SnykToolWindowPanel.kt | 60 ++-- .../SnykToolWindowSnykScanListenerLS.kt | 6 +- .../kotlin/snyk/common/AnnotatorCommon.kt | 5 +- .../snyk/common/annotator/SnykAnnotator.kt | 288 +++++++++--------- .../SnykIntentionActionBase.kt | 2 +- .../kotlin/snyk/common/lsp/RangeConverter.kt | 48 +++ .../snyk/common/lsp/SnykLanguageClient.kt | 18 +- .../container/ContainerBulkFileListener.kt | 2 +- .../kotlin/snyk/iac/IacBulkFileListener.kt | 5 +- .../snyk/iac/IgnoreButtonActionListener.kt | 5 +- src/main/kotlin/snyk/net/HttpClient.kt | 82 ----- .../kotlin/snyk/net/HttpLoggingInterceptor.kt | 32 -- src/test/kotlin/snyk/net/HttpClientTest.kt | 54 ---- 15 files changed, 240 insertions(+), 381 deletions(-) create mode 100644 src/main/kotlin/snyk/common/lsp/RangeConverter.kt delete mode 100644 src/main/kotlin/snyk/net/HttpClient.kt delete mode 100644 src/main/kotlin/snyk/net/HttpLoggingInterceptor.kt delete mode 100644 src/test/kotlin/snyk/net/HttpClientTest.kt diff --git a/src/main/kotlin/io/snyk/plugin/Severity.kt b/src/main/kotlin/io/snyk/plugin/Severity.kt index 88a4f0e16..cab4acd78 100644 --- a/src/main/kotlin/io/snyk/plugin/Severity.kt +++ b/src/main/kotlin/io/snyk/plugin/Severity.kt @@ -72,15 +72,6 @@ enum class Severity { private const val SEVERITY_MEDIUM = "medium" private const val SEVERITY_LOW = "low" - fun getFromIndex(index: Int): Severity = - when (index) { - 4 -> CRITICAL - 3 -> HIGH - 2 -> MEDIUM - 1 -> LOW - else -> UNKNOWN - } - fun getFromName(name: String): Severity = when (name) { SEVERITY_CRITICAL -> CRITICAL diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt index f8b0e1766..7ed43d59e 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt @@ -1,7 +1,6 @@ package io.snyk.plugin.services import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.invokeLater import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.fileEditor.FileDocumentManager @@ -158,7 +157,7 @@ class SnykTaskQueueService(val project: Project) { } } logger.debug("Container scan completed") - invokeLater { refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } }) } @@ -201,7 +200,7 @@ class SnykTaskQueueService(val project: Project) { } } logger.debug("IaC scan completed") - invokeLater { refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } }) } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt index 41beb5c27..aec98ef48 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -179,19 +179,19 @@ class SnykToolWindowPanel( override fun scanningIacFinished(iacResult: IacResult) { ApplicationManager.getApplication().invokeLater { displayIacResults(iacResult) - if (iacResult.getVisibleErrors().isNotEmpty()) { - notifyAboutErrorsIfNeeded(ProductType.IAC, iacResult) - } - refreshAnnotationsForOpenFiles(project) } + if (iacResult.getVisibleErrors().isNotEmpty()) { + notifyAboutErrorsIfNeeded(ProductType.IAC, iacResult) + } + refreshAnnotationsForOpenFiles(project) } override fun scanningContainerFinished(containerResult: ContainerResult) { ApplicationManager.getApplication().invokeLater { displayContainerResults(containerResult) - notifyAboutErrorsIfNeeded(ProductType.CONTAINER, containerResult) - refreshAnnotationsForOpenFiles(project) } + notifyAboutErrorsIfNeeded(ProductType.CONTAINER, containerResult) + refreshAnnotationsForOpenFiles(project) } private fun notifyAboutErrorsIfNeeded( @@ -214,38 +214,38 @@ class SnykToolWindowPanel( override fun scanningIacError(snykError: SnykError) { var iacResultsCount: Int? = null - ApplicationManager.getApplication().invokeLater { - if (snykError.code != null && ignorableErrorCodes.contains(snykError.code)) { - iacResultsCount = NODE_NOT_SUPPORTED_STATE - } else { - SnykBalloonNotificationHelper.showError(snykError.message, project) - if (snykError.message.startsWith(AUTH_FAILED_TEXT)) { - pluginSettings().token = null - } + if (snykError.code != null && ignorableErrorCodes.contains(snykError.code)) { + iacResultsCount = NODE_NOT_SUPPORTED_STATE + } else { + SnykBalloonNotificationHelper.showError(snykError.message, project) + if (snykError.message.startsWith(AUTH_FAILED_TEXT)) { + pluginSettings().token = null } + } + ApplicationManager.getApplication().invokeLater { removeAllChildren(listOf(rootIacIssuesTreeNode)) updateTreeRootNodesPresentation(iacResultsCount = iacResultsCount) chooseMainPanelToDisplay() - refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } override fun scanningContainerError(snykError: SnykError) { var containerResultsCount: Int? = null - ApplicationManager.getApplication().invokeLater { - if (snykError == ContainerService.NO_IMAGES_TO_SCAN_ERROR) { - containerResultsCount = NODE_NOT_SUPPORTED_STATE - } else { - SnykBalloonNotificationHelper.showError(snykError.message, project) - if (snykError.message.startsWith(AUTH_FAILED_TEXT)) { - pluginSettings().token = null - } + if (snykError == ContainerService.NO_IMAGES_TO_SCAN_ERROR) { + containerResultsCount = NODE_NOT_SUPPORTED_STATE + } else { + SnykBalloonNotificationHelper.showError(snykError.message, project) + if (snykError.message.startsWith(AUTH_FAILED_TEXT)) { + pluginSettings().token = null } + } + ApplicationManager.getApplication().invokeLater { removeAllChildren(listOf(rootContainerIssuesTreeNode)) updateTreeRootNodesPresentation(containerResultsCount = containerResultsCount) chooseMainPanelToDisplay() - refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } }, ) @@ -256,20 +256,20 @@ class SnykToolWindowPanel( SnykResultsFilteringListener.SNYK_FILTERING_TOPIC, object : SnykResultsFilteringListener { override fun filtersChanged() { + val codeResultsLS = + getSnykCachedResultsForProduct(project, ProductType.CODE_SECURITY) ?: return ApplicationManager.getApplication().invokeLater { - val codeResultsLS = - getSnykCachedResultsForProduct(project, ProductType.CODE_SECURITY) ?: return@invokeLater scanListenerLS.displaySnykCodeResults(codeResultsLS) } + val ossResultsLS = + getSnykCachedResultsForProduct(project, ProductType.OSS) ?: return ApplicationManager.getApplication().invokeLater { - val ossResultsLS = - getSnykCachedResultsForProduct(project, ProductType.OSS) ?: return@invokeLater scanListenerLS.displayOssResults(ossResultsLS) } + val snykCachedResults = getSnykCachedResults(project) ?: return ApplicationManager.getApplication().invokeLater { - val snykCachedResults = getSnykCachedResults(project) ?: return@invokeLater snykCachedResults.currentIacResult?.let { displayIacResults(it) } snykCachedResults.currentContainerResult?.let { displayContainerResults(it) } } @@ -391,8 +391,8 @@ class SnykToolWindowPanel( ApplicationManager.getApplication().invokeLater { doCleanUi(true) - refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } private fun doCleanUi(reDisplayDescription: Boolean) { diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt index 68f3f837f..081bf42ff 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -77,22 +77,22 @@ class SnykToolWindowSnykScanListenerLS( this.snykToolWindowPanel.triggerSelectionListeners = false val snykCachedResults = getSnykCachedResults(project) displaySnykCodeResults(snykCachedResults?.currentSnykCodeResultsLS ?: emptyMap()) - refreshAnnotationsForOpenFiles(project) this.snykToolWindowPanel.triggerSelectionListeners = true } + refreshAnnotationsForOpenFiles(project) } override fun scanningOssFinished() { if (disposed) return + cancelOssIndicator(project) ApplicationManager.getApplication().invokeLater { - cancelOssIndicator(project) this.rootOssIssuesTreeNode.userObject = "$OSS_ROOT_TEXT (scanning finished)" this.snykToolWindowPanel.triggerSelectionListeners = false val snykCachedResults = getSnykCachedResults(project) displayOssResults(snykCachedResults?.currentOSSResultsLS ?: emptyMap()) - refreshAnnotationsForOpenFiles(project) this.snykToolWindowPanel.triggerSelectionListeners = true } + refreshAnnotationsForOpenFiles(project) } override fun scanningError(snykScan: SnykScanParams) { diff --git a/src/main/kotlin/snyk/common/AnnotatorCommon.kt b/src/main/kotlin/snyk/common/AnnotatorCommon.kt index 8f2747bb7..ac5d6fc81 100644 --- a/src/main/kotlin/snyk/common/AnnotatorCommon.kt +++ b/src/main/kotlin/snyk/common/AnnotatorCommon.kt @@ -1,7 +1,6 @@ package snyk.common import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.invokeLater import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile @@ -37,7 +36,7 @@ object AnnotatorCommon { SnykProductsOrSeverityListener.SNYK_ENABLEMENT_TOPIC, object : SnykProductsOrSeverityListener { override fun enablementChanged() { - invokeLater { refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } } ) @@ -46,7 +45,7 @@ object AnnotatorCommon { SnykSettingsListener.SNYK_SETTINGS_TOPIC, object : SnykSettingsListener { override fun settingsChanged() { - invokeLater { refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } } ) diff --git a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt index fa7a94879..ce9301a3f 100644 --- a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -17,16 +17,20 @@ import com.intellij.openapi.editor.markup.GutterIconRenderer import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.guessProjectForFile import com.intellij.openapi.util.TextRange +import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiFile import icons.SnykIcons import io.snyk.plugin.Severity import io.snyk.plugin.getSnykCachedResultsForProduct import io.snyk.plugin.getSnykToolWindowPanel import io.snyk.plugin.toLanguageServerURL +import org.eclipse.lsp4j.CodeAction import org.eclipse.lsp4j.CodeActionContext import org.eclipse.lsp4j.CodeActionParams +import org.eclipse.lsp4j.Command import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.jsonrpc.messages.Either import org.jetbrains.concurrency.runAsync import snyk.common.AnnotatorCommon import snyk.common.ProductType @@ -37,6 +41,7 @@ import snyk.common.annotator.SnykAnnotationAttributeKey.medium import snyk.common.annotator.SnykAnnotationAttributeKey.unknown import snyk.common.annotator.SnykAnnotator.SnykAnnotation import snyk.common.lsp.LanguageServerWrapper +import snyk.common.lsp.RangeConverter import snyk.common.lsp.ScanIssue import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException @@ -45,8 +50,12 @@ import javax.swing.Icon private const val CODEACTION_TIMEOUT = 5000L +typealias SnykAnnotationInput = Pair>> +typealias SnykAnnotationList = List + abstract class SnykAnnotator(private val product: ProductType) : - ExternalAnnotator>, List>(), Disposable, DumbAware { + ExternalAnnotator(), Disposable, DumbAware { + private val lineMarkerProviderDescriptor: SnykLineMarkerProvider = getLineMarkerProvider() val logger = logger() protected var disposed = false @@ -65,72 +74,92 @@ abstract class SnykAnnotator(private val product: ProductType) : val annotationSeverity: HighlightSeverity, val annotationMessage: String, val range: TextRange, - val intention: IntentionAction, + val intentionActions: MutableList = mutableListOf(), var gutterIconRenderer: GutterIconRenderer? = null ) // overrides needed for the Annotator to invoke apply(). We don't do anything here - override fun collectInformation(file: PsiFile): Pair> { - return Pair(file, getIssuesForFile(file) + override fun collectInformation(file: PsiFile): SnykAnnotationInput? { + val map = getIssuesForFile(file) .filter { AnnotatorCommon.isSeverityToShow(it.getSeverityAsEnum()) } - .distinctBy { it.id }) + .sortedByDescending { it.getSeverityAsEnum() } + .groupBy { it.range } + .toMap() + + return Pair(file, map) } - override fun doAnnotate(initialInfo: Pair>): List { + override fun doAnnotate(initialInfo: SnykAnnotationInput): SnykAnnotationList { if (disposed) return emptyList() - AnnotatorCommon.prepareAnnotate(initialInfo.first) if (!LanguageServerWrapper.getInstance().isInitialized) return emptyList() - val lineMarkerProviderDescriptor: SnykLineMarkerProvider = getLineMarkerProvider() + val psiFile = initialInfo.first val gutterIconEnabled = LineMarkerSettings.getSettings().isEnabled(lineMarkerProviderDescriptor) + AnnotatorCommon.prepareAnnotate(psiFile) + + val codeActions = initialInfo.second + .map { entry -> + entry.key to getCodeActions(psiFile.virtualFile, entry.key).map { it.right } + .sortedBy { it.title } + }.toMap() + val annotations = mutableListOf() - val gutterIcons: MutableSet = mutableSetOf() + initialInfo.second.forEach { entry -> + val textRange = RangeConverter.convertToTextRange(psiFile, entry.key) + if (textRange == null || textRange.isEmpty) { + logger.warn("Invalid range for range: $textRange") + return@forEach + } + annotations.addAll( + doAnnotateIssue(entry, textRange, gutterIconEnabled, codeActions) + ) + } + return annotations.sortedByDescending { it.issue.getSeverityAsEnum() } + } - initialInfo.second.sortedByDescending { it.getSeverityAsEnum() }.forEach { issue -> - val textRange = textRange(initialInfo.first, issue.range) + private fun doAnnotateIssue( + entry: Map.Entry>, + textRange: TextRange, + gutterIconEnabled: Boolean, + codeActions: Map>, + ): List { + val gutterIcons: MutableSet = mutableSetOf() + val annotations = mutableListOf() + entry.value.forEach { issue -> val highlightSeverity = issue.getSeverityAsEnum().getHighlightSeverity() val annotationMessage = issue.annotationMessage() - if (textRange == null) { - logger.warn("Invalid range for issue: $issue") - return@forEach - } - if (!textRange.isEmpty) { - val detailAnnotation = SnykAnnotation( - issue, - highlightSeverity, - annotationMessage, - textRange, - ShowDetailsIntentionAction(annotationMessage, issue) - ) - - val gutterIconRenderer = - if (gutterIconEnabled && !gutterIcons.contains(textRange)) { - gutterIcons.add(textRange) - SnykShowDetailsGutterRenderer(detailAnnotation) - } else { - null - } - detailAnnotation.gutterIconRenderer = gutterIconRenderer - annotations.add(detailAnnotation) - - annotations.addAll( - getAnnotationsForCodeActions( - initialInfo, - issue, - highlightSeverity, - textRange - ) - ) - } + val detailAnnotation = SnykAnnotation( + issue, + highlightSeverity, + annotationMessage, + textRange, + ) + + val gutterIconRenderer = + if (gutterIconEnabled && !gutterIcons.contains(textRange)) { + gutterIcons.add(textRange) + SnykShowDetailsGutterRenderer(detailAnnotation) + } else { + null + } + + val languageServerIntentionActions = codeActions[entry.key]?.let { range -> + getCodeActionsAsIntentionActions(issue, range) + } ?: emptyList() + + detailAnnotation.gutterIconRenderer = gutterIconRenderer + detailAnnotation.intentionActions.add(ShowDetailsIntentionAction(annotationMessage, issue)) + detailAnnotation.intentionActions.addAll(languageServerIntentionActions) + annotations.add(detailAnnotation) } return annotations } override fun apply( psiFile: PsiFile, - annotationResult: List, + annotationResult: SnykAnnotationList, holder: AnnotationHolder, ) { if (disposed) return @@ -138,29 +167,51 @@ abstract class SnykAnnotator(private val product: ProductType) : annotationResult .forEach { annotation -> if (!annotation.range.isEmpty) { - val annoBuilder = holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) + val annoBuilder = holder + .newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) .range(annotation.range) .textAttributes(getTextAttributeKeyBySeverity(annotation.issue.getSeverityAsEnum())) - .withFix(annotation.intention) + + annotation.intentionActions.forEach { + annoBuilder.withFix(it) + } + if (annotation.gutterIconRenderer != null) { annoBuilder.gutterIconRenderer(SnykShowDetailsGutterRenderer(annotation)) } + annoBuilder.create() } } } - private fun getAnnotationsForCodeActions( - initial: Pair>, + private fun getCodeActionsAsIntentionActions( issue: ScanIssue, - highlightSeverity: HighlightSeverity, - textRange: TextRange, - ): MutableList { - val addedAnnotationsList = mutableListOf() + codeActions: List + ): MutableList { + val addedIntentionActions = mutableListOf() + + codeActions + .filter { action -> + val diagnosticCode = action.diagnostics?.get(0)?.code?.left + val ruleId = issue.ruleId() + diagnosticCode == ruleId + } + .forEach { action -> + addedIntentionActions.add(CodeActionIntention(issue, action, product)) + } + + return addedIntentionActions + } + + private fun getCodeActions( + file: VirtualFile, + range: Range + ): List> { val params = CodeActionParams( - TextDocumentIdentifier(initial.first.virtualFile.toLanguageServerURL()), - issue.range, + TextDocumentIdentifier(file.toLanguageServerURL()), + range, CodeActionContext(emptyList()), ) val languageServer = LanguageServerWrapper.getInstance().languageServer @@ -169,29 +220,10 @@ abstract class SnykAnnotator(private val product: ProductType) : languageServer.textDocumentService .codeAction(params).get(CODEACTION_TIMEOUT, TimeUnit.MILLISECONDS) ?: emptyList() } catch (ignored: TimeoutException) { - logger.info("Timeout fetching code actions for issue: $issue") + logger.info("Timeout fetching code actions for range: $range") emptyList() } - - codeActions - .filter { a -> - val diagnosticCode = a.right.diagnostics?.get(0)?.code?.left - val ruleId = issue.ruleId() - diagnosticCode == ruleId - } - .sortedBy { it.right.title }.forEach { action -> - val codeAction = action.right - val title = codeAction.title - val codeActionAnnotation = SnykAnnotation( - issue, - highlightSeverity, - title, - textRange, - CodeActionIntention(issue, codeAction, product) - ) - addedAnnotationsList.add(codeActionAnnotation) - } - return addedAnnotationsList + return codeActions } private fun getLineMarkerProvider(): SnykLineMarkerProvider { @@ -222,87 +254,51 @@ abstract class SnykAnnotator(private val product: ProductType) : ?.toSet() ?: emptySet() - /** Public for Tests only */ - fun textRange( - psiFile: PsiFile, - range: Range, - ): TextRange? { - try { - val document = - psiFile.viewProvider.document ?: throw IllegalArgumentException("No document found for $psiFile") - val startRow = range.start.line - val endRow = range.end.line - val startCol = range.start.character - val endCol = range.end.character - - if (startRow < 0 || startRow > document.lineCount - 1) { - return null - } - if (endRow < 0 || endRow > document.lineCount - 1 || endRow < startRow) { - return null - } - - val lineOffSet = document.getLineStartOffset(startRow) + startCol - val lineOffSetEnd = document.getLineStartOffset(endRow) + endCol - - if (lineOffSet < 0 || lineOffSet > document.textLength - 1) { - return null - } - if (lineOffSetEnd < 0 || lineOffSetEnd < lineOffSet || lineOffSetEnd > document.textLength - 1) { - return null - } - - return TextRange.create(lineOffSet, lineOffSetEnd) - } catch (e: IllegalArgumentException) { - logger.warn(e) - return TextRange.EMPTY_RANGE + class SnykShowDetailsGutterRenderer(val annotation: SnykAnnotation) : GutterIconRenderer() { + override fun equals(other: Any?): Boolean { + return annotation == other } - } -} -class SnykShowDetailsGutterRenderer(val annotation: SnykAnnotation) : GutterIconRenderer() { - override fun equals(other: Any?): Boolean { - return annotation == other - } - - override fun hashCode(): Int { - return annotation.hashCode() - } + override fun hashCode(): Int { + return annotation.hashCode() + } - override fun getIcon(): Icon { - return SnykIcons.getSeverityIcon(annotation.issue.getSeverityAsEnum()) - } + override fun getIcon(): Icon { + return SnykIcons.getSeverityIcon(annotation.issue.getSeverityAsEnum()) + } - override fun getClickAction(): AnAction? { - if (annotation.intention !is ShowDetailsIntentionAction) return null - return getShowDetailsNavigationAction(annotation.intention) - } + override fun getClickAction(): AnAction? { + val intention = + annotation.intentionActions.firstOrNull { it is ShowDetailsIntentionAction } ?: return null + return getShowDetailsNavigationAction(intention as ShowDetailsIntentionAction) + } - private fun getShowDetailsNavigationAction(intention: ShowDetailsIntentionAction) = - object : AnAction() { - override fun actionPerformed(e: AnActionEvent) { - runAsync { - val virtualFile = intention.issue.virtualFile ?: return@runAsync - val project = guessProjectForFile(virtualFile) ?: return@runAsync - val toolWindowPanel = getSnykToolWindowPanel(project) ?: return@runAsync - intention.selectNodeAndDisplayDescription(toolWindowPanel) + private fun getShowDetailsNavigationAction(intention: ShowDetailsIntentionAction) = + object : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + runAsync { + val virtualFile = intention.issue.virtualFile ?: return@runAsync + val project = guessProjectForFile(virtualFile) ?: return@runAsync + val toolWindowPanel = getSnykToolWindowPanel(project) ?: return@runAsync + intention.selectNodeAndDisplayDescription(toolWindowPanel) + } } } - } - override fun getTooltipText(): String { - return annotation.annotationMessage - } + override fun getTooltipText(): String { + return annotation.annotationMessage + } - override fun getAccessibleName(): String { - return annotation.annotationMessage - } + override fun getAccessibleName(): String { + return annotation.annotationMessage + } - override fun isNavigateAction(): Boolean { - return true - } + override fun isNavigateAction(): Boolean { + return true + } - override fun isDumbAware(): Boolean { - return true + override fun isDumbAware(): Boolean { + return true + } } } diff --git a/src/main/kotlin/snyk/common/intentionactions/SnykIntentionActionBase.kt b/src/main/kotlin/snyk/common/intentionactions/SnykIntentionActionBase.kt index fd2ad6f71..e0cadb66e 100644 --- a/src/main/kotlin/snyk/common/intentionactions/SnykIntentionActionBase.kt +++ b/src/main/kotlin/snyk/common/intentionactions/SnykIntentionActionBase.kt @@ -7,7 +7,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.Iconable import com.intellij.psi.PsiFile -abstract class SnykIntentionActionBase() : IntentionAction, Iconable, PriorityAction { +abstract class SnykIntentionActionBase : IntentionAction, Iconable, PriorityAction { override fun startInWriteAction(): Boolean = true diff --git a/src/main/kotlin/snyk/common/lsp/RangeConverter.kt b/src/main/kotlin/snyk/common/lsp/RangeConverter.kt new file mode 100644 index 000000000..216606fe9 --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/RangeConverter.kt @@ -0,0 +1,48 @@ +package snyk.common.lsp + +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiFile +import org.eclipse.lsp4j.Range + +class RangeConverter { + companion object { + val logger = logger() + /** Public for Tests only */ + fun convertToTextRange( + psiFile: PsiFile, + range: Range, + ): TextRange? { + try { + val document = + psiFile.viewProvider.document ?: throw IllegalArgumentException("No document found for $psiFile") + val startRow = range.start.line + val endRow = range.end.line + val startCol = range.start.character + val endCol = range.end.character + + if (startRow < 0 || startRow > document.lineCount - 1) { + return null + } + if (endRow < 0 || endRow > document.lineCount - 1 || endRow < startRow) { + return null + } + + val lineOffSet = document.getLineStartOffset(startRow) + startCol + val lineOffSetEnd = document.getLineStartOffset(endRow) + endCol + + if (lineOffSet < 0 || lineOffSet > document.textLength - 1) { + return null + } + if (lineOffSetEnd < 0 || lineOffSetEnd < lineOffSet || lineOffSetEnd > document.textLength - 1) { + return null + } + + return TextRange.create(lineOffSet, lineOffSetEnd) + } catch (e: IllegalArgumentException) { + logger.warn(e) + return TextRange.EMPTY_RANGE + } + } + } +} diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index 29f192460..e8af51dd1 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -19,11 +19,9 @@ import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectLocator import com.intellij.openapi.project.ProjectManager -import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.VfsUtilCore -import com.intellij.openapi.vfs.VirtualFileManager import io.snyk.plugin.SnykFile import io.snyk.plugin.events.SnykScanListenerLS import io.snyk.plugin.getContentRootVirtualFiles @@ -187,19 +185,17 @@ class SnykLanguageClient : private fun refreshUI(): CompletableFuture { val completedFuture: CompletableFuture = CompletableFuture.completedFuture(null) if (disposed) return completedFuture - - ProjectManager - .getInstance() - .openProjects - .filter { !it.isDisposed } - .forEach { project -> - runAsync { + runAsync { + ProjectManager + .getInstance() + .openProjects + .filter { !it.isDisposed } + .forEach { project -> ReadAction.run { if (!project.isDisposed) refreshAnnotationsForOpenFiles(project) } } - } - VirtualFileManager.getInstance().asyncRefresh() + } return completedFuture } diff --git a/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt b/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt index ac28a3f7e..6b80f37e3 100644 --- a/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt +++ b/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt @@ -76,8 +76,8 @@ class ContainerBulkFileListener : SnykBulkFileListener() { snykCachedResults.currentContainerResult = newContainerCache ApplicationManager.getApplication().invokeLater { getSnykToolWindowPanel(project)?.displayContainerResults(newContainerCache) - refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } private fun makeObsolete(containerIssuesForImage: ContainerIssuesForImage): ContainerIssuesForImage = diff --git a/src/main/kotlin/snyk/iac/IacBulkFileListener.kt b/src/main/kotlin/snyk/iac/IacBulkFileListener.kt index 20545ff09..be1f02083 100644 --- a/src/main/kotlin/snyk/iac/IacBulkFileListener.kt +++ b/src/main/kotlin/snyk/iac/IacBulkFileListener.kt @@ -49,14 +49,15 @@ class IacBulkFileListener : SnykBulkFileListener() { } .forEach(::markObsolete) - val changed = iacRelatedVFsAffected.isNotEmpty() // for new/deleted/renamed files we also need to "dirty" the cache, too + val changed = + iacRelatedVFsAffected.isNotEmpty() // for new/deleted/renamed files we also need to "dirty" the cache, too if (changed) { log.debug("update IaC cache for $iacRelatedVFsAffected") currentIacResult.iacScanNeeded = true ApplicationManager.getApplication().invokeLater { getSnykToolWindowPanel(project)?.displayIacResults(currentIacResult) - refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } } diff --git a/src/main/kotlin/snyk/iac/IgnoreButtonActionListener.kt b/src/main/kotlin/snyk/iac/IgnoreButtonActionListener.kt index baac7f5a3..217ce8601 100644 --- a/src/main/kotlin/snyk/iac/IgnoreButtonActionListener.kt +++ b/src/main/kotlin/snyk/iac/IgnoreButtonActionListener.kt @@ -1,6 +1,5 @@ package snyk.iac -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task @@ -51,9 +50,7 @@ class IgnoreButtonActionListener( isEnabled = false text = IGNORED_ISSUE_BUTTON_TEXT } - ApplicationManager.getApplication().invokeLater { - refreshAnnotationsForOpenFiles(project) - } + refreshAnnotationsForOpenFiles(project) } catch (e: IgnoreException) { SnykBalloonNotificationHelper.showError( "Ignoring did not succeed. Error message: ${e.message})", project diff --git a/src/main/kotlin/snyk/net/HttpClient.kt b/src/main/kotlin/snyk/net/HttpClient.kt deleted file mode 100644 index 0559b48d6..000000000 --- a/src/main/kotlin/snyk/net/HttpClient.kt +++ /dev/null @@ -1,82 +0,0 @@ -package snyk.net - -import com.intellij.ide.BrowserUtil -import com.intellij.notification.NotificationAction -import io.snyk.plugin.getSSLContext -import io.snyk.plugin.getX509TrustManager -import io.snyk.plugin.ui.SnykBalloonNotificationHelper -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import java.security.SecureRandom -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit -import javax.net.ssl.HostnameVerifier -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager - -/** - * An HTTP client. - * - * An `HttpClient` can be used in API clients based on Retrofit. It can be used - * to configure per-client state, like: a proxy, an authenticator, etc. - * - * Default timeout values: - * - _connect_ - 30 seconds - * - _read_ - 60 seconds - * - _write_ - 60 seconds - */ -class HttpClient( - var connectTimeout: Long = 30, - var readTimeout: Long = 60, - var writeTimeout: Long = 60, - private var disableSslVerification: Boolean = false, - var interceptors: List = listOf() -) { - companion object { - const val BALLOON_MESSAGE_ILLEGAL_STATE_EXCEPTION = - "Could not initialize SSL security to communicate with the Snyk services (%s)." - } - - fun build(): OkHttpClient { - try { - val httpClientBuilder = OkHttpClient.Builder() - .connectTimeout(connectTimeout, TimeUnit.SECONDS) - .readTimeout(readTimeout, TimeUnit.SECONDS) - .writeTimeout(writeTimeout, TimeUnit.SECONDS) - .sslSocketFactory(getSSLContext().socketFactory, getX509TrustManager()) - httpClientBuilder.interceptors().addAll(interceptors) - - if (disableSslVerification) { - httpClientBuilder.ignoreAllSslErrors() - } - - return httpClientBuilder.build() - } catch (e: IllegalStateException) { - val message = String.format(BALLOON_MESSAGE_ILLEGAL_STATE_EXCEPTION, e.localizedMessage) - SnykBalloonNotificationHelper.showError( - message, - null, - NotificationAction.createSimple("Contact support...") { - BrowserUtil.browse("https://snyk.io/contact-us/?utm_source=JETBRAINS_IDE") - }) - throw e - } - } -} - -private fun OkHttpClient.Builder.ignoreAllSslErrors() { - val unsafeTrustManager = object : X509TrustManager { - override fun checkClientTrusted(certs: Array, authType: String) = Unit - override fun checkServerTrusted(certs: Array, authType: String) = Unit - override fun getAcceptedIssuers(): Array = arrayOf() - } - - val insecureSocketFactory = SSLContext.getInstance("TLSv1.2").apply { - val trustAllCertificates = arrayOf(unsafeTrustManager) - init(null, trustAllCertificates, SecureRandom()) - }.socketFactory - - sslSocketFactory(insecureSocketFactory, unsafeTrustManager) - hostnameVerifier(HostnameVerifier { _, _ -> true }) -} diff --git a/src/main/kotlin/snyk/net/HttpLoggingInterceptor.kt b/src/main/kotlin/snyk/net/HttpLoggingInterceptor.kt deleted file mode 100644 index d2e162f6b..000000000 --- a/src/main/kotlin/snyk/net/HttpLoggingInterceptor.kt +++ /dev/null @@ -1,32 +0,0 @@ -package snyk.net - -import com.intellij.openapi.diagnostic.Logger -import okhttp3.Interceptor -import okhttp3.Response -import okhttp3.internal.closeQuietly -import okio.Buffer - -class HttpLoggingInterceptor(private val log: Logger) : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - - val buffer = Buffer() - val requestBody = request.body - requestBody?.writeTo(buffer) - log.warn("--> ${request.method} ${request.url}, payload=${buffer.readUtf8()}") - - val response = chain.proceed(request) - val responseBody = response.body - val responseStr = responseBody?.string() - var responseBodyStr = "" - if (responseStr != null) { - if (responseStr.isNotEmpty()) { - responseBodyStr = responseStr.take(2000) - } - } - log.warn("<-- HTTP Response: code=${response.code}, message=${response.message}, body=${responseBodyStr}") - responseBody?.closeQuietly() - - return chain.proceed(request) - } -} diff --git a/src/test/kotlin/snyk/net/HttpClientTest.kt b/src/test/kotlin/snyk/net/HttpClientTest.kt deleted file mode 100644 index 9a084edf3..000000000 --- a/src/test/kotlin/snyk/net/HttpClientTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package snyk.net - -import io.mockk.every -import io.mockk.justRun -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.mockkStatic -import io.mockk.unmockkAll -import io.mockk.verify -import io.snyk.plugin.ui.SnykBalloonNotificationHelper -import junit.framework.TestCase.assertFalse -import org.junit.After -import org.junit.Before -import org.junit.Test -import javax.net.ssl.TrustManagerFactory - -class HttpClientTest { - - @Before - fun setUp() { - unmockkAll() - } - - @After - fun tearDown() { - unmockkAll() - } - - @Test - fun `should use SSLContext with TLSv12 configured`() { - val client = HttpClient().build() - assertFalse(client.sslSocketFactory.supportedCipherSuites.contains("TLS_RSA_WITH_DES_CBC_SHA")) - } - - @Test(expected = IllegalStateException::class) - fun `should display balloon error message if ssl context cannot be initialized`() { - mockkStatic(TrustManagerFactory::class) - val trustManagerFactory = mockk(relaxed = true) - every { TrustManagerFactory.getInstance(any()) } returns trustManagerFactory - val exception = IllegalStateException("Test exception. Don't panic") - every { trustManagerFactory.trustManagers } throws exception - - mockkObject(SnykBalloonNotificationHelper) - justRun { SnykBalloonNotificationHelper.showError(any(), null, any()) } - - try { - HttpClient().build() - } finally { - val balloonMessage = - String.format(HttpClient.BALLOON_MESSAGE_ILLEGAL_STATE_EXCEPTION, exception.localizedMessage) - verify(exactly = 1) { SnykBalloonNotificationHelper.showError(balloonMessage, null, any()) } - } - } -} From 0f2595caf8c6b88f78a425bdc9d418a060d45f9c Mon Sep 17 00:00:00 2001 From: Arvyd Date: Mon, 9 Sep 2024 14:19:53 +0200 Subject: [PATCH 14/18] chore: fixed parser of the Readme according to the latest state of the docs, to properly find the plugin description --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index bf62b3dae..37e1c8d12 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -175,7 +175,7 @@ tasks { untilBuild.set(properties("pluginUntilBuild")) val content = File("$projectDir/README.md").readText() - val startIndex = content.indexOf("# JetBrains plugins") + val startIndex = content.indexOf("# JetBrains plugin") val descriptionFromReadme = content.substring(startIndex).lines().joinToString("\n").run { markdownToHTML(this) } pluginDescription.set(descriptionFromReadme) From 3a34b1f5b3d6785348d973d930c6fd0b88f54334 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:40:26 +0000 Subject: [PATCH 15/18] docs: synchronizing README from snyk/user-docs --- README.md | 80 +++++++++++++++++-------------------------------------- 1 file changed, 24 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index b6bfa8ce5..d79c44f21 100644 --- a/README.md +++ b/README.md @@ -2,58 +2,30 @@ description: Use this documentation to get started with the JetBrains plugin. --- -# JetBrains plugins - +# JetBrains plugin -Snyk offers IDE integrations that allow you to use the functionality of Snyk in your Integrated Development Environment. This page describes the Snyk JetBrains plugins. For information about all of the IDE plugins and their use, see [Snyk for IDEs](https://docs.snyk.io/ide-tools) in the docs. +## **Scan early, fix as you develop: elevate your security posture** -Snyk supports JetBrains plugins from version 2020.2 for [IntelliJ IDEA](https://snyk.io/lp/intellij-ide-plugin/) and [WebStorm](https://snyk.io/lp/webstorm-ide-plugin/) as well as Android Studio, AppCode, GoLand, PhpStorm, PyCharm, Rider, and RubyMine. +Integrating security checks early in your development lifecycle helps you pass security reviews seamlessly and avoid expensive fixes down the line. +The Snyk JetBrains plugin allows you to analyze your code, open-source dependencies, Docker images, and Infrastructure as Code (IaC) configurations. With actionable insights directly in your IDE, you can address issues as they arise. -Snyk uses Python in order to scan and find your dependencies. If you are using multiple Python versions, use the -`-command` option to specify the correct Python command for execution. The plugin does not detect the Python version associated with the project. +**Key features:** +* **In-line issue highlighting:** Security issues are flagged directly within your code, categorized by type and severity for quick identification and resolution. +* **Comprehensive scanning:** The extension scans for a wide range of security issues, including: + * [**Open Source Security**](https://snyk.io/product/open-source-security-management/)**:** Detects vulnerabilities and license issues in both direct and transitive open-source dependencies. Automated fix suggestions simplify remediation. Explore more in the [Snyk Open Source documentation](https://docs.snyk.io/scan-using-snyk/snyk-open-source). + * [**Code Security**](https://snyk.io/product/snyk-code/)**:** Identifies security vulnerabilities in your custom code. Explore more in the [Snyk Code documentation](https://docs.snyk.io/scan-using-snyk/snyk-code). + * [**IaC Security**](https://snyk.io/product/infrastructure-as-code-security/)**:** Uncovers configuration issues in your Infrastructure as Code templates (Terraform, Kubernetes, CloudFormation, Azure Resource Manager). Explore more in the [IaC documentation](https://docs.snyk.io/scan-using-snyk/snyk-iac). + * [**Container Security**](https://snyk.io/product/container-vulnerability-management/): Finds security vulnerabilities in your base images; supports all the [operating system distributions supported by Snyk Container](https://docs.snyk.io/scan-using-snyk/snyk-container/how-snyk-container-works/operating-system-distributions-supported-by-snyk-container). See also the [Snyk Container](https://docs.snyk.io/scan-using-snyk/snyk-container) docs. +* **Broad language and framework support:** Snyk Open Source and Snyk Code cover a wide array of package managers, programming languages, and frameworks, with ongoing updates to support the latest technologies. For the most up-to-date information on supported languages, package managers, and frameworks, see the [supported language technologies pages](https://docs.snyk.io/supported-languages-package-managers-and-frameworks). -The Snyk JetBrains plugins provide analysis of your code, containers, and Infrastructure as Code configurations. The plugin is based on the Snyk CLI and also uses Snyk APIs. The plugin supports product features in the CLI for Snyk Open Source and Snyk Container as well as for Snyk Code and Snyk IaC with some limitations. +## How to install and set up the extension -Snyk scans for vulnerabilities and misconfigurations and returns results with security issues categorized by issue type and severity. -For open source, you receive automated algorithm-based fix suggestions for both direct and transitive dependencies. For containers, you can automate upgrades to the most secure base image to quickly resolve numerous vulnerabilities. This single plugin provides a Java vulnerability scanner, a custom code vulnerability scanner, an open-source security scanner, and an application security plugin. +The latest Snyk JetBrains plugin is supported by all JetBrains IDEs 2023.3 or newer. -Snyk scans for the following types of issues: - -[**Open Source Security**](https://snyk.io/product/open-source-security-management/) - security vulnerabilities and license issues in both direct and in-direct (transitive) open-source dependencies pulled into the Snyk Project. See also the [Open Source docs](https://docs.snyk.io/products/snyk-open-source). - -[**Code Security**](https://snyk.io/product/snyk-code/) - security vulnerabilities in your code. See also the [Snyk Code docs](https://docs.snyk.io/products/snyk-code). - -[**Container Security**](https://snyk.io/product/container-vulnerability-management/) - security vulnerabilities in your base images. See also the [Snyk Container docs](https://docs.snyk.io/products/snyk-container). - -[**Infrastructure as Code (IaC) Security**](https://snyk.io/product/infrastructure-as-code-security/) - configuration issues in your IaC templates: Terraform, Kubernetes, CloudFormation, and Azure Resource Manager. See also the [Snyk Infrastructure as Code docs](https://docs.snyk.io/products/snyk-infrastructure-as-code). - -The JetBrains plugins also provide the [**Open Source Advisor**](https://snyk.io/advisor/) to help you find the best package for your next project. Information is provided on the package health of the direct dependencies you are using including popularity, maintenance, risk, and community insights. - -After you complete the installation steps on this page and the [configuration](https://docs.snyk.io/ide-tools/jetbrains-plugins/configuration-environment-variables-and-proxy-for-the-jetbrains-plugins) and [authentication](https://docs.snyk.io/ide-tools/jetbrains-plugins/authentication-for-the-jetbrains-plugins) steps on the next two pages, continue by following the instructions in the other JetBrains plugins docs: - -* [Run an analysis with the JetBrains plugins](https://docs.snyk.io/ide-tools/jetbrains-plugins/run-an-analysis-with-the-jetbrains-plugins) -* [JetBrains analysis results: Open Source](https://docs.snyk.io/ide-tools/jetbrains-plugins/jetbrains-analysis-results-snyk-open-source) -* [JetBrains analysis results: Snyk Code](https://docs.snyk.io/ide-tools/jetbrains-plugins/jetbrains-analysis-results-snyk-code) -* [JetBrains analysis results: Snyk IaC Configuration](https://docs.snyk.io/ide-tools/jetbrains-plugins/jetbrains-analysis-results-snyk-iac-configuration) -* [JetBrains analysis results: Snyk Container](https://docs.snyk.io/ide-tools/jetbrains-plugins/jetbrains-analysis-results-snyk-container) -* [How Snyk Container and Kubernetes JetBrains integration works](https://docs.snyk.io/ide-tools/jetbrains-plugins/how-snyk-container-and-kubernetes-jetbrains-integration-works) -* [Filter JetBrains results](https://docs.snyk.io/ide-tools/jetbrains-plugins/filter-jetbrains-results) -* [Troubleshooting for the JetBrains plugin](https://docs.snyk.io/ide-tools/jetbrains-plugins/troubleshooting-for-the-jetbrains-plugin) - - -## Supported languages, package managers, and frameworks - -* For Snyk Open Source, the JetBrains plugin supports the languages and package managers supported by Snyk Open Source and the CLI. For more information, see [Supported languages, frameworks, and feature availability overview, Open Source section](https://docs.snyk.io/scan-applications/supported-languages-and-frameworks/supported-languages-frameworks-and-feature-availability-overview#open-source-and-licensing-snyk-open-source). -* For Snyk Code, the JetBrains plugin supports all the languages and frameworks supported by Snyk Code. For more information, see [Supported languages, frameworks, and feature availability overview, Snyk Code section](https://docs.snyk.io/scan-applications/supported-languages-and-frameworks/supported-languages-frameworks-and-feature-availability-overview#code-analysis-snyk-code). Before scanning your repositories with Snyk Code, ensure you have [enabled Snyk Code](../../../scan-with-snyk/snyk-code/configure-snyk-code.md). -* For Snyk Container: the JetBrains plugin supports all the [operating system distributions supported by Snyk Container](https://docs.snyk.io/products/snyk-container/snyk-container-security-basics/supported-operating-system-distributions). -* For Snyk IaC, the JetBrains plugin supports the following IaC templates: Terraform, Kubernetes, CloudFormation, and Azure Resource Manager. - -## Supported operating systems and architecture - - -Snyk Plugins are not supported on any Operating System that has reached End Of Life (EOL) with the distributor. +An older plugin version is supported by JetBrains IDEs 2020.3 or newer. You can use the Snyk JetBrains plugin in the following environments: @@ -63,23 +35,19 @@ You can use the Snyk JetBrains plugin in the following environments: * Windows: 386, AMD64, and ARM64 * MacOS: AMD64 and ARM64 -## **Install the JetBrains plugin** +Install the plugin at any time free of charge from the [JetBrains marketplace](https://plugins.jetbrains.com/plugin/10972-snyk-vulnerability-scanner) and use it with any Snyk account, including the Free plan. For more information, see the [IDEA plugin installation guide](https://www.jetbrains.com/help/idea/managing-plugins.html). -The Snyk JetBrains plugin is available for installation on the [JetBrains marketplace](https://plugins.jetbrains.com/plugin/10972-snyk-vulnerability-scanner). +When the extension is installed, it automatically downloads the [Snyk CLI,](https://docs.snyk.io/snyk-cli) which includes the [Language Server](https://docs.snyk.io/scm-ide-and-ci-cd-integrations/snyk-ide-plugins-and-extensions/snyk-language-server). -Install using the IDE plugins library: +Continue by following the instructions in the other JetBrains plugin docs: -1. Open the **Preferences** window in the IDE. -2. Navigate to the **Plugins** tab. -3. In the **Plugins** tab, search for **Snyk**. -4. Select the **Snyk vulnerability scanning** plugin. -5. Click on the **Install** button. -6. When the installation is complete, restart the IDE. - -
Select the Snyk vulnerability scanning plugin

Select the Snyk vulnerability scanning plugin

- -Continue with the steps on the JetBrains [configuration](https://docs.snyk.io/ide-tools/jetbrains-plugins/configuration-environment-variables-and-proxy-for-the-jetbrains-plugins) page. +* [Configuration, environment variables, and proxy for the JetBrains plugins](https://docs.snyk.io/scm-ide-and-ci-cd-integrations/snyk-ide-plugins-and-extensions/jetbrains-plugins/configuration-environment-variables-and-proxy-for-the-jetbrains-plugins) +* [JetBrains plugin authentication](https://docs.snyk.io/scm-ide-and-ci-cd-integrations/snyk-ide-plugins-and-extensions/jetbrains-plugins/authentication-for-the-jetbrains-plugins) +* [JetBrains plugin folder trust](https://docs.snyk.io/scm-ide-and-ci-cd-integrations/snyk-ide-plugins-and-extensions/jetbrains-plugins/jetbrains-plugin-folder-trust) +* [Run an analysis with the JetBrains plugins](https://docs.snyk.io/scm-ide-and-ci-cd-integrations/snyk-ide-plugins-and-extensions/jetbrains-plugins/run-an-analysis-with-the-jetbrains-plugins) ## Support +For troubleshooting and known issues, see [Troubleshooting for the JetBrains plugin](https://docs.snyk.io/scm-ide-and-ci-cd-integrations/snyk-ide-plugins-and-extensions/jetbrains-plugins/troubleshooting-for-the-jetbrains-plugin). + If you need help, submit a [request](https://support.snyk.io/hc/en-us/requests/new) to Snyk Support. From fc50f9b324f083f5365270d73bd045cf21635f3a Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Mon, 9 Sep 2024 16:50:54 +0200 Subject: [PATCH 16/18] feat: add capability to display hover (#605) * feat: add capability to display hover This is disabled by default and can be enabled in the IntelliJ registry with the key snyk.documentationHoversEnabled. * fix: minimize read lock during code vision calculation, improve annotations --- CHANGELOG.md | 1 + src/main/kotlin/io/snyk/plugin/Utils.kt | 26 +----- .../ui/toolwindow/SnykPluginDisposable.kt | 18 ++-- .../SnykToolWindowSnykScanListenerLS.kt | 4 +- .../snyk/common/annotator/SnykAnnotator.kt | 11 ++- .../snyk/common/lsp/LSCodeVisionProvider.kt | 67 +++++++-------- .../lsp/LSDocumentationTargetProvider.kt | 85 +++++++++++++++++++ .../snyk/common/lsp/LanguageServerWrapper.kt | 12 +-- src/main/resources/META-INF/plugin.xml | 18 ++-- 9 files changed, 159 insertions(+), 83 deletions(-) create mode 100644 src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 1de8ad615..48ddba793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - add color and highlighting setting for Snyk issues - add dialog to choose reference branch when net new scanning - always display info nodes +- add option in IntelliJ registry to display tooltips with issue information ### Fixes - add name to code vision provider diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 18881c6a1..e6c316a02 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -58,14 +58,9 @@ import java.io.File import java.io.FileNotFoundException import java.net.URI import java.nio.file.Path -import java.security.KeyStore import java.util.Objects.nonNull import java.util.SortedSet import java.util.concurrent.TimeUnit -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509TrustManager import javax.swing.JComponent private val logger = Logger.getInstance("#io.snyk.plugin.UtilsKt") @@ -223,6 +218,8 @@ fun isFileListenerEnabled(): Boolean = pluginSettings().fileListenerEnabled fun isSnykIaCLSEnabled(): Boolean = false +fun isDocumentationHoverEnabled(): Boolean = Registry.get("snyk.isDocumentationHoverEnabled").asBoolean() + fun getWaitForResultsTimeout(): Long = Registry.intValue( "snyk.timeout.results.waiting", @@ -233,25 +230,6 @@ const val DEFAULT_TIMEOUT_FOR_SCAN_WAITING_MIN = 12L val DEFAULT_TIMEOUT_FOR_SCAN_WAITING_MS = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_FOR_SCAN_WAITING_MIN, TimeUnit.MINUTES).toInt() -fun getSSLContext(): SSLContext { - val trustManager = getX509TrustManager() - val sslContext = SSLContext.getInstance("TLSv1.2") - sslContext.init(null, arrayOf(trustManager), null) - return sslContext -} - -fun getX509TrustManager(): X509TrustManager { - val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm() - ) - trustManagerFactory.init(null as KeyStore?) - val trustManagers: Array = trustManagerFactory.trustManagers - check(!(trustManagers.size != 1 || trustManagers[0] !is X509TrustManager)) { - ("Unexpected default trust managers:${trustManagers.contentToString()}") - } - return trustManagers[0] as X509TrustManager -} - fun findPsiFileIgnoringExceptions(virtualFile: VirtualFile, project: Project): PsiFile? { return if (!virtualFile.isValid || project.isDisposed) { null diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykPluginDisposable.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykPluginDisposable.kt index 25389d06c..90bc96daf 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykPluginDisposable.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykPluginDisposable.kt @@ -15,14 +15,25 @@ import java.util.concurrent.TimeUnit */ @Service(Service.Level.APP, Service.Level.PROJECT) class SnykPluginDisposable : Disposable, AppLifecycleListener { + private var disposed = false + get() { + return ApplicationManager.getApplication().isDisposed || field + } + + fun isDisposed() = disposed + + override fun dispose() { + disposed = true + } + companion object { @NotNull - fun getInstance(): Disposable { + fun getInstance(): SnykPluginDisposable { return ApplicationManager.getApplication().getService(SnykPluginDisposable::class.java) } @NotNull - fun getInstance(@NotNull project: Project): Disposable { + fun getInstance(@NotNull project: Project): SnykPluginDisposable { return project.getService(SnykPluginDisposable::class.java) } } @@ -46,7 +57,4 @@ class SnykPluginDisposable : Disposable, AppLifecycleListener { // do nothing } } - - override fun dispose() = Unit - } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt index 081bf42ff..fc6abc2dd 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -117,10 +117,10 @@ class SnykToolWindowSnykScanListenerLS( // TODO implement } } + refreshAnnotationsForOpenFiles(project) } - override fun onPublishDiagnostics(product: String, snykFile: SnykFile, issueList: List) { - } + override fun onPublishDiagnostics(product: String, snykFile: SnykFile, issueList: List) {} fun displaySnykCodeResults(snykResults: Map>) { if (disposed) return diff --git a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt index ce9301a3f..f482120a5 100644 --- a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -48,7 +48,7 @@ import java.util.concurrent.TimeoutException import javax.swing.Icon -private const val CODEACTION_TIMEOUT = 5000L +private const val CODEACTION_TIMEOUT = 10L typealias SnykAnnotationInput = Pair>> typealias SnykAnnotationList = List @@ -78,8 +78,9 @@ abstract class SnykAnnotator(private val product: ProductType) : var gutterIconRenderer: GutterIconRenderer? = null ) - // overrides needed for the Annotator to invoke apply(). We don't do anything here override fun collectInformation(file: PsiFile): SnykAnnotationInput? { + if (disposed) return null + if (!LanguageServerWrapper.getInstance().isInitialized) return null val map = getIssuesForFile(file) .filter { AnnotatorCommon.isSeverityToShow(it.getSeverityAsEnum()) } .sortedByDescending { it.getSeverityAsEnum() } @@ -111,9 +112,7 @@ abstract class SnykAnnotator(private val product: ProductType) : logger.warn("Invalid range for range: $textRange") return@forEach } - annotations.addAll( - doAnnotateIssue(entry, textRange, gutterIconEnabled, codeActions) - ) + annotations.addAll(doAnnotateIssue(entry, textRange, gutterIconEnabled, codeActions)) } return annotations.sortedByDescending { it.issue.getSeverityAsEnum() } } @@ -218,7 +217,7 @@ abstract class SnykAnnotator(private val product: ProductType) : val codeActions = try { languageServer.textDocumentService - .codeAction(params).get(CODEACTION_TIMEOUT, TimeUnit.MILLISECONDS) ?: emptyList() + .codeAction(params).get(CODEACTION_TIMEOUT, TimeUnit.SECONDS) ?: emptyList() } catch (ignored: TimeoutException) { logger.info("Timeout fetching code actions for range: $range") emptyList() diff --git a/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt b/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt index 16518ecfd..610ccc4fa 100644 --- a/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt +++ b/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt @@ -16,6 +16,7 @@ import com.intellij.openapi.progress.Task.Backgroundable import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFile import icons.SnykIcons import io.snyk.plugin.toLanguageServerURL import org.eclipse.lsp4j.CodeLens @@ -26,7 +27,7 @@ import java.awt.event.MouseEvent import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException -private const val CODELENS_FETCH_TIMEOUT = 2L +private const val CODELENS_FETCH_TIMEOUT = 10L @Suppress("UnstableApiUsage") class LSCodeVisionProvider : CodeVisionProvider, CodeVisionGroupSettingProvider { @@ -47,44 +48,42 @@ class LSCodeVisionProvider : CodeVisionProvider, CodeVisionGroupSettingPro } override fun computeCodeVision(editor: Editor, uiData: Unit): CodeVisionState { - if (editor.project == null) return CodeVisionState.READY_EMPTY + if (LanguageServerWrapper.getInstance().isDisposed()) return CodeVisionState.READY_EMPTY if (!LanguageServerWrapper.getInstance().isInitialized) return CodeVisionState.READY_EMPTY + val project = editor.project ?: return CodeVisionState.READY_EMPTY - return ReadAction.compute { - val project = editor.project ?: return@compute CodeVisionState.READY_EMPTY - val document = editor.document - val file = PsiDocumentManager.getInstance(project).getPsiFile(document) - ?: return@compute CodeVisionState.READY_EMPTY - val params = CodeLensParams(TextDocumentIdentifier(file.virtualFile.toLanguageServerURL())) - val lenses = mutableListOf>() - val codeLenses = try { - LanguageServerWrapper.getInstance().languageServer.textDocumentService.codeLens(params) - .get(CODELENS_FETCH_TIMEOUT, TimeUnit.SECONDS) - } catch (ignored: TimeoutException) { - logger.info("Timeout fetching code lenses for : $file") - emptyList() - } + val document = editor.document - if (codeLenses == null) { - return@compute CodeVisionState.READY_EMPTY - } - codeLenses.forEach { codeLens -> - val range = TextRange( - document.getLineStartOffset(codeLens.range.start.line) + codeLens.range.start.character, - document.getLineEndOffset(codeLens.range.end.line) + codeLens.range.end.character - ) + val file = ReadAction.compute { + PsiDocumentManager.getInstance(project).getPsiFile(document) + } ?: return CodeVisionState.READY_EMPTY - val entry = ClickableTextCodeVisionEntry( - text = codeLens.command.title, - providerId = id, - onClick = LSCommandExecutionHandler(codeLens), - extraActions = emptyList(), - icon = SnykIcons.TOOL_WINDOW - ) - lenses.add(range to entry) - } - return@compute CodeVisionState.Ready(lenses) + val params = CodeLensParams(TextDocumentIdentifier(file.virtualFile.toLanguageServerURL())) + val lenses = mutableListOf>() + val codeLenses = try { + LanguageServerWrapper.getInstance().languageServer.textDocumentService.codeLens(params) + .get(CODELENS_FETCH_TIMEOUT, TimeUnit.SECONDS) ?: return CodeVisionState.READY_EMPTY + } catch (ignored: TimeoutException) { + logger.info("Timeout fetching code lenses for : $file") + return CodeVisionState.READY_EMPTY + } + + codeLenses.forEach { codeLens -> + val range = TextRange( + document.getLineStartOffset(codeLens.range.start.line) + codeLens.range.start.character, + document.getLineEndOffset(codeLens.range.end.line) + codeLens.range.end.character + ) + + val entry = ClickableTextCodeVisionEntry( + text = codeLens.command.title, + providerId = id, + onClick = LSCommandExecutionHandler(codeLens), + extraActions = emptyList(), + icon = SnykIcons.TOOL_WINDOW + ) + lenses.add(range to entry) } + return CodeVisionState.Ready(lenses) } private class LSCommandExecutionHandler(private val codeLens: CodeLens) : (MouseEvent?, Editor) -> Unit { diff --git a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt b/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt new file mode 100644 index 000000000..e59af9db0 --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt @@ -0,0 +1,85 @@ +@file:Suppress("UnstableApiUsage") + +package snyk.common.lsp + +import com.intellij.markdown.utils.convertMarkdownToHtml +import com.intellij.model.Pointer +import com.intellij.openapi.Disposable +import com.intellij.platform.backend.documentation.DocumentationResult +import com.intellij.platform.backend.documentation.DocumentationTarget +import com.intellij.platform.backend.documentation.DocumentationTargetProvider +import com.intellij.platform.backend.presentation.TargetPresentation +import com.intellij.psi.PsiFile +import icons.SnykIcons +import io.snyk.plugin.isDocumentationHoverEnabled +import io.snyk.plugin.toLanguageServerURL +import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable +import org.eclipse.lsp4j.Hover +import org.eclipse.lsp4j.HoverParams +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.TextDocumentIdentifier +import java.util.concurrent.TimeUnit + +class SnykDocumentationTargetPointer(private val documentationTarget: DocumentationTarget) : + Pointer { + + override fun dereference(): DocumentationTarget { + return documentationTarget + } +} + +class LSDocumentationTargetProvider : DocumentationTargetProvider, Disposable { + private var disposed = false + get() { + return SnykPluginDisposable.getInstance().isDisposed() || field + } + + fun isDisposed() = disposed + + override fun documentationTargets(file: PsiFile, offset: Int): MutableList { + val languageServerWrapper = LanguageServerWrapper.getInstance() + if (disposed || !languageServerWrapper.isInitialized || !isDocumentationHoverEnabled()) return mutableListOf() + + val lineNumber = file.viewProvider.document.getLineNumber(offset) + val lineStartOffset = file.viewProvider.document.getLineStartOffset(lineNumber) + val hoverParams = HoverParams( + TextDocumentIdentifier(file.virtualFile.toLanguageServerURL()), + Position(lineNumber, offset - lineStartOffset) + ) + val hover = + languageServerWrapper.languageServer.textDocumentService.hover(hoverParams).get(2000, TimeUnit.MILLISECONDS) + if (hover == null || hover.contents.right.value.isEmpty()) return mutableListOf() + return mutableListOf(SnykDocumentationTarget(hover)) + } + + inner class SnykDocumentationTarget(private val hover: Hover) : DocumentationTarget { + override fun computeDocumentationHint(): String? { + val htmlText = convertMarkdownToHtml(hover.contents.right.value) + if (htmlText.isEmpty()) { + return null + } + return htmlText.split("\n")[0] + } + + override fun computeDocumentation(): DocumentationResult? { + val htmlText = convertMarkdownToHtml(hover.contents.right.value) + if (htmlText.isEmpty()) { + return null + } + return DocumentationResult.documentation(htmlText) + } + + override fun computePresentation(): TargetPresentation { + return TargetPresentation.builder("Snyk Security").icon(SnykIcons.TOOL_WINDOW).presentation() + } + + override fun createPointer(): Pointer { + return SnykDocumentationTargetPointer(this) + } + } + + + override fun dispose() { + disposed = true + } +} diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 42a898eff..34908e7ef 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -561,8 +561,13 @@ class LanguageServerWrapper( return "" } - companion object { + override fun dispose() { + disposed = true + shutdown() + } + + companion object { private var instance: LanguageServerWrapper? = null fun getInstance() = instance ?: LanguageServerWrapper().also { @@ -571,10 +576,5 @@ class LanguageServerWrapper( } } - override fun dispose() { - disposed = true - shutdown() - } - } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d2fc3bd25..75c1e420a 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -19,9 +19,9 @@ com.intellij.modules.xml - + + - - - + + + From e5391aba163a95464ed0af221fdf41f830397fb3 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 11 Sep 2024 13:09:50 +0200 Subject: [PATCH 17/18] fix: show hand icon, refactor info node text construction (#609) * fix: show hand icon, refactor info node text construction * fix: tests * fix: tests --- .../toolwindow/SnykToolWindowSnykScanListenerLS.kt | 7 ++++--- .../snyk/plugin/extensions/SnykControllerImplTest.kt | 4 ++-- .../plugin/ui/BranchChooserComboBoxDialogTest.kt | 5 ++++- .../SnykToolWindowSnykScanListenerLSTest.kt | 6 +++--- .../snyk/container/ContainerBulkFileListenerTest.kt | 12 +++++++++--- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt index fc6abc2dd..ae02db22e 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -286,11 +286,12 @@ class SnykToolWindowSnykScanListenerLS( val issuesCount = issues.size val ignoredIssuesCount = issues.count { it.isIgnored() } if (issuesCount != 0) { - text = if (issuesCount == 1) { - "$issuesCount vulnerability found by Snyk" + val plural = if (issuesCount == 1) { + "y" } else { - "✋ $issuesCount vulnerabilities found by Snyk" + "ies" } + text = "✋ $issuesCount vulnerabilit$plural found by Snyk" if (pluginSettings().isGlobalIgnoresFeatureEnabled) { text += ", $ignoredIssuesCount ignored" } diff --git a/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt b/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt index 83c5e605b..2918772a7 100644 --- a/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt +++ b/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt @@ -53,8 +53,8 @@ class SnykControllerImplTest : LightPlatformTestCase() { val controller = SnykControllerImpl(project) controller.scan() - PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue() + PlatformTestUtil.dispatchAllEventsInIdeEventQueue() - verify { languageServerWrapper.sendScanCommand(project) } + verify (timeout = 5000){ languageServerWrapper.sendScanCommand(project) } } } diff --git a/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt index 17c14e897..c55631bb7 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt @@ -3,12 +3,15 @@ package io.snyk.plugin.ui import com.intellij.openapi.components.service import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.VirtualFileSystem import com.intellij.testFramework.LightPlatform4TestCase import com.intellij.testFramework.PlatformTestUtil import io.mockk.CapturingSlot import io.mockk.mockk import io.mockk.unmockkAll import io.mockk.verify +import io.snyk.plugin.toVirtualFile import okio.Path.Companion.toPath import org.eclipse.lsp4j.DidChangeConfigurationParams import org.eclipse.lsp4j.services.LanguageServer @@ -30,10 +33,10 @@ class BranchChooserComboBoxDialogTest : LightPlatform4TestCase() { unmockkAll() folderConfig = FolderConfig(project.basePath.toString(), "testBranch") service().addFolderConfig(folderConfig) - project.basePath?.let { service().addTrustedPath(it.toNioPathOrNull()!!) } val languageServerWrapper = LanguageServerWrapper.getInstance() languageServerWrapper.isInitialized = true languageServerWrapper.languageServer = lsMock + project.basePath?.let { service().addTrustedPath(it.toPath().parent!!.toNioPath()) } cut = BranchChooserComboBoxDialog(project) } diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt index 0a876e8e6..967ddd64b 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -148,8 +148,8 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { TestCase.assertEquals(rootTreeNode.children().toList()[1].toString(), " Code Security") TestCase.assertEquals(rootTreeNode.children().toList()[2].toString(), " Code Quality") TestCase.assertEquals( + "✋ 1 vulnerability found by Snyk, 0 ignored", rootTreeNode.children().toList()[4].toString(), - "1 vulnerability found by Snyk, 0 ignored", ) TestCase.assertEquals( rootTreeNode.children().toList()[5].toString(), @@ -188,12 +188,12 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { TestCase.assertEquals(rootTreeNode.children().toList()[2].toString(), " Code Quality") TestCase.assertTrue(rootTreeNode.children().toList()[3].toString().contains("Click to choose base branch for")) TestCase.assertEquals( + "✋ 1 vulnerability found by Snyk, 0 ignored", rootTreeNode.children().toList()[4].toString(), - "1 vulnerability found by Snyk, 0 ignored", ) TestCase.assertEquals( - rootTreeNode.children().toList()[5].toString(), "⚡ 1 vulnerabilities can be fixed automatically", + rootTreeNode.children().toList()[5].toString(), ) } diff --git a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt index 6c05e2797..0e2ec34a1 100644 --- a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt +++ b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt @@ -1,6 +1,9 @@ package snyk.container import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.application.invokeAndWaitIfNeeded +import com.intellij.openapi.application.invokeLater import com.intellij.openapi.components.service import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.vfs.VirtualFile @@ -28,6 +31,7 @@ import java.io.File import java.nio.file.Files import java.nio.file.LinkOption import java.nio.file.Paths +import java.time.Duration import java.util.concurrent.TimeUnit import kotlin.io.path.notExists @@ -70,12 +74,14 @@ class ContainerBulkFileListenerTest : BasePlatformTestCase() { setUpContainerTest() val path = createNewFileInProjectRoot().toPath() Files.write(path, "\n".toByteArray(Charsets.UTF_8)) - VirtualFileManager.getInstance().syncRefresh() var virtualFile: VirtualFile? = null - await().atMost(2, TimeUnit.SECONDS).until { + invokeLater { + VirtualFileManager.getInstance().syncRefresh() virtualFile = VirtualFileManager.getInstance().findFileByNioPath(path) - virtualFile?.isValid ?: false } + PlatformTestUtil.dispatchAllEventsInIdeEventQueue() + await().timeout(5, TimeUnit.SECONDS).until { virtualFile?.isValid ?: false } + ApplicationManager.getApplication().runWriteAction { val file = PsiManager.getInstance(project).findFile(virtualFile!!) From 4de8e1166f798bcc816341b5bdc2b2624d61d5f0 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 11 Sep 2024 15:58:19 +0200 Subject: [PATCH 18/18] * feat: enable documentation hovers * docs: update changelog --- CHANGELOG.md | 1 + .../snyk/common/lsp/LSDocumentationTargetProvider.kt | 10 +--------- .../kotlin/snyk/common/lsp/LanguageServerSettings.kt | 2 ++ src/main/resources/META-INF/plugin.xml | 2 +- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48ddba793..6a954a600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - add dialog to choose reference branch when net new scanning - always display info nodes - add option in IntelliJ registry to display tooltips with issue information +- display documentation info when hovering over issue ### Fixes - add name to code vision provider diff --git a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt b/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt index e59af9db0..66fb4ae47 100644 --- a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt +++ b/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt @@ -53,16 +53,8 @@ class LSDocumentationTargetProvider : DocumentationTargetProvider, Disposable { } inner class SnykDocumentationTarget(private val hover: Hover) : DocumentationTarget { - override fun computeDocumentationHint(): String? { - val htmlText = convertMarkdownToHtml(hover.contents.right.value) - if (htmlText.isEmpty()) { - return null - } - return htmlText.split("\n")[0] - } - override fun computeDocumentation(): DocumentationResult? { - val htmlText = convertMarkdownToHtml(hover.contents.right.value) + val htmlText = hover.contents.right.value if (htmlText.isEmpty()) { return null } diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt index a48a5223c..8b57c0b1d 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt @@ -43,6 +43,8 @@ data class LanguageServerSettings( @SerializedName("enableSnykOSSQuickFixCodeActions") val enableSnykOSSQuickFixCodeActions: String? = null, @SerializedName("requiredProtocolVersion") val requiredProtocolVersion: String = pluginSettings().requiredLsProtocolVersion.toString(), + @SerializedName("hoverVerbosity") val hoverVerbosity: Int = 1, + @SerializedName("outputFormat") val outputFormat: String = "html", @SerializedName("enableDeltaFindings") val enableDeltaFindings: String = pluginSettings().isDeltaFindingsEnabled().toString(), @SerializedName("folderConfigs") val folderConfigs: List = service().getAll().values.toList() ) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 75c1e420a..d5e39c49b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -43,7 +43,7 @@ defaultValue="720000" description="Snyk timeout (milliseconds) to wait for results during scan"/>