Skip to content

Commit 264465c

Browse files
authored
Merge pull request #634 from snyk/fix/add-iac-ignore-button
fix: add iac ignore button [IDE-683]
2 parents 2e5282f + 7031ce3 commit 264465c

File tree

6 files changed

+193
-7
lines changed

6 files changed

+193
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
- If $/snyk.hasAuthenticated transmits an API URL, this is saved in the settings.
66
- Add "plugin installed" analytics event (sent after authentication)
77
- Added a description of custom endpoints to settings dialog.
8-
8+
- Add option to ignore IaC issues
99
### Fixed
1010
- folder-specific configs are availabe on opening projects, not only on restart of the IDE
11-
1211
## [2.10.0]
1312
### Changed
1413
- save git folder config in settings

src/main/kotlin/io/snyk/plugin/cli/ConsoleCommandRunner.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,5 +89,6 @@ open class ConsoleCommandRunner {
8989

9090
companion object {
9191
const val PROCESS_CANCELLED_BY_USER = "PROCESS_CANCELLED_BY_USER"
92+
const val SAVING_POLICY_FILE = "Saving .snyk policy file...\n"
9293
}
9394
}

src/main/kotlin/io/snyk/plugin/services/CliAdapter.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ abstract class CliAdapter<CliIssues, R : CliResult<CliIssues>>(val project: Proj
7373
rawStr == ConsoleCommandRunner.PROCESS_CANCELLED_BY_USER -> {
7474
getProductResult(null)
7575
}
76+
rawStr == ConsoleCommandRunner.SAVING_POLICY_FILE -> {
77+
getProductResult(null)
78+
}
7679
rawStr.isEmpty() -> {
7780
getErrorResult(CLI_PRODUCE_NO_OUTPUT)
7881
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package io.snyk.plugin.ui.jcef
2+
3+
import com.intellij.openapi.diagnostic.LogLevel
4+
import com.intellij.openapi.diagnostic.Logger
5+
import com.intellij.openapi.project.Project
6+
import com.intellij.ui.jcef.JBCefBrowserBase
7+
import com.intellij.ui.jcef.JBCefJSQuery
8+
import io.snyk.plugin.getContentRootPaths
9+
import io.snyk.plugin.runInBackground
10+
import io.snyk.plugin.ui.SnykBalloonNotificationHelper
11+
import org.cef.browser.CefBrowser
12+
import org.cef.browser.CefFrame
13+
import org.cef.handler.CefLoadHandlerAdapter
14+
import snyk.common.IgnoreService
15+
import snyk.common.lsp.LanguageServerWrapper
16+
import java.io.IOException
17+
import kotlin.io.path.Path
18+
import kotlin.io.path.relativeTo
19+
20+
class IgnoreInFileHandler(
21+
private val project: Project,
22+
) {
23+
val logger = Logger.getInstance(this::class.java).apply {
24+
// tie log level to language server log level
25+
val languageServerWrapper = LanguageServerWrapper.getInstance()
26+
if (languageServerWrapper.logger.isDebugEnabled) this.setLevel(LogLevel.DEBUG)
27+
if (languageServerWrapper.logger.isTraceEnabled) this.setLevel(LogLevel.TRACE)
28+
}
29+
30+
fun generateIgnoreInFileCommand(jbCefBrowser: JBCefBrowserBase): CefLoadHandlerAdapter {
31+
val applyIgnoreInFileQuery = JBCefJSQuery.create(jbCefBrowser)
32+
33+
applyIgnoreInFileQuery.addHandler { value ->
34+
val params = value.split("|@", limit = 2)
35+
val issueId = params[0] // ID of issue that needs to be ignored
36+
val filePath = params[1]
37+
// Computed path that will be used in the snyk ignore command for the --path arg
38+
val computedPath = Path(filePath).relativeTo(project.getContentRootPaths().firstOrNull()!!).toString();
39+
// Avoid blocking the UI thread
40+
runInBackground("Snyk: applying ignore...") {
41+
val result = try {
42+
applyIgnoreInFileAndSave(issueId, computedPath)
43+
} catch (e: IOException) {
44+
logger.error("Error ignoring in file: $filePath. e:$e")
45+
Result.failure(e)
46+
} catch (e: Exception) {
47+
logger.error("Unexpected error applying ignore. e:$e")
48+
Result.failure(e)
49+
}
50+
51+
if (result.isSuccess) {
52+
val script = """
53+
window.receiveIgnoreInFileResponse(true);
54+
""".trimIndent()
55+
jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0)
56+
} else {
57+
val errorMessage = "Error ignoring in file: ${result.exceptionOrNull()?.message}"
58+
SnykBalloonNotificationHelper.showError(errorMessage, project)
59+
val errorScript = """
60+
window.receiveIgnoreInFileResponse(false, "$errorMessage");
61+
""".trimIndent()
62+
jbCefBrowser.cefBrowser.executeJavaScript(errorScript, jbCefBrowser.cefBrowser.url, 0)
63+
}
64+
}
65+
66+
return@addHandler JBCefJSQuery.Response("success")
67+
}
68+
69+
return object : CefLoadHandlerAdapter() {
70+
override fun onLoadEnd(browser: CefBrowser, frame: CefFrame, httpStatusCode: Int) {
71+
if (frame.isMain) {
72+
val script = """
73+
(function() {
74+
if (window.applyIgnoreInFileQuery) {
75+
return;
76+
}
77+
window.applyIgnoreInFileQuery = function(value) { ${applyIgnoreInFileQuery.inject("value")} };
78+
})();
79+
""".trimIndent()
80+
browser.executeJavaScript(script, browser.url, 0)
81+
}
82+
}
83+
}
84+
}
85+
86+
fun applyIgnoreInFileAndSave(issueId: String, filePath: String): Result<Unit> {
87+
val ignoreService = IgnoreService(project)
88+
if (issueId != "" && filePath != "") {
89+
ignoreService.ignoreInstance(issueId, filePath)
90+
} else {
91+
logger.error("[applyIgnoreInFileAndSave] Failed to find document for: $filePath")
92+
val errorMessage = "Failed to find document for: $filePath"
93+
SnykBalloonNotificationHelper.showError(errorMessage, project)
94+
return Result.failure(IOException(errorMessage))
95+
}
96+
return Result.success(Unit)
97+
}
98+
}

src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import io.snyk.plugin.ui.baseGridConstraintsAnchorWest
1414
import io.snyk.plugin.ui.descriptionHeaderPanel
1515
import io.snyk.plugin.ui.jcef.ApplyFixHandler
1616
import io.snyk.plugin.ui.jcef.GenerateAIFixHandler
17+
import io.snyk.plugin.ui.jcef.IgnoreInFileHandler
1718
import io.snyk.plugin.ui.jcef.JCEFUtils
1819
import io.snyk.plugin.ui.jcef.LoadHandlerGenerator
1920
import io.snyk.plugin.ui.jcef.OpenFileLoadHandlerGenerator
@@ -70,7 +71,16 @@ class SuggestionDescriptionPanelFromLS(
7071
loadHandlerGenerators += {
7172
applyFixHandler.generateApplyFixCommand(it)
7273
}
74+
75+
}
76+
ScanIssue.INFRASTRUCTURE_AS_CODE ->
77+
{
78+
val applyIgnoreInFileHandler = IgnoreInFileHandler(project)
79+
loadHandlerGenerators +={
80+
applyIgnoreInFileHandler.generateIgnoreInFileCommand(it)
81+
}
7382
}
83+
7484
}
7585
val html = this.getCustomCssAndScript()
7686
val jbCefBrowserComponent =
@@ -165,13 +175,17 @@ class SuggestionDescriptionPanelFromLS(
165175

166176
val editorColorsManager = EditorColorsManager.getInstance()
167177
val editorUiTheme = editorColorsManager.schemeForCurrentUITheme
178+
val lsNonce = extractLsNonceIfPresent(html)
179+
var nonce = getNonce()
180+
if (lsNonce != "") {
181+
nonce = lsNonce
182+
}
168183

169184
html = html.replace("\${ideStyle}", "<style nonce=\${nonce}>$ideStyle</style>")
170185
html = html.replace("\${headerEnd}", "")
171186
html = html.replace("\${ideScript}", "<script nonce=\${nonce}>$ideScript</script>")
172187

173188

174-
val nonce = getNonce()
175189
html = html.replace("\${nonce}", nonce)
176190
html = html.replace("--default-font: ", "--default-font: \"${JBUI.Fonts.label().asPlain().family}\", ")
177191
html = html.replace("var(--text-color)", UIUtil.getLabelForeground().toHex())
@@ -199,7 +213,17 @@ class SuggestionDescriptionPanelFromLS(
199213

200214
return html
201215
}
202-
216+
private fun extractLsNonceIfPresent(html: String): String{
217+
// When the nonce is injected by the IDE, it is of format nonce-${nonce}
218+
if (!html.contains("\${nonce}") && html.contains("nonce-")){
219+
val nonceStartPosition = html.indexOf("nonce-")
220+
// Length of LS nonce
221+
val startIndex = nonceStartPosition + "nonce-".length
222+
val endIndex = startIndex + 24
223+
return html.substring(startIndex, endIndex ).trim()
224+
}
225+
return ""
226+
}
203227
private fun getNonce(): String {
204228
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
205229
return (1..32)
@@ -209,7 +233,6 @@ class SuggestionDescriptionPanelFromLS(
209233

210234
private fun getCustomScript(): String {
211235
return """
212-
(function () {
213236
// Utility function to show/hide an element based on a toggle value
214237
function toggleElement(element, action) {
215238
if (!element) return;
@@ -341,6 +364,7 @@ class SuggestionDescriptionPanelFromLS(
341364
console.log('Applying fix', patch);
342365
}
343366
367+
344368
// DOM element references
345369
const generateAiFixBtn = document.getElementById("generate-ai-fix");
346370
const applyFixBtn = document.getElementById('apply-fix')
@@ -360,10 +384,11 @@ class SuggestionDescriptionPanelFromLS(
360384
361385
const diffNumElem = document.getElementById("diff-number");
362386
const diffNum2Elem = document.getElementById("diff-number2");
387+
const ignoreContainer = document.getElementById("ignore-container");
388+
363389
364390
let diffSelectedIndex = 0;
365391
let fixes = [];
366-
367392
// Event listener for Generate AI fix button
368393
generateAiFixBtn?.addEventListener("click", generateAIFix);
369394
applyFixBtn?.addEventListener('click', applyFix);
@@ -372,6 +397,8 @@ class SuggestionDescriptionPanelFromLS(
372397
nextDiffElem?.addEventListener("click", nextDiff);
373398
previousDiffElem?.addEventListener("click", previousDiff);
374399
400+
toggleElement(ignoreContainer, "show");
401+
375402
// This function will be called once the response is received from the Language Server
376403
window.receiveAIFixResponse = function (fixesResponse) {
377404
fixes = [...fixesResponse];
@@ -392,7 +419,6 @@ class SuggestionDescriptionPanelFromLS(
392419
console.error('Failed to apply fix', success);
393420
}
394421
};
395-
})();
396422
""".trimIndent()
397423
}
398424
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package io.snyk.plugin.ui.jcef
2+
3+
import com.intellij.openapi.application.WriteAction
4+
import com.intellij.openapi.fileEditor.FileEditorManager
5+
import com.intellij.openapi.vfs.VirtualFile
6+
import com.intellij.psi.PsiFile
7+
import com.intellij.testFramework.PlatformTestUtil
8+
import com.intellij.testFramework.fixtures.BasePlatformTestCase
9+
import io.mockk.every
10+
import io.mockk.mockk
11+
import io.mockk.unmockkAll
12+
import io.mockk.verify
13+
import io.snyk.plugin.resetSettings
14+
import org.eclipse.lsp4j.ExecuteCommandParams
15+
import org.eclipse.lsp4j.services.LanguageServer
16+
import snyk.common.IgnoreService
17+
import snyk.common.annotator.SnykCodeAnnotator
18+
import snyk.common.annotator.SnykIaCAnnotator
19+
import snyk.common.lsp.LanguageServerWrapper
20+
import snyk.common.lsp.commands.COMMAND_EXECUTE_CLI
21+
import java.io.File
22+
import java.nio.file.Paths
23+
import java.util.concurrent.CompletableFuture
24+
import java.util.function.BooleanSupplier
25+
26+
class IgnoreInFileHandlerTest : BasePlatformTestCase() {
27+
private lateinit var ignorer: IgnoreInFileHandler
28+
private val fileName = "fargate.json"
29+
val lsMock = mockk<LanguageServer>()
30+
31+
override fun getTestDataPath(): String {
32+
val resource = SnykIaCAnnotator::class.java.getResource("/iac-test-results")
33+
requireNotNull(resource) { "Make sure that the resource $resource exists!" }
34+
return Paths.get(resource.toURI()).toString()
35+
}
36+
37+
override fun setUp() {
38+
super.setUp()
39+
unmockkAll()
40+
resetSettings(project)
41+
val languageServerWrapper = LanguageServerWrapper.getInstance()
42+
languageServerWrapper.languageServer = lsMock
43+
languageServerWrapper.isInitialized = true
44+
ignorer = IgnoreInFileHandler(project)
45+
}
46+
47+
fun `test issue should be ignored in file`() {
48+
every { lsMock.workspaceService.executeCommand(any()) } returns CompletableFuture.completedFuture(null)
49+
val filePath = this.getTestDataPath()+ File.separator + fileName;
50+
ignorer.applyIgnoreInFileAndSave("SNYK-CC-TF-61", filePath )
51+
val projectBasePath = project.basePath ?: "";
52+
53+
// Expected args for executeCommandParams
54+
val args: List<String> = arrayListOf(projectBasePath, "ignore", "--id=SNYK-CC-TF-61", "--path=${filePath}")
55+
56+
val executeCommandParams = ExecuteCommandParams (COMMAND_EXECUTE_CLI, args);
57+
verify { lsMock.workspaceService.executeCommand(executeCommandParams) }
58+
}
59+
}

0 commit comments

Comments
 (0)