Skip to content

Commit

Permalink
Merge pull request #634 from snyk/fix/add-iac-ignore-button
Browse files Browse the repository at this point in the history
fix: add iac ignore button [IDE-683]
  • Loading branch information
DariusZdroba authored Nov 19, 2024
2 parents 2e5282f + 7031ce3 commit 264465c
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 7 deletions.
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/io/snyk/plugin/cli/ConsoleCommandRunner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/io/snyk/plugin/services/CliAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ abstract class CliAdapter<CliIssues, R : CliResult<CliIssues>>(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)
}
Expand Down
98 changes: 98 additions & 0 deletions src/main/kotlin/io/snyk/plugin/ui/jcef/IgnoreInFileHandler.kt
Original file line number Diff line number Diff line change
@@ -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<Unit> {
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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}", "<style nonce=\${nonce}>$ideStyle</style>")
html = html.replace("\${headerEnd}", "")
html = html.replace("\${ideScript}", "<script nonce=\${nonce}>$ideScript</script>")


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())
Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand Down Expand Up @@ -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')
Expand All @@ -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);
Expand All @@ -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];
Expand All @@ -392,7 +419,6 @@ class SuggestionDescriptionPanelFromLS(
console.error('Failed to apply fix', success);
}
};
})();
""".trimIndent()
}
}
Expand Down
59 changes: 59 additions & 0 deletions src/test/kotlin/io/snyk/plugin/ui/jcef/IgnoreInFileHandlerTest.kt
Original file line number Diff line number Diff line change
@@ -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<LanguageServer>()

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<String> = arrayListOf(projectBasePath, "ignore", "--id=SNYK-CC-TF-61", "--path=${filePath}")

val executeCommandParams = ExecuteCommandParams (COMMAND_EXECUTE_CLI, args);
verify { lsMock.workspaceService.executeCommand(executeCommandParams) }
}
}

0 comments on commit 264465c

Please sign in to comment.