Skip to content
Permalink
Browse files Browse the repository at this point in the history
feat: add trust management mechanism (#187)
  • Loading branch information
michelkaporin committed Nov 29, 2022
1 parent 18cbb0c commit b3229f0
Show file tree
Hide file tree
Showing 22 changed files with 922 additions and 104 deletions.
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Expand Up @@ -60,4 +60,4 @@ dist: build
env:
- GO111MODULE=on
- CGO_ENABLED=0
- LS_PROTOCOL_VERSION=3
- LS_PROTOCOL_VERSION=4
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -37,7 +37,7 @@ tools:
@echo "==> Installing go-licenses"
@go install github.com/google/go-licenses@latest
ifeq (,$(wildcard ./.bin/golangci-lint*))
@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b .bin/ v1.48.0
@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b .bin/ v1.50.0
else
@echo "==> golangci-lint is already installed"
endif
Expand Down
40 changes: 37 additions & 3 deletions README.md
Expand Up @@ -75,6 +75,15 @@ Right now the language server supports the following actions:
}
```

- Trust Notification
- method: `$/snyk.addTrustedFolders`
- payload:
```json
{
"trustedFolders": ["/a/path/to/trust"]
}
```

## Installation

### Download
Expand Down Expand Up @@ -132,12 +141,37 @@ within `initializationOptions?: LSPAny;` we support the following settings:
"organization": "a string", // The name of your organization, e.g. the output of: curl -H "Authorization: token $(snyk config get api)" https://snyk.io/api/cli-config/settings/sast | jq .org
"enableTelemetry": "true", // Whether or not user analytics can be tracked
"manageBinariesAutomatically": "true", // Whether or not CLI/LS binaries will be downloaded & updated automatically
"cliPath": "/a/patch/snyk-cli" // The path where the CLI can be found, or where it should be downloaded to
"token": "secret-token" // The Snyk token, e.g.: snyk config get api
"automaticAuthentication": "true" // Whether or not LS will automatically authenticate on scan start (default: true)
"cliPath": "/a/patch/snyk-cli", // The path where the CLI can be found, or where it should be downloaded to
"token": "secret-token", // The Snyk token, e.g.: snyk config get api
"automaticAuthentication": "true", // Whether or not LS will automatically authenticate on scan start (default: true)
"enableTrustedFoldersFeature": "true", // Whether or not LS will prompt to trust a folder (default: true)
"trustedFolders": ["/a/trusted/path", "/another/trusted/path"], // An array of folder that should be trusted
}
```

#### Workspace Trust

As part of examining the codebase for vulnerabilities, Snyk may automatically execute code on your computer to obtain
additional data for analysis. For example, this includes invoking the package manager (e.g., pip, gradle, maven, yarn,
npm, etc.)
to get dependency information for Snyk Open Source. Invoking these programs on untrusted code that has malicious
configurations may expose your system to malicious code execution and exploits.

To safeguard from using the language server on untrusted folders, our language server will ask for folder trust
before running scans against these folders. When in doubt, do not grant trust.

The trust feature is enabled by default. When a folder is trusted, all sub-folders are also trusted. After a folder
is trusted, Snyk Language Server notifies the Language Server Client with the custom `$/snyk.addTrustedFolders`
notification,
which contains a list of currently trusted folder paths. Based on this, a client can then implement logic to intercept
this notification and persist the decision and trust in the IDE or Editor storage mechanism.

Trust dialogs can be disabled by setting `enableTrustedFoldersFeature` to `false` in the initialization options. This
will disable all trust prompts and checks.

An initial set of trusted folders can be provided by setting `trustedFolders` to an array of paths in the
`initializationOptions`. These folders will be trusted on startup and will not prompt the user to trust them.

#### Environment variables

Snyk LS and Snyk CLI support and need certain environment variables to function:
Expand Down
79 changes: 52 additions & 27 deletions application/config/config.go
Expand Up @@ -119,33 +119,35 @@ func (c *CliSettings) DefaultBinaryInstallPath() string {
}

type Config struct {
configLoaded concurrency.AtomicBool
cliSettings *CliSettings
configFile string
format string
isErrorReportingEnabled concurrency.AtomicBool
isSnykCodeEnabled concurrency.AtomicBool
isSnykOssEnabled concurrency.AtomicBool
isSnykIacEnabled concurrency.AtomicBool
isSnykContainerEnabled concurrency.AtomicBool
isSnykAdvisorEnabled concurrency.AtomicBool
isTelemetryEnabled concurrency.AtomicBool
manageBinariesAutomatically concurrency.AtomicBool
logPath string
organization string
snykCodeAnalysisTimeout time.Duration
snykApiUrl string
snykCodeApiUrl string
token string
deviceId string
clientCapabilities lsp.ClientCapabilities
m sync.Mutex
path string
defaultDirs []string
integrationName string
integrationVersion string
automaticAuthentication bool
tokenChangeChannels []chan string
configLoaded concurrency.AtomicBool
cliSettings *CliSettings
configFile string
format string
isErrorReportingEnabled concurrency.AtomicBool
isSnykCodeEnabled concurrency.AtomicBool
isSnykOssEnabled concurrency.AtomicBool
isSnykIacEnabled concurrency.AtomicBool
isSnykContainerEnabled concurrency.AtomicBool
isSnykAdvisorEnabled concurrency.AtomicBool
isTelemetryEnabled concurrency.AtomicBool
manageBinariesAutomatically concurrency.AtomicBool
logPath string
organization string
snykCodeAnalysisTimeout time.Duration
snykApiUrl string
snykCodeApiUrl string
token string
deviceId string
clientCapabilities lsp.ClientCapabilities
m sync.Mutex
path string
defaultDirs []string
integrationName string
integrationVersion string
automaticAuthentication bool
tokenChangeChannels []chan string
trustedFolders []string
trustedFoldersFeatureEnabled bool
}

func CurrentConfig() *Config {
Expand Down Expand Up @@ -185,6 +187,7 @@ func New() *Config {
c.snykCodeApiUrl = defaultDeeproxyApiUrl
c.snykCodeAnalysisTimeout = snykCodeAnalysisTimeoutFromEnv()
c.token = ""
c.trustedFoldersFeatureEnabled = true
c.clientSettingsFromEnv()
c.deviceId = c.determineDeviceId()
c.addDefaults()
Expand All @@ -211,6 +214,16 @@ func (c *Config) determineDeviceId() string {
}
}

func (c *Config) IsTrustedFolderFeatureEnabled() bool {
return c.trustedFoldersFeatureEnabled
}

func (c *Config) SetTrustedFolderFeatureEnabled(enabled bool) {
c.m.Lock()
defer c.m.Unlock()
c.trustedFoldersFeatureEnabled = enabled
}

func (c *Config) Load() {
files := c.configFiles()
for _, fileName := range files {
Expand Down Expand Up @@ -538,3 +551,15 @@ func (c *Config) SetIntegrationName(integrationName string) {
func (c *Config) SetIntegrationVersion(integrationVersion string) {
c.integrationVersion = integrationVersion
}

func (c *Config) TrustedFolders() []string {
c.m.Lock()
defer c.m.Unlock()
return c.trustedFolders
}

func (c *Config) SetTrustedFolders(folderPaths []string) {
c.m.Lock()
defer c.m.Unlock()
c.trustedFolders = folderPaths
}
1 change: 1 addition & 0 deletions application/config/config_test.go
Expand Up @@ -45,6 +45,7 @@ func TestConfigDefaults(t *testing.T) {
assert.True(t, c.IsSnykIacEnabled(), "Snyk IaC should be enabled by default")
assert.Equal(t, "", c.LogPath(), "Logpath should be empty by default")
assert.Equal(t, "md", c.Format(), "Output format should be md by default")
assert.Empty(t, c.trustedFolders)
}

func Test_TokenChanged_ChannelsInformed(t *testing.T) {
Expand Down
21 changes: 18 additions & 3 deletions application/server/configuration.go
Expand Up @@ -19,6 +19,7 @@ package server
import (
"context"
"os"
"reflect"
"strconv"
"strings"

Expand All @@ -37,7 +38,7 @@ func WorkspaceDidChangeConfiguration(srv *jrpc2.Server) jrpc2.Handler {
defer log.Info().Str("method", "WorkspaceDidChangeConfiguration").Interface("params", params).Msg("DONE")

emptySettings := lsp.Settings{}
if params.Settings != emptySettings {
if !reflect.DeepEqual(params.Settings, emptySettings) {
// client used settings push
UpdateSettings(ctx, params.Settings)
return true, nil
Expand Down Expand Up @@ -65,7 +66,7 @@ func WorkspaceDidChangeConfiguration(srv *jrpc2.Server) jrpc2.Handler {
return false, err
}

if fetchedSettings[0] != emptySettings {
if !reflect.DeepEqual(fetchedSettings[0], emptySettings) {
UpdateSettings(ctx, fetchedSettings[0])
return true, nil
}
Expand All @@ -86,7 +87,7 @@ func UpdateSettings(ctx context.Context, settings lsp.Settings) {

func writeSettings(ctx context.Context, settings lsp.Settings, initialize bool) {
emptySettings := lsp.Settings{}
if settings == emptySettings {
if reflect.DeepEqual(settings, emptySettings) {
return
}
updateToken(settings.Token)
Expand All @@ -98,6 +99,20 @@ func writeSettings(ctx context.Context, settings lsp.Settings, initialize bool)
updateTelemetry(settings)
updateOrganization(settings)
manageBinariesAutomatically(settings)
updateTrustedFolders(settings)
}

func updateTrustedFolders(settings lsp.Settings) {
trustedFoldersFeatureEnabled, err := strconv.ParseBool(settings.EnableTrustedFoldersFeature)
if err == nil {
config.CurrentConfig().SetTrustedFolderFeatureEnabled(trustedFoldersFeatureEnabled)
} else {
config.CurrentConfig().SetTrustedFolderFeatureEnabled(true)
}

if settings.TrustedFolders != nil {
config.CurrentConfig().SetTrustedFolders(settings.TrustedFolders)
}
}

func updateAutoAuthentication(settings lsp.Settings) {
Expand Down
14 changes: 13 additions & 1 deletion application/server/configuration_test.go
Expand Up @@ -71,7 +71,7 @@ func Test_WorkspaceDidChangeConfiguration_Push(t *testing.T) {
assert.Equal(t, "token", config.CurrentConfig().Token())
}

func callBackMock(ctx context.Context, request *jrpc2.Request) (interface{}, error) {
func callBackMock(_ context.Context, request *jrpc2.Request) (interface{}, error) {
jsonRPCRecorder.Record(*request)
if request.Method() == "workspace/configuration" {
return []lsp.Settings{sampleSettings}, nil
Expand Down Expand Up @@ -151,6 +151,7 @@ func Test_UpdateSettings(t *testing.T) {
ManageBinariesAutomatically: "false",
CliPath: "C:\\Users\\CliPath\\snyk-ls.exe",
Token: "a fancy token",
TrustedFolders: []string{"trustedPath1", "trustedPath2"},
}

UpdateSettings(context.Background(), settings)
Expand All @@ -171,6 +172,8 @@ func Test_UpdateSettings(t *testing.T) {
assert.False(t, c.ManageBinariesAutomatically())
assert.Equal(t, "C:\\Users\\CliPath\\snyk-ls.exe", c.CliSettings().Path())
assert.Equal(t, "a fancy token", c.Token())
assert.Contains(t, c.TrustedFolders(), "trustedPath1")
assert.Contains(t, c.TrustedFolders(), "trustedPath2")
})

t.Run("blank organisation is ignored", func(t *testing.T) {
Expand Down Expand Up @@ -209,6 +212,15 @@ func Test_UpdateSettings(t *testing.T) {
assert.Empty(t, os.Getenv("b"))
assert.Empty(t, os.Getenv(";"))
})
t.Run("trusted folders", func(t *testing.T) {
config.SetCurrentConfig(config.New())

UpdateSettings(context.Background(), lsp.Settings{TrustedFolders: []string{"/a/b", "/b/c"}})

c := config.CurrentConfig()
assert.Contains(t, c.TrustedFolders(), "/a/b")
assert.Contains(t, c.TrustedFolders(), "/b/c")
})

t.Run("manage binaries automatically", func(t *testing.T) {
t.Run("true", func(t *testing.T) {
Expand Down
30 changes: 28 additions & 2 deletions application/server/execute_command.go
Expand Up @@ -25,7 +25,9 @@ import (
"github.com/rs/zerolog/log"
sglsp "github.com/sourcegraph/go-lsp"

"github.com/snyk/snyk-ls/application/config"
"github.com/snyk/snyk-ls/application/di"
"github.com/snyk/snyk-ls/application/server/lsp"
"github.com/snyk/snyk-ls/domain/ide/command"
"github.com/snyk/snyk-ls/domain/ide/workspace"
"github.com/snyk/snyk-ls/domain/snyk"
Expand All @@ -49,10 +51,18 @@ func ExecuteCommandHandler(srv *jrpc2.Server) jrpc2.Handler {
}
navigateToLocation(srv, args)
case snyk.WorkspaceScanCommand:
workspace.Get().ClearIssues(bgCtx)
workspace.Get().ScanWorkspace(bgCtx)
w := workspace.Get()
w.ClearIssues(bgCtx)
w.ScanWorkspace(bgCtx)
handleUntrustedFolders(bgCtx, srv)
case snyk.OpenBrowserCommand:
command.OpenBrowser(params.Arguments[0].(string))
case snyk.TrustWorkspaceFoldersCommand:
err := TrustWorkspaceFolders()
if err != nil {
log.Err(err).Msgf("Error on %s command", snyk.TrustWorkspaceFoldersCommand)
notification.SendError(err)
}
case snyk.LoginCommand:
authenticator := di.Authenticator()
_, err := authenticator.Authenticate(context.Background())
Expand All @@ -75,3 +85,19 @@ func ExecuteCommandHandler(srv *jrpc2.Server) jrpc2.Handler {
return nil, nil
})
}

func TrustWorkspaceFolders() error {
if !config.CurrentConfig().IsTrustedFolderFeatureEnabled() {
return nil
}

trustedFolderPaths := config.CurrentConfig().TrustedFolders()
_, untrusted := workspace.Get().GetFolderTrust()
for _, folder := range untrusted {
trustedFolderPaths = append(trustedFolderPaths, folder.Path())
}

config.CurrentConfig().SetTrustedFolders(trustedFolderPaths)
notification.Send(lsp.SnykTrustedFoldersParams{TrustedFolders: trustedFolderPaths})
return nil
}

0 comments on commit b3229f0

Please sign in to comment.