diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4202712a2..f8f90753e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/EXTENSIONS.md b/EXTENSIONS.md
new file mode 100644
index 000000000..4ff847e9c
--- /dev/null
+++ b/EXTENSIONS.md
@@ -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
+io.snyk.snyk-intellij-plugin
+```
+
+### Declare the extension
+
+Add an extension for `io.snyk.snyk-intellij-plugin.controllerManager` to your plugin's `plugin.xml` file.
+
+```xml
+
+
+
+```
+
+### Optional dependency
+
+The dependency on Snyk can also be optional:
+
+```xml
+io.snyk.snyk-intellij-plugin
+```
+
+Declare the controller extension in the dependency's config file, located relative to the `plugin.xml`:
+
+```xml
+
+
+
+
+
+
+```
+
+### 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.
diff --git a/build.gradle.kts b/build.gradle.kts
index d4bfdf8a2..d26e9714f 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -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
}
diff --git a/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt b/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt
index 7f136add0..3452716d2 100644
--- a/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt
+++ b/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt
@@ -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
@@ -27,8 +30,17 @@ import java.util.*
private val LOG = logger()
+private const val EXTENSION_POINT_CONTROLLER_MANAGER = "io.snyk.snyk-intellij-plugin.controllerManager"
+
class SnykPostStartupActivity : ProjectActivity {
+ private object ExtensionPointsUtil {
+ val controllerManager =
+ ExtensionPointName.create(
+ EXTENSION_POINT_CONTROLLER_MANAGER
+ )
+ }
+
private var listenersActivated = false
val settings = pluginSettings()
@@ -95,6 +107,10 @@ class SnykPostStartupActivity : ProjectActivity {
if (isContainerEnabled()) {
getKubernetesImageCache(project)?.scanProjectForKubernetesFiles()
}
+
+ ExtensionPointsUtil.controllerManager.extensionList.forEach {
+ it.register(SnykControllerImpl(project))
+ }
}
}
diff --git a/src/main/kotlin/io/snyk/plugin/extensions/SnykController.kt b/src/main/kotlin/io/snyk/plugin/extensions/SnykController.kt
new file mode 100644
index 000000000..dcad7de47
--- /dev/null
+++ b/src/main/kotlin/io/snyk/plugin/extensions/SnykController.kt
@@ -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?
+}
diff --git a/src/main/kotlin/io/snyk/plugin/extensions/SnykControllerImpl.kt b/src/main/kotlin/io/snyk/plugin/extensions/SnykControllerImpl.kt
new file mode 100644
index 000000000..3365b1611
--- /dev/null
+++ b/src/main/kotlin/io/snyk/plugin/extensions/SnykControllerImpl.kt
@@ -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
+ }
+}
diff --git a/src/main/kotlin/io/snyk/plugin/extensions/SnykControllerManager.kt b/src/main/kotlin/io/snyk/plugin/extensions/SnykControllerManager.kt
new file mode 100644
index 000000000..700157dcc
--- /dev/null
+++ b/src/main/kotlin/io/snyk/plugin/extensions/SnykControllerManager.kt
@@ -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)
+}
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index a030417dd..0086cb328 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -100,4 +100,10 @@
description="Snyk: Show Low severity issues"/>
+
+
+
+
diff --git a/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt b/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt
new file mode 100644
index 000000000..c7373371e
--- /dev/null
+++ b/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt
@@ -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
+ }
+ }
+}