diff --git a/README.md b/README.md index a9b13d9a6..fb00bf977 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,7 @@ within `initializationOptions?: LSPAny;` we support the following settings: "snykCodeApi": "https://deeproxy.snyk.io", // Specifies the Snyk Code API endpoint to use. Default is https://deeproxy.snyk.io "enableSnykLearnCodeActions": "true", // show snyk learns code actions "enableSnykOSSQuickFixCodeActions": "true", // show quickfixes for supported OSS package manager issues + "enableSnykOpenBrowserActions": "false", // show code actions to open issue descriptions "enableDeltaFindings": "false", // only display issues that are not new and thus not on the base branch "requiredProtocolVersion": "11", "additionalParams": "--all-projects", // Any extra params for Open Source scans using the Snyk CLI, separated by spaces diff --git a/application/codeaction/codeaction.go b/application/codeaction/codeaction.go index ab0ad4c63..9c271e58c 100644 --- a/application/codeaction/codeaction.go +++ b/application/codeaction/codeaction.go @@ -82,6 +82,8 @@ func (c *CodeActionsService) GetCodeActions(params types.CodeActionParams) []typ func (c *CodeActionsService) updateIssuesWithQuickFix(quickFixGroupables []types.Groupable, issues []snyk.Issue) []snyk.Issue { // we only allow one quickfix, so it needs to be grouped quickFix := c.getQuickFixAction(quickFixGroupables) + // update title with number of issues + quickFix.Title = fmt.Sprintf("%s and fix %d issues", quickFix.Title, len(quickFixGroupables)) var updatedIssues []snyk.Issue for _, issue := range issues { diff --git a/application/config/config.go b/application/config/config.go index 706a6cbeb..08ef6b109 100644 --- a/application/config/config.go +++ b/application/config/config.go @@ -193,6 +193,7 @@ type Config struct { storage storage.StorageWithCallbacks m sync.Mutex clientProtocolVersion string + isOpenBrowserActionEnabled bool } func CurrentConfig() *Config { @@ -849,7 +850,7 @@ func (c *Config) SetSnykLearnCodeActionsEnabled(enabled bool) { c.enableSnykLearnCodeActions = enabled } -func (c *Config) IsSnyOSSQuickFixCodeActionsEnabled() bool { +func (c *Config) IsSnykOSSQuickFixCodeActionsEnabled() bool { return c.enableSnykOSSQuickFixCodeActions } @@ -942,3 +943,11 @@ func (c *Config) AuthenticationMethod() types.AuthenticationMethod { func (c *Config) SetAuthenticationMethod(authMethod types.AuthenticationMethod) { c.authenticationMethod = authMethod } + +func (c *Config) IsSnykOpenBrowserActionEnabled() bool { + return c.isOpenBrowserActionEnabled +} + +func (c *Config) SetSnykOpenBrowserActionsEnabled(enable bool) { + c.isOpenBrowserActionEnabled = enable +} diff --git a/application/server/configuration.go b/application/server/configuration.go index 626aa79d9..d875eb2e3 100644 --- a/application/server/configuration.go +++ b/application/server/configuration.go @@ -127,10 +127,20 @@ func writeSettings(c *config.Config, settings types.Settings, initialize bool) { updateAutoScan(c, settings) updateSnykLearnCodeActions(c, settings) updateSnykOSSQuickFixCodeActions(c, settings) + updateSnykOpenBrowserCodeActions(c, settings) updateDeltaFindings(c, settings) updateFolderConfig(settings) } +func updateSnykOpenBrowserCodeActions(c *config.Config, settings types.Settings) { + enable := false + if settings.EnableSnykOpenBrowserActions == "true" { + enable = true + } + + c.SetSnykOpenBrowserActionsEnabled(enable) +} + func updateFolderConfig(settings types.Settings) { gitconfig.SetBaseBranch(settings.FolderConfig) } diff --git a/application/server/configuration_test.go b/application/server/configuration_test.go index 898af4971..56401ef73 100644 --- a/application/server/configuration_test.go +++ b/application/server/configuration_test.go @@ -176,28 +176,29 @@ func Test_UpdateSettings(t *testing.T) { tempDir1 := filepath.Join(t.TempDir(), "tempDir1") tempDir2 := filepath.Join(t.TempDir(), "tempDir2") settings := types.Settings{ - ActivateSnykOpenSource: "false", - ActivateSnykCode: "false", - ActivateSnykIac: "false", - Insecure: "true", - Endpoint: "https://api.snyk.io", - AdditionalParams: "--all-projects -d", - AdditionalEnv: "a=b;c=d", - Path: "addPath", - SendErrorReports: "true", - Organization: expectedOrgId, - ManageBinariesAutomatically: "false", - CliPath: "C:\\Users\\CliPath\\snyk-ls.exe", - Token: "a fancy token", - FilterSeverity: types.DefaultSeverityFilter(), - TrustedFolders: []string{"trustedPath1", "trustedPath2"}, - OsPlatform: "windows", - OsArch: "amd64", - RuntimeName: "java", - RuntimeVersion: "1.8.0_275", - ScanningMode: "manual", - AuthenticationMethod: types.OAuthAuthentication, - SnykCodeApi: sampleSettings.SnykCodeApi, + ActivateSnykOpenSource: "false", + ActivateSnykCode: "false", + ActivateSnykIac: "false", + Insecure: "true", + Endpoint: "https://api.snyk.io", + AdditionalParams: "--all-projects -d", + AdditionalEnv: "a=b;c=d", + Path: "addPath", + SendErrorReports: "true", + Organization: expectedOrgId, + ManageBinariesAutomatically: "false", + CliPath: "C:\\Users\\CliPath\\snyk-ls.exe", + Token: "a fancy token", + FilterSeverity: types.DefaultSeverityFilter(), + TrustedFolders: []string{"trustedPath1", "trustedPath2"}, + OsPlatform: "windows", + OsArch: "amd64", + RuntimeName: "java", + RuntimeVersion: "1.8.0_275", + ScanningMode: "manual", + AuthenticationMethod: types.OAuthAuthentication, + SnykCodeApi: sampleSettings.SnykCodeApi, + EnableSnykOpenBrowserActions: "true", FolderConfig: []types.FolderConfig{ { FolderPath: tempDir1, @@ -234,6 +235,7 @@ func Test_UpdateSettings(t *testing.T) { assert.Equal(t, settings.RuntimeVersion, c.RuntimeVersion()) assert.False(t, c.IsAutoScanEnabled()) assert.Equal(t, sampleSettings.SnykCodeApi, c.SnykCodeApi()) + assert.Equal(t, true, c.IsSnykOpenBrowserActionEnabled()) err := initTestRepo(t, tempDir1) assert.NoError(t, err) diff --git a/domain/ide/codelens/codelens.go b/domain/ide/codelens/codelens.go index 87c1d9a6f..787bc7541 100644 --- a/domain/ide/codelens/codelens.go +++ b/domain/ide/codelens/codelens.go @@ -17,6 +17,8 @@ package codelens import ( + "fmt" + sglsp "github.com/sourcegraph/go-lsp" "github.com/snyk/snyk-ls/domain/ide/converter" @@ -25,6 +27,11 @@ import ( "github.com/snyk/snyk-ls/internal/types" ) +type lensesWithIssueCount struct { + lensCommands []types.CommandData + issueCount int +} + func GetFor(filePath string) (lenses []sglsp.CodeLens) { f := workspace.Get().GetFolderContaining(filePath) if f == nil { @@ -34,31 +41,36 @@ func GetFor(filePath string) (lenses []sglsp.CodeLens) { issues := f.IssuesForFile(filePath) // group by range first - lensesByRange := make(map[snyk.Range][]types.CommandData) + lensesByRange := make(map[snyk.Range]*lensesWithIssueCount) for _, issue := range issues { for _, lens := range issue.CodelensCommands { - commands := lensesByRange[issue.Range] - if commands == nil { - commands = []types.CommandData{} + lensesWithIssueCountsForRange := lensesByRange[issue.Range] + if lensesWithIssueCountsForRange == nil { + lensesWithIssueCountsForRange = &lensesWithIssueCount{ + lensCommands: []types.CommandData{}, + issueCount: 0, + } } - commands = append(commands, lens) - lensesByRange[issue.Range] = commands + lensesWithIssueCountsForRange.lensCommands = append(lensesWithIssueCountsForRange.lensCommands, lens) + lensesWithIssueCountsForRange.issueCount++ + lensesByRange[issue.Range] = lensesWithIssueCountsForRange } } for r, commands := range lensesByRange { lensCommands := getLensCommands(commands) for _, command := range lensCommands { - lenses = append(lenses, getCodeLensFromCommand(r, command)) + lens := getCodeLensFromCommand(r, command) + lenses = append(lenses, lens) } } return lenses } -func getLensCommands(inputCommands []types.CommandData) []types.CommandData { +func getLensCommands(lensesWithIssueCount *lensesWithIssueCount) []types.CommandData { groupableByType := map[types.GroupingType][]types.Groupable{} - for _, groupable := range inputCommands { + for _, groupable := range lensesWithIssueCount.lensCommands { commands := groupableByType[groupable.GetGroupingType()] if commands == nil { commands = []types.Groupable{} @@ -73,6 +85,7 @@ func getLensCommands(inputCommands []types.CommandData) []types.CommandData { // right now we can always group by max semver version, as // code only has one quickfix available, and iac none at all qf, ok := types.MaxSemver()(lensCommands).(types.CommandData) + qf.Title = fmt.Sprintf("%s and fix %d issues", qf.Title, lensesWithIssueCount.issueCount) if ok { lenses = append(lenses, qf) } diff --git a/infrastructure/oss/code_actions.go b/infrastructure/oss/code_actions.go index bb913cf91..eb9f6ebbc 100644 --- a/infrastructure/oss/code_actions.go +++ b/infrastructure/oss/code_actions.go @@ -32,9 +32,10 @@ import ( ) func (i *ossIssue) AddCodeActions(learnService learn.Service, ep error_reporting.ErrorReporter, affectedFilePath string, issueRange snyk.Range) (actions []snyk.CodeAction) { + c := config.CurrentConfig() if reflect.DeepEqual(issueRange, snyk.Range{}) { - config.CurrentConfig().Logger().Debug().Str("issue", i.Id).Msg("skipping adding code action, as issueRange is empty") - return + c.Logger().Debug().Str("issue", i.Id).Msg("skipping adding code action, as issueRange is empty") + return actions } quickFixAction := i.AddQuickFixAction(affectedFilePath, issueRange) @@ -42,15 +43,21 @@ func (i *ossIssue) AddCodeActions(learnService learn.Service, ep error_reporting actions = append(actions, *quickFixAction) } - title := fmt.Sprintf("Open description of '%s affecting package %s' in browser (Snyk)", i.Title, i.PackageName) - command := &types.CommandData{ - Title: title, - CommandId: types.OpenBrowserCommand, - Arguments: []any{i.CreateIssueURL().String()}, - } + if c.IsSnykOpenBrowserActionEnabled() { + title := fmt.Sprintf("Open description of '%s affecting package %s' in browser (Snyk)", i.Title, i.PackageName) + command := &types.CommandData{ + Title: title, + CommandId: types.OpenBrowserCommand, + Arguments: []any{i.CreateIssueURL().String()}, + } - action, _ := snyk.NewCodeAction(title, nil, command) - actions = append(actions, action) + action, err := snyk.NewCodeAction(title, nil, command) + if err != nil { + c.Logger().Err(err).Msgf("could not create code action %s", title) + } else { + actions = append(actions, action) + } + } codeAction := i.AddSnykLearnAction(learnService, ep) if codeAction != nil { @@ -90,7 +97,7 @@ func (i *ossIssue) AddSnykLearnAction(learnService learn.Service, ep error_repor func (i *ossIssue) AddQuickFixAction(affectedFilePath string, issueRange snyk.Range) *snyk.CodeAction { logger := config.CurrentConfig().Logger().With().Str("method", "oss.AddQuickFixAction").Logger() - if !config.CurrentConfig().IsSnyOSSQuickFixCodeActionsEnabled() { + if !config.CurrentConfig().IsSnykOSSQuickFixCodeActionsEnabled() { return nil } logger.Debug().Msg("create deferred quickfix code action") @@ -98,7 +105,7 @@ func (i *ossIssue) AddQuickFixAction(affectedFilePath string, issueRange snyk.Ra if quickfixEdit == "" { return nil } - upgradeMessage := "Upgrade to " + quickfixEdit + " (Snyk)" + upgradeMessage := "⚡️ Upgrade to " + quickfixEdit autofixEditCallback := func() *snyk.WorkspaceEdit { edit := &snyk.WorkspaceEdit{} singleTextEdit := snyk.TextEdit{ diff --git a/infrastructure/oss/issue.go b/infrastructure/oss/issue.go index 5b8b93982..2a9c465b8 100644 --- a/infrastructure/oss/issue.go +++ b/infrastructure/oss/issue.go @@ -53,7 +53,7 @@ func toIssue( for _, codeAction := range codeActions { if strings.Contains(codeAction.Title, "Upgrade to") { codelensCommands = append(codelensCommands, types.CommandData{ - Title: "⚡ Fix this issue: " + codeAction.Title, + Title: codeAction.Title, CommandId: types.CodeFixCommand, Arguments: []any{ codeAction.Uuid, diff --git a/infrastructure/oss/oss_test.go b/infrastructure/oss/oss_test.go index 0c4fd4cab..88e5a76c5 100644 --- a/infrastructure/oss/oss_test.go +++ b/infrastructure/oss/oss_test.go @@ -112,146 +112,55 @@ func Test_toIssue_LearnParameterConversion(t *testing.T) { assert.Equal(t, "url", (issue.AdditionalData).(snyk.OssIssueData).Lesson) } -//nolint:dupl // test cases differ by package name -func Test_toIssue_CodeActions_WithNPMFix(t *testing.T) { - config.CurrentConfig().SetSnykOSSQuickFixCodeActionsEnabled(true) - - sampleOssIssue := sampleIssue() - scanner := CLIScanner{ - learnService: getLearnMock(t), - } - sampleOssIssue.UpgradePath = []any{"false", "pkg@v2"} - - issue := toIssue("testPath", sampleOssIssue, &scanResult{}, snyk.Range{Start: snyk.Position{Line: 1}}, scanner.learnService, scanner.errorReporter) - - assert.Equal(t, sampleOssIssue.Id, issue.ID) - assert.Equal(t, 3, len(issue.CodeActions)) - assert.Equal(t, "Upgrade to \"pkg\": \"v2\" (Snyk)", issue.CodeActions[0].Title) - assert.Equal(t, "Open description of 'THOU SHALL NOT PASS affecting package pkg' in browser (Snyk)", - issue.CodeActions[1].Title) - assert.Equal(t, "Learn more about THOU SHALL NOT PASS (Snyk)", issue.CodeActions[2].Title) - assert.Equal(t, 1, len(issue.CodelensCommands)) - assert.Equal(t, "⚡ Fix this issue: Upgrade to \"pkg\": \"v2\" (Snyk)", issue.CodelensCommands[0].Title) -} - -//nolint:dupl // test cases differ by package name -func Test_toIssue_CodeActions_WithScopedNPMFix(t *testing.T) { - config.CurrentConfig().SetSnykOSSQuickFixCodeActionsEnabled(true) - - sampleOssIssue := sampleIssue() - scanner := CLIScanner{ - learnService: getLearnMock(t), - } - sampleOssIssue.UpgradePath = []any{"false", "@org/pkg@v2"} - - issue := toIssue("testPath", sampleOssIssue, &scanResult{}, snyk.Range{Start: snyk.Position{Line: 1}}, scanner.learnService, scanner.errorReporter) - - assert.Equal(t, sampleOssIssue.Id, issue.ID) - assert.Equal(t, 3, len(issue.CodeActions)) - assert.Equal(t, "Upgrade to \"@org/pkg\": \"v2\" (Snyk)", issue.CodeActions[0].Title) - assert.Equal(t, "Open description of 'THOU SHALL NOT PASS affecting package pkg' in browser (Snyk)", - issue.CodeActions[1].Title) - assert.Equal(t, "Learn more about THOU SHALL NOT PASS (Snyk)", issue.CodeActions[2].Title) - assert.Equal(t, 1, len(issue.CodelensCommands)) - assert.Equal(t, "⚡ Fix this issue: Upgrade to \"@org/pkg\": \"v2\" (Snyk)", issue.CodelensCommands[0].Title) -} - -func Test_toIssue_CodeActions_WithGomodFix(t *testing.T) { - config.CurrentConfig().SetSnykOSSQuickFixCodeActionsEnabled(true) - - sampleOssIssue := sampleIssue() - scanner := CLIScanner{ - learnService: getLearnMock(t), - } - sampleOssIssue.PackageManager = "gomodules" - sampleOssIssue.UpgradePath = []any{"false", "pkg@v2"} - - issue := toIssue("testPath", sampleOssIssue, &scanResult{}, nonEmptyRange(), scanner.learnService, scanner.errorReporter) - - assert.Equal(t, sampleOssIssue.Id, issue.ID) - assert.Equal(t, 3, len(issue.CodeActions)) - assert.Equal(t, "Upgrade to vv2 (Snyk)", issue.CodeActions[0].Title) - assert.Equal(t, 1, len(issue.CodelensCommands)) - assert.Equal(t, "⚡ Fix this issue: Upgrade to vv2 (Snyk)", issue.CodelensCommands[0].Title) -} - func nonEmptyRange() snyk.Range { return snyk.Range{Start: snyk.Position{Line: 1}} } -func Test_toIssue_CodeActions_WithMavenFix(t *testing.T) { - config.CurrentConfig().SetSnykOSSQuickFixCodeActionsEnabled(true) - - sampleOssIssue := sampleIssue() - scanner := CLIScanner{ - learnService: getLearnMock(t), +func Test_toIssue_CodeActions(t *testing.T) { + const flashy = "⚡️ " + tests := []struct { + name string + packageName string + packageManager string + expectedUpgrade string + openBrowserEnabled bool + }{ + {"WithNPMFix", "pkg@v2", "npm", "Upgrade to \"pkg\": \"v2\"", false}, + {"WithScopedNPMFix", "@org/pkg@v2", "npm", "Upgrade to \"@org/pkg\": \"v2\"", true}, + {"WithGomodFix", "pkg@v2", "gomodules", "Upgrade to vv2", true}, + {"WithMavenFix", "pkg@v2", "maven", "Upgrade to v2", true}, + {"WithMavenFixForBuildGradle", "a:pkg@v2", "maven", "Upgrade to v2", true}, + {"WithGradleFix", "a:pkg@v2", "gradle", "Upgrade to pkg:v2", true}, } - sampleOssIssue.PackageManager = "maven" - sampleOssIssue.UpgradePath = []any{"false", "pkg@v2"} - - issue := toIssue("testPath", sampleOssIssue, &scanResult{}, nonEmptyRange(), scanner.learnService, scanner.errorReporter) - - assert.Equal(t, sampleOssIssue.Id, issue.ID) - assert.Equal(t, 3, len(issue.CodeActions)) - assert.Equal(t, "Upgrade to v2 (Snyk)", issue.CodeActions[0].Title) - assert.Equal(t, 1, len(issue.CodelensCommands)) - assert.Equal(t, "⚡ Fix this issue: Upgrade to v2 (Snyk)", issue.CodelensCommands[0].Title) -} - -func Test_toIssue_CodeActions_WithMavenFixForBuildGradle(t *testing.T) { - config.CurrentConfig().SetSnykOSSQuickFixCodeActionsEnabled(true) - - sampleOssIssue := sampleIssue() - scanner := CLIScanner{ - learnService: getLearnMock(t), + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + config.CurrentConfig().SetSnykOSSQuickFixCodeActionsEnabled(true) + config.CurrentConfig().SetSnykOpenBrowserActionsEnabled(test.openBrowserEnabled) + + sampleOssIssue := sampleIssue() + scanner := CLIScanner{ + learnService: getLearnMock(t), + } + sampleOssIssue.PackageManager = test.packageManager + sampleOssIssue.UpgradePath = []any{"false", test.packageName} + + issue := toIssue("testPath", sampleOssIssue, &scanResult{}, snyk.Range{Start: snyk.Position{Line: 1}}, scanner.learnService, scanner.errorReporter) + + assert.Equal(t, sampleOssIssue.Id, issue.ID) + assert.Equal(t, flashy+test.expectedUpgrade, issue.CodeActions[0].Title) + assert.Equal(t, 1, len(issue.CodelensCommands)) + assert.Equal(t, flashy+test.expectedUpgrade, issue.CodelensCommands[0].Title) + + if test.openBrowserEnabled { + assert.Equal(t, 3, len(issue.CodeActions)) + assert.Equal(t, "Open description of 'THOU SHALL NOT PASS affecting package pkg' in browser (Snyk)", issue.CodeActions[1].Title) + assert.Equal(t, "Learn more about THOU SHALL NOT PASS (Snyk)", issue.CodeActions[2].Title) + } else { + assert.Equal(t, 2, len(issue.CodeActions)) + assert.Equal(t, "Learn more about THOU SHALL NOT PASS (Snyk)", issue.CodeActions[1].Title) + } + }) } - sampleOssIssue.PackageManager = "maven" - sampleOssIssue.UpgradePath = []any{"false", "a:pkg@v2"} - - issue := toIssue("testPath", sampleOssIssue, &scanResult{}, nonEmptyRange(), scanner.learnService, scanner.errorReporter) - - assert.Equal(t, sampleOssIssue.Id, issue.ID) - assert.Equal(t, 3, len(issue.CodeActions)) - assert.Equal(t, "Upgrade to v2 (Snyk)", issue.CodeActions[0].Title) - assert.Equal(t, 1, len(issue.CodelensCommands)) - assert.Equal(t, "⚡ Fix this issue: Upgrade to v2 (Snyk)", issue.CodelensCommands[0].Title) - - // TODO: remove once https://snyksec.atlassian.net/browse/OSM-1775 is fixed - issue = toIssue("build.gradle", sampleOssIssue, &scanResult{}, nonEmptyRange(), scanner.learnService, scanner.errorReporter) - - assert.Equal(t, sampleOssIssue.Id, issue.ID) - assert.Equal(t, 3, len(issue.CodeActions)) - assert.Equal(t, "Upgrade to pkg:v2 (Snyk)", issue.CodeActions[0].Title) - assert.Equal(t, 1, len(issue.CodelensCommands)) - assert.Equal(t, "⚡ Fix this issue: Upgrade to pkg:v2 (Snyk)", issue.CodelensCommands[0].Title) - - // TODO: remove once https://snyksec.atlassian.net/browse/OSM-1775 is fixed - issue = toIssue("build.gradle.kts", sampleOssIssue, &scanResult{}, nonEmptyRange(), scanner.learnService, scanner.errorReporter) - - assert.Equal(t, sampleOssIssue.Id, issue.ID) - assert.Equal(t, 3, len(issue.CodeActions)) - assert.Equal(t, "Upgrade to pkg:v2 (Snyk)", issue.CodeActions[0].Title) - assert.Equal(t, 1, len(issue.CodelensCommands)) - assert.Equal(t, "⚡ Fix this issue: Upgrade to pkg:v2 (Snyk)", issue.CodelensCommands[0].Title) -} - -func Test_toIssue_CodeActions_WithGradleFix(t *testing.T) { - config.CurrentConfig().SetSnykOSSQuickFixCodeActionsEnabled(true) - - sampleOssIssue := sampleIssue() - scanner := CLIScanner{ - learnService: getLearnMock(t), - } - sampleOssIssue.PackageManager = "gradle" - sampleOssIssue.UpgradePath = []any{"false", "a:pkg@v2"} - - issue := toIssue("testPath", sampleOssIssue, &scanResult{}, nonEmptyRange(), scanner.learnService, scanner.errorReporter) - - assert.Equal(t, sampleOssIssue.Id, issue.ID) - assert.Equal(t, 3, len(issue.CodeActions)) - assert.Equal(t, "Upgrade to pkg:v2 (Snyk)", issue.CodeActions[0].Title) - assert.Equal(t, 1, len(issue.CodelensCommands)) - assert.Equal(t, "⚡ Fix this issue: Upgrade to pkg:v2 (Snyk)", issue.CodelensCommands[0].Title) } func Test_toIssue_CodeActions_WithoutFix(t *testing.T) { diff --git a/internal/types/lsp.go b/internal/types/lsp.go index 63cc4ac77..b82e98e52 100644 --- a/internal/types/lsp.go +++ b/internal/types/lsp.go @@ -556,6 +556,7 @@ type Settings struct { SnykCodeApi string `json:"snykCodeApi,omitempty"` EnableSnykLearnCodeActions string `json:"enableSnykLearnCodeActions,omitempty"` EnableSnykOSSQuickFixCodeActions string `json:"enableSnykOSSQuickFixCodeActions,omitempty"` + EnableSnykOpenBrowserActions string `json:"enableSnykOpenBrowserActions,omitempty"` EnableDeltaFindings string `json:"enableDeltaFindings,omitempty"` // should this be global? RequiredProtocolVersion string `json:"requiredProtocolVersion,omitempty"` // global settings end