Skip to content

Commit

Permalink
feat: snyk controller extension point
Browse files Browse the repository at this point in the history
Add an extension point to allow third-party plugins to interact with Snyk in
the IDE.
  • Loading branch information
cmars committed Jan 25, 2024
1 parent e363e9c commit 99a4b2f
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 1 deletion.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Snyk Changelog

## [2.6.1]
## [2.7.0]
### Added
- Snyk controller extension point

## [2.6.2]
### Fixed
- remove more intellij-specific code and configuration

Expand Down
102 changes: 102 additions & 0 deletions EXTENSIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Snyk Extensions

This plugin offers an extension point for integrating Snyk with other Jetbrains IDE Plugins.

## What is the Snyk Controller?

The Snyk Controller is, put simply, a way to control Snyk from other plugins.

## Why would I use the Snyk Controller extension?

There are a few of use cases for the Snyk controller:

### Initiating a Snyk scan

There may be situations in which a plugin wants to initiate a security scan, especially at the end of a workflow which
introduces changes to the project source code, manifest dependencies, OCI container builds, infrastructure files,
etc. -- anything Snyk can scan.

### Determining whether Snyk is authenticated to a user

It might be useful to know whether the user has authenticated with Snyk; is Snyk ready to run scans?

## How do I use the extension in my plugin?

### Add a dependency on this plugin to your plugin's `plugin.xml` file.

Release >= 2.7.0 provides the extension point.

```xml
<depends>io.snyk.snyk-intellij-plugin</depends>
```

### Declare the extension

Add an extension for `io.snyk.snyk-intellij-plugin.controllerManager` to your plugin's `plugin.xml` file.

```xml
<extensions defaultExtensionNs="io.snyk.snyk-intellij-plugin">
<controllerManager implementation="com.example.demointellij.MySnykControllerManager"/>
</extensions>
```

### Optional dependency

The dependency on Snyk can also be optional:

```xml
<depends optional="true" config-file="optional/withSnyk.xml">io.snyk.snyk-intellij-plugin</depends>
```

Declare the controller extension in the dependency's config file, located relative to the `plugin.xml`:

```xml
<!-- Contents of optional/withSnyk.xml -->
<idea-plugin>
<extensions defaultExtensionNs="io.snyk.snyk-intellij-plugin">
<controllerManager implementation="com.example.demointellij.HelloWorldControllerManager" />
</extensions>
</idea-plugin>
```

### Implement the controller manager interface

The manager will be instantiated and passed an instance of the controller when the Snyk plugin is initialized.

```kotlin
package com.example.demointellij

import io.snyk.plugin.extensions.SnykController
import io.snyk.plugin.extensions.SnykControllerManager

class HelloWorldControllerManager : SnykControllerManager {
override fun register(controller: SnykController) {
Utils().getSnykControllerService().setController(controller)
}
}
```

Snyk recommends building a [service](https://plugins.jetbrains.com/docs/intellij/plugin-services.html) to receive the
controller instance and provide it to other parts of your plugin.

### Use the controller in your plugin

See [SnykController](https://github.com/snyk/snyk-intellij-plugin/blob/main/src/main/kotlin/io/snyk/plugin/extensions/SnykController.kt)
for the current Snyk methods supported.

#### Initiating a scan

```kotlin
Utils().getSnykControllerService().getController()?.scan()
```

## What compatibility guarantees are there, for consumers of this extension?

Our [semantic version](https://semver.org/) releases indicate the compatibility of the extension API with respect to
past releases.

With a minor version increment, new methods may be added to interfaces, but existing methods will not be removed or
their prototypes changed.

With a major version increment, there are no compatibility guarantees. Snyk suggests checking the release notes, source
changes, and compatibility testing before upgrading.
18 changes: 18 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,24 @@ tasks {
}
}

val createOpenApiSourceJar by registering(Jar::class) {
// Java sources
from(sourceSets.main.get().java) {
include("**/*.java")
}
// Kotlin sources
from(kotlin.sourceSets.main.get().kotlin) {
include("**/*.kt")
}
destinationDirectory.set(layout.buildDirectory.dir("libs"))
archiveClassifier.set("src")
}

buildPlugin {
dependsOn(createOpenApiSourceJar)
from(createOpenApiSourceJar) { into("lib/src") }
}

buildSearchableOptions {
enabled = false
}
Expand Down
16 changes: 16 additions & 0 deletions src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import com.intellij.ide.plugins.PluginInstaller
import com.intellij.ide.plugins.PluginStateListener
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.openapi.vfs.VirtualFileManager
import io.snyk.plugin.extensions.SnykControllerImpl
import io.snyk.plugin.extensions.SnykControllerManager
import io.snyk.plugin.snykcode.SnykCodeBulkFileListener
import io.snyk.plugin.snykcode.core.AnalysisData
import io.snyk.plugin.snykcode.core.SnykCodeIgnoreInfoHolder
Expand All @@ -27,8 +30,17 @@ import java.util.*

private val LOG = logger<SnykPostStartupActivity>()

private const val EXTENSION_POINT_CONTROLLER_MANAGER = "io.snyk.snyk-intellij-plugin.controllerManager"

class SnykPostStartupActivity : ProjectActivity {

private object ExtensionPointsUtil {
val controllerManager =
ExtensionPointName.create<SnykControllerManager>(
EXTENSION_POINT_CONTROLLER_MANAGER
)
}

private var listenersActivated = false
val settings = pluginSettings()

Expand Down Expand Up @@ -95,6 +107,10 @@ class SnykPostStartupActivity : ProjectActivity {
if (isContainerEnabled()) {
getKubernetesImageCache(project)?.scanProjectForKubernetesFiles()
}

ExtensionPointsUtil.controllerManager.extensionList.forEach {
it.register(SnykControllerImpl(project))
}
}
}

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

/**
* SnykController is used by third-party plugins to interact with the Snyk plugin.
*/
interface SnykController {

/**
* scan enqueues a scan of the project for vulnerabilities.
*/
fun scan()

/**
* userId returns the current authenticated Snyk user's ID.
*
* If no user is authenticated, this will return null.
*/
fun userId(): String?
}
27 changes: 27 additions & 0 deletions src/main/kotlin/io/snyk/plugin/extensions/SnykControllerImpl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.snyk.plugin.extensions

import com.intellij.openapi.project.Project
import io.snyk.plugin.getSnykApiService
import io.snyk.plugin.getSnykTaskQueueService

/**
* SnykController is used by third-party plugins to interact with the Snyk plugin.
*/
class SnykControllerImpl(val project: Project) : SnykController {

/**
* scan enqueues a scan of the project for vulnerabilities.
*/
override fun scan() {
getSnykTaskQueueService(project)?.scan()
}

/**
* userId returns the current authenticated Snyk user's ID.
*
* If no user is authenticated, this will return null.
*/
override fun userId(): String? {
return getSnykApiService().userId
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/io/snyk/plugin/extensions/SnykControllerManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.snyk.plugin.extensions

/**
* SnykControllerManager is the extension point interface
* which other plugins can implement in order to integrate with Snyk.
*/
interface SnykControllerManager {

/**
* register is called by the Snyk IntelliJ plugin to pass the #SnykController to extension point implementers.
*/
fun register(controller: SnykController)
}
6 changes: 6 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,10 @@
description="Snyk: Show Low severity issues"/>
</group>
</actions>

<extensionPoints>
<extensionPoint
name="controllerManager"
interface="io.snyk.plugin.extensions.SnykControllerManager"/>
</extensionPoints>
</idea-plugin>
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package io.snyk.plugin.extensions

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.testFramework.LightPlatformTestCase
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.replaceService
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.spyk
import io.mockk.unmockkAll
import io.mockk.verify
import io.snyk.plugin.extensions.SnykControllerImpl
import io.snyk.plugin.getCliFile
import io.snyk.plugin.getContainerService
import io.snyk.plugin.getIacService
import io.snyk.plugin.getOssService
import io.snyk.plugin.getSnykCachedResults
import io.snyk.plugin.getSnykCliDownloaderService
import io.snyk.plugin.isCliInstalled
import io.snyk.plugin.isContainerEnabled
import io.snyk.plugin.isIacEnabled
import io.snyk.plugin.net.CliConfigSettings
import io.snyk.plugin.net.LocalCodeEngine
import io.snyk.plugin.pluginSettings
import io.snyk.plugin.removeDummyCliFile
import io.snyk.plugin.resetSettings
import io.snyk.plugin.services.SnykApiService
import io.snyk.plugin.services.SnykTaskQueueService
import io.snyk.plugin.services.download.CliDownloader
import io.snyk.plugin.services.download.LatestReleaseInfo
import io.snyk.plugin.services.download.SnykCliDownloaderService
import io.snyk.plugin.setupDummyCliFile
import org.awaitility.Awaitility.await
import snyk.container.ContainerResult
import snyk.iac.IacResult
import snyk.oss.OssResult
import snyk.oss.OssService
import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded
import java.util.concurrent.TimeUnit

@Suppress("FunctionName")
class SnykControllerImplTest : LightPlatformTestCase() {

private lateinit var ossServiceMock: OssService
private lateinit var downloaderServiceMock: SnykCliDownloaderService

override fun setUp() {
super.setUp()
unmockkAll()
resetSettings(project)
ossServiceMock = mockk(relaxed = true)
project.replaceService(OssService::class.java, ossServiceMock, project)
mockkStatic("io.snyk.plugin.UtilsKt")
mockkStatic("snyk.trust.TrustedProjectsKt")
downloaderServiceMock = spyk(SnykCliDownloaderService())
every { downloaderServiceMock.requestLatestReleasesInformation() } returns LatestReleaseInfo(
"http://testUrl",
"testReleaseInfo",
"testTag"
)
every { getSnykCliDownloaderService() } returns downloaderServiceMock
every { downloaderServiceMock.isFourDaysPassedSinceLastCheck() } returns false
every { confirmScanningAndSetWorkspaceTrustedStateIfNeeded(any(), any()) } returns true
}

override fun tearDown() {
unmockkAll()
resetSettings(project)
removeDummyCliFile()
super.tearDown()
}

fun testControllerCanTriggerScan() {
mockkStatic("io.snyk.plugin.UtilsKt")
every { isCliInstalled() } returns true
val fakeResult = OssResult(emptyList())
every { getOssService(project)?.scan() } returns fakeResult

val settings = pluginSettings()
settings.ossScanEnable = true
settings.snykCodeSecurityIssuesScanEnable = false
settings.snykCodeQualityIssuesScanEnable = false
settings.iacScanEnabled = false
settings.containerScanEnabled = false

getSnykCachedResults(project)?.currentContainerResult = null

val controller = SnykControllerImpl(project)
controller.scan()

PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue()

await().atMost(2, TimeUnit.SECONDS).until {
getSnykCachedResults(project)?.currentOssResults != null
}
}
}

0 comments on commit 99a4b2f

Please sign in to comment.