Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add iac ignore button [IDE-683] #634

Merged
merged 9 commits into from
Nov 19, 2024
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
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 () {
Copy link
Contributor Author

@DariusZdroba DariusZdroba Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When extracting the script to migrate it to Language Server, we need acces to some variables which are locked in the scope of this self invoked function and can't be accessed outside,

we can have all the logic encapsulated in a self invoked function in Language Server, that would require some minimal changes to all IDE's that inject scripts.

// 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
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) }
}
}
Loading