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

feat: add language server and report analytics via ls [HEAD-911] #458

Merged
merged 15 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ repositories {

dependencies {
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.11.0"))
implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.21.1")

implementation("org.commonmark:commonmark:0.21.0")
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.segment.analytics.java:analytics:3.4.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class SnykPostStartupActivity : ProjectActivity {

if (!ApplicationManager.getApplication().isUnitTestMode) {
getSnykTaskQueueService(project)?.downloadLatestRelease()
getSnykTaskQueueService(project)?.initializeLanguageServer()
}

val feedbackRequestShownMoreThenTwoWeeksAgo =
Expand Down
79 changes: 72 additions & 7 deletions src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package io.snyk.plugin.services

import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.progress.BackgroundTaskQueue
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
Expand All @@ -29,8 +31,15 @@
import io.snyk.plugin.pluginSettings
import io.snyk.plugin.snykcode.core.RunUtils
import io.snyk.plugin.ui.SnykBalloonNotifications
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.apache.commons.lang.SystemUtils
import org.jetbrains.annotations.TestOnly
import snyk.common.SnykError
import snyk.common.lsp.LanguageServerWrapper
import snyk.common.lsp.commands.ScanDoneEvent
import snyk.pluginInfo
import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded
import java.nio.file.Paths

Expand All @@ -40,6 +49,7 @@
private val taskQueue = BackgroundTaskQueue(project, "Snyk")
private val taskQueueIac = BackgroundTaskQueue(project, "Snyk: Iac")
private val taskQueueContainer = BackgroundTaskQueue(project, "Snyk: Container")
val ls = LanguageServerWrapper()

private val settings
get() = pluginSettings()
Expand Down Expand Up @@ -73,8 +83,22 @@
})
}

@OptIn(DelicateCoroutinesApi::class)
fun initializeLanguageServer() {
waitUntilCliDownloadedIfNeeded(EmptyProgressIndicator())
ls.initialize()
GlobalScope.launch {
ls.process.errorStream.bufferedReader().forEachLine { println(it) }
}
GlobalScope.launch {
ls.startListening()
}

ls.sendInitializeMessage(project)
}

fun scan() {
taskQueue.run(object : Task.Backgroundable(project, "Snyk wait for changed files to be saved on disk", true) {
taskQueue.run(object : Task.Backgroundable(project, "Snyk: initializing...", true) {
override fun run(indicator: ProgressIndicator) {
project.basePath?.let {
if (!confirmScanningAndSetWorkspaceTrustedStateIfNeeded(project, Paths.get(it))) return
Expand All @@ -84,14 +108,12 @@
FileDocumentManager.getInstance().saveAllDocuments()
}
indicator.checkCanceled()
waitUntilCliDownloadedIfNeeded(indicator)
indicator.checkCanceled()

if (settings.snykCodeSecurityIssuesScanEnable || settings.snykCodeQualityIssuesScanEnable) {
scheduleSnykCodeScan()
}

waitUntilCliDownloadedIfNeeded(indicator)
indicator.checkCanceled()

if (settings.ossScanEnable) {
scheduleOssScan()
}
Expand Down Expand Up @@ -209,6 +231,7 @@
taskQueue.run(object : Task.Backgroundable(project, "Snyk Open Source is scanning", true) {
override fun run(indicator: ProgressIndicator) {
if (!isCliInstalled()) return
val start = System.currentTimeMillis()
val snykCachedResults = getSnykCachedResults(project) ?: return
if (snykCachedResults.currentOssResults != null) return

Expand All @@ -220,13 +243,23 @@
} finally {
ossScanProgressIndicator = null
}
val end = System.currentTimeMillis()
if (ossResult == null || project.isDisposed) return

if (indicator.isCanceled) {
taskQueuePublisher?.stopped(wasOssRunning = true)
} else {
if (ossResult.isSuccessful()) {
scanPublisher?.scanningOssFinished(ossResult)
val scanDoneEvent = getScanDoneEvent(
end - start,
"Snyk Open Source",
ossResult.criticalSeveritiesCount(),
ossResult.highSeveritiesCount(),
ossResult.mediumSeveritiesCount(),
ossResult.lowSeveritiesCount()
)
ls.sendReportAnalyticsCommand(scanDoneEvent)
} else {
scanPublisher?.scanningOssError(ossResult.getFirstError()!!)
}
Expand All @@ -236,6 +269,37 @@
})
}

private fun getScanDoneEvent(
duration: Long, product: String, critical: Int, high: Int, medium: Int, low: Int
): ScanDoneEvent {
Fixed Show fixed Hide fixed
return ScanDoneEvent(
ScanDoneEvent.Data(
type = "analytics",
attributes = ScanDoneEvent.Attributes(
deviceId = settings.userAnonymousId,
application = ApplicationInfo.getInstance().fullApplicationName,
applicationVersion = ApplicationInfo.getInstance().fullVersion,
os = SystemUtils.OS_NAME,
arch = SystemUtils.OS_ARCH,
integrationName = pluginInfo.integrationName,
integrationVersion = pluginInfo.integrationVersion,
integrationEnvironment = pluginInfo.integrationEnvironment,
integrationEnvironmentVersion = pluginInfo.integrationEnvironmentVersion,
eventType = "Scan done",
status = "Succeeded",
scanType = product,
uniqueIssueCount = ScanDoneEvent.UniqueIssueCount(
critical = critical,
high = high,
medium = medium,
low = low
),
durationMs = "$duration",
)
)
)
}

private fun scheduleIacScan() {
taskQueueIac.run(object : Task.Backgroundable(project, "Snyk Infrastructure as Code is scanning", true) {
override fun run(indicator: ProgressIndicator) {
Expand Down Expand Up @@ -266,10 +330,11 @@
scanPublisher?.scanningIacFinished(iacResult)
} else {
val error = iacResult.getFirstError()
if (error == null)
if (error == null) {
SnykError("unknown IaC error", project.basePath ?: "")
else
} else {
scanPublisher?.scanningIacError(error)
}
}
}
logger.debug("IaC scan completed")
Expand Down
58 changes: 58 additions & 0 deletions src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
@file:Suppress("unused")

package snyk.common.lsp

import com.google.gson.annotations.SerializedName
import io.snyk.plugin.pluginSettings
import org.apache.commons.lang.SystemUtils
import snyk.pluginInfo

data class LanguageServerSettings(
@SerializedName("activateSnykOpenSource") val activateSnykOpenSource: String? = "false",
@SerializedName("activateSnykCode") val activateSnykCode: String? = "false",
@SerializedName("activateSnykIac") val activateSnykIac: String? = "false",
@SerializedName("insecure") val insecure: String?,
@SerializedName("endpoint") val endpoint: String?,
@SerializedName("additionalParams") val additionalParams: String? = null,
@SerializedName("additionalEnv") val additionalEnv: String? = null,
@SerializedName("path") val path: String? = null,
@SerializedName("sendErrorReports") val sendErrorReports: String? = "false",
@SerializedName("organization") val organization: String? = null,
@SerializedName("enableTelemetry") val enableTelemetry: String? = "false",
@SerializedName("manageBinariesAutomatically") val manageBinariesAutomatically: String? = "false",
@SerializedName("cliPath") val cliPath: String?,
@SerializedName("token") val token: String?,
@SerializedName("integrationName") val integrationName: String? = pluginInfo.integrationName,
@SerializedName("integrationVersion") val integrationVersion: String? = pluginInfo.integrationVersion,
@SerializedName("automaticAuthentication") val automaticAuthentication: String? = "false",
@SerializedName("deviceId") val deviceId: String? = pluginSettings().userAnonymousId,
@SerializedName("filterSeverity") val filterSeverity: SeverityFilter? = null,
@SerializedName("enableTrustedFoldersFeature") val enableTrustedFoldersFeature: String? = "false",
@SerializedName("trustedFolders") val trustedFolders: List<String>? = emptyList(),
@SerializedName("activateSnykCodeSecurity") val activateSnykCodeSecurity: String? = "false",
@SerializedName("activateSnykCodeQuality") val activateSnykCodeQuality: String? = "false",
@SerializedName("osPlatform") val osPlatform: String? = SystemUtils.OS_NAME,
@SerializedName("osArch") val osArch: String? = SystemUtils.OS_ARCH,
@SerializedName("runtimeVersion") val runtimeVersion: String? = SystemUtils.JAVA_VERSION,
@SerializedName("runtimeName") val runtimeName: String? = SystemUtils.JAVA_RUNTIME_NAME,
@SerializedName("scanningMode") val scanningMode: String? = null,
@SerializedName("authenticationMethod") val authenticationMethod: AuthenticationMethod? = null,
@SerializedName("snykCodeApi") val snykCodeApi: String? = null,
@SerializedName("enableSnykLearnCodeActions") val enableSnykLearnCodeActions: String? = null,
@SerializedName("enableAnalytics") val enableAnalytics: Boolean = false // TODO: enable when service ready
Dismissed Show dismissed Hide dismissed
)

data class SeverityFilter(
@SerializedName("critical") val critical: Boolean?,
@SerializedName("high") val high: Boolean?,
@SerializedName("medium") val medium: Boolean?,
@SerializedName("low") val low: Boolean?
)

enum class AuthenticationMethod {
@SerializedName("token")
TokenAuthentication,

@SerializedName("oauth")
OAuthAuthentication
}
98 changes: 98 additions & 0 deletions src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package snyk.common.lsp

import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import io.snyk.plugin.getCliFile
import io.snyk.plugin.pluginSettings
import org.eclipse.lsp4j.ClientInfo
import org.eclipse.lsp4j.ExecuteCommandParams
import org.eclipse.lsp4j.InitializeParams
import org.eclipse.lsp4j.WorkspaceFolder
import org.eclipse.lsp4j.jsonrpc.Launcher
import org.eclipse.lsp4j.launch.LSPLauncher
import org.eclipse.lsp4j.services.LanguageClient
import org.eclipse.lsp4j.services.LanguageServer
import snyk.common.getEndpointUrl
import snyk.common.lsp.commands.ScanDoneEvent
import snyk.pluginInfo

class LanguageServerWrapper(private val lsPath: String = getCliFile().absolutePath) {
private val gson = com.google.gson.Gson()

/**
* The language client is used to receive messages from LS
*/
lateinit var languageClient: LanguageClient

/**
* The language server allows access to the actual LS implementation
*/
lateinit var languageServer: LanguageServer

/**
* The launcher is used to start the language server as a separate process. and provides access to the LS
*/
private lateinit var launcher: Launcher<LanguageServer>

/**
* Process is the started IO process
*/
lateinit var process: Process

fun initialize() {
val cmd = listOf(lsPath, "language-server", "-l", "debug")

val processBuilder = ProcessBuilder(cmd)
process = processBuilder.start()
languageClient = SnykLanguageClient()
launcher = LSPLauncher.createClientLauncher(languageClient, process.inputStream, process.outputStream)
languageServer = launcher.remoteProxy
}

fun startListening() {
// Start the server
launcher.startListening()
}

fun sendInitializeMessage(project: Project) {
val workspaceFolders = mutableListOf<WorkspaceFolder>()
ProjectRootManager.getInstance(project)
.contentRoots
.mapNotNull { WorkspaceFolder(it.url, it.name) }.toCollection(workspaceFolders)

val params = InitializeParams()
params.processId = ProcessHandle.current().pid().toInt()
params.clientInfo = ClientInfo("${pluginInfo.integrationName}/lsp4j")
params.initializationOptions = getInitializationOptions()
params.workspaceFolders = workspaceFolders

languageServer.initialize(params).get()
}

fun sendReportAnalyticsCommand(scanDoneEvent: ScanDoneEvent) {
val eventString = gson.toJson(scanDoneEvent)
val param = ExecuteCommandParams()
param.command = "snyk.reportAnalytics"
param.arguments = listOf(eventString)
languageServer.workspaceService.executeCommand(param)
}

private fun getInitializationOptions(): LanguageServerSettings {
val ps = pluginSettings()
return LanguageServerSettings(
activateSnykOpenSource = "false",
activateSnykCode = "false",
activateSnykIac = "false",
insecure = ps.ignoreUnknownCA.toString(),
endpoint = getEndpointUrl(),
cliPath = getCliFile().absolutePath,
token = ps.token,
filterSeverity = SeverityFilter(
critical = ps.criticalSeverityEnabled,
high = ps.highSeverityEnabled,
medium = ps.mediumSeverityEnabled,
low = ps.lowSeverityEnabled
),
)
}
}
42 changes: 42 additions & 0 deletions src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package snyk.common.lsp

import com.intellij.openapi.diagnostic.Logger
import org.eclipse.lsp4j.MessageActionItem
import org.eclipse.lsp4j.MessageParams
import org.eclipse.lsp4j.MessageType
import org.eclipse.lsp4j.PublishDiagnosticsParams
import org.eclipse.lsp4j.ShowMessageRequestParams
import org.eclipse.lsp4j.services.LanguageClient
import java.util.concurrent.CompletableFuture

class SnykLanguageClient : LanguageClient {
val logger = Logger.getInstance("Snyk Language Server")
override fun telemetryEvent(`object`: Any?) {
// do nothing
}

override fun publishDiagnostics(diagnostics: PublishDiagnosticsParams?) {
// do nothing
}

override fun showMessage(messageParams: MessageParams?) {
// do nothing
}

override fun showMessageRequest(requestParams: ShowMessageRequestParams?): CompletableFuture<MessageActionItem> {
// do nothing
TODO()
}

override fun logMessage(message: MessageParams?) {
message?.let {
when (it.type) {
MessageType.Error -> logger.error(it.message)
MessageType.Warning -> logger.warn(it.message)
MessageType.Info -> logger.info(it.message)
MessageType.Log -> logger.debug(it.message)
null -> logger.info(it.message)
}
}
}
}
Loading
Loading