Skip to content

Commit

Permalink
feat: integrate experimental option to get Snyk Code results from Lan…
Browse files Browse the repository at this point in the history
…guage Server (pre-alpha) [IDE-134] (#474)

* feat: initial merge with spike (probably still lots of errors)

* feat: add registry feature flag

* fix: compilation errors

* feat: add feature flag to code

* fix: integration with feature flag

* fix: rendering of custom UI, default value of feature flag

* fix: revert type in plugin to Community Edition (default)

* fix: populate quality issues correctly

* fix: issue title in description panel (LS source)

* feat: add dataflow steps

* refactoring: error handling snyk scan, extracting functionality

* feat: add snyk code annotator

* feat: feature parity to snyk code annotator

* feat: add LS code actions as quickfixes

* feat: add progress, request message and custom message handling, add fix information in tree

* fix: only resolve code action if edit and command is null

* fix: duplication of code actions

* feat: run code updating code actions in background

* feat: prioritize fixes over other code actions

* feat: add codevision provider (code lenses)

* fix: UI for tree view, code vision provider exceptions

* chore: update to LSP4j 0.22.0

* feat: better useragent / integration info when starting up language server

* chore: cleanup linting smell

* feat: activate analytics reporting

* chore: clean up linting smell

* fix: tests

* feat: enable Snyk Code annotations for all languages

* feat: update ls configuration from intellij settings

* fix: NullPointerException in Annotator

* feat: make message requests non-modal and show as balloon

* fix: edge condition in annotator

* fix: obsoletion update in cache

* feat: rescan on save

* chore: update job message

* feat: preference toggle for automatic scanning

* feat: applying autofix from code lens is now working

* fix: test `sendInitializeMessage should send an initialize message to the language server`

* feat: implement code lens and inline value refresh handler

* fix: capability registration

* fix: exceptions

* fix: reduce timeouts

* fix: a few nullpointerexceptions

* Merge branch 'feat/IDE-134_show-snyk-code-findings-in-custom-ui' of /Users/bdoetsch/workspace/snyk-intellij-plugin with conflicts.

* chore: update detekt rules

* chore: formatting

* fix: make sort order in tree view equal to the old sort order

* fix: only display code vision when feature flag enabled

* fix: only display code vision when feature flag enabled

* fix: source dataflow from new issue data field

* chore: update gradle intellij plugin

* fix: dataflow and navigation

* fix: root tree node texts for snyk code

* fix: only "start" scan once, fix handleCodeLensRefresh and handleInlineValueRefresh

* fix: isSnykCodeRunning, cache

* fix: progress, don't set indeterminate = true

* fix: progress, don't set indeterminate = true, fix late file listener
  • Loading branch information
bastiandoetsch authored Mar 4, 2024
1 parent b6c2ff5 commit c24b7ff
Show file tree
Hide file tree
Showing 45 changed files with 2,275 additions and 343 deletions.
26 changes: 18 additions & 8 deletions .github/detekt/detekt-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
# https://github.com/detekt/detekt/blob/master/detekt-core/src/main/resources/default-detekt-config.yml

formatting:
Indentation:
continuationIndentSize: 8
MaxLineLength:
active: false
ParameterListWrapping:
Expand All @@ -21,9 +19,21 @@ complexity:

style:
ReturnCount:
active: true
max: 5
excludedFunctions: 'equals'
excludeLabeled: false
excludeReturnFromLambda: true
excludeGuardClauses: true
active: false # we don't want to limit the number of return statements, as guard checks are often used

MagicNumber:
active: false

ForbiddenComment:
active: false

MaxLineLength:
active: false

UseCheckOrError:
active: false


exceptions:
TooGenericExceptionCaught:
active: false
6 changes: 3 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ fun properties(key: String) = project.findProperty(key).toString()

plugins {
id("org.jetbrains.changelog") version "2.1.2"
id("org.jetbrains.intellij") version "1.16.1"
id("org.jetbrains.intellij") version "1.17.2"
id("org.jetbrains.kotlin.jvm") version "1.9.0"
id("io.gitlab.arturbosch.detekt") version ("1.23.4")
id("pl.allegro.tech.build.axion-release") version "1.13.6"
Expand Down Expand Up @@ -67,11 +67,11 @@ dependencies {
}

// configuration for gradle-intellij-plugin plugin.
// read more: https://github.com/JetBrains/gradle-intellij-plugin
// read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html
intellij {
version.set(properties("platformVersion"))
// https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
// type.set("GO")
// type.set("IU")
downloadSources.set(properties("platformDownloadSources").toBoolean())

// plugin dependencies: uses `platformPlugins` property from the gradle.properties file.
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/io/snyk/plugin/SnykBulkFileListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,12 @@ abstract class SnykBulkFileListener : BulkFileListener {
for (project in ProjectUtil.getOpenProjects()) {
if (project.isDisposed) continue
after(project, getAfterVirtualFiles(events))
forwardEvents(events)
}
}

open fun forwardEvents(events: MutableList<out VFileEvent>) {}

abstract fun after(project: Project, virtualFilesAffected: Set<VirtualFile>)

/****************************** Common/Util methods **************************/
Expand Down
42 changes: 34 additions & 8 deletions src/main/kotlin/io/snyk/plugin/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Document
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileEditor.FileEditorManager
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.io.toNioPathOrNull
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowManager
Expand Down Expand Up @@ -48,14 +52,17 @@ import snyk.amplitude.AmplitudeExperimentService
import snyk.common.SnykCachedResults
import snyk.common.UIComponentFinder
import snyk.common.isSnykTenant
import snyk.common.lsp.ScanState
import snyk.container.ContainerService
import snyk.container.KubernetesImageCache
import snyk.errorHandler.SentryErrorReporter
import snyk.iac.IacScanService
import snyk.oss.OssService
import snyk.oss.OssTextRangeFinder
import snyk.pluginInfo
import snyk.whoami.WhoamiService
import java.io.File
import java.io.FileNotFoundException
import java.net.URI
import java.nio.file.Path
import java.security.KeyStore
Expand Down Expand Up @@ -83,6 +90,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 Expand Up @@ -170,7 +178,9 @@ fun isOssRunning(project: Project): Boolean {
}

fun isSnykCodeRunning(project: Project): Boolean =
AnalysisData.instance.isUpdateAnalysisInProgress(project) || RunUtils.instance.isFullRescanRequested(project)
AnalysisData.instance.isUpdateAnalysisInProgress(project)
|| RunUtils.instance.isFullRescanRequested(project)
|| ScanState.scanInProgress[ScanState.SNYK_CODE] == true

fun isIacRunning(project: Project): Boolean {
val indicator = getSnykTaskQueueService(project)?.iacScanProgressIndicator
Expand Down Expand Up @@ -237,7 +247,7 @@ fun isContainerEnabled(): Boolean = true

fun isFileListenerEnabled(): Boolean = pluginSettings().fileListenerEnabled

fun isNewRefactoredTreeEnabled(): Boolean = Registry.`is`("snyk.preview.new.refactored.tree.enabled", false)
fun isSnykCodeLSEnabled(): Boolean = Registry.`is`("snyk.preview.snyk.code.ls.enabled", true)

fun isReportFalsePositivesEnabled(): Boolean = Registry.`is`("snyk.code.report.false.positives.enabled", false) &&
pluginSettings().reportFalsePositivesEnabled == true
Expand Down Expand Up @@ -301,10 +311,7 @@ fun navigateToSource(
selectionEndOffset: Int? = null
) {
if (!virtualFile.isValid) return
val psiFile = RunUtils.computeInReadActionInSmartMode(project) {
PsiManager.getInstance(project).findFile(virtualFile)
} ?: return
val textLength = psiFile.textLength
val textLength = virtualFile.contentsToByteArray().size
if (selectionStartOffset in (0 until textLength)) {
// jump to Source
PsiNavigationSupport.getInstance().createNavigatable(
Expand Down Expand Up @@ -402,10 +409,15 @@ fun getArch(): String {
return archMap[value] ?: value
}

fun VirtualFile.toPsiFile(project: Project): PsiFile? {
return PsiManager.getInstance(project).findFile(this)
fun String.toVirtualFile(): VirtualFile =
StandardFileSystems.local().refreshAndFindFileByPath(this) ?: throw FileNotFoundException(this)

fun VirtualFile.getPsiFile(project: Project): PsiFile? {
return runReadAction { PsiManager.getInstance(project).findFile(this) }
}

fun VirtualFile.getDocument(): Document? = runReadAction { FileDocumentManager.getInstance().getDocument(this) }

fun Project.getContentRootPaths(): SortedSet<Path> {
return getContentRootVirtualFiles()
.mapNotNull { it.path.toNioPathOrNull() }
Expand All @@ -414,3 +426,17 @@ fun Project.getContentRootPaths(): SortedSet<Path> {

fun Project.getContentRootVirtualFiles() = ProjectRootManager.getInstance(this).contentRoots
.filter { it.exists() && it.isDirectory }.toSet()

fun getUserAgentString(): String {
// $APPLICATION/$APPLICATION_VERSION ($GOOS;$GOARCH[;$BINARY_NAME]) [$SNYK_INTEGRATION_NAME/$SNYK_INTEGRATION_VERSION [($SNYK_INTEGRATION_ENVIRONMENT/$SNYK_INTEGRATION_ENVIRONMENT_VERSION)]]
val integrationName = pluginInfo.integrationName
val integrationVersion = pluginInfo.integrationVersion
val integrationEnvironment = pluginInfo.integrationEnvironment
val integrationEnvironmentVersion = pluginInfo.integrationEnvironmentVersion
val os = SystemUtils.OS_NAME
val arch = SystemUtils.OS_ARCH

return "$integrationEnvironment/$integrationEnvironmentVersion " +
"($os;$arch) $integrationName/$integrationVersion " +
"($integrationEnvironment/$integrationEnvironmentVersion)"
}
75 changes: 58 additions & 17 deletions src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ package io.snyk.plugin.analytics

import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import io.snyk.plugin.Severity
import io.snyk.plugin.events.SnykCodeScanListenerLS
import io.snyk.plugin.events.SnykScanListener
import io.snyk.plugin.isSnykCodeLSEnabled
import io.snyk.plugin.snykcode.SnykCodeResults
import io.snyk.plugin.snykcode.core.SnykCodeFile
import snyk.common.SnykError
import snyk.common.lsp.LanguageServerWrapper
import snyk.common.lsp.ScanIssue
import snyk.common.lsp.SnykScanParams
import snyk.common.lsp.commands.ScanDoneEvent
import snyk.container.ContainerResult
import snyk.iac.IacResult
Expand All @@ -14,7 +20,12 @@ 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
duration: Long,
product: String,
critical: Int,
high: Int,
medium: Int,
low: Int
): ScanDoneEvent {
return ScanDoneEvent(
ScanDoneEvent.Data(
Expand All @@ -32,6 +43,32 @@ class AnalyticsScanListener(val project: Project) {
)
}

private val snykCodeScanListenerLS = object : SnykCodeScanListenerLS {
var start = 0L
override fun scanningStarted(snykScan: SnykScanParams) {
start = System.currentTimeMillis()
}

override fun scanningSnykCodeFinished(snykCodeResults: Map<SnykCodeFile, List<ScanIssue>>) {
val duration = System.currentTimeMillis() - start
val product = "Snyk Code"
val issues = snykCodeResults.values.flatten()
val scanDoneEvent = getScanDoneEvent(
duration,
product,
issues.count { it.getSeverityAsEnum() == Severity.CRITICAL },
issues.count { it.getSeverityAsEnum() == Severity.HIGH },
issues.count { it.getSeverityAsEnum() == Severity.MEDIUM },
issues.count { it.getSeverityAsEnum() == Severity.LOW },
)
LanguageServerWrapper.getInstance().sendReportAnalyticsCommand(scanDoneEvent)
}

override fun scanningSnykCodeError(snykScan: SnykScanParams) {
// do nothing
}
}

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

Expand All @@ -54,18 +91,14 @@ class AnalyticsScanListener(val project: Project) {
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)
}
val scanDoneEvent = getScanDoneEvent(
duration,
product,
snykCodeResults?.totalCriticalCount ?: 0,
snykCodeResults?.totalErrorsCount ?: 0,
snykCodeResults?.totalWarnsCount ?: 0,
snykCodeResults?.totalInfosCount ?: 0,
)
LanguageServerWrapper.getInstance().sendReportAnalyticsCommand(scanDoneEvent)
}

Expand Down Expand Up @@ -110,8 +143,16 @@ class AnalyticsScanListener(val project: Project) {
}
}

fun initScanListener() = project.messageBus.connect().subscribe(
SnykScanListener.SNYK_SCAN_TOPIC,
snykScanListener,
)
fun initScanListener() {
project.messageBus.connect().subscribe(
SnykScanListener.SNYK_SCAN_TOPIC,
snykScanListener,
)

if (isSnykCodeLSEnabled())
project.messageBus.connect().subscribe(
SnykCodeScanListenerLS.SNYK_SCAN_TOPIC,
snykCodeScanListenerLS,
)
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/io/snyk/plugin/events/SnykCodeScanListenerLS.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.snyk.plugin.events

import com.intellij.util.messages.Topic
import io.snyk.plugin.snykcode.core.SnykCodeFile
import snyk.common.lsp.ScanIssue
import snyk.common.lsp.SnykScanParams

interface SnykCodeScanListenerLS {
companion object {
val SNYK_SCAN_TOPIC =
Topic.create("Snyk scan LS", SnykCodeScanListenerLS::class.java)
}

fun scanningStarted(snykScan: SnykScanParams) {}

fun scanningSnykCodeFinished(snykCodeResults: Map<SnykCodeFile, List<ScanIssue>>)

fun scanningSnykCodeError(snykScan: SnykScanParams)
}
16 changes: 1 addition & 15 deletions src/main/kotlin/io/snyk/plugin/net/TokenInterceptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package io.snyk.plugin.net
import com.google.gson.Gson
import com.intellij.openapi.project.ProjectManager
import io.snyk.plugin.getSnykCliAuthenticationService
import io.snyk.plugin.getUserAgentString
import io.snyk.plugin.getWhoamiService
import io.snyk.plugin.pluginSettings
import okhttp3.Interceptor
import okhttp3.Response
import org.apache.commons.lang3.SystemUtils
import snyk.common.needsSnykToken
import snyk.pluginInfo
import java.time.OffsetDateTime
Expand Down Expand Up @@ -48,20 +48,6 @@ class TokenInterceptor(private var projectManager: ProjectManager? = null) : Int
.addHeader("x-snyk-ide", "${pluginInfo.integrationName}-${pluginInfo.integrationVersion}")
return chain.proceed(request.build())
}

fun getUserAgentString(): String {
// $APPLICATION/$APPLICATION_VERSION ($GOOS;$GOARCH[;$BINARY_NAME]) [$SNYK_INTEGRATION_NAME/$SNYK_INTEGRATION_VERSION [($SNYK_INTEGRATION_ENVIRONMENT/$SNYK_INTEGRATION_ENVIRONMENT_VERSION)]]
val integrationName = pluginInfo.integrationName
val integrationVersion = pluginInfo.integrationVersion
val integrationEnvironment = pluginInfo.integrationEnvironment
val integrationEnvironmentVersion = pluginInfo.integrationEnvironmentVersion
val os = SystemUtils.OS_NAME
val arch = SystemUtils.OS_ARCH

return "$integrationEnvironment/$integrationEnvironmentVersion " +
"($os;$arch) $integrationName/$integrationVersion " +
"($integrationEnvironment/$integrationEnvironmentVersion)"
}
}

@Suppress("PropertyName")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class SnykApplicationSettingsStateService : PersistentStateComponent<SnykApplica
var organization: String? = null
var ignoreUnknownCA = false
var cliVersion: String? = null
var scanOnSave: Boolean = true

// can't be private -> serialization will not work
@Deprecated("left for old users migration only")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package io.snyk.plugin.services

import com.intellij.openapi.components.*
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.RoamingType
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.util.xmlb.XmlSerializerUtil

@Service
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import io.snyk.plugin.isCliDownloading
import io.snyk.plugin.isCliInstalled
import io.snyk.plugin.isContainerEnabled
import io.snyk.plugin.isIacEnabled
import io.snyk.plugin.isSnykCodeLSEnabled
import io.snyk.plugin.isSnykCodeRunning
import io.snyk.plugin.net.ClientException
import io.snyk.plugin.pluginSettings
Expand Down Expand Up @@ -98,7 +99,11 @@ class SnykTaskQueueService(val project: Project) {
indicator.checkCanceled()

if (settings.snykCodeSecurityIssuesScanEnable || settings.snykCodeQualityIssuesScanEnable) {
scheduleSnykCodeScan()
if (!isSnykCodeLSEnabled()) {
scheduleSnykCodeScan()
} else {
LanguageServerWrapper.getInstance().sendScanCommand()
}
}
if (settings.ossScanEnable) {
scheduleOssScan()
Expand Down
Loading

0 comments on commit c24b7ff

Please sign in to comment.