Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .windsurfrules
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
** general **
always be concise, direct and don't try to appease me.

** how to implement **
always write and update test cases. iterate until they pass.
use existing mocks, don't write new ones.
if you use mocks, use mockk to generate them.
always run the tests after editing.
use junit4 syntax

** security **
determine the absolute path of the project directory. you can do that e.g. by executing pwd on the shell within the directory.
always use snyk for sca and code scanning. scan with both tools. you need to pass the absolute path of the directory we are scanning.
run snyk code tests after each edit. pass the absolute path of the project directory as a parameter
run snyk sca tests after updating the package manager / manifest file / build file. pass the absolute path of the project directory as a parameter.
run snyk sca and code test before committing. fix issues before committing.

** fixing issues **
fix issues if they are fixable. take the snyk scan results and the test results as input.

** committing **
when asked to commit, always use conventional commit messages (Conventional Commit Style (Subject + Body)). be descriptive in the body. if you find a JIRA issue (IDE-XXXX) in the branch name, use it as a postfix to the subject line in the format [IDE-XXXX]
when asked to push, always use 'git push --set-upstream origin $(git_current_branch)' with git_current_branch being the current branch we are on
never force push
never push without asking
never commit the hashicorp gomod
regularly fetch main branch and offer to merge it into git_current_branch
don't touch the copyright header
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Snyk Security Changelog
## [2.13.1]
### Fixed
- fixed not initialized exception in error handling during language server startup
- fixed handling of special characters in filepaths

## [2.13.0]

### Changed
Expand Down
81 changes: 10 additions & 71 deletions src/main/kotlin/io/snyk/plugin/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -421,12 +421,17 @@ fun String.toVirtualFile(): VirtualFile {
return if (!this.startsWith("file:")) {
StandardFileSystems.local().refreshAndFindFileByPath(this) ?: throw FileNotFoundException(this)
} else {
val filePath = Paths.get(this.toFilePathString())
val filePath = fromUriToPath()
VirtualFileManager.getInstance().refreshAndFindFileByNioPath(filePath)
?: throw FileNotFoundException(this)
}
}

fun String.fromUriToPath(): Path {
val filePath = Paths.get(URI.create(this))
return filePath.normalize()
}
Comment on lines +430 to +433
Copy link
Contributor

Choose a reason for hiding this comment

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

The fromUriToPath() method doesn't handle potential exceptions from URI.create(). If an invalid URI string is passed, this will throw an IllegalArgumentException. Consider adding exception handling to gracefully handle malformed URIs.


fun String.toVirtualFileOrNull(): VirtualFile? {
return try {
this.toVirtualFile()
Expand All @@ -436,77 +441,11 @@ fun String.toVirtualFileOrNull(): VirtualFile? {
}

fun VirtualFile.toLanguageServerURI(): String {
return this.url.toFileURIString()
}

/**
* Normalizes a string that represents a file path or URI.
*
* This should be called on a string that represents an absolute path to a local file, or a file uri for a local file.
* Relative paths and files on network shares are not currently supported.
*
* We deliberately avoid use of the Path, File and URI libraries as these will make decisions on how paths are handled
* based on the underlying operating system. This approach provides consistency.
*
* @param forceUseForwardSlashes Whether to force the use of forward slashses as path separators, even for files on
* Windows. Unix systems will always use forward slashes.
* @return The normalized string.
*/
private fun String.toNormalizedFilePath(forceUseForwardSlashes: Boolean): String {
val fileScheme = "file:"
val windowsSeparator = "\\"
val unixSeparator = "/"

// Strip the scheme and standardise separators on Unix for now.
val normalizedPath = this.removePrefix(fileScheme).replace(windowsSeparator, unixSeparator)
var targetSeparator = unixSeparator

// Split the path into parts, filtering out any blanks or references to the current directory.
val parts = normalizedPath.split(unixSeparator).filter { it.isNotBlank() && it != "." }.mapIndexed { idx, value ->
if (idx == 0) {
// Since we only support local files, we can use the first element of the path can tell us whether we
// are dealing with a Windows or unix file.
if (value.startsWithWindowsDriveLetter()) {
// Change to using Windows separators (if allowed) and capitalize the drive letter.
if (!forceUseForwardSlashes) targetSeparator = windowsSeparator
value.uppercase()
} else {
// On a Unix system, start with a slash representing root.
unixSeparator + value
}
} else value
}

// Removing any references to the parent directory (we have already removed references to the current directory).
val stack = mutableListOf<String>()
for (part in parts) {
if (part == "..") {
if (stack.isNotEmpty()) stack.removeAt(stack.size - 1)
} else stack.add(part)
}
return stack.joinToString(targetSeparator)
}

/**
* Converts a string representing a file path to a normalised form. @see io.snyk.plugin.UtilsKt.toNormalizedFilePath
*/
fun String.toFilePathString(): String {
return this.toNormalizedFilePath(forceUseForwardSlashes = false)
return this.path.fromPathToUriString()
}

/**
* Converts a string representing a file path to a normalised form. @see io.snyk.plugin.UtilsKt.toNormalizedFilePath
*/
fun String.toFileURIString(): String {
var pathString = this.toNormalizedFilePath(forceUseForwardSlashes = true)

// If we are handling a Windows path it may not have a leading slash, so add one.
if (pathString.startsWithWindowsDriveLetter()) {
pathString = "/$pathString"
}

// Add a file scheme. We use two slashes as standard.
return "file://$pathString"
fun String.fromPathToUriString(): String {
return Paths.get(this).normalize().toUri().toASCIIString()
}
Comment on lines +447 to 449
Copy link
Contributor

Choose a reason for hiding this comment

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

The fromPathToUriString() method should include exception handling for cases where the path contains characters that can't be properly encoded in a URI or when the path format is invalid.


private fun String.startsWithWindowsDriveLetter(): Boolean {
Expand All @@ -517,7 +456,7 @@ fun VirtualFile.getDocument(): Document? = runReadAction { FileDocumentManager.g

fun Project.getContentRootPaths(): SortedSet<Path> {
return getContentRootVirtualFiles()
.mapNotNull { it.path.toNioPathOrNull() }
.mapNotNull { it.path.toNioPathOrNull()?.normalize() }
.toSortedSet()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import io.snyk.plugin.pluginSettings
import io.snyk.plugin.refreshAnnotationsForOpenFiles
import io.snyk.plugin.ui.SnykBalloonNotificationHelper
import org.jetbrains.annotations.TestOnly
import org.jetbrains.concurrency.runAsync
import snyk.common.lsp.LanguageServerWrapper
import snyk.common.lsp.ScanState
import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded
Expand Down Expand Up @@ -56,7 +57,13 @@ class SnykTaskQueueService(val project: Project) {

// wait for modules to be loaded and indexed so we can add all relevant content roots
DumbService.getInstance(project).runWhenSmart {
languageServerWrapper.addContentRoots(project)
runAsync {
try {
languageServerWrapper.addContentRoots(project)
} catch (e: RuntimeException) {
logger.error("unable to add content roots for project $project", e)
}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ class SnykSettingsDialog(
// TODO: check for concrete project roots, and if we received a message for them
// this is an edge case, when a project is opened after ls initialization and
// preferences dialog is opened before ls sends the additional parameters
additionalParametersTextField.isEnabled = LanguageServerWrapper.getInstance().folderConfigsRefreshed.isNotEmpty()
additionalParametersTextField.isEnabled = LanguageServerWrapper.getInstance().getFolderConfigsRefreshed().isNotEmpty()
additionalParametersTextField.text = getAdditionalParams(project)
scanOnSaveCheckbox.isSelected = applicationSettings.scanOnSave
cliReleaseChannelDropDown.selectedItem = applicationSettings.cliReleaseChannel
Expand Down
55 changes: 39 additions & 16 deletions src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.openapi.vfs.VirtualFile
import io.snyk.plugin.fromUriToPath
import io.snyk.plugin.getCliFile
import io.snyk.plugin.getContentRootVirtualFiles
import io.snyk.plugin.getSnykTaskQueueService
import io.snyk.plugin.getWaitForResultsTimeout
import io.snyk.plugin.pluginSettings
import io.snyk.plugin.runInBackground
import io.snyk.plugin.toFilePathString
import io.snyk.plugin.toLanguageServerURI
import io.snyk.plugin.ui.SnykBalloonNotificationHelper
import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable
import org.eclipse.lsp4j.ClientCapabilities
import org.eclipse.lsp4j.ClientInfo
Expand Down Expand Up @@ -64,9 +65,9 @@ import snyk.common.lsp.commands.COMMAND_WORKSPACE_FOLDER_SCAN
import snyk.common.lsp.commands.SNYK_GENERATE_ISSUE_DESCRIPTION
import snyk.common.lsp.progress.ProgressManager
import snyk.common.lsp.settings.FolderConfigSettings
import snyk.common.lsp.settings.IssueViewOptions
import snyk.common.lsp.settings.LanguageServerSettings
import snyk.common.lsp.settings.SeverityFilter
import snyk.common.lsp.settings.IssueViewOptions
import snyk.common.removeTrailingSlashesIfPresent
import snyk.pluginInfo
import snyk.trust.WorkspaceTrustService
Expand Down Expand Up @@ -97,7 +98,7 @@ class LanguageServerWrapper(

// internal for test set up
internal val configuredWorkspaceFolders: MutableSet<WorkspaceFolder> = Collections.synchronizedSet(mutableSetOf())
internal var folderConfigsRefreshed: MutableMap<String, Boolean> = ConcurrentHashMap()
private var folderConfigsRefreshed: MutableMap<String, Boolean> = ConcurrentHashMap()
private var disposed = false
get() {
return ApplicationManager.getApplication().isDisposed || field
Expand Down Expand Up @@ -199,11 +200,11 @@ class LanguageServerWrapper(
LanguageServerRestartListener.getInstance()
refreshFeatureFlags()
} else {
logger.warn("Language Server initialization did not succeed")
logger.error("Snyk Language Server process launch failed.")
}
} catch (e: Exception) {
logger.warn(e)
process.destroy()
logger.error("Initialization of Snyk Language Server failed", e)
if (processIsAlive()) process.destroyForcibly()
Copy link
Contributor

Choose a reason for hiding this comment

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

The process destruction should check if the process is initialized before attempting to destroy it forcibly. Consider using if (::process.isInitialized && process.isAlive) process.destroyForcibly() to avoid potential exceptions.

isInitialized = false
}
}
Expand All @@ -217,7 +218,7 @@ class LanguageServerWrapper(
lsp4jLogger.level = Level.OFF
messageProducerLogger.level = Level.OFF
try {
val shouldShutdown = lsIsAlive()
val shouldShutdown = processIsAlive()
executorService.submit {
if (shouldShutdown) {
val project = ProjectUtil.getActiveProject()
Expand All @@ -233,19 +234,19 @@ class LanguageServerWrapper(
// we don't care
} finally {
try {
if (lsIsAlive()) languageServer.exit()
if (processIsAlive()) languageServer.exit()
} catch (ignore: Exception) {
// do nothing
} finally {
if (lsIsAlive()) process.destroyForcibly()
if (processIsAlive()) process.destroyForcibly()
}
lsp4jLogger.level = previousLSP4jLogLevel
messageProducerLogger.level = previousMessageProducerLevel
configuredWorkspaceFolders.clear()
}
}

private fun lsIsAlive() = ::process.isInitialized && process.isAlive
private fun processIsAlive() = ::process.isInitialized && process.isAlive

fun getWorkspaceFoldersFromRoots(project: Project): Set<WorkspaceFolder> {
if (disposed || project.isDisposed) return emptySet()
Expand Down Expand Up @@ -336,7 +337,7 @@ class LanguageServerWrapper(
added.filter { !configuredWorkspaceFolders.contains(it) },
removed.filter { configuredWorkspaceFolders.contains(it) },
)
if (params.event.added.size > 0 || params.event.removed.size > 0) {
if (params.event.added.isNotEmpty() || params.event.removed.isNotEmpty()) {
languageServer.workspaceService.didChangeWorkspaceFolders(params)
configuredWorkspaceFolders.removeAll(removed)
configuredWorkspaceFolders.addAll(added)
Expand Down Expand Up @@ -485,11 +486,12 @@ class LanguageServerWrapper(
// the folderConfigs in language server
val folderConfigs = configuredWorkspaceFolders
.filter {
val folderPath = it.uri.toFilePathString()
val folderPath = it.uri.fromUriToPath().toString()
folderConfigsRefreshed[folderPath] == true
}.map {
val folderPath = it.uri.toFilePathString()
service<FolderConfigSettings>().getFolderConfig(folderPath) }
val folderPath = it.uri.fromUriToPath().toString()
service<FolderConfigSettings>().getFolderConfig(folderPath)
}
.toList()

return LanguageServerSettings(
Expand Down Expand Up @@ -621,7 +623,13 @@ class LanguageServerWrapper(

fun addContentRoots(project: Project) {
if (disposed || project.isDisposed) return
ensureLanguageServerInitialized()
if (!ensureLanguageServerInitialized()) {
SnykBalloonNotificationHelper.showWarn(
"Unable to initialize the Snyk Language Server. The plugin will be non-functional.",
project
)
return
}
ensureLanguageServerProtocolVersion(project)
updateConfiguration(false)
val added = getWorkspaceFoldersFromRoots(project)
Expand Down Expand Up @@ -665,7 +673,13 @@ class LanguageServerWrapper(
executeCommand(param)
}

fun sendSubmitIgnoreRequestCommand(workflow: String, issueId: String, ignoreType: String, ignoreReason: String, ignoreExpirationDate: String) {
fun sendSubmitIgnoreRequestCommand(
workflow: String,
issueId: String,
ignoreType: String,
ignoreReason: String,
ignoreExpirationDate: String
) {
if (!ensureLanguageServerInitialized()) throw RuntimeException("couldn't initialize language server")
try {
val param = ExecuteCommandParams()
Expand Down Expand Up @@ -753,6 +767,15 @@ class LanguageServerWrapper(
shutdown()
}

fun getFolderConfigsRefreshed(): Map<String?, Boolean?> {
return Collections.unmodifiableMap(this.folderConfigsRefreshed)
}

fun updateFolderConfigRefresh(folderPath: String, refreshed: Boolean) {
val path = Paths.get(folderPath).normalize().toAbsolutePath().toString()
this.folderConfigsRefreshed[path] = refreshed
}


companion object {
private var instance: LanguageServerWrapper? = null
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ class SnykLanguageClient :
val service = service<FolderConfigSettings>()
service.addAll(folderConfigs)
folderConfigs.forEach {
LanguageServerWrapper.getInstance().folderConfigsRefreshed[it.folderPath] = true
LanguageServerWrapper.getInstance().updateFolderConfigRefresh(it.folderPath, true)
}
}
}
Expand Down
Loading
Loading