diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a9ef4e8d..300b724fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,9 @@ - If $/snyk.hasAuthenticated transmits an API URL, this is saved in the settings. - Add "plugin installed" analytics event (sent after authentication) - Added a description of custom endpoints to settings dialog. - +- Add option to ignore IaC issues ### Fixed - folder-specific configs are availabe on opening projects, not only on restart of the IDE - ## [2.10.0] ### Changed - save git folder config in settings diff --git a/src/main/kotlin/io/snyk/plugin/cli/ConsoleCommandRunner.kt b/src/main/kotlin/io/snyk/plugin/cli/ConsoleCommandRunner.kt index 70bd701fd..d16246634 100644 --- a/src/main/kotlin/io/snyk/plugin/cli/ConsoleCommandRunner.kt +++ b/src/main/kotlin/io/snyk/plugin/cli/ConsoleCommandRunner.kt @@ -89,5 +89,6 @@ open class ConsoleCommandRunner { companion object { const val PROCESS_CANCELLED_BY_USER = "PROCESS_CANCELLED_BY_USER" + const val SAVING_POLICY_FILE = "Saving .snyk policy file...\n" } } diff --git a/src/main/kotlin/io/snyk/plugin/services/CliAdapter.kt b/src/main/kotlin/io/snyk/plugin/services/CliAdapter.kt index 976f96636..4bbef31ec 100644 --- a/src/main/kotlin/io/snyk/plugin/services/CliAdapter.kt +++ b/src/main/kotlin/io/snyk/plugin/services/CliAdapter.kt @@ -73,6 +73,9 @@ abstract class CliAdapter>(val project: Proj rawStr == ConsoleCommandRunner.PROCESS_CANCELLED_BY_USER -> { getProductResult(null) } + rawStr == ConsoleCommandRunner.SAVING_POLICY_FILE -> { + getProductResult(null) + } rawStr.isEmpty() -> { getErrorResult(CLI_PRODUCE_NO_OUTPUT) } diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/IgnoreInFileHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/IgnoreInFileHandler.kt new file mode 100644 index 000000000..f8960212b --- /dev/null +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/IgnoreInFileHandler.kt @@ -0,0 +1,98 @@ +package io.snyk.plugin.ui.jcef + +import com.intellij.openapi.diagnostic.LogLevel +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import com.intellij.ui.jcef.JBCefBrowserBase +import com.intellij.ui.jcef.JBCefJSQuery +import io.snyk.plugin.getContentRootPaths +import io.snyk.plugin.runInBackground +import io.snyk.plugin.ui.SnykBalloonNotificationHelper +import org.cef.browser.CefBrowser +import org.cef.browser.CefFrame +import org.cef.handler.CefLoadHandlerAdapter +import snyk.common.IgnoreService +import snyk.common.lsp.LanguageServerWrapper +import java.io.IOException +import kotlin.io.path.Path +import kotlin.io.path.relativeTo + +class IgnoreInFileHandler( + private val project: Project, + ) { + val logger = Logger.getInstance(this::class.java).apply { + // tie log level to language server log level + val languageServerWrapper = LanguageServerWrapper.getInstance() + if (languageServerWrapper.logger.isDebugEnabled) this.setLevel(LogLevel.DEBUG) + if (languageServerWrapper.logger.isTraceEnabled) this.setLevel(LogLevel.TRACE) + } + + fun generateIgnoreInFileCommand(jbCefBrowser: JBCefBrowserBase): CefLoadHandlerAdapter { + val applyIgnoreInFileQuery = JBCefJSQuery.create(jbCefBrowser) + + applyIgnoreInFileQuery.addHandler { value -> + val params = value.split("|@", limit = 2) + val issueId = params[0] // ID of issue that needs to be ignored + val filePath = params[1] + // Computed path that will be used in the snyk ignore command for the --path arg + val computedPath = Path(filePath).relativeTo(project.getContentRootPaths().firstOrNull()!!).toString(); + // Avoid blocking the UI thread + runInBackground("Snyk: applying ignore...") { + val result = try { + applyIgnoreInFileAndSave(issueId, computedPath) + } catch (e: IOException) { + logger.error("Error ignoring in file: $filePath. e:$e") + Result.failure(e) + } catch (e: Exception) { + logger.error("Unexpected error applying ignore. e:$e") + Result.failure(e) + } + + if (result.isSuccess) { + val script = """ + window.receiveIgnoreInFileResponse(true); + """.trimIndent() + jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) + } else { + val errorMessage = "Error ignoring in file: ${result.exceptionOrNull()?.message}" + SnykBalloonNotificationHelper.showError(errorMessage, project) + val errorScript = """ + window.receiveIgnoreInFileResponse(false, "$errorMessage"); + """.trimIndent() + jbCefBrowser.cefBrowser.executeJavaScript(errorScript, jbCefBrowser.cefBrowser.url, 0) + } + } + + return@addHandler JBCefJSQuery.Response("success") + } + + return object : CefLoadHandlerAdapter() { + override fun onLoadEnd(browser: CefBrowser, frame: CefFrame, httpStatusCode: Int) { + if (frame.isMain) { + val script = """ + (function() { + if (window.applyIgnoreInFileQuery) { + return; + } + window.applyIgnoreInFileQuery = function(value) { ${applyIgnoreInFileQuery.inject("value")} }; + })(); + """.trimIndent() + browser.executeJavaScript(script, browser.url, 0) + } + } + } + } + + fun applyIgnoreInFileAndSave(issueId: String, filePath: String): Result { + val ignoreService = IgnoreService(project) + if (issueId != "" && filePath != "") { + ignoreService.ignoreInstance(issueId, filePath) + } else { + logger.error("[applyIgnoreInFileAndSave] Failed to find document for: $filePath") + val errorMessage = "Failed to find document for: $filePath" + SnykBalloonNotificationHelper.showError(errorMessage, project) + return Result.failure(IOException(errorMessage)) + } + return Result.success(Unit) + } +} diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt index 30c58e465..e50ebb15d 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt @@ -14,6 +14,7 @@ import io.snyk.plugin.ui.baseGridConstraintsAnchorWest import io.snyk.plugin.ui.descriptionHeaderPanel import io.snyk.plugin.ui.jcef.ApplyFixHandler import io.snyk.plugin.ui.jcef.GenerateAIFixHandler +import io.snyk.plugin.ui.jcef.IgnoreInFileHandler import io.snyk.plugin.ui.jcef.JCEFUtils import io.snyk.plugin.ui.jcef.LoadHandlerGenerator import io.snyk.plugin.ui.jcef.OpenFileLoadHandlerGenerator @@ -70,7 +71,16 @@ class SuggestionDescriptionPanelFromLS( loadHandlerGenerators += { applyFixHandler.generateApplyFixCommand(it) } + + } + ScanIssue.INFRASTRUCTURE_AS_CODE -> + { + val applyIgnoreInFileHandler = IgnoreInFileHandler(project) + loadHandlerGenerators +={ + applyIgnoreInFileHandler.generateIgnoreInFileCommand(it) + } } + } val html = this.getCustomCssAndScript() val jbCefBrowserComponent = @@ -165,13 +175,17 @@ class SuggestionDescriptionPanelFromLS( val editorColorsManager = EditorColorsManager.getInstance() val editorUiTheme = editorColorsManager.schemeForCurrentUITheme + val lsNonce = extractLsNonceIfPresent(html) + var nonce = getNonce() + if (lsNonce != "") { + nonce = lsNonce + } html = html.replace("\${ideStyle}", "") html = html.replace("\${headerEnd}", "") html = html.replace("\${ideScript}", "") - val nonce = getNonce() html = html.replace("\${nonce}", nonce) html = html.replace("--default-font: ", "--default-font: \"${JBUI.Fonts.label().asPlain().family}\", ") html = html.replace("var(--text-color)", UIUtil.getLabelForeground().toHex()) @@ -199,7 +213,17 @@ class SuggestionDescriptionPanelFromLS( return html } - + private fun extractLsNonceIfPresent(html: String): String{ + // When the nonce is injected by the IDE, it is of format nonce-${nonce} + if (!html.contains("\${nonce}") && html.contains("nonce-")){ + val nonceStartPosition = html.indexOf("nonce-") + // Length of LS nonce + val startIndex = nonceStartPosition + "nonce-".length + val endIndex = startIndex + 24 + return html.substring(startIndex, endIndex ).trim() + } + return "" + } private fun getNonce(): String { val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') return (1..32) @@ -209,7 +233,6 @@ class SuggestionDescriptionPanelFromLS( private fun getCustomScript(): String { return """ - (function () { // Utility function to show/hide an element based on a toggle value function toggleElement(element, action) { if (!element) return; @@ -341,6 +364,7 @@ class SuggestionDescriptionPanelFromLS( console.log('Applying fix', patch); } + // DOM element references const generateAiFixBtn = document.getElementById("generate-ai-fix"); const applyFixBtn = document.getElementById('apply-fix') @@ -360,10 +384,11 @@ class SuggestionDescriptionPanelFromLS( const diffNumElem = document.getElementById("diff-number"); const diffNum2Elem = document.getElementById("diff-number2"); + const ignoreContainer = document.getElementById("ignore-container"); + let diffSelectedIndex = 0; let fixes = []; - // Event listener for Generate AI fix button generateAiFixBtn?.addEventListener("click", generateAIFix); applyFixBtn?.addEventListener('click', applyFix); @@ -372,6 +397,8 @@ class SuggestionDescriptionPanelFromLS( nextDiffElem?.addEventListener("click", nextDiff); previousDiffElem?.addEventListener("click", previousDiff); + toggleElement(ignoreContainer, "show"); + // This function will be called once the response is received from the Language Server window.receiveAIFixResponse = function (fixesResponse) { fixes = [...fixesResponse]; @@ -392,7 +419,6 @@ class SuggestionDescriptionPanelFromLS( console.error('Failed to apply fix', success); } }; - })(); """.trimIndent() } } diff --git a/src/test/kotlin/io/snyk/plugin/ui/jcef/IgnoreInFileHandlerTest.kt b/src/test/kotlin/io/snyk/plugin/ui/jcef/IgnoreInFileHandlerTest.kt new file mode 100644 index 000000000..199e37a1d --- /dev/null +++ b/src/test/kotlin/io/snyk/plugin/ui/jcef/IgnoreInFileHandlerTest.kt @@ -0,0 +1,59 @@ +package io.snyk.plugin.ui.jcef + +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiFile +import com.intellij.testFramework.PlatformTestUtil +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import io.snyk.plugin.resetSettings +import org.eclipse.lsp4j.ExecuteCommandParams +import org.eclipse.lsp4j.services.LanguageServer +import snyk.common.IgnoreService +import snyk.common.annotator.SnykCodeAnnotator +import snyk.common.annotator.SnykIaCAnnotator +import snyk.common.lsp.LanguageServerWrapper +import snyk.common.lsp.commands.COMMAND_EXECUTE_CLI +import java.io.File +import java.nio.file.Paths +import java.util.concurrent.CompletableFuture +import java.util.function.BooleanSupplier + +class IgnoreInFileHandlerTest : BasePlatformTestCase() { + private lateinit var ignorer: IgnoreInFileHandler + private val fileName = "fargate.json" + val lsMock = mockk() + + override fun getTestDataPath(): String { + val resource = SnykIaCAnnotator::class.java.getResource("/iac-test-results") + requireNotNull(resource) { "Make sure that the resource $resource exists!" } + return Paths.get(resource.toURI()).toString() + } + + override fun setUp() { + super.setUp() + unmockkAll() + resetSettings(project) + val languageServerWrapper = LanguageServerWrapper.getInstance() + languageServerWrapper.languageServer = lsMock + languageServerWrapper.isInitialized = true + ignorer = IgnoreInFileHandler(project) + } + + fun `test issue should be ignored in file`() { + every { lsMock.workspaceService.executeCommand(any()) } returns CompletableFuture.completedFuture(null) + val filePath = this.getTestDataPath()+ File.separator + fileName; + ignorer.applyIgnoreInFileAndSave("SNYK-CC-TF-61", filePath ) + val projectBasePath = project.basePath ?: ""; + + // Expected args for executeCommandParams + val args: List = arrayListOf(projectBasePath, "ignore", "--id=SNYK-CC-TF-61", "--path=${filePath}") + + val executeCommandParams = ExecuteCommandParams (COMMAND_EXECUTE_CLI, args); + verify { lsMock.workspaceService.executeCommand(executeCommandParams) } + } +}