Skip to content

Commit

Permalink
feat: render html from snyk-ls [IDE-214] (#496)
Browse files Browse the repository at this point in the history
* feat: render html from snyk-ls

* test: add tests

* refactor: move code to the right panel

* refactor: remove unused code

* test: try to fix tests
  • Loading branch information
teodora-sandu authored Apr 3, 2024
1 parent b04d888 commit 12fd4ef
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 8 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Snyk Security Changelog

## [2.7.14]

### Added
- Render Snyk Code vulnerabilities using HTML served by the Language Server behind a feature flag.

## [2.7.13]
### Added
- (LS Preview) added timeout to commands executed via code lenses
Expand Down
14 changes: 14 additions & 0 deletions src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import com.intellij.ui.BrowserHyperlinkListener
import com.intellij.ui.ColorUtil
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.components.ActionLink
import com.intellij.ui.jcef.JBCefApp
import com.intellij.ui.jcef.JBCefBrowser
import com.intellij.ui.jcef.JBCefBrowserBuilder
import com.intellij.uiDesigner.core.GridConstraints
import com.intellij.uiDesigner.core.GridLayoutManager
import com.intellij.uiDesigner.core.Spacer
Expand Down Expand Up @@ -373,3 +376,14 @@ fun expandTreeNodeRecursively(tree: JTree, node: DefaultMutableTreeNode) {
expandTreeNodeRecursively(tree, it as DefaultMutableTreeNode)
}
}

//JComponent, String
fun getJBCefBrowserIfSupported () : Pair<JBCefBrowser?, String> {
if (!JBCefApp.isSupported()) {
return null to ""
}
val cefClient = JBCefApp.getInstance().createClient()
val jbCefBrowser = JBCefBrowserBuilder().setClient(cefClient).setEnableOpenDevToolsMenuItem(true).build()

return jbCefBrowser to jbCefBrowser.cefBrowser.url
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ class SuggestionDescriptionPanel(
private val snykCodeFile: SnykCodeFile,
private val suggestion: SuggestionForFile,
private val suggestionIndex: Int
) : IssueDescriptionPanelBase(title = suggestion.title, severity = suggestion.getSeverityAsEnum()) {
) : IssueDescriptionPanelBase(
title = suggestion.title,
severity = suggestion.getSeverityAsEnum()
) {
val project = snykCodeFile.project

private val suggestionRange: MyTextRange? = suggestion.ranges?.getOrNull(suggestionIndex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ import com.intellij.util.ui.JBInsets
import com.intellij.util.ui.UIUtil
import io.snyk.plugin.getDocument
import io.snyk.plugin.navigateToSource
import io.snyk.plugin.pluginSettings
import io.snyk.plugin.snykcode.core.SnykCodeFile
import io.snyk.plugin.toVirtualFile
import io.snyk.plugin.ui.DescriptionHeaderPanel
import io.snyk.plugin.ui.SnykBalloonNotificationHelper
import io.snyk.plugin.ui.baseGridConstraintsAnchorWest
import io.snyk.plugin.ui.descriptionHeaderPanel
import io.snyk.plugin.ui.getJBCefBrowserIfSupported
import io.snyk.plugin.ui.panelGridConstraints
import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel
import io.snyk.plugin.ui.wrapWithScrollPane
import snyk.common.lsp.DataFlow
import snyk.common.lsp.ScanIssue
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Dimension
import java.awt.Font
Expand All @@ -33,11 +39,33 @@ import kotlin.math.max
class SuggestionDescriptionPanelFromLS(
snykCodeFile: SnykCodeFile,
private val issue: ScanIssue
) : IssueDescriptionPanelBase(title = getIssueTitle(issue), severity = issue.getSeverityAsEnum()) {
) : IssueDescriptionPanelBase(
title = getIssueTitle(issue),
severity = issue.getSeverityAsEnum()
) {
val project = snykCodeFile.project
private val unexpectedErrorMessage = "Snyk encountered an issue while rendering the vulnerability description. Please try again, or contact support if the problem persists. We apologize for any inconvenience caused.";

init {
createUI()
if (pluginSettings().isGlobalIgnoresFeatureEnabled && issue.additionalData.details != null) {
val (jbCefBrowser, jbCefBrowserUrl) = getJBCefBrowserIfSupported()
if (jbCefBrowser == null) {
val statePanel = StatePanel(SnykToolWindowPanel.SELECT_ISSUE_TEXT)
this.add(wrapWithScrollPane(statePanel), BorderLayout.CENTER)
SnykBalloonNotificationHelper.showError(unexpectedErrorMessage, null)
} else {
val panel = JPanel()
panel.add(jbCefBrowser.component, BorderLayout())
this.add(
wrapWithScrollPane(panel),
BorderLayout.CENTER
)

jbCefBrowser.loadHTML(issue.additionalData.details, jbCefBrowserUrl)
}
} else {
createUI()
}
}

override fun secondRowTitlePanel(): DescriptionHeaderPanel = descriptionHeaderPanel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import javax.swing.JPanel

class VulnerabilityDescriptionPanel(
private val groupedVulns: Collection<Vulnerability>
) : IssueDescriptionPanelBase(title = groupedVulns.first().title, severity = groupedVulns.first().getSeverity()) {
) : IssueDescriptionPanelBase(
title = groupedVulns.first().title,
severity = groupedVulns.first().getSeverity()
) {

private val labelProvider: LabelProvider = LabelProvider()
private val vulnerability = groupedVulns.first()
Expand Down
5 changes: 3 additions & 2 deletions src/main/kotlin/snyk/common/lsp/Types.kt
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,7 @@ data class IssueData(
@SerializedName("priorityScore") val priorityScore: Int,
@SerializedName("hasAIFix") val hasAIFix: Boolean,
@SerializedName("dataFlow") val dataFlow: List<DataFlow>,
@SerializedName("isIgnored") val isIgnored: Boolean?,
@SerializedName("ignoreDetails") val ignoreDetails: IgnoreDetails?,
@SerializedName("details") val details: String?,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand Down Expand Up @@ -231,6 +230,7 @@ data class IssueData(
if (priorityScore != other.priorityScore) return false
if (hasAIFix != other.hasAIFix) return false
if (dataFlow != other.dataFlow) return false
if (details != other.details) return false

return true
}
Expand All @@ -251,6 +251,7 @@ data class IssueData(
result = 31 * result + priorityScore
result = 31 * result + hasAIFix.hashCode()
result = 31 * result + dataFlow.hashCode()
result = 31 * result + details.hashCode()
return result
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import javax.swing.JTextArea

class BaseImageRemediationDetailPanel(
private val project: Project,
private val imageIssues: ContainerIssuesForImage
private val imageIssues: ContainerIssuesForImage,
) : IssueDescriptionPanelBase(
title = imageIssues.imageName,
severity = imageIssues.getSeverities().maxOrNull() ?: Severity.UNKNOWN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import javax.swing.JPanel

class ContainerIssueDetailPanel(
private val groupedVulns: Collection<ContainerIssue>
) : IssueDescriptionPanelBase(title = groupedVulns.first().title, severity = groupedVulns.first().getSeverity()) {
) : IssueDescriptionPanelBase(
title = groupedVulns.first().title,
severity = groupedVulns.first().getSeverity()
) {

private val issue = groupedVulns.first()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
@file:Suppress("FunctionName")
package io.snyk.plugin.ui.toolwindow

import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.ui.jcef.JBCefBrowser
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.snyk.plugin.Severity
import io.snyk.plugin.pluginSettings
import io.snyk.plugin.resetSettings
import io.snyk.plugin.snykcode.core.SnykCodeFile
import io.snyk.plugin.ui.getJBCefBrowserIfSupported
import io.snyk.plugin.ui.toolwindow.panels.SuggestionDescriptionPanelFromLS
import org.cef.browser.CefBrowserFactory
import org.cef.browser.CefRequestContext
import org.eclipse.lsp4j.Position
import org.eclipse.lsp4j.Range
import org.junit.Before
import org.junit.Test
import snyk.UIComponentFinder.getJBCEFBrowser
import snyk.UIComponentFinder.getJLabelByText
import snyk.code.annotator.SnykCodeAnnotator
import snyk.common.lsp.CommitChangeLine
import snyk.common.lsp.DataFlow
import snyk.common.lsp.ExampleCommitFix
import snyk.common.lsp.ScanIssue
import java.nio.file.Paths
import javax.swing.JPanel

class SuggestionDescriptionPanelFromLSTest : BasePlatformTestCase() {
private lateinit var cut: SuggestionDescriptionPanelFromLS
private val fileName = "app.js"
private lateinit var snykCodeFile: SnykCodeFile
private lateinit var issue: ScanIssue

private lateinit var file: VirtualFile
private lateinit var psiFile: PsiFile

override fun getTestDataPath(): String {
val resource = SnykCodeAnnotator::class.java.getResource("/test-fixtures/code/annotator")
requireNotNull(resource) { "Make sure that the resource $resource exists!" }
return Paths.get(resource.toURI()).toString()
}

@Before
override fun setUp() {
super.setUp()
unmockkAll()
resetSettings(project)

file = myFixture.copyFileToProject(fileName)
psiFile = WriteAction.computeAndWait<PsiFile, Throwable> { psiManager.findFile(file)!! }
snykCodeFile = SnykCodeFile(psiFile.project, psiFile.virtualFile)

issue = mockk<ScanIssue>()
every { issue.additionalData.message } returns "Test message"
every { issue.additionalData.isSecurityType } returns true
every { issue.additionalData.cwe } returns null
every { issue.additionalData.repoDatasetSize } returns 1
every { issue.additionalData.exampleCommitFixes } returns listOf(ExampleCommitFix("https://commit-url", listOf(
CommitChangeLine("1", 1, "lineChange")
)))
every { issue.additionalData.dataFlow } returns listOf(DataFlow(0, getTestDataPath(), Range(Position(1, 1), Position(1, 1)), ""))
every { issue.title } returns "Test title"
every { issue.getSeverityAsEnum() } returns Severity.CRITICAL
}

@Test
fun `test createUI should build panel with issue message as overview label if the feature flag is not enabled`() {
every { issue.additionalData.details } returns "<html>HTML message</html>"
every { issue.additionalData.details } returns null
cut = SuggestionDescriptionPanelFromLS(snykCodeFile, issue)

val actual = getJLabelByText(cut, "<html>Test message</html>")
assertNotNull(actual)

val actualBrowser = getJBCEFBrowser(cut)
assertNull(actualBrowser)
}

@Test
fun `test createUI should build panel with issue message as overview label if the details are empty even if feature flag is enabled`() {
pluginSettings().isGlobalIgnoresFeatureEnabled = true

every { issue.additionalData.details } returns null
cut = SuggestionDescriptionPanelFromLS(snykCodeFile, issue)

val actual = getJLabelByText(cut, "<html>Test message</html>")
assertNotNull(actual)

val actualBrowser = getJBCEFBrowser(cut)
assertNull(actualBrowser)
}

@Test
fun `test createUI should show nothing if feature flag is enabled but JCEF is not`() {
pluginSettings().isGlobalIgnoresFeatureEnabled = true

mockkStatic("io.snyk.plugin.ui.UIUtilsKt")
every { getJBCefBrowserIfSupported() } returns (null to "")

every { issue.additionalData.details } returns "<html>HTML message</html>"
cut = SuggestionDescriptionPanelFromLS(snykCodeFile, issue)

val actual = getJLabelByText(cut, "<html>Test message</html>")
assertNull(actual)

val actualBrowser = getJBCEFBrowser(cut)
assertNull(actualBrowser)
}

@Test
fun `test createUI should build panel with HTML from details if feature flag is enabled`() {
pluginSettings().isGlobalIgnoresFeatureEnabled = true

val mockJBCefBrowser = mockk<JBCefBrowser>()
every { mockJBCefBrowser.component } returns JPanel()
every { mockJBCefBrowser.loadHTML(eq("<html>HTML message</html>"), eq("http://foo/bar")) } returns
mockkStatic("io.snyk.plugin.ui.UIUtilsKt")
every { getJBCefBrowserIfSupported() } returns (mockJBCefBrowser to "http://foo/bar")

every { issue.additionalData.details } returns "<html>HTML message</html>"
cut = SuggestionDescriptionPanelFromLS(snykCodeFile, issue)

val actual = getJLabelByText(cut, "<html>Test message</html>")
assertNull(actual)
}
}
17 changes: 17 additions & 0 deletions src/test/kotlin/snyk/UIComponentFinder.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package snyk

import com.intellij.ui.jcef.JBCefBrowser
import java.awt.Container
import javax.swing.JButton
import javax.swing.JLabel
Expand Down Expand Up @@ -53,4 +54,20 @@ object UIComponentFinder {
}
return found
}

fun getJBCEFBrowser(parent: Container): JBCefBrowser.MyPanel? {
val components = parent.components
var found: JBCefBrowser.MyPanel? = null
for (component in components) {
if (component is JBCefBrowser.MyPanel) {
found = component
} else if (component is Container) {
found = getJBCEFBrowser(component)
}
if (found != null) {
break
}
}
return found
}
}

0 comments on commit 12fd4ef

Please sign in to comment.