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 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
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 {
Dismissed Show dismissed Hide dismissed
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
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
}
Loading
Loading