Skip to content

Commit

Permalink
feat: add language server and report analytics via ls [HEAD-911] (#458)
Browse files Browse the repository at this point in the history
* feat: integrate language server into jetbrains and prepare report analytics

* feat: add oss scan data, clean up a bit

* fix: detekt newline finding

* fix: erroneous rename

* refactor: move report Analytics to AnalyticsScanListener

* feat: add tests for language server wrapper

* feat: add test for initialization options

* feat: add test for event mapping

* refactor: remove unused var, introduce listener object

* feat: add tests for scanListener

* feat: ensure feature doesn't break UX
  • Loading branch information
bastiandoetsch authored Nov 13, 2023
1 parent 6293e59 commit 3bb216f
Show file tree
Hide file tree
Showing 11 changed files with 629 additions and 10 deletions.
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
8 changes: 7 additions & 1 deletion src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import snyk.iac.IacBulkFileListener
import snyk.oss.OssBulkFileListener
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.*

private val LOG = logger<SnykPostStartupActivity>()

Expand Down Expand Up @@ -66,6 +66,12 @@ class SnykPostStartupActivity : ProjectActivity {

if (!ApplicationManager.getApplication().isUnitTestMode) {
getSnykTaskQueueService(project)?.downloadLatestRelease()
try {
getSnykTaskQueueService(project)?.initializeLanguageServer()
getAnalyticsScanListener(project)?.initScanListener()
} catch (ignored: Exception) {
// do nothing to not break UX for analytics
}
}

val feedbackRequestShownMoreThenTwoWeeksAgo =
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/io/snyk/plugin/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.ToolWindow
Expand All @@ -24,6 +23,7 @@ import com.intellij.psi.PsiManager
import com.intellij.util.Alarm
import com.intellij.util.FileContentUtil
import com.intellij.util.messages.Topic
import io.snyk.plugin.analytics.AnalyticsScanListener
import io.snyk.plugin.net.ClientException
import io.snyk.plugin.services.SnykAnalyticsService
import io.snyk.plugin.services.SnykApiService
Expand Down Expand Up @@ -55,7 +55,6 @@ import snyk.oss.OssTextRangeFinder
import snyk.whoami.WhoamiService
import java.io.File
import java.net.URI
import java.net.URL
import java.nio.file.Path
import java.security.KeyStore
import java.util.Objects.nonNull
Expand All @@ -81,6 +80,7 @@ fun getSnykTaskQueueService(project: Project): SnykTaskQueueService? = project.s
fun getSnykToolWindowPanel(project: Project): SnykToolWindowPanel? = project.serviceIfNotDisposed()

fun getSnykCachedResults(project: Project): SnykCachedResults? = project.serviceIfNotDisposed()
fun getAnalyticsScanListener(project: Project): AnalyticsScanListener? = project.serviceIfNotDisposed()

fun getContainerService(project: Project): ContainerService? = project.serviceIfNotDisposed()

Expand Down
117 changes: 117 additions & 0 deletions src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package io.snyk.plugin.analytics

import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import io.snyk.plugin.events.SnykScanListener
import io.snyk.plugin.getSnykTaskQueueService
import io.snyk.plugin.snykcode.SnykCodeResults
import snyk.common.SnykError
import snyk.common.lsp.commands.ScanDoneEvent
import snyk.container.ContainerResult
import snyk.iac.IacResult
import snyk.oss.OssResult

@Service(Service.Level.PROJECT)
class AnalyticsScanListener(val project: Project) {
fun getScanDoneEvent(
duration: Long, product: String, critical: Int, high: Int, medium: Int, low: Int
): ScanDoneEvent {
return ScanDoneEvent(
ScanDoneEvent.Data(
attributes = ScanDoneEvent.Attributes(
scanType = product,
uniqueIssueCount = ScanDoneEvent.UniqueIssueCount(
critical = critical,
high = high,
medium = medium,
low = low
),
durationMs = "$duration",
)
)
)
}

val snykScanListener = object : SnykScanListener {
var start: Long = 0

override fun scanningStarted() {
start = System.currentTimeMillis()
}

override fun scanningOssFinished(ossResult: OssResult) {
val scanDoneEvent = getScanDoneEvent(
System.currentTimeMillis() - start,
"Snyk Open Source",
ossResult.criticalSeveritiesCount(),
ossResult.highSeveritiesCount(),
ossResult.mediumSeveritiesCount(),
ossResult.lowSeveritiesCount()
)
getSnykTaskQueueService(project)?.ls?.sendReportAnalyticsCommand(scanDoneEvent)
}

override fun scanningSnykCodeFinished(snykCodeResults: SnykCodeResults?) {
val duration = System.currentTimeMillis() - start
val product = "Snyk Code"
val scanDoneEvent = if (snykCodeResults != null) {
getScanDoneEvent(
duration,
product,
snykCodeResults.totalCriticalCount,
snykCodeResults.totalErrorsCount,
snykCodeResults.totalWarnsCount,
snykCodeResults.totalInfosCount,
)
} else {
getScanDoneEvent(duration, product, 0, 0, 0, 0)
}
getSnykTaskQueueService(project)?.ls?.sendReportAnalyticsCommand(scanDoneEvent)
}

override fun scanningIacFinished(iacResult: IacResult) {
val scanDoneEvent = getScanDoneEvent(
System.currentTimeMillis() - start,
"Snyk IaC",
iacResult.criticalSeveritiesCount(),
iacResult.highSeveritiesCount(),
iacResult.mediumSeveritiesCount(),
iacResult.lowSeveritiesCount()
)
getSnykTaskQueueService(project)?.ls?.sendReportAnalyticsCommand(scanDoneEvent)
}

override fun scanningContainerFinished(containerResult: ContainerResult) {
val scanDoneEvent = getScanDoneEvent(
System.currentTimeMillis() - start,
"Snyk Container",
containerResult.criticalSeveritiesCount(),
containerResult.highSeveritiesCount(),
containerResult.mediumSeveritiesCount(),
containerResult.lowSeveritiesCount()
)
getSnykTaskQueueService(project)?.ls?.sendReportAnalyticsCommand(scanDoneEvent)
}

override fun scanningOssError(snykError: SnykError) {
// do nothing
}

override fun scanningIacError(snykError: SnykError) {
// do nothing
}

override fun scanningSnykCodeError(snykError: SnykError) {
// do nothing
}

override fun scanningContainerError(snykError: SnykError) {
// do nothing
}
}

fun initScanListener() = project.messageBus.connect().subscribe(
SnykScanListener.SNYK_SCAN_TOPIC,
snykScanListener,
)
}
33 changes: 26 additions & 7 deletions src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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 +30,12 @@ import io.snyk.plugin.net.ClientException
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.jetbrains.annotations.TestOnly
import snyk.common.SnykError
import snyk.common.lsp.LanguageServerWrapper
import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded
import java.nio.file.Paths

Expand All @@ -40,6 +45,7 @@ class SnykTaskQueueService(val project: Project) {
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 +79,22 @@ class SnykTaskQueueService(val project: Project) {
})
}

@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 +104,12 @@ class SnykTaskQueueService(val project: Project) {
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 @@ -266,10 +284,11 @@ class SnykTaskQueueService(val project: Project) {
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
)

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
}
Loading

0 comments on commit 3bb216f

Please sign in to comment.