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
96 changes: 96 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,96 @@
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.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.File
import java.io.IOException

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 = filePath.removePrefix("${project.basePath}${File.separator}")
DariusZdroba marked this conversation as resolved.
Show resolved Hide resolved
// 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 Down Expand Up @@ -341,9 +365,20 @@ class SuggestionDescriptionPanelFromLS(
console.log('Applying fix', patch);
}

function applyIgnoreInFile() {
console.log('Applying ignore', issue);
if (!issue) return;

window.applyIgnoreInFileQuery(issue + '|@' + filePath + ' > ' + resourcePath);
toggleElement(ignoreInFileBtn, "hide");
console.log('Applying ignore');
}
DariusZdroba marked this conversation as resolved.
Show resolved Hide resolved

// DOM element references
const generateAiFixBtn = document.getElementById("generate-ai-fix");
const applyFixBtn = document.getElementById('apply-fix')
const ignoreContainer = document.getElementById("ignore-container");
const ignoreInFileBtn = document.getElementById('ignore-file-issue')
const retryGenerateFixBtn = document.getElementById('retry-generate-fix')

const fixLoadingIndicatorElem = document.getElementById("fix-loading-indicator");
Expand All @@ -363,15 +398,19 @@ class SuggestionDescriptionPanelFromLS(

let diffSelectedIndex = 0;
let fixes = [];

let issue = ignoreInFileBtn.getAttribute('issue')
let resourcePath = ignoreInFileBtn.getAttribute('resourcePath')
let filePath = ignoreInFileBtn.getAttribute('filePath')
// Event listener for Generate AI fix button
generateAiFixBtn?.addEventListener("click", generateAIFix);
applyFixBtn?.addEventListener('click', applyFix);
retryGenerateFixBtn?.addEventListener('click', reGenerateAIFix);

ignoreInFileBtn?.addEventListener("click", applyIgnoreInFile);
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 @@ -382,6 +421,17 @@ class SuggestionDescriptionPanelFromLS(
showAIFixes(fixes);
};

window.receiveIgnoreInFileResponse = function (success){
console.log('[[receiveIgnoreInFileResponse]]', success);
if(success){
ignoreInFileBtn.disabled = true;
console.log('Ignored in file', success);
document.getElementById('ignore-file-issue').disabled = true;
}else{
toggleElement(ignoreInFileBtn, "show");
console.error('Failed to apply fix', success);
}
}
DariusZdroba marked this conversation as resolved.
Show resolved Hide resolved
window.receiveApplyFixResponse = function (success) {
console.log('[[receiveApplyFixResponse]]', success);
if (success) {
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