Skip to content

Commit

Permalink
feat: ✨ option to enable/disable automatic updates and to specify the…
Browse files Browse the repository at this point in the history
… CLI path (#397)

* feat: add preference UI for update enablement and cli path choice

* feat: disable cli downloads if requested and save/load cli from given path

* feat: show balloon notification, if download is needed but not possible

* docs: update changelog

* fix: .snyk

* chore: ⬆️ update dependencies

* fix: 🐛 test

* feat: 💄 improve UI descriptiveness

* chore: sanitize input for cli path
  • Loading branch information
bastiandoetsch authored Jul 12, 2022
1 parent 2ca9e1e commit efa9acb
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 63 deletions.
10 changes: 9 additions & 1 deletion .snyk
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ version: v1.25.0
ignore:
'*':
- build/*:
reason: None Given
reason: Build data
expires: 2032-05-20T07:12:38.106Z
created: 2022-04-20T07:12:38.108Z
- src/integTest/*:
reason: Test data
expires: 2032-07-14T12:33:33.331Z
created: 2022-06-14T12:33:33.333Z
- src/test/*:
reason: Test data
expires: 2032-07-14T12:33:33.331Z
created: 2022-06-14T12:33:33.333Z
patch: {}
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
# Snyk Changelog

## [2.4.38]

### Added

- option to disable automatic CLI downloads
- option to specify the file path of the Snyk CLI executable

## [2.4.37]

### Fixed

- Found Container vulnerabilities now grouped by ID (similar to OSS results);
- For Container images with no remediation/fix available issues count(grouped by severity) now is shown.
- Container multi-images (OSS multi-build-managers) scan with no auth now redirect to auth panel.

### Added

- In the result's tree, second level nodes(file/image) now have number of vulnerabilities/issues found in it.

## [2.4.36]
Expand Down
10 changes: 5 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,26 @@ repositories {
}

dependencies {
implementation("org.commonmark:commonmark:0.18.2")
implementation("org.commonmark:commonmark:0.19.0")
implementation("com.google.code.gson:gson:2.9.0")
implementation("com.segment.analytics.java:analytics:3.2.0")
implementation("io.sentry:sentry:5.7.2")
implementation("io.sentry:sentry:6.0.0")
implementation("io.snyk.code.sdk:snyk-code-client:2.3.4")
implementation("ly.iterative.itly:plugin-iteratively:1.2.11")
implementation("ly.iterative.itly:plugin-schema-validator:1.2.11") {
exclude(group = "org.slf4j")
}
implementation("ly.iterative.itly:sdk-jvm:1.2.11")
testImplementation("com.squareup.okhttp3:mockwebserver:4.9.3")
testImplementation("com.squareup.okhttp3:mockwebserver:4.10.0")
testImplementation("junit:junit:4.13.2") {
exclude(group = "org.hamcrest")
}
testImplementation("org.hamcrest:hamcrest:2.2")
testImplementation("io.mockk:mockk:1.12.2")
testImplementation("io.mockk:mockk:1.12.2") // updating this breaks tests
testImplementation("org.awaitility:awaitility:4.2.0")
runtimeOnly("org.jetbrains.kotlin:kotlin-reflect:1.4.32")

detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.19.0")
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.20.0")
}

// configuration for gradle-intellij-plugin plugin.
Expand Down
4 changes: 3 additions & 1 deletion src/integTest/kotlin/io/snyk/plugin/TestUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ fun resetSettings(project: Project?) {
}

/** low level avoiding download the CLI file */
fun mockCliDownload() {
fun mockCliDownload(): RequestBuilder {
val requestBuilderMockk = mockk<RequestBuilder>(relaxed = true)
justRun { requestBuilderMockk.saveToFile(any(), any()) }
mockkObject(HttpRequestHelper)
every { HttpRequestHelper.createRequest(CliDownloader.LATEST_RELEASE_DOWNLOAD_URL) } returns requestBuilderMockk
every { HttpRequestHelper.createRequest(CliDownloader.LATEST_RELEASES_URL) } returns requestBuilderMockk
return requestBuilderMockk
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@ import com.intellij.testFramework.PlatformTestUtil
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.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.pluginSettings
import io.snyk.plugin.removeDummyCliFile
import io.snyk.plugin.resetSettings
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
Expand All @@ -35,10 +38,20 @@ import java.util.concurrent.TimeUnit
@Suppress("FunctionName")
class SnykTaskQueueServiceTest : LightPlatformTestCase() {

private lateinit var downloaderServiceMock: SnykCliDownloaderService

override fun setUp() {
super.setUp()
unmockkAll()
resetSettings(project)
mockkStatic("io.snyk.plugin.UtilsKt")
downloaderServiceMock = spyk(SnykCliDownloaderService())
every { downloaderServiceMock.requestLatestReleasesInformation() } returns LatestReleaseInfo(
"http://testUrl",
"testReleaseInfo",
"testTag"
)
every { getSnykCliDownloaderService() } returns downloaderServiceMock
}

override fun tearDown() {
Expand Down Expand Up @@ -70,15 +83,38 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() {
@Test
fun testCliDownloadBeforeScanIfNeeded() {
val cliFile = getCliFile()

mockkStatic("io.snyk.plugin.UtilsKt")
every { getCliFile().exists() } returns false
every { isCliInstalled() } returns false

val downloaderMock = mockk<CliDownloader>()
service<SnykCliDownloaderService>().downloader = downloaderMock
val downloaderMock = setupMockForDownloadTest()
every { downloaderMock.expectedSha() } returns "test"
every { downloaderMock.downloadFile(any(), any(), any()) } returns cliFile
setupAppSettingsForDownloadTests()

val snykTaskQueueService = project.service<SnykTaskQueueService>()
snykTaskQueueService.scan()
// needed due to luck of disposing services by Idea test framework (bug?)
Disposer.dispose(service<SnykApiService>())

assertTrue(snykTaskQueueService.getTaskQueue().isEmpty)

verify { downloaderMock.downloadFile(any(), any(), any()) }
}

@Test
fun testDontDownloadCLIIfUpdatesDisabled() {
val downloaderMock = setupMockForDownloadTest()
val settings = setupAppSettingsForDownloadTests()
settings.manageBinariesAutomatically = false

val snykTaskQueueService = project.service<SnykTaskQueueService>()
snykTaskQueueService.scan()
// needed due to luck of disposing services by Idea test framework (bug?)
Disposer.dispose(service<SnykApiService>())

assertTrue(snykTaskQueueService.getTaskQueue().isEmpty)

verify(exactly = 0) { downloaderMock.downloadFile(any(), any(), any()) }
}

private fun setupAppSettingsForDownloadTests(): SnykApplicationSettingsStateService {
every { getOssService(project)?.scan() } returns OssResult(null)

val settings = pluginSettings()
Expand All @@ -87,15 +123,16 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() {
settings.snykCodeQualityIssuesScanEnable = false
settings.iacScanEnabled = false
settings.containerScanEnabled = false
return settings
}

val snykTaskQueueService = project.service<SnykTaskQueueService>()
snykTaskQueueService.scan()
// needed due to luck of disposing services by Idea test framework (bug?)
Disposer.dispose(service<SnykApiService>())

assertTrue(snykTaskQueueService.getTaskQueue().isEmpty)
private fun setupMockForDownloadTest(): CliDownloader {
every { getCliFile().exists() } returns false
every { isCliInstalled() } returns false

verify { downloaderMock.downloadFile(any(), any(), any()) }
val downloaderMock = mockk<CliDownloader>()
getSnykCliDownloaderService().downloader = downloaderMock
return downloaderMock
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.intellij.util.io.HttpRequests
import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.spyk
import io.mockk.unmockkAll
import io.mockk.verify
Expand All @@ -15,8 +16,10 @@ import io.snyk.plugin.mockCliDownload
import io.snyk.plugin.pluginSettings
import io.snyk.plugin.removeDummyCliFile
import io.snyk.plugin.resetSettings
import io.snyk.plugin.services.SnykApplicationSettingsStateService
import org.apache.http.HttpStatus
import org.junit.Test
import java.io.File
import java.net.SocketTimeoutException
import java.time.LocalDate
import java.time.LocalDateTime
Expand All @@ -28,12 +31,15 @@ class CliDownloaderServiceIntegTest : LightPlatformTestCase() {
private lateinit var downloader: CliDownloader
private lateinit var cut: SnykCliDownloaderService
private lateinit var cutSpy: SnykCliDownloaderService
private val cliFile = getCliFile()
private lateinit var cliFile: File

override fun setUp() {
super.setUp()
unmockkAll()
resetSettings(project)
mockkStatic("io.snyk.plugin.UtilsKt")
every { pluginSettings() } returns SnykApplicationSettingsStateService()
cliFile = getCliFile()
cut = project.service()
cutSpy = spyk(cut)
errorHandler = mockk()
Expand All @@ -51,6 +57,9 @@ class CliDownloaderServiceIntegTest : LightPlatformTestCase() {
super.tearDown()
}

/**
* Needs an internet connection - real test if release info can be downloaded
*/
@Test
fun testGetLatestReleasesInformation() {
val latestReleaseInfo = project.service<SnykCliDownloaderService>().requestLatestReleasesInformation()
Expand Down Expand Up @@ -170,25 +179,18 @@ class CliDownloaderServiceIntegTest : LightPlatformTestCase() {
@Test
fun testCliSilentAutoUpdateWhenPreviousUpdateInfoIsNull() {
val currentDate = LocalDate.now()

val settings = pluginSettings()

settings.cliVersion = ""
settings.lastCheckDate = null

ensureCliFileExistent()

every { downloader.downloadFile(any(), any(), any()) } returns cliFile
every { cutSpy.requestLatestReleasesInformation() } returns LatestReleaseInfo(
"http://testUrl", "testReleaseInfo", "testTag"
)
justRun { cutSpy.downloadLatestRelease(any(), any()) }

cutSpy.cliSilentAutoUpdate(EmptyProgressIndicator(), project)

assertTrue(getCliFile().exists())

assertEquals(currentDate, settings.getLastCheckDate())
assertEquals(
cutSpy.getLatestReleaseInfo()!!.tagName,
"v" + settings.cliVersion
)
verify { cutSpy.downloadLatestRelease(any(), any()) }
}

@Test
Expand Down
3 changes: 1 addition & 2 deletions src/main/kotlin/io/snyk/plugin/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ 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.cli.Platform
import io.snyk.plugin.services.SnykAnalyticsService
import io.snyk.plugin.services.SnykApiService
import io.snyk.plugin.services.SnykApplicationSettingsStateService
Expand Down Expand Up @@ -85,7 +84,7 @@ fun getSnykCliDownloaderService(): SnykCliDownloaderService = getApplicationServ

fun getSnykProjectSettingsService(project: Project): SnykProjectSettingsStateService? = project.serviceIfNotDisposed()

fun getCliFile() = File(getPluginPath(), Platform.current().snykWrapperFileName)
fun getCliFile() = File(pluginSettings().cliPath)

fun isCliInstalled(): Boolean = ApplicationManager.getApplication().isUnitTestMode || getCliFile().exists()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import com.intellij.openapi.components.Storage
import com.intellij.openapi.project.Project
import com.intellij.util.xmlb.XmlSerializerUtil
import io.snyk.plugin.Severity
import io.snyk.plugin.cli.Platform
import io.snyk.plugin.getPluginPath
import io.snyk.plugin.getSnykProjectSettingsService
import io.snyk.plugin.isProjectSettingsAvailable
import org.jetbrains.kotlin.konan.file.File
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
Expand All @@ -24,6 +27,8 @@ import java.util.UUID
)
class SnykApplicationSettingsStateService : PersistentStateComponent<SnykApplicationSettingsStateService> {

var cliPath: String = getPluginPath() + File.separator + Platform.current().snykWrapperFileName
var manageBinariesAutomatically: Boolean = true
var fileListenerEnabled: Boolean = true
var token: String? = null
var customEndpointUrl: String? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import com.intellij.util.io.HttpRequests
import io.snyk.plugin.cli.Platform
import io.snyk.plugin.events.SnykCliDownloadListener
import io.snyk.plugin.getCliFile
import io.snyk.plugin.isCliInstalled
import io.snyk.plugin.pluginSettings
import io.snyk.plugin.services.download.HttpRequestHelper.createRequest
import io.snyk.plugin.tail
import io.snyk.plugin.ui.SnykBalloonNotificationHelper
import java.io.IOException
import java.time.LocalDate
import java.time.temporal.ChronoUnit
Expand Down Expand Up @@ -42,6 +44,7 @@ class SnykCliDownloaderService {
}

fun requestLatestReleasesInformation(): LatestReleaseInfo? {
if (!pluginSettings().manageBinariesAutomatically) return null
try {
val result = createRequest(CliDownloader.LATEST_RELEASES_URL).readString()
val response = "v" + result.removeSuffix("\n")
Expand All @@ -57,6 +60,15 @@ class SnykCliDownloaderService {
}

fun downloadLatestRelease(indicator: ProgressIndicator, project: Project) {
if (!pluginSettings().manageBinariesAutomatically) {
if (!isCliInstalled()) {
val msg =
"The plugin cannot scan without Snyk CLI, but automatic download is disabled. " +
"Please put a Snyk CLI executable in ${pluginSettings().cliPath} and retry."
SnykBalloonNotificationHelper.showError(msg, project)
}
return
}
cliDownloadPublisher.cliDownloadStarted()
indicator.isIndeterminate = true
currentProgressIndicator = indicator
Expand Down
Loading

0 comments on commit efa9acb

Please sign in to comment.