Skip to content

Commit

Permalink
feat: add reference folder to branch chooser (#652)
Browse files Browse the repository at this point in the history
* feat: add reference folder to branch chooser

This code change introduces the ability to specify a reference folder for Snyk scans, in addition to the existing base branch option. This is particularly useful for projects that are not under Git version control.

Here's a breakdown of the changes:

* **`FolderConfig`**:  A new field `referenceFolderPath` has been added to store the path of the reference folder.  Additionally, `scanCommandConfig` has been added to support running commands before and after a scan, optionally only on the reference folder.
* **`SnykToolWindowPanel.kt`**:
    * `getRootNodeText()` now displays the reference folder information if it's set, otherwise it falls back to the base branch.
    * The UI now updates to reflect the chosen reference folder. Error handling has been slightly improved.
* **`BranchChooserComboboxDialog.kt`**:
    * This dialog now includes a file chooser for selecting the reference folder.
    * The dialog validates that either a base branch or a reference folder is selected. The member `comboBoxes` has been renamed to `baseBranches` and changed to a `MutableMap<FolderConfig, ComboBox<String>>`. Also a new `MutableMap<FolderConfig, TextFieldWithBrowseButton>` has been introduced for the reference folders.
* **`BranchChooserComboBoxDialogTest.kt`**:  The tests have been updated to reflect the changes in `BranchChooserComboboxDialog.kt`, specifically the change from `comboBoxes` to `baseBranches`.
* **`Utils.kt`**: A new function `isExecutable()` has been added, likely used for validating pre/post scan commands.
* **`SnykLanguageClient.kt`**:  A debug log message has been added to show the hash codes of the old and new tokens during authentication, likely for debugging token refresh issues.

Key improvements:

* **Support for non-Git projects**:  The reference folder option allows Snyk to scan projects not managed by Git.
* **More flexible configuration**:  Users can now specify both a base branch and a reference folder, or just one of them.
* **Improved UI**: The tool window displays the chosen reference folder.
* **Better error handling**:  The code includes more robust error handling for configuration updates.

This change makes the Snyk plugin more versatile and user-friendly, especially for users working with projects outside of Git. The addition of pre/post scan commands provides further customization options for the scanning process.  The debugging enhancements in `SnykLanguageClient.kt` should help diagnose token-related issues more easily.

* fix: set folderConfig properties as nullable

* fix: always set reference folder

* fix: use sha256 for logging a hashed token

The changes introduce a SHA-256 hashing function for strings and use it to log the hash of the Snyk authentication token instead of the Java `hashCode()` method.  This improves security by avoiding logging potentially sensitive information in plain text, even in debug logs.  The `hashCode()` method is not suitable for security purposes as it's not cryptographically secure and can lead to collisions.

Here's a breakdown:

* **`src/main/kotlin/io/snyk/plugin/Utils.kt`**: This file adds the `sha256()` extension function to the `String` class. This function computes the SHA-256 hash of the string and returns it as a hexadecimal string.

* **`src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt`**:  In the `hasAuthenticated` function:
    - The old token is now fetched with a null-safe operator (`?: ""`) to handle cases where no token is present.
    -  The logging lines using `oldToken.hashCode()` and `param.token.hashCode()` are replaced with `oldToken.sha256()` and `param.token?.sha256()` respectively. This change ensures that the actual token value is never logged, even in debug mode.  The null-safe operator (`?.`) on `param.token` handles the case where the new token might be null.

In summary, these changes improve the security of the plugin by preventing the accidental logging of sensitive authentication tokens. They replace the insecure `hashCode()` method with a proper cryptographic hash function (SHA-256) for logging purposes, offering better protection against information leakage.

* docs: update CHANGELOG.md

This changelog entry describes the following changes for version 2.11.1:

**Changed:**

* Added support for 2025.1.
* Added the ability to select a reference folder instead of a branch for delta scanning, displaying only net-new issues.

**Fixed:**

* Fixed a bug related to workspace folder configuration on language server (re-)start.

* fix: add test to check for validation, renamed dialog

---------

Co-authored-by: Abdelrahman Shawki Hassan <[email protected]>
  • Loading branch information
bastiandoetsch and ShawkyZ authored Feb 10, 2025
1 parent c8ccdec commit 56229ce
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 114 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## [2.11.1]
### Changed
- support 2025.1
- support 2025.1.
- allow to select a reference folder instead of a branch for delta scanning and display of only net-new issues.

### Fixed
- workspace folder configuration on language server (re-)start
Expand Down
12 changes: 12 additions & 0 deletions src/main/kotlin/io/snyk/plugin/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.psi.PsiFile
Expand Down Expand Up @@ -61,8 +62,10 @@ import snyk.iac.IacScanService
import java.io.File
import java.io.FileNotFoundException
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.security.MessageDigest
import java.util.Objects.nonNull
import java.util.SortedSet
import java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -459,8 +462,17 @@ fun VirtualFile.isInContent(project: Project): Boolean {
}
}

fun VirtualFile.isExecutable(): Boolean = this.toNioPathOrNull()?.let { Files.isExecutable(it) } == true

fun VirtualFile.isWhitelistedForInclusion() = this.name == "project.assets.json" && this.parent.name == "obj"

fun String.sha256(): String {
val bytes = this.toByteArray()
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(bytes)
return digest.fold("") { str, it -> str + "%02x".format(it) }
}

inline fun runInBackground(
title: String,
project: Project? = null,
Expand Down
82 changes: 0 additions & 82 deletions src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt

This file was deleted.

133 changes: 133 additions & 0 deletions src/main/kotlin/io/snyk/plugin/ui/ReferenceChooserComboboxDialog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package io.snyk.plugin.ui

import com.intellij.openapi.components.service
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.TextComponentAccessor
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.util.ui.GridBag
import com.intellij.util.ui.JBUI
import io.snyk.plugin.runInBackground
import snyk.common.lsp.FolderConfig
import snyk.common.lsp.LanguageServerWrapper
import snyk.common.lsp.settings.FolderConfigSettings
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel


class ReferenceChooserDialog(val project: Project) : DialogWrapper(true) {
var baseBranches: MutableMap<FolderConfig, ComboBox<String>> = mutableMapOf()
private val referenceFolders: MutableMap<FolderConfig, TextFieldWithBrowseButton> = mutableMapOf()

init {
init()
title = "Choose base branch for net-new issue scanning"
}

override fun createCenterPanel(): JComponent {
val folderConfigs = service<FolderConfigSettings>().getAllForProject(project)
folderConfigs.forEach { folderConfig ->
val comboBox = ComboBox(folderConfig.localBranches?.sorted()?.toTypedArray() ?: emptyArray())
comboBox.selectedItem = folderConfig.baseBranch
comboBox.name = folderConfig.folderPath
baseBranches[folderConfig] = comboBox
referenceFolders[folderConfig] = configureReferenceFolder(folderConfig)
}
val gridBagLayout = GridBagLayout()
val dialogPanel = JPanel(gridBagLayout)
val gridBag = GridBag()
gridBag.defaultFill = GridBagConstraints.BOTH
gridBag.insets = JBUI.insets(20)
gridBag.defaultPaddingX = 20
gridBag.defaultPaddingY = 20

baseBranches.forEach {
dialogPanel.add(JLabel("Base Branch for ${it.value.name}: "), gridBag.nextLine())
dialogPanel.add(it.value, gridBag.nextLine())
val referenceFolder = referenceFolders[it.key]
dialogPanel.add(JLabel("Reference Folder for ${referenceFolder!!.name}: "), gridBag.nextLine())
dialogPanel.add(referenceFolder, gridBag.nextLine())
}
return dialogPanel
}

private fun configureReferenceFolder(folderConfig: FolderConfig): TextFieldWithBrowseButton {
val referenceFolder = TextFieldWithBrowseButton()
referenceFolder.text = folderConfig.referenceFolderPath ?: ""
referenceFolder.name = folderConfig.folderPath

referenceFolder.toolTipText =
"Optional. Here you can specify a reference directory to be used for scanning."

val descriptor = FileChooserDescriptor(
false,
true,
false,
false,
false,
false
)

referenceFolder.addBrowseFolderListener(
"",
"Please choose the reference folder you want to use:",
null,
descriptor,
TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT,
)
return referenceFolder
}

public override fun doOKAction() {
super.doOKAction()
execute()
}

fun execute() {
val folderConfigSettings = service<FolderConfigSettings>()
baseBranches.forEach {
val folderConfig: FolderConfig = it.key

val baseBranch = getSelectedItem(it.value) ?: ""
val referenceFolderControl = referenceFolders[folderConfig]
val referenceFolder = referenceFolderControl?.text ?: ""
if (baseBranch.isNotBlank()) {
folderConfigSettings.addFolderConfig(folderConfig.copy(baseBranch = baseBranch))
}
if (referenceFolder.isNotBlank()) {
folderConfigSettings.addFolderConfig(folderConfig.copy(referenceFolderPath = referenceFolder))
}
}

if (doValidate() == null) {
runInBackground("Snyk: updating configuration") {
LanguageServerWrapper.getInstance().updateConfiguration(true)
}
}
}

override fun doValidate(): ValidationInfo? {
baseBranches.forEach { entry ->
val baseBranch = entry.value
val refFolder = referenceFolders[entry.key]

val baseBranchSelected = !getSelectedItem(baseBranch).isNullOrBlank()
val refFolderSelected = refFolder != null && refFolder.text.isNotBlank()
if (!baseBranchSelected && !refFolderSelected) {
return ValidationInfo(
"Please select a base branch for ${baseBranch.name} or a reference folder",
baseBranch
)
}
}
return null
}

private fun getSelectedItem(baseBranch: ComboBox<String>) = baseBranch.selectedItem?.toString()
}
51 changes: 32 additions & 19 deletions src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ import io.snyk.plugin.pluginSettings
import io.snyk.plugin.refreshAnnotationsForOpenFiles
import io.snyk.plugin.services.SnykApplicationSettingsStateService
import io.snyk.plugin.snykToolWindow
import io.snyk.plugin.ui.BranchChooserComboBoxDialog
import io.snyk.plugin.toVirtualFile
import io.snyk.plugin.ui.ReferenceChooserDialog
import io.snyk.plugin.ui.SnykBalloonNotificationHelper
import io.snyk.plugin.ui.expandTreeNodeRecursively
import io.snyk.plugin.ui.toolwindow.nodes.DescriptionHolderTreeNode
Expand All @@ -73,6 +74,7 @@ import org.jetbrains.annotations.TestOnly
import org.jetbrains.concurrency.runAsync
import snyk.common.ProductType
import snyk.common.SnykError
import snyk.common.lsp.FolderConfig
import snyk.common.lsp.LanguageServerWrapper
import snyk.common.lsp.ScanIssue
import snyk.common.lsp.SnykScanParams
Expand Down Expand Up @@ -120,9 +122,16 @@ class SnykToolWindowPanel(
}
}

private fun getRootNodeText(folderPath: String, baseBranch: String) =
"Click to choose base branch for $folderPath [ current: $baseBranch ]"

private fun getRootNodeText(folderConfig: FolderConfig): String {
val detail = if (folderConfig.referenceFolderPath.isNullOrBlank()) {
folderConfig.baseBranch
} else {
folderConfig.referenceFolderPath
}
return "Click to choose base branch or reference folder for ${
folderConfig.folderPath.toVirtualFile().toNioPath().fileName
}: [ current: $detail ]"
}

/** Flag used to recognize not-user-initiated Description panel reload cases for purposes like:
* - disable Itly logging
Expand All @@ -140,7 +149,7 @@ class SnykToolWindowPanel(

init {
val folderConfig = service<FolderConfigSettings>().getFolderConfig(project.basePath.toString())
val rootNodeText = folderConfig?.let { getRootNodeText(it.folderPath, it.baseBranch) }
val rootNodeText = folderConfig?.let { getRootNodeText(it) }
?: "Choose branch on ${project.basePath}"
rootTreeNode.info = rootNodeText

Expand Down Expand Up @@ -365,7 +374,7 @@ class SnykToolWindowPanel(

if (lastPathComponent is ChooseBranchNode && capturedNavigateToSourceEnabled && !capturedSmartReloadMode) {
invokeLater {
BranchChooserComboBoxDialog(project).show()
ReferenceChooserDialog(project).show()
}
}

Expand Down Expand Up @@ -476,7 +485,7 @@ class SnykToolWindowPanel(
),
) {
rootNodesToUpdate.forEach {
if (it.childCount>0) it.removeAllChildren()
if (it.childCount > 0) it.removeAllChildren()
(vulnerabilitiesTree.model as DefaultTreeModel).reload(it)
}
}
Expand All @@ -491,7 +500,12 @@ class SnykToolWindowPanel(
enableCodeScanAccordingToServerSetting()
displayEmptyDescription()
} catch (e: Exception) {
displaySnykError(SnykError(e.message ?: "Exception while initializing plugin {${e.message}", ""))
displaySnykError(
SnykError(
e.message ?: "Exception while initializing plugin {${e.message}",
""
)
)
logger.error("Failed to apply Snyk settings", e)
}
}
Expand Down Expand Up @@ -589,7 +603,8 @@ class SnykToolWindowPanel(
val newOssTreeNodeText = getNewOssTreeNodeText(settings, realError, ossResultsCount, addHMLPostfix)
newOssTreeNodeText?.let { rootOssTreeNode.userObject = it }

val newSecurityIssuesNodeText = getNewSecurityIssuesNodeText(settings, securityIssuesCount, addHMLPostfix)
val newSecurityIssuesNodeText =
getNewSecurityIssuesNodeText(settings, securityIssuesCount, addHMLPostfix)
newSecurityIssuesNodeText?.let { rootSecurityIssuesTreeNode.userObject = it }

val newQualityIssuesNodeText = getNewQualityIssuesNodeText(settings, qualityIssuesCount, addHMLPostfix)
Expand All @@ -598,23 +613,20 @@ class SnykToolWindowPanel(
val newIacTreeNodeText = getNewIacTreeNodeText(settings, iacResultsCount, addHMLPostfix)
newIacTreeNodeText?.let { rootIacIssuesTreeNode.userObject = it }

val newContainerTreeNodeText = getNewContainerTreeNodeText(settings, containerResultsCount, addHMLPostfix)
val newContainerTreeNodeText =
getNewContainerTreeNodeText(settings, containerResultsCount, addHMLPostfix)
newContainerTreeNodeText?.let { rootContainerIssuesTreeNode.userObject = it }

val newRootTreeNodeText = getNewRootTreeNodeText()
newRootTreeNodeText.let { rootTreeNode.info = it }
}

private fun getNewRootTreeNodeText() : String {
private fun getNewRootTreeNodeText(): String {
val folderConfig = service<FolderConfigSettings>().getFolderConfig(project.basePath.toString())
if (folderConfig?.let {
getRootNodeText(
it.folderPath,
it.baseBranch
)
} != null) return folderConfig.let { getRootNodeText(it.folderPath, it.baseBranch) }
return "Choose branch on ${project.basePath}"
if (folderConfig?.let { getRootNodeText(it) } != null) return getRootNodeText(folderConfig)
return "Choose branch on ${project.basePath}"
}

private fun getNewContainerTreeNodeText(
settings: SnykApplicationSettingsStateService,
containerResultsCount: Int?,
Expand Down Expand Up @@ -783,7 +795,8 @@ class SnykToolWindowPanel(

fun displayContainerResults(containerResult: ContainerResult) {
val userObjectsForExpandedChildren = userObjectsForExpandedNodes(rootContainerIssuesTreeNode)
val selectedNodeUserObject = TreeUtil.findObjectInPath(vulnerabilitiesTree.selectionPath, Any::class.java)
val selectedNodeUserObject =
TreeUtil.findObjectInPath(vulnerabilitiesTree.selectionPath, Any::class.java)

rootContainerIssuesTreeNode.removeAllChildren()

Expand Down
Loading

0 comments on commit 56229ce

Please sign in to comment.