Skip to content

Commit

Permalink
Merge pull request #417 from snyk/feat/trust
Browse files Browse the repository at this point in the history
feat: add workspace trust feature
  • Loading branch information
michelkaporin authored Nov 29, 2022
2 parents c8eb31d + ab6c0b4 commit 56682f4
Show file tree
Hide file tree
Showing 14 changed files with 363 additions and 7 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Snyk Changelog

## [2.4.48]
### Added

- Project trust feature.

## [2.4.47]

### Fixed
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {
exclude(group = "org.slf4j")
}
implementation("ly.iterative.itly:sdk-jvm:1.2.11")
testImplementation("com.google.jimfs:jimfs:1.2")
testImplementation("com.squareup.okhttp3:mockwebserver:4.10.0")
testImplementation("junit:junit:4.13.2") {
exclude(group = "org.hamcrest")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import org.junit.Test
import snyk.container.ContainerResult
import snyk.iac.IacResult
import snyk.oss.OssResult
import snyk.trust.WorkspaceTrustService
import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded
import java.util.concurrent.TimeUnit

@Suppress("FunctionName")
Expand All @@ -51,13 +53,15 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() {
mockSnykApiServiceSastEnabled()
replaceSnykApiServiceMockInContainer()
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 { confirmScanningAndSetWorkspaceTrustedStateIfNeeded(any()) } returns true
}

private fun mockSnykApiServiceSastEnabled() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,40 @@ import com.intellij.testFramework.LightPlatform4TestCase
import com.intellij.testFramework.replaceService
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.verify
import io.snyk.plugin.services.SnykAnalyticsService
import io.snyk.plugin.services.SnykCliAuthenticationService
import io.snyk.plugin.ui.toolwindow.panels.SnykAuthPanel
import org.junit.Test
import snyk.trust.WorkspaceTrustService
import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded
import javax.swing.JButton
import javax.swing.JLabel

class SnykAuthPanelIntegTest : LightPlatform4TestCase() {

private val analyticsService: SnykAnalyticsService = mockk(relaxed = true)
private val cliAuthenticationService = mockk<SnykCliAuthenticationService>(relaxed = true)
private val workspaceTrustServiceMock = mockk<WorkspaceTrustService>(relaxed = true)

override fun setUp() {
super.setUp()
unmockkAll()
mockkStatic("snyk.trust.TrustedProjectsKt")
every { confirmScanningAndSetWorkspaceTrustedStateIfNeeded(any()) } returns true
val application = ApplicationManager.getApplication()
application.replaceService(SnykAnalyticsService::class.java, analyticsService, application)
application.replaceService(WorkspaceTrustService::class.java, workspaceTrustServiceMock, application)
project.replaceService(SnykCliAuthenticationService::class.java, cliAuthenticationService, project)
}

override fun tearDown() {
unmockkAll()
val application = ApplicationManager.getApplication()
application.replaceService(SnykAnalyticsService::class.java, SnykAnalyticsService(), application)
application.replaceService(WorkspaceTrustService::class.java, workspaceTrustServiceMock, application)
project.replaceService(SnykCliAuthenticationService::class.java, SnykCliAuthenticationService(project), project)
super.tearDown()
}
Expand All @@ -40,20 +48,25 @@ class SnykAuthPanelIntegTest : LightPlatform4TestCase() {
fun `should display right authenticate button text`() {
val cut = SnykAuthPanel(project)
val authenticateButton = UIComponentFinder.getComponentByCondition(cut, JButton::class) {
it.text == SnykAuthPanel.AUTHENTICATE_BUTTON_TEXT
it.text == SnykAuthPanel.TRUST_AND_SCAN_BUTTON_TEXT
}
assertNotNull(authenticateButton)
assertEquals("Test code now", authenticateButton!!.text)
assertEquals("Trust project and scan", authenticateButton!!.text)
}

@Test
fun `should display right description label`() {
val expectedText = """
|<html><ol>
|<html>
|<ol>
| <li align="left">Authenticate to Snyk.io</li>
| <li align="left">Analyze code for issues and vulnerabilities</li>
| <li align="left">Improve your code and upgrade dependencies</li>
|</ol>
|<br>
|When scanning project files, Snyk may automatically execute code<br>such as invoking the package manager to get dependency information.<br>You should only scan projects you trust. <a href="https://docs.snyk.io/ide-tools/jetbrains-plugins/folder-trust">More info</a>
|<br>
|<br>
|</html>
""".trimMargin()

Expand All @@ -72,7 +85,7 @@ class SnykAuthPanelIntegTest : LightPlatform4TestCase() {

val cut = SnykAuthPanel(project)
val authenticateButton = UIComponentFinder.getComponentByCondition(cut, JButton::class) {
it.text == SnykAuthPanel.AUTHENTICATE_BUTTON_TEXT
it.text == SnykAuthPanel.TRUST_AND_SCAN_BUTTON_TEXT
}
assertNotNull(authenticateButton)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import snyk.iac.IgnoreButtonActionListener
import snyk.iac.ui.toolwindow.IacFileTreeNode
import snyk.iac.ui.toolwindow.IacIssueTreeNode
import snyk.oss.Vulnerability
import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded
import javax.swing.JButton
import javax.swing.JEditorPane
import javax.swing.JLabel
Expand Down Expand Up @@ -94,12 +95,14 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() {
super.setUp()
unmockkAll()
resetSettings(project)
mockkStatic("snyk.trust.TrustedProjectsKt")
pluginSettings().token = fakeApiToken // needed to avoid forced Auth panel showing
pluginSettings().pluginFirstRun = false
// ToolWindow need to be reinitialised for every test as Project is recreated for Heavy tests
// also we MUST do it *before* any actual test code due to initialisation of SnykScanListener in init{}
toolWindowPanel = project.service()
setupDummyCliFile()
every { confirmScanningAndSetWorkspaceTrustedStateIfNeeded(any()) } returns true
}

override fun tearDown() {
Expand Down
59 changes: 59 additions & 0 deletions src/integTest/kotlin/snyk/trust/WorkspaceTrustServiceTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@file:Suppress("FunctionName")

package snyk.trust

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.testFramework.replaceService
import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
import org.hamcrest.core.IsEqual.equalTo
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Test
import java.nio.file.Paths

class WorkspaceTrustServiceTest : BasePlatformTestCase() {

private val workspaceTrustSettingsMock = mockk<WorkspaceTrustSettings>()
private lateinit var cut: WorkspaceTrustService

private class IntegTestDisposable : Disposable {
override fun dispose() {}
}

@Before
public override fun setUp() {
super.setUp()
unmockkAll()

val application = ApplicationManager.getApplication()
application.replaceService(
WorkspaceTrustSettings::class.java,
workspaceTrustSettingsMock,
IntegTestDisposable()
)

cut = WorkspaceTrustService()
}

@Test
fun `test isPathTrusted should return false if no trusted path in settings available`() {
every { workspaceTrustSettingsMock.getTrustedPaths() } returns listOf()

val path = Paths.get("/project")

assertThat(cut.isPathTrusted(path), equalTo(false))
}

@Test
fun `test isPathTrusted should return true if trusted path in settings available`() {
every { workspaceTrustSettingsMock.getTrustedPaths() } returns listOf("/project")

val path = Paths.get("/project")

assertThat(cut.isPathTrusted(path), equalTo(true))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import io.snyk.plugin.snykcode.core.RunUtils
import io.snyk.plugin.ui.SnykBalloonNotifications
import org.jetbrains.annotations.TestOnly
import snyk.common.SnykError
import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded
import java.nio.file.Paths

@Service
class SnykTaskQueueService(val project: Project) {
Expand Down Expand Up @@ -73,6 +75,10 @@ class SnykTaskQueueService(val project: Project) {
fun scan() {
taskQueue.run(object : Task.Backgroundable(project, "Snyk wait for changed files to be saved on disk", true) {
override fun run(indicator: ProgressIndicator) {
project.basePath?.let {
if (!confirmScanningAndSetWorkspaceTrustedStateIfNeeded(Paths.get(it))) return
}

ApplicationManager.getApplication().invokeAndWait {
FileDocumentManager.getInstance().saveAllDocuments()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.snyk.plugin.ui.toolwindow.panels

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.uiDesigner.core.GridConstraints.ANCHOR_EAST
import com.intellij.uiDesigner.core.GridConstraints.ANCHOR_NORTHWEST
Expand All @@ -24,12 +25,15 @@ import io.snyk.plugin.ui.baseGridConstraints
import io.snyk.plugin.ui.boldLabel
import io.snyk.plugin.ui.getReadOnlyClickableHtmlJEditorPaneFixedSize
import io.snyk.plugin.ui.getStandardLayout
import snyk.SnykBundle
import snyk.amplitude.api.ExperimentUser
import snyk.analytics.AuthenticateButtonIsClicked
import snyk.analytics.AuthenticateButtonIsClicked.EventSource
import snyk.analytics.AuthenticateButtonIsClicked.Ide
import snyk.analytics.AuthenticateButtonIsClicked.builder
import snyk.trust.WorkspaceTrustService
import java.awt.event.ActionEvent
import java.nio.file.Paths
import javax.swing.AbstractAction
import javax.swing.JButton
import javax.swing.JLabel
Expand All @@ -39,7 +43,7 @@ class SnykAuthPanel(val project: Project) : JPanel(), Disposable {

init {
name = "authPanel"
val authButton = JButton(object : AbstractAction(AUTHENTICATE_BUTTON_TEXT) {
val authButton = JButton(object : AbstractAction(TRUST_AND_SCAN_BUTTON_TEXT) {
override fun actionPerformed(e: ActionEvent?) {
val analytics = getSnykAnalyticsService()
analytics.logAuthenticateButtonIsClicked(authenticateEvent())
Expand All @@ -49,6 +53,12 @@ class SnykAuthPanel(val project: Project) : JPanel(), Disposable {
pluginSettings().token = token
SnykCodeParams.instance.sessionToken = token

// explicitly add the project to workspace trusted paths, because
// scan can be auto-triggered depending on "settings.pluginFirstRun" value
project.basePath?.let {
service<WorkspaceTrustService>().addTrustedPath(Paths.get(it))
}

val userId = analytics.obtainUserId(token)
if (userId.isNotBlank()) {
analytics.setUserId(userId)
Expand Down Expand Up @@ -102,20 +112,26 @@ class SnykAuthPanel(val project: Project) : JPanel(), Disposable {
}

private fun descriptionLabelText(): String {
val trustWarningDescription = SnykBundle.message("snyk.panel.auth.trust.warning.text")
return """
|<html><ol>
|<html>
|<ol>
| <li align="left">Authenticate to Snyk.io</li>
| <li align="left">Analyze code for issues and vulnerabilities</li>
| <li align="left">Improve your code and upgrade dependencies</li>
|</ol>
|<br>
|$trustWarningDescription
|<br>
|<br>
|</html>
""".trimMargin()
}

override fun dispose() {}

companion object {
const val AUTHENTICATE_BUTTON_TEXT = "Test code now"
const val TRUST_AND_SCAN_BUTTON_TEXT = "Trust project and scan"
val messagePolicyAndTermsHtml =
"""
<br>
Expand Down
73 changes: 73 additions & 0 deletions src/main/kotlin/snyk/trust/TrustedProjects.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
@file:JvmName("TrustedProjectsKt")
package snyk.trust

import com.intellij.openapi.application.invokeAndWaitIfNeeded
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.ui.MessageDialogBuilder
import com.intellij.openapi.ui.Messages
import snyk.SnykBundle
import java.nio.file.Files
import java.nio.file.Path

private val LOG = Logger.getInstance("snyk.trust.TrustedProjects")

/**
* Shows the "Trust and Scan Project?" dialog, if the user wasn't asked yet if they trust this project,
* and sets the project trusted state according to the user choice.
*
* @return `false` if the user chose not to scan the project at all; `true` otherwise
*/
fun confirmScanningAndSetWorkspaceTrustedStateIfNeeded(projectFileOrDir: Path): Boolean {
val projectDir = if (Files.isDirectory(projectFileOrDir)) projectFileOrDir else projectFileOrDir.parent

val trustService = service<WorkspaceTrustService>()
val trustedState = trustService.isPathTrusted(projectDir)
if (!trustedState) {
LOG.info("Asking user to trust the project ${projectDir.fileName}")
return when (confirmScanningUntrustedProject(projectDir)) {
ScanUntrustedProjectChoice.TRUST_AND_SCAN -> {
trustService.addTrustedPath(projectDir)
true
}

ScanUntrustedProjectChoice.CANCEL -> false
}
}

return true
}

private fun confirmScanningUntrustedProject(projectDir: Path): ScanUntrustedProjectChoice {
val fileName = projectDir.fileName ?: projectDir.toString()
val title = SnykBundle.message("snyk.trust.dialog.warning.title", fileName)
val message = SnykBundle.message("snyk.trust.dialog.warning.text")
val trustButton = SnykBundle.message("snyk.trust.dialog.warning.button.trust")
val distrustButton = SnykBundle.message("snyk.trust.dialog.warning.button.distrust")

var choice = ScanUntrustedProjectChoice.CANCEL

invokeAndWaitIfNeeded {
val result = MessageDialogBuilder
.yesNo(title, message)
.icon(Messages.getWarningIcon())
.yesText(trustButton)
.noText(distrustButton)
.show()

choice = if (result == Messages.YES) {
LOG.info("User trusts the project $fileName for scans")
ScanUntrustedProjectChoice.TRUST_AND_SCAN
} else {
LOG.info("User doesn't trust the project $fileName for scans")
ScanUntrustedProjectChoice.CANCEL
}
}

return choice
}

enum class ScanUntrustedProjectChoice {
TRUST_AND_SCAN,
CANCEL;
}
Loading

0 comments on commit 56682f4

Please sign in to comment.