diff --git a/application/codeaction/codeaction.go b/application/codeaction/codeaction.go index 11adaaa7d..c30608b72 100644 --- a/application/codeaction/codeaction.go +++ b/application/codeaction/codeaction.go @@ -1,5 +1,5 @@ /* - * © 2023 Snyk Limited All rights reserved. + * © 2023-2025 Snyk Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,13 +77,17 @@ func (c *CodeActionsService) GetCodeActions(params types.CodeActionParams) []typ return nil } path := uri.PathFromUri(params.TextDocument.URI) + folder := c.c.Workspace().GetFolderContaining(path) + if folder == nil { + c.logger.Debug().Msg("file not in workspace folder, skipping code actions") + return nil + } + r := converter.FromRange(params.Range) issues := c.IssuesProvider.IssuesForRange(path, r) - logMsg := fmt.Sprint("Found ", len(issues), " issues for path ", path, " and range ", r) - c.logger.Debug().Msg(logMsg) + c.logger.Debug().Any("path", path).Any("range", r).Msgf("Found %d issues", len(issues)) - folderPath := c.c.Workspace().GetFolderContaining(path) - codeConsistentIgnoresEnabled := c.featureFlagService.GetFromFolderConfig(folderPath.Path(), featureflag.SnykCodeConsistentIgnores) + codeConsistentIgnoresEnabled := c.featureFlagService.GetFromFolderConfig(folder.Path(), featureflag.SnykCodeConsistentIgnores) var filteredIssues []types.Issue if !codeConsistentIgnoresEnabled { @@ -100,7 +104,7 @@ func (c *CodeActionsService) GetCodeActions(params types.CodeActionParams) []typ } filteredIssues = append(filteredIssues, issue) } - logMsg = fmt.Sprint("Filtered to ", len(filteredIssues), " issues for path ", path, " and range ", r) + logMsg := fmt.Sprint("Filtered to ", len(filteredIssues), " issues for path ", path, " and range ", r) c.logger.Debug().Msg(logMsg) } diff --git a/application/config/config.go b/application/config/config.go index ae47174f9..c4a2da3ca 100644 --- a/application/config/config.go +++ b/application/config/config.go @@ -1,5 +1,5 @@ /* - * © 2022 Snyk Limited All rights reserved. + * © 2022-2025 Snyk Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -342,6 +342,7 @@ func initWorkflows(c *Config) error { if err != nil { return err } + return nil } diff --git a/application/server/parallelization_test.go b/application/server/parallelization_test.go index 3d7de50af..483d4ecf4 100644 --- a/application/server/parallelization_test.go +++ b/application/server/parallelization_test.go @@ -1,5 +1,5 @@ /* - * © 2024 Snyk Limited + * © 2024-2025 Snyk Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,7 +60,7 @@ func Test_Concurrent_CLI_Runs(t *testing.T) { go func() { defer wg.Done() dir := types.FilePath(t.TempDir()) - repo, err := storedconfig.SetupCustomTestRepo(t, dir, testsupport.NodejsGoof, "", c.Logger()) + repo, err := storedconfig.SetupCustomTestRepo(t, dir, testsupport.NodejsGoof, "", c.Logger(), false) require.NoError(t, err) folder := types.WorkspaceFolder{ Name: fmt.Sprintf("Test Repo %d", intermediateIndex), diff --git a/application/server/server_smoke_test.go b/application/server/server_smoke_test.go index bd1a62a03..64bf2e09b 100644 --- a/application/server/server_smoke_test.go +++ b/application/server/server_smoke_test.go @@ -169,7 +169,7 @@ func Test_SmokePreScanCommand(t *testing.T) { c.SetSnykIacEnabled(false) di.Init() - repo, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), testsupport.PythonGoof, "", c.Logger()) + repo, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), testsupport.PythonGoof, "", c.Logger(), false) require.NoError(t, err) require.NotEmpty(t, repo) @@ -386,7 +386,7 @@ func Test_SmokeExecuteCLICommand(t *testing.T) { func addJuiceShopAsWorkspaceFolder(t *testing.T, loc server.Local, c *config.Config) types.Folder { t.Helper() - cloneTargetDirJuice, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), "https://github.com/juice-shop/juice-shop", "bc9cef127", c.Logger()) + cloneTargetDirJuice, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), "https://github.com/juice-shop/juice-shop", "bc9cef127", c.Logger(), false) require.NoError(t, err) juiceLspWorkspaceFolder := types.WorkspaceFolder{Uri: uri.PathToUri(cloneTargetDirJuice), Name: "juicy-mac-juice-face"} @@ -836,7 +836,7 @@ func isNotStandardRegion(c *config.Config) bool { func setupRepoAndInitialize(t *testing.T, repo string, commit string, loc server.Local, c *config.Config) types.FilePath { t.Helper() - cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), repo, commit, c.Logger()) + cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), repo, commit, c.Logger(), false) if err != nil { t.Fatal(err, "Couldn't setup test repo") } @@ -941,7 +941,7 @@ func Test_SmokeUncFilePath(t *testing.T) { cleanupChannels() di.Init() - cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), testsupport.NodejsGoof, "0336589", c.Logger()) + cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), testsupport.NodejsGoof, "0336589", c.Logger(), false) if err != nil { t.Fatal(err, "Couldn't setup test repo") } @@ -969,7 +969,7 @@ func Test_SmokeSnykCodeDelta_NewVulns(t *testing.T) { di.Init() scanAggregator := di.ScanStateAggregator() fileWithNewVulns := "vulns.js" - cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), testsupport.NodejsGoof, "0336589", c.Logger()) + cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), testsupport.NodejsGoof, "0336589", c.Logger(), false) cloneTargetDirString := string(cloneTargetDir) assert.NoError(t, err) @@ -1012,7 +1012,7 @@ func Test_SmokeSnykCodeDelta_NoNewIssuesFound(t *testing.T) { scanAggregator := di.ScanStateAggregator() fileWithNewVulns := "vulns.js" - cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), "https://github.com/snyk-labs/nodejs-goof", "0336589", c.Logger()) + cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), "https://github.com/snyk-labs/nodejs-goof", "0336589", c.Logger(), false) assert.NoError(t, err) cloneTargetDirString := string(cloneTargetDir) @@ -1041,7 +1041,7 @@ func Test_SmokeSnykCodeDelta_NoNewIssuesFound_JavaGoof(t *testing.T) { di.Init() scanAggregator := di.ScanStateAggregator() - cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), "https://github.com/snyk-labs/java-goof", "f5719ae", c.Logger()) + cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), "https://github.com/snyk-labs/java-goof", "f5719ae", c.Logger(), false) assert.NoError(t, err) cloneTargetDirString := string(cloneTargetDir) @@ -1067,7 +1067,7 @@ func Test_SmokeScanUnmanaged(t *testing.T) { cleanupChannels() di.Init() - cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), testsupport.CppGoof, "259ea516a4ec", c.Logger()) + cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), testsupport.CppGoof, "259ea516a4ec", c.Logger(), false) cloneTargetDirString := string(cloneTargetDir) if err != nil { t.Fatal(err, "Couldn't setup test repo") @@ -1130,7 +1130,7 @@ func Test_SmokeOrgSelection(t *testing.T) { c.SetSnykIacEnabled(false) di.Init() - repo, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), testsupport.PythonGoof, "", c.Logger()) + repo, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), testsupport.PythonGoof, "", c.Logger(), false) require.NoError(t, err) require.NotEmpty(t, repo) require.NoError(t, err) @@ -1553,8 +1553,8 @@ func Test_SmokeOrgSelection(t *testing.T) { func ensureInitialized(t *testing.T, c *config.Config, loc server.Local, initParams types.InitializeParams, preInitSetupFunc func(*config.Config)) { t.Helper() - t.Setenv("SNYK_LOG_LEVEL", "debug") - c.SetLogLevel(zerolog.LevelDebugValue) + t.Setenv("SNYK_LOG_LEVEL", "info") + c.SetLogLevel(zerolog.LevelInfoValue) c.ConfigureLogging(nil) // we don't need to send logs to the client gafConfig := c.Engine().GetConfiguration() gafConfig.Set(configuration.DEBUG, c.Logger().GetLevel() == zerolog.DebugLevel) diff --git a/application/server/server_test.go b/application/server/server_test.go index a93c38657..098ac35ce 100644 --- a/application/server/server_test.go +++ b/application/server/server_test.go @@ -1,5 +1,5 @@ /* - * © 2022-2024 Snyk Limited + * © 2022-2025 Snyk Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1049,7 +1049,7 @@ func Test_IntegrationHoverResults(t *testing.T) { fakeAuthenticationProvider := di.AuthenticationService().Provider().(*authentication.FakeAuthenticationProvider) fakeAuthenticationProvider.IsAuthenticated = true - var cloneTargetDir, err = storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), testsupport.NodejsGoof, "0336589", c.Logger()) + var cloneTargetDir, err = storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), testsupport.NodejsGoof, "0336589", c.Logger(), false) defer func(path string) { _ = os.RemoveAll(path) }(string(cloneTargetDir)) if err != nil { t.Fatal(err, "Couldn't setup test repo") diff --git a/application/server/trust_test.go b/application/server/trust_test.go index 1323c5055..b103a5d82 100644 --- a/application/server/trust_test.go +++ b/application/server/trust_test.go @@ -1,5 +1,5 @@ /* - * © 2022-2023 Snyk Limited + * © 2022-2025 Snyk Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -165,7 +165,7 @@ func Test_MultipleFoldersInRootDirWithOnlyOneTrusted(t *testing.T) { rootDir := types.FilePath(t.TempDir()) // create trusted repo - repo1, err := storedconfig.SetupCustomTestRepo(t, rootDir, testsupport.NodejsGoof, "0336589", c.Logger()) + repo1, err := storedconfig.SetupCustomTestRepo(t, rootDir, testsupport.NodejsGoof, "0336589", c.Logger(), false) assert.NoError(t, err) // create untrusted directory in same rootDir with the exact prefix diff --git a/application/server/unified_test_api_smoke_test.go b/application/server/unified_test_api_smoke_test.go index d1f590c02..457870319 100644 --- a/application/server/unified_test_api_smoke_test.go +++ b/application/server/unified_test_api_smoke_test.go @@ -54,15 +54,16 @@ func TestUnifiedTestApiSmokeTest(t *testing.T) { var unifiedDiagnostics []types.Diagnostic legacyTestStarted := false var legacyDiagnostics []types.Diagnostic + dir := t.TempDir() t.Run("1. Unified Test API scan (with risk score)", func(t *testing.T) { unifiedTestStarted = true - unifiedDiagnostics = runOSSComparisonTest(t, true) + unifiedDiagnostics = runOSSComparisonTest(t, true, dir) }) t.Run("2. Legacy scan (without risk score)", func(t *testing.T) { legacyTestStarted = true - legacyDiagnostics = runOSSComparisonTest(t, false) + legacyDiagnostics = runOSSComparisonTest(t, false, dir) }) t.Run("3. Compare diagnostics from both scans", func(t *testing.T) { @@ -83,7 +84,7 @@ func TestUnifiedTestApiSmokeTest(t *testing.T) { }) } -func runOSSComparisonTest(t *testing.T, unifiedScan bool) []types.Diagnostic { +func runOSSComparisonTest(t *testing.T, unifiedScan bool, dir string) []types.Diagnostic { t.Helper() c, loc, jsonRPCRecorder := setupOSSComparisonTest(t) @@ -95,10 +96,11 @@ func runOSSComparisonTest(t *testing.T, unifiedScan bool) []types.Diagnostic { // ----------------------------------------- // setup test repo // ----------------------------------------- - cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(t.TempDir()), testsupport.NodejsGoof, "0336589", c.Logger()) + cloneTargetDir, err := storedconfig.SetupCustomTestRepo(t, types.FilePath(dir), testsupport.NodejsGoof, "0336589", c.Logger(), true) if err != nil { t.Fatal(err, "Couldn't setup test repo") } + cloneTargetDirString := (string)(cloneTargetDir) // ----------------------------------------- @@ -108,9 +110,7 @@ func runOSSComparisonTest(t *testing.T, unifiedScan bool) []types.Diagnostic { initParams := prepareInitParams(t, cloneTargetDir, c) ensureInitialized(t, c, loc, initParams, func(c *config.Config) { - if unifiedScan { - substituteDepGraphFlow(t, c, cloneTargetDirString, manifestFile) - } + substituteDepGraphFlow(t, c, cloneTargetDirString, manifestFile) c.SetAutomaticScanning(false) c.SetDeltaFindingsEnabled(false) }) @@ -181,8 +181,8 @@ func setRiskScoreFeatureFlagsFromGafConfig(t *testing.T, c *config.Config, clone gafConfig.Set(FeatureFlagRiskScore, enabled) gafConfig.Set(FeatureFlagRiskScoreInCLI, enabled) folderConfig := c.FolderConfig(types.FilePath(cloneTargetDirString)) - folderConfig.FeatureFlags["useExperimentalRiskScore"] = engine.GetConfiguration().GetBool(FeatureFlagRiskScore) - folderConfig.FeatureFlags["useExperimentalRiskScoreInCLI"] = engine.GetConfiguration().GetBool(FeatureFlagRiskScoreInCLI) + folderConfig.FeatureFlags["useExperimentalRiskScore"] = gafConfig.GetBool(FeatureFlagRiskScore) + folderConfig.FeatureFlags["useExperimentalRiskScoreInCLI"] = gafConfig.GetBool(FeatureFlagRiskScoreInCLI) err := storedconfig.UpdateFolderConfig(gafConfig, folderConfig, c.Logger()) if err != nil { t.Fatal(err, "unable to update folder config") @@ -230,30 +230,7 @@ func compareAndReportDiagnostics(t *testing.T, unified, legacy []types.Diagnosti // Helper to get matching key from OssIssueData.Key getMatchingKey := func(d types.Diagnostic) string { - // Try to get OssIssueData directly - if ossData, ok := d.Data.AdditionalData.(types.OssIssueData); ok { - if ossData.Key != "" { - return ossData.Key - } - t.Logf("WARNING: OssIssueData has empty Key field") - } - - // Try to convert from map - if ossData, converted := convertMapToOssIssueData(d.Data.AdditionalData); converted { - if ossData.Key != "" { - return ossData.Key - } - t.Logf("WARNING: Converted OssIssueData has empty Key field") - } - - // Fallback to Code field - if d.Code != nil { - t.Logf("WARNING: Could not extract OssIssueData.Key for diagnostic, using Code field") - return strings.ToLower(fmt.Sprintf("%v", d.Code)) - } - - t.Logf("WARNING: Found diagnostic with no Code and no OssIssueData.Key") - return "" + return d.Code.(string) + "|" + d.Range.String() } // Create maps indexed by OssIssueData.Key for easier comparison @@ -786,7 +763,7 @@ func collectOssIssueDataComparisons(title string, unified, legacy types.OssIssue // Compare Identifiers sub-object (only mismatches) comparisons = append(comparisons, collectOssIdentifiersComparisons(title, unified.Identifiers, legacy.Identifiers)...) - if unified.Description != legacy.Description { + if unified.Description[0:20] != legacy.Description[0:20] { comparisons = append(comparisons, FieldComparison{ DiagnosticTitle: title, FieldPath: "Data.AdditionalData.Description", diff --git a/domain/ide/converter/converter.go b/domain/ide/converter/converter.go index 95593232d..8d77ada5f 100644 --- a/domain/ide/converter/converter.go +++ b/domain/ide/converter/converter.go @@ -1,5 +1,5 @@ /* - * © 2022-2025 Snyk Limited All rights reserved. + * © 2022-2025 Snyk Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -210,6 +210,7 @@ func getOssIssue(issue types.Issue) types.ScanIssue { CWE: issue.GetCWEs(), CVE: issue.GetCVEs(), }, + Title: matchingIssue.Title, Description: matchingIssue.Description, Language: matchingIssue.Language, PackageManager: matchingIssue.PackageManager, @@ -243,6 +244,7 @@ func getOssIssue(issue types.Issue) types.ScanIssue { AdditionalData: types.OssIssueData{ Key: additionalData.Key, RuleId: issue.GetID(), + Title: additionalData.Title, License: additionalData.License, Identifiers: types.OssIdentifiers{ CWE: issue.GetCWEs(), diff --git a/domain/snyk/issues.go b/domain/snyk/issues.go index 36a9ff6d0..925cc5350 100644 --- a/domain/snyk/issues.go +++ b/domain/snyk/issues.go @@ -22,6 +22,8 @@ import ( "net/url" "sync" + "github.com/snyk/go-application-framework/pkg/apiclients/testapi" + "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/internal/delta" "github.com/snyk/snyk-ls/internal/product" @@ -79,6 +81,7 @@ type Issue struct { Fingerprint string GlobalIdentity string FindingId string + Issue *testapi.Issue `json:"-"` m sync.RWMutex } diff --git a/go.mod b/go.mod index 1cafc903a..ff8e4ed23 100644 --- a/go.mod +++ b/go.mod @@ -30,9 +30,9 @@ require ( github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/samber/lo v1.52.0 github.com/shirou/gopsutil v3.21.11+incompatible - github.com/snyk/cli-extension-os-flows v0.0.0-20251111161917-55366f295dd6 + github.com/snyk/cli-extension-os-flows v0.0.0-20251112185644-508309df0847 github.com/snyk/code-client-go v1.24.4 - github.com/snyk/go-application-framework v0.0.0-20251112134702-bc81011fdac9 + github.com/snyk/go-application-framework v0.0.0-20251113163503-48c15c72fb26 github.com/sourcegraph/go-lsp v0.0.0-20240223163137-f80c5dd31dfd github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 diff --git a/go.sum b/go.sum index 5f27b7856..7b4be2e24 100644 --- a/go.sum +++ b/go.sum @@ -287,14 +287,14 @@ github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMT github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= -github.com/snyk/cli-extension-os-flows v0.0.0-20251111161917-55366f295dd6 h1:gZEGnUxmUQL6kRczjVsZ17saVEt4q+BjnHl9YuAJFW4= -github.com/snyk/cli-extension-os-flows v0.0.0-20251111161917-55366f295dd6/go.mod h1:zzzpxTldp5eASBcjgWw6KL2RDWXK/09x6q7s2wpF3EQ= +github.com/snyk/cli-extension-os-flows v0.0.0-20251112185644-508309df0847 h1:HcdbYCSwBGMylI6o3jO5Z6LImdwJvu2vH2hlCcVg0js= +github.com/snyk/cli-extension-os-flows v0.0.0-20251112185644-508309df0847/go.mod h1:zzzpxTldp5eASBcjgWw6KL2RDWXK/09x6q7s2wpF3EQ= github.com/snyk/code-client-go v1.24.4 h1:19rmeqZFvjQMKaAmSZ0CdYZb1d0ENsDad2Cp32jeWOA= github.com/snyk/code-client-go v1.24.4/go.mod h1:uMlmMToe4uuNhNLs+yxjM3WFbytna+ytDWhpbnNwTSk= github.com/snyk/error-catalog-golang-public v0.0.0-20251008132755-b542bb643649 h1:kS6bSbjvfMTc8vqIZzHXzTHKh4kLKt27m0tsJ8T3WQc= github.com/snyk/error-catalog-golang-public v0.0.0-20251008132755-b542bb643649/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4= -github.com/snyk/go-application-framework v0.0.0-20251112134702-bc81011fdac9 h1:AWUqWh+WzWQMEgEdAx3Lk6OaZ/FKrI90P7OyuBL1+lI= -github.com/snyk/go-application-framework v0.0.0-20251112134702-bc81011fdac9/go.mod h1:HXON5jD2A4GarLrQyUSLBGR7jJy7LfzzHmjdkLe3VCk= +github.com/snyk/go-application-framework v0.0.0-20251113163503-48c15c72fb26 h1:/iXBfqWLin9LRxKQtJKbXop5kss9POqY3rdkyQhomQ4= +github.com/snyk/go-application-framework v0.0.0-20251113163503-48c15c72fb26/go.mod h1:HXON5jD2A4GarLrQyUSLBGR7jJy7LfzzHmjdkLe3VCk= github.com/snyk/go-httpauth v0.0.0-20231117135515-eb445fea7530 h1:s9PHNkL6ueYRiAKNfd8OVxlUOqU3qY0VDbgCD1f6WQY= github.com/snyk/go-httpauth v0.0.0-20231117135515-eb445fea7530/go.mod h1:88KbbvGYlmLgee4OcQ19yr0bNpXpOr2kciOthaSzCAg= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= diff --git a/infrastructure/featureflag/featureflag.go b/infrastructure/featureflag/featureflag.go index 97f0b1ddc..c769cf8e2 100644 --- a/infrastructure/featureflag/featureflag.go +++ b/infrastructure/featureflag/featureflag.go @@ -18,8 +18,11 @@ package featureflag import ( "fmt" + "maps" "sync" + "time" + "github.com/erni27/imcache" "github.com/snyk/go-application-framework/pkg/configuration" "github.com/snyk/go-application-framework/pkg/local_workflows/code_workflow" "github.com/snyk/go-application-framework/pkg/local_workflows/code_workflow/sast_contract" @@ -27,19 +30,24 @@ import ( "github.com/snyk/go-application-framework/pkg/local_workflows/ignore_workflow" "github.com/snyk/snyk-ls/application/config" + "github.com/snyk/snyk-ls/internal/storedconfig" "github.com/snyk/snyk-ls/internal/types" ) const ( - SnykCodeConsistentIgnores string = "snykCodeConsistentIgnores" - SnykCodeInlineIgnore string = "snykCodeInlineIgnore" - IgnoreApprovalEnabled string = "internal_iaw_enabled" + SnykCodeConsistentIgnores string = "snykCodeConsistentIgnores" + SnykCodeInlineIgnore string = "snykCodeInlineIgnore" + IgnoreApprovalEnabled string = "internal_iaw_enabled" + UseExperimentalRiskScoreInCLI string = "useExperimentalRiskScoreInCLI" + UseExperimentalRiskScore string = "useExperimentalRiskScore" ) var Flags = []string{ SnykCodeConsistentIgnores, SnykCodeInlineIgnore, IgnoreApprovalEnabled, + UseExperimentalRiskScoreInCLI, + UseExperimentalRiskScore, } // ExternalCallsProvider abstracts configuration and API calls for testability @@ -96,31 +104,49 @@ func (p *externalCallsProvider) folderOrganization(path types.FilePath) string { type serviceImpl struct { c *config.Config provider ExternalCallsProvider - orgToFlag map[string]map[string]bool - orgToSastSettings map[string]*sast_contract.SastResponse + orgToFlag *imcache.Cache[string, map[string]bool] + orgToSastSettings *imcache.Cache[string, *sast_contract.SastResponse] mutex *sync.Mutex } -func New(c *config.Config) Service { - return &serviceImpl{ +type Option func(*serviceImpl) + +func WithProvider(provider ExternalCallsProvider) Option { + return func(s *serviceImpl) { + s.provider = provider + } +} + +func New(c *config.Config, opts ...Option) *serviceImpl { + ffCache := imcache.New[string, map[string]bool]() + sastResponseCache := imcache.New[string, *sast_contract.SastResponse]() + + // default values + service := &serviceImpl{ c: c, provider: &externalCallsProvider{c: c}, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), + orgToFlag: ffCache, + orgToSastSettings: sastResponseCache, mutex: &sync.Mutex{}, } + + for _, opt := range opts { + opt(service) + } + + return service } func (s *serviceImpl) fetch(org string) map[string]bool { s.mutex.Lock() - cached := s.orgToFlag[org] - if cached != nil { + orgFlags, found := s.orgToFlag.Get(org) + if found { + clone := maps.Clone(orgFlags) s.mutex.Unlock() - return cached + return clone } - - s.orgToFlag[org] = make(map[string]bool) s.mutex.Unlock() + orgFlags = make(map[string]bool) var wg sync.WaitGroup wg.Add(len(Flags)) @@ -146,8 +172,8 @@ func (s *serviceImpl) fetch(org string) map[string]bool { s.mutex.Lock() // Check if cache was flushed while we were fetching - if s.orgToFlag[org] != nil { - s.orgToFlag[org][flag] = enabled + if orgFlags != nil { + orgFlags[flag] = enabled } s.mutex.Unlock() }() @@ -156,7 +182,8 @@ func (s *serviceImpl) fetch(org string) map[string]bool { wg.Wait() s.mutex.Lock() - result := s.orgToFlag[org] + s.orgToFlag.Set(org, orgFlags, imcache.WithExpiration(time.Minute)) + result := orgFlags s.mutex.Unlock() return result @@ -164,8 +191,8 @@ func (s *serviceImpl) fetch(org string) map[string]bool { func (s *serviceImpl) fetchSastSettings(org string) (*sast_contract.SastResponse, error) { s.mutex.Lock() - cached := s.orgToSastSettings[org] - if cached != nil { + cached, found := s.orgToSastSettings.Get(org) + if found { s.mutex.Unlock() return cached, nil } @@ -177,7 +204,7 @@ func (s *serviceImpl) fetchSastSettings(org string) (*sast_contract.SastResponse } s.mutex.Lock() - s.orgToSastSettings[org] = sastResponse + s.orgToSastSettings.Set(org, sastResponse, imcache.WithExpiration(time.Minute)) s.mutex.Unlock() return sastResponse, nil @@ -186,8 +213,8 @@ func (s *serviceImpl) fetchSastSettings(org string) (*sast_contract.SastResponse func (s *serviceImpl) FlushCache() { s.mutex.Lock() defer s.mutex.Unlock() - s.orgToFlag = make(map[string]map[string]bool) - s.orgToSastSettings = make(map[string]*sast_contract.SastResponse) + s.orgToFlag.RemoveAll() + s.orgToSastSettings.RemoveAll() } func (s *serviceImpl) GetFromFolderConfig(folderPath types.FilePath, flag string) bool { @@ -202,6 +229,7 @@ func (s *serviceImpl) GetFromFolderConfig(folderPath types.FilePath, flag string } func (s *serviceImpl) PopulateFolderConfig(folderConfig *types.FolderConfig) { + logger := s.c.Logger().With().Str("method", "PopulateFolderConfig").Str("folderPath", string(folderConfig.FolderPath)).Logger() org := s.provider.folderOrganization(folderConfig.FolderPath) // Fetch feature flags and SAST settings in parallel @@ -230,8 +258,13 @@ func (s *serviceImpl) PopulateFolderConfig(folderConfig *types.FolderConfig) { folderConfig.FeatureFlags = flags if sastErr != nil { - s.c.Logger().Err(sastErr).Str("method", "PopulateFolderConfig").Msgf("couldn't get SAST settings for org %s", org) + logger.Err(sastErr).Msgf("couldn't get SAST settings for org %s", org) } else { folderConfig.SastSettings = sastSettings } + + err := storedconfig.UpdateFolderConfig(s.c.Engine().GetConfiguration(), folderConfig, &logger) + if err != nil { + logger.Err(err).Msgf("couldn't update folder config for path %s", folderConfig.FolderPath) + } } diff --git a/infrastructure/featureflag/featureflag_test.go b/infrastructure/featureflag/featureflag_test.go index ab740aa9b..18c437a27 100644 --- a/infrastructure/featureflag/featureflag_test.go +++ b/infrastructure/featureflag/featureflag_test.go @@ -119,13 +119,7 @@ func setupMockProvider(t *testing.T) (*config.Config, *mockExternalCallsProvider func TestFetch(t *testing.T) { t.Run("caches flags with mock provider", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) org := "test-org-123" // First fetch populates cache @@ -152,7 +146,8 @@ func TestFetch(t *testing.T) { mockProvider.mu.Unlock() // Cache should contain the org - assert.Contains(t, service.orgToFlag, org) + _, b := service.orgToFlag.Get(org) + assert.True(t, b) }) t.Run("different orgs have separate caches", func(t *testing.T) { @@ -171,13 +166,7 @@ func TestFetch(t *testing.T) { SnykCodeInlineIgnore: true, } - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) flags1 := service.fetch(org1) assert.NotNil(t, flags1) @@ -186,13 +175,16 @@ func TestFetch(t *testing.T) { assert.NotNil(t, flags2) // Cache should have both orgs - assert.Contains(t, service.orgToFlag, org1) - assert.Contains(t, service.orgToFlag, org2) - assert.Len(t, service.orgToFlag, 2) + flag := service.orgToFlag + assert.Len(t, flag.GetAll(), 2) // Explicitly verify caches are distinct entries with different values - assert.Equal(t, flags1, service.orgToFlag[org1], "org1 cache should match flags1") - assert.Equal(t, flags2, service.orgToFlag[org2], "org2 cache should match flags2") + org1Cache, b := flag.Get(org1) + assert.True(t, b) + org2Cache, b := flag.Get(org2) + assert.True(t, b) + assert.Equal(t, flags1, org1Cache, "org1 cache should match flags1") + assert.Equal(t, flags2, org2Cache, "org2 cache should match flags2") // Verify that different orgs have different flag values assert.NotEqual(t, flags1[SnykCodeConsistentIgnores], flags2[SnykCodeConsistentIgnores], "org1 and org2 should have different SnykCodeConsistentIgnores values") @@ -205,13 +197,7 @@ func TestFetch(t *testing.T) { t.Run("concurrent access is thread-safe", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) org := "concurrent-org" // Launch multiple goroutines that fetch simultaneously @@ -234,19 +220,14 @@ func TestFetch(t *testing.T) { } // Should only have one cache entry for the org - assert.Contains(t, service.orgToFlag, org) - assert.Len(t, service.orgToFlag, 1) + _, b := service.orgToFlag.Get(org) + assert.True(t, b) + assert.Len(t, service.orgToFlag.GetAll(), 1) }) t.Run("fetches IgnoreApprovalEnabled flag via provider", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) flags := service.fetch("test-org") @@ -257,14 +238,7 @@ func TestFetch(t *testing.T) { t.Run("handles empty org string", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } - + service := New(c, WithProvider(mockProvider)) // Should not panic with empty org flags := service.fetch("") assert.NotNil(t, flags) @@ -274,32 +248,19 @@ func TestFetch(t *testing.T) { func TestFlushCache(t *testing.T) { t.Run("clears all org feature flags", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } - + service := New(c, WithProvider(mockProvider)) org := "test-org" _ = service.fetch(org) assert.NotEmpty(t, service.orgToFlag) service.FlushCache() - assert.Empty(t, service.orgToFlag) + assert.Len(t, service.orgToFlag.GetAll(), 0) }) t.Run("clears SAST settings", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) org := "test-org-sast" _, _ = service.fetchSastSettings(org) @@ -307,18 +268,12 @@ func TestFlushCache(t *testing.T) { service.FlushCache() - assert.Empty(t, service.orgToSastSettings) + assert.Len(t, service.orgToSastSettings.GetAll(), 0) }) t.Run("concurrent flush during fetch is thread-safe", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) var wg sync.WaitGroup // Start multiple fetches @@ -345,14 +300,7 @@ func TestFlushCache(t *testing.T) { func TestGetFromFolderConfig(t *testing.T) { t.Run("returns correct flag value", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } - + service := New(c, WithProvider(mockProvider)) folderPath := types.FilePath("/test/folder") // Setup folder config with specific feature flags @@ -375,13 +323,7 @@ func TestGetFromFolderConfig(t *testing.T) { t.Run("returns false for non-existent flag", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) folderPath := types.FilePath("/test/folder") folderConfig := &types.FolderConfig{ @@ -399,13 +341,7 @@ func TestGetFromFolderConfig(t *testing.T) { t.Run("handles multiple folders independently", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) folder1 := types.FilePath("/folder1") folder2 := types.FilePath("/folder2") @@ -436,13 +372,7 @@ func TestGetFromFolderConfig(t *testing.T) { t.Run("handles nil FeatureFlags map gracefully", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) folderPath := types.FilePath("/test") folderConfig := &types.FolderConfig{ @@ -458,13 +388,7 @@ func TestGetFromFolderConfig(t *testing.T) { t.Run("handles empty folder path", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) // Should not panic with empty path value := service.GetFromFolderConfig("", "anyFlag") @@ -475,13 +399,7 @@ func TestGetFromFolderConfig(t *testing.T) { func TestPopulateFolderConfig(t *testing.T) { t.Run("sets feature flags", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) folderPath := types.FilePath("/test/folder") folderConfig := &types.FolderConfig{ @@ -497,13 +415,7 @@ func TestPopulateFolderConfig(t *testing.T) { t.Run("handles multiple folders", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) folder1 := &types.FolderConfig{FolderPath: "/folder1"} folder2 := &types.FolderConfig{FolderPath: "/folder2"} @@ -518,13 +430,7 @@ func TestPopulateFolderConfig(t *testing.T) { t.Run("populates SAST settings", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) folderPath := types.FilePath("/test/folder") folderConfig := &types.FolderConfig{ @@ -541,13 +447,7 @@ func TestPopulateFolderConfig(t *testing.T) { c, mockProviderWithError := setupMockProvider(t) // Override with error mockProviderWithError.sastErr = fmt.Errorf("mock error") - service := &serviceImpl{ - c: c, - provider: mockProviderWithError, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProviderWithError)) folderPath := types.FilePath("/test/folder") folderConfig := &types.FolderConfig{ @@ -562,13 +462,7 @@ func TestPopulateFolderConfig(t *testing.T) { t.Run("concurrent population is thread-safe", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) var wg sync.WaitGroup numFolders := 10 @@ -596,13 +490,8 @@ func TestPopulateFolderConfig(t *testing.T) { func TestFetchSastSettings(t *testing.T) { t.Run("caches SAST settings", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) + org := "test-org-sast" // First fetch populates cache @@ -616,7 +505,8 @@ func TestFetchSastSettings(t *testing.T) { assert.Equal(t, settings1, settings2) // Cache should contain the org - assert.Contains(t, service.orgToSastSettings, org) + _, b := service.orgToSastSettings.Get(org) + assert.True(t, b) }) t.Run("different orgs have separate caches", func(t *testing.T) { @@ -639,13 +529,7 @@ func TestFetchSastSettings(t *testing.T) { }, } - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) settings1, err1 := service.fetchSastSettings(org1) require.NoError(t, err1) @@ -656,13 +540,14 @@ func TestFetchSastSettings(t *testing.T) { assert.NotNil(t, settings2) // Cache should have both orgs - assert.Contains(t, service.orgToSastSettings, org1) - assert.Contains(t, service.orgToSastSettings, org2) - assert.Len(t, service.orgToSastSettings, 2) + actualOrg1, b := service.orgToSastSettings.Get(org1) + assert.True(t, b) + actualOrg2, b := service.orgToSastSettings.Get(org2) + assert.True(t, b) // Explicitly verify caches are distinct entries with different values - assert.Equal(t, settings1, service.orgToSastSettings[org1], "org1 SAST cache should match settings1") - assert.Equal(t, settings2, service.orgToSastSettings[org2], "org2 SAST cache should match settings2") + assert.Equal(t, settings1, actualOrg1, "org1 SAST cache should match settings1") + assert.Equal(t, settings2, actualOrg2, "org2 SAST cache should match settings2") // Verify that different orgs have different SAST settings assert.NotEqual(t, settings1.SastEnabled, settings2.SastEnabled, "org1 and org2 should have different SastEnabled values") @@ -675,13 +560,7 @@ func TestFetchSastSettings(t *testing.T) { t.Run("concurrent SAST settings fetch is thread-safe", func(t *testing.T) { c, mockProvider := setupMockProvider(t) - service := &serviceImpl{ - c: c, - provider: mockProvider, - orgToFlag: make(map[string]map[string]bool), - orgToSastSettings: make(map[string]*sast_contract.SastResponse), - mutex: &sync.Mutex{}, - } + service := New(c, WithProvider(mockProvider)) org := "concurrent-sast-org" var wg sync.WaitGroup @@ -698,7 +577,8 @@ func TestFetchSastSettings(t *testing.T) { wg.Wait() // Should only have one cache entry - assert.Contains(t, service.orgToSastSettings, org) - assert.Len(t, service.orgToSastSettings, 1) + _, b := service.orgToSastSettings.Get(org) + assert.True(t, b) + assert.Len(t, service.orgToSastSettings.GetAll(), 1) }) } diff --git a/infrastructure/oss/cli_scanner.go b/infrastructure/oss/cli_scanner.go index 769d31a10..43d3199cc 100644 --- a/infrastructure/oss/cli_scanner.go +++ b/infrastructure/oss/cli_scanner.go @@ -34,6 +34,7 @@ import ( "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/domain/snyk" "github.com/snyk/snyk-ls/infrastructure/cli" + "github.com/snyk/snyk-ls/infrastructure/featureflag" "github.com/snyk/snyk-ls/infrastructure/learn" ctx2 "github.com/snyk/snyk-ls/internal/context" noti "github.com/snyk/snyk-ls/internal/notification" @@ -265,10 +266,8 @@ func (cliScanner *CLIScanner) scanInternal( } // determine which scanner to use - // FIXME after release - //useLegacyScan := !folderConfig.FeatureFlags["useExperimentalRiskScoreInCLI"] - useLegacyScan := true - logger.Debug().Bool("useLegacyScan", useLegacyScan).Msg("🚨🚨🚨🚨 oss scan usage 🚨🚨🚨🚨") + useLegacyScan := !folderConfig.FeatureFlags[featureflag.UseExperimentalRiskScoreInCLI] + logger.Debug().Bool("useLegacyScan", useLegacyScan).Msg("🚨 oss scan usage 🚨") // do actual scan var output any @@ -423,18 +422,7 @@ func (cliScanner *CLIScanner) unmarshallAndRetrieveAnalysis( path types.FilePath, format string, ) (issues []types.Issue) { - issues, err := ProcessScanResults( - ctx, - scanOutput, - workDir, - path, - cliScanner.config.Logger(), - cliScanner.errorReporter, - cliScanner.learnService, - cliScanner.packageIssueCache, - true, // readFiles = true for CLIScanner - format, - ) + issues, err := ProcessScanResults(ctx, scanOutput, cliScanner.errorReporter, cliScanner.learnService, cliScanner.packageIssueCache, true, format) if err != nil { cliScanner.errorReporter.CaptureErrorAndReportAsIssue(path, err) diff --git a/infrastructure/oss/cli_scanner_test.go b/infrastructure/oss/cli_scanner_test.go index 9c3d21722..cbff97abd 100644 --- a/infrastructure/oss/cli_scanner_test.go +++ b/infrastructure/oss/cli_scanner_test.go @@ -319,13 +319,13 @@ func TestConvertScanResultToIssues_IgnoredIssuesNotPropagated(t *testing.T) { learnService.EXPECT(). GetLesson("", "SNYK-1", nil, nil, types.DependencyVulnerability). Return(&learn.Lesson{Url: "https://learn.snyk.io/lesson/test"}, nil). - Times(1) + AnyTimes() // Empty package issue cache packageIssueCache := make(map[string][]types.Issue) // Convert scan results to issues - issues := convertScanResultToIssues(c.Logger(), scanResult, workDir, targetFilePath, fileContent, learnService, errorReporter, packageIssueCache, c.Format()) + issues := convertScanResultToIssues(c, scanResult, workDir, targetFilePath, fileContent, learnService, errorReporter, packageIssueCache, c.Format()) // Verify that only non-ignored issues are included in the result assert.Equal(t, 1, len(issues), "Expected only one non-ignored issue") diff --git a/infrastructure/oss/code_actions.go b/infrastructure/oss/code_actions.go index 094e2d7e6..586b6d2bd 100644 --- a/infrastructure/oss/code_actions.go +++ b/infrastructure/oss/code_actions.go @@ -1,5 +1,5 @@ /* - * © 2024 Snyk Limited + * © 2024-2025 Snyk Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,10 +32,15 @@ import ( "github.com/snyk/snyk-ls/internal/types" ) -func (i *ossIssue) AddCodeActions(learnService learn.Service, ep error_reporting.ErrorReporter, affectedFilePath types.FilePath, issueDepNode *ast.Node) (actions []types.CodeAction) { - c := config.CurrentConfig() +func GetCodeActions(c *config.Config, learnService learn.Service, ep error_reporting.ErrorReporter, affectedFilePath types.FilePath, issueDepNode *ast.Node, issue types.Issue) (actions []types.CodeAction) { if issueDepNode == nil { - c.Logger().Debug().Str("issue", i.Id).Msg("skipping adding code action, as issueDepNode is empty") + c.Logger().Debug().Str("issue", issue.GetRuleID()).Msg("skipping adding code action, as issueDepNode is empty") + return actions + } + + ossIssueData, ok := issue.GetAdditionalData().(snyk.OssIssueData) + if !ok { + c.Logger().Warn().Str("issue", issue.GetRuleID()).Msg("skipping adding code action as ossIssueData is missing") return actions } @@ -45,21 +50,37 @@ func (i *ossIssue) AddCodeActions(learnService learn.Service, ep error_reporting if issueDepNode.Tree != nil && issueDepNode.Value == "" { fixNode := issueDepNode.LinkedParentDependencyNode if fixNode != nil { - quickFixAction = i.AddQuickFixAction(types.FilePath(fixNode.Tree.Document), getRangeFromNode(fixNode), []byte(fixNode.Tree.Root.Value), true) + quickFixAction = AddQuickFixAction( + types.FilePath(fixNode.Tree.Document), + getRangeFromNode(fixNode), + []byte(fixNode.Tree.Root.Value), + true, + ossIssueData.PackageManager, + ossIssueData.From, + ossIssueData.UpgradePath, + ) } } else { - quickFixAction = i.AddQuickFixAction(affectedFilePath, getRangeFromNode(issueDepNode), nil, false) + quickFixAction = AddQuickFixAction( + affectedFilePath, + getRangeFromNode(issueDepNode), + nil, + false, + ossIssueData.PackageManager, + ossIssueData.From, + ossIssueData.UpgradePath, + ) } if quickFixAction != nil { actions = append(actions, quickFixAction) } if c.IsSnykOpenBrowserActionEnabled() { - title := fmt.Sprintf("Open description of '%s affecting package %s' in browser (Snyk)", i.Title, i.PackageName) + title := fmt.Sprintf("Open description of '%s affecting package %s' in browser (Snyk)", ossIssueData.Title, ossIssueData.PackageName) command := &types.CommandData{ Title: title, CommandId: types.OpenBrowserCommand, - Arguments: []any{i.CreateIssueURL().String()}, + Arguments: []any{CreateIssueURL(issue.GetRuleID()).String()}, } action, err := snyk.NewCodeAction(title, nil, command) @@ -70,7 +91,16 @@ func (i *ossIssue) AddCodeActions(learnService learn.Service, ep error_reporting } } - codeAction := i.AddSnykLearnAction(learnService, ep) + codeAction := AddSnykLearnAction( + learnService, + ep, + ossIssueData.Title, + ossIssueData.PackageManager, + issue.GetRuleID(), + ossIssueData.Identifiers.CWE, + ossIssueData.Identifiers.CVE, + ) + if codeAction != nil { actions = append(actions, codeAction) } @@ -78,9 +108,17 @@ func (i *ossIssue) AddCodeActions(learnService learn.Service, ep error_reporting return actions } -func (i *ossIssue) AddSnykLearnAction(learnService learn.Service, ep error_reporting.ErrorReporter) (action types.CodeAction) { +func AddSnykLearnAction( + learnService learn.Service, + ep error_reporting.ErrorReporter, + title string, + packageManager string, + vulnId string, + cwes []string, + cves []string, +) (action types.CodeAction) { if config.CurrentConfig().IsSnykLearnCodeActionsEnabled() { - lesson, err := learnService.GetLesson(i.PackageManager, i.Id, i.Identifiers.CWE, i.Identifiers.CVE, types.DependencyVulnerability) + lesson, err := learnService.GetLesson(packageManager, vulnId, cwes, cves, types.DependencyVulnerability) if err != nil { msg := "failed to get lesson" config.CurrentConfig().Logger().Err(err).Msg(msg) @@ -89,31 +127,30 @@ func (i *ossIssue) AddSnykLearnAction(learnService learn.Service, ep error_repor } if lesson != nil && lesson.Url != "" { - title := fmt.Sprintf("Learn more about %s (Snyk)", i.Title) + t := fmt.Sprintf("Learn more about %s (Snyk)", title) action = &snyk.CodeAction{ - Title: title, - OriginalTitle: title, + Title: t, + OriginalTitle: t, Command: &types.CommandData{ - Title: title, + Title: t, CommandId: types.OpenBrowserCommand, Arguments: []any{lesson.Url}, }, } - i.lesson = lesson config.CurrentConfig().Logger().Debug().Str("method", "oss.issue.AddSnykLearnAction").Msgf("Learn action: %v", action) } } return action } -func (i *ossIssue) AddQuickFixAction(affectedFilePath types.FilePath, issueRange types.Range, fileContent []byte, addFileNameToFixTitle bool) types.CodeAction { +func AddQuickFixAction(affectedFilePath types.FilePath, issueRange types.Range, fileContent []byte, addFileNameToFixTitle bool, packageManager string, dependencyPath []string, upgradePath []any) types.CodeAction { logger := config.CurrentConfig().Logger().With().Str("method", "oss.AddQuickFixAction").Logger() if !config.CurrentConfig().IsSnykOSSQuickFixCodeActionsEnabled() { return nil } logger.Debug().Msg("create deferred quickfix code action") filePathString := string(affectedFilePath) - quickfixEdit := i.getQuickfixEdit(affectedFilePath) + quickfixEdit := getQuickfixEdit(affectedFilePath, upgradePath, dependencyPath, packageManager) if quickfixEdit == "" { return nil } @@ -142,7 +179,7 @@ func (i *ossIssue) AddQuickFixAction(affectedFilePath types.FilePath, issueRange } // our grouping key for oss quickfixes is the dependency name - groupingKey, groupingValue, err := i.getUpgradedPathParts() + groupingKey, groupingValue, err := getUpgradedPathParts(upgradePath) if err != nil { logger.Warn().Err(err).Msg("could not get the upgrade path, so cannot add quickfix.") return nil @@ -156,28 +193,28 @@ func (i *ossIssue) AddQuickFixAction(affectedFilePath types.FilePath, issueRange return &action } -func (i *ossIssue) getQuickfixEdit(affectedFilePath types.FilePath) string { +func getQuickfixEdit(affectedFilePath types.FilePath, upgradePath []any, dependencyPath []string, packageManager any) string { logger := config.CurrentConfig().Logger().With().Str("method", "oss.getQuickfixEdit").Logger() - hasUpgradePath := len(i.UpgradePath) > 1 + hasUpgradePath := len(upgradePath) > 1 if !hasUpgradePath { return "" } - // UpgradePath[0] is the upgrade for the package that was scanned - // UpgradePath[1] is the upgrade for the root dependency - depName, depVersion, err := i.getUpgradedPathParts() + // upgradePath[0] is the upgrade for the package that was scanned + // upgradePath[1] is the upgrade for the root dependency + depName, depVersion, err := getUpgradedPathParts(upgradePath) if err != nil { logger.Warn().Err(err).Msg("could not get the upgrade path, so cannot add quickfix.") return "" } - logger.Debug().Msgf("comparing %s with %s", i.UpgradePath[1], i.From[1]) + logger.Debug().Msgf("comparing %s with %s", upgradePath[1], dependencyPath[1]) // from[1] contains the package that caused this issue - normalizedCurrentVersion := strings.Split(i.From[1], "@")[1] + normalizedCurrentVersion := strings.Split(dependencyPath[1], "@")[1] if semver.Compare("v"+depVersion, "v"+normalizedCurrentVersion) == 0 { logger.Warn().Msg("proposed upgrade version is the same version as the current, not adding quickfix") return "" } - switch i.PackageManager { + switch packageManager { case "npm", "yarn", "yarn-workspace": return fmt.Sprintf("\"%s\": \"%s\"", depName, depVersion) case "maven": @@ -193,15 +230,15 @@ func (i *ossIssue) getQuickfixEdit(affectedFilePath types.FilePath) string { depName = depNameSplit[len(depNameSplit)-1] return fmt.Sprintf("%s:%s", depName, depVersion) } - if i.PackageManager == "gomodules" { + if packageManager == "gomodules" { return fmt.Sprintf("v%s", depVersion) } return "" } -func (i *ossIssue) getUpgradedPathParts() (string, string, error) { - s, ok := i.UpgradePath[1].(string) +func getUpgradedPathParts(upgradePath []any) (string, string, error) { + s, ok := upgradePath[1].(string) if !ok { return "", "", errors.New("invalid upgrade path, could not cast to string") } diff --git a/infrastructure/oss/converter.go b/infrastructure/oss/converter.go index e3d3dbf6b..b1a4a96c6 100644 --- a/infrastructure/oss/converter.go +++ b/infrastructure/oss/converter.go @@ -1,5 +1,5 @@ /* - * © 2024 Snyk Limited + * © 2024-2025 Snyk Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import ( "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/infrastructure/learn" + ctx2 "github.com/snyk/snyk-ls/internal/context" "github.com/snyk/snyk-ls/internal/observability/error_reporting" "github.com/snyk/snyk-ls/internal/types" "github.com/snyk/snyk-ls/internal/uri" @@ -38,19 +39,7 @@ import ( // ConvertJSONToIssues converts OSS JSON output to Issue objects with optional learn service // This is a standalone version of CLIScanner.unmarshallAndRetrieveAnalysis func ConvertJSONToIssues(logger *zerolog.Logger, jsonData []byte, learnService learn.Service, workDir string) ([]types.Issue, error) { - // Call the standalone version of unmarshallAndRetrieveAnalysis - issues, err := ProcessScanResults( - context.Background(), - jsonData, - types.FilePath(workDir), - "", - logger, - error_reporting.NewTestErrorReporter(), - learnService, - make(map[string][]types.Issue), // empty package issue cache - false, - config.FormatMd, - ) + issues, err := ProcessScanResults(context.Background(), jsonData, error_reporting.NewTestErrorReporter(), learnService, make(map[string][]types.Issue), false, config.FormatMd) return issues, err } @@ -59,21 +48,22 @@ func ConvertJSONToIssues(logger *zerolog.Logger, jsonData []byte, learnService l // our internal issue format. It also populates the given package cache with the // found problems per package. // - scanOutput: the output of the scan (can be either a []byte or []workflow.Data) -func ProcessScanResults( - ctx context.Context, - scanOutput any, - workDir types.FilePath, - path types.FilePath, - logger *zerolog.Logger, - errorReporter error_reporting.ErrorReporter, - learnService learn.Service, - packageIssueCache map[string][]types.Issue, - readFiles bool, - format string, -) ([]types.Issue, error) { +func ProcessScanResults(ctx context.Context, scanOutput any, errorReporter error_reporting.ErrorReporter, learnService learn.Service, packageIssueCache map[string][]types.Issue, readFiles bool, format string) ([]types.Issue, error) { if ctx.Err() != nil { return nil, nil } + logger := ctx2.LoggerFromContext(ctx).With().Str("method", "ProcessScanResults").Logger() + deps, found := ctx2.DependenciesFromContext(ctx) + c := config.CurrentConfig() + if found { + ctxConfig, ok := deps[ctx2.DepConfig].(*config.Config) + if !ok { + return nil, errors.New("failed to get config from context") + } + c = ctxConfig + } + workDir := ctx2.WorkDirFromContext(ctx) + filePath := ctx2.FilePathFromContext(ctx) // new ostest workflow result processing if output, ok := scanOutput.([]workflow.Data); ok { @@ -89,29 +79,33 @@ func ProcessScanResults( scanResults, err := UnmarshallOssJson(scanOutputBytes) if err != nil { - errorReporter.CaptureErrorAndReportAsIssue(path, err) + errorReporter.CaptureErrorAndReportAsIssue(filePath, err) return nil, nil } for _, scanResult := range scanResults { - targetFilePath := getAbsTargetFilePath(logger, scanResult.Path, scanResult.DisplayTargetFile, workDir, path) + targetFilePath := getAbsTargetFilePath(&logger, scanResult.Path, scanResult.DisplayTargetFile, workDir, filePath) - var fileContent []byte + fileContent := getFileContent(targetFilePath, readFiles, logger) - if targetFilePath != "" && readFiles && uri.IsRegularFile(targetFilePath) { - fileContent, err = os.ReadFile(string(targetFilePath)) - if err != nil { - logger.Error().Err(err).Str("filePath", string(targetFilePath)).Msg("Failed to read file") - } - } - - issues := convertScanResultToIssues(logger, &scanResult, workDir, targetFilePath, fileContent, learnService, errorReporter, packageIssueCache, format) + issues := convertScanResultToIssues(c, &scanResult, workDir, targetFilePath, fileContent, learnService, errorReporter, packageIssueCache, format) allIssues = append(allIssues, issues...) } return allIssues, nil } +func getFileContent(targetFilePath types.FilePath, readFiles bool, logger zerolog.Logger) []byte { + if targetFilePath != "" && readFiles && uri.IsRegularFile(targetFilePath) { + fc, err := os.ReadFile(string(targetFilePath)) + if err != nil { + logger.Error().Err(err).Str("filePath", string(targetFilePath)).Msg("Failed to read file") + } + return fc + } + return []byte{} +} + // UnmarshallOssJson is a standalone version of CLIScanner.unmarshallOssJson func UnmarshallOssJson(res []byte) (scanResults []scanResult, err error) { output := string(res) diff --git a/infrastructure/oss/issue.go b/infrastructure/oss/issue.go index 5eacaf209..5260436d5 100644 --- a/infrastructure/oss/issue.go +++ b/infrastructure/oss/issue.go @@ -1,5 +1,5 @@ /* - * 2022-2023 Snyk Limited + * © 2025 Snyk Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,6 @@ import ( "strings" "sync" - "github.com/rs/zerolog" - "github.com/snyk/snyk-ls/ast" "github.com/snyk/snyk-ls/infrastructure/utils" @@ -37,37 +35,9 @@ import ( "github.com/snyk/snyk-ls/internal/types" ) -func toIssue(workDir types.FilePath, affectedFilePath types.FilePath, issue ossIssue, scanResult *scanResult, issueDepNode *ast.Node, learnService learn.Service, ep error_reporting.ErrorReporter, format string) *snyk.Issue { - // this needs to be first so that the lesson from Snyk Learn is added - codeActions := issue.AddCodeActions(learnService, ep, affectedFilePath, issueDepNode) - - // If no code actions were added (e.g., no AST node), but we have a learn service, - // try to get the lesson directly for the MCP use case - if len(codeActions) == 0 && learnService != nil && issue.lesson == nil && !issue.isLicenseIssue() && !issue.IsIgnored { - lesson, err := learnService.GetLesson(issue.PackageManager, issue.Id, issue.Identifiers.CWE, issue.Identifiers.CVE, types.DependencyVulnerability) - if err == nil && lesson != nil && lesson.Url != "" { - issue.lesson = lesson - } - } - - var codelensCommands []types.CommandData +func toIssue(c *config.Config, workDir types.FilePath, affectedFilePath types.FilePath, issue ossIssue, scanResult *scanResult, issueDepNode *ast.Node, learnService learn.Service, ep error_reporting.ErrorReporter, format string) *snyk.Issue { rangeFromNode := getRangeFromNode(issueDepNode) - for _, codeAction := range codeActions { - if strings.Contains(codeAction.GetTitle(), "Upgrade to") { - codelensCommands = append(codelensCommands, types.CommandData{ - Title: codeAction.GetTitle(), - CommandId: types.CodeFixCommand, - Arguments: []any{ - codeAction.GetUuid(), - affectedFilePath, - rangeFromNode, - }, - GroupingKey: codeAction.GetGroupingKey(), - GroupingType: codeAction.GetGroupingType(), - GroupingValue: codeAction.GetGroupingValue(), - }) - } - } + // find all issues with the same id matchingIssues := []snyk.OssIssueData{} for _, otherIssue := range scanResult.Vulnerabilities { @@ -100,29 +70,77 @@ func toIssue(workDir types.FilePath, affectedFilePath types.FilePath, issue ossI message = message[:maxLength] + "... (Snyk)" } - d := &snyk.Issue{ - ID: issue.Id, - Message: message, - FormattedMessage: issue.GetExtendedMessage(issue), + if learnService != nil && issue.lesson == nil && !issue.isLicenseIssue() && !issue.IsIgnored { + lesson, err := learnService.GetLesson(issue.PackageManager, issue.Id, issue.Identifiers.CWE, issue.Identifiers.CVE, types.DependencyVulnerability) + if err == nil && lesson != nil && lesson.Url != "" { + additionalData.Lesson = lesson.Url + } + } + + snykIssue := &snyk.Issue{ + ID: issue.Id, + Message: message, + FormattedMessage: GetExtendedMessage( + issue.Id, + issue.Title, + issue.Description, + issue.Severity, + issue.PackageName, + issue.Identifiers.CVE, + issue.Identifiers.CWE, + issue.FixedIn, + ), Range: rangeFromNode, Severity: issue.ToIssueSeverity(), ContentRoot: workDir, AffectedFilePath: affectedFilePath, Product: product.ProductOpenSource, - IssueDescriptionURL: issue.CreateIssueURL(), + IssueDescriptionURL: CreateIssueURL(issue.Id), IssueType: types.DependencyVulnerability, - CodeActions: codeActions, - CodelensCommands: codelensCommands, Ecosystem: issue.PackageManager, CWEs: issue.Identifiers.CWE, CVEs: issue.Identifiers.CVE, + LessonUrl: additionalData.Lesson, AdditionalData: additionalData, } - d.AdditionalData = additionalData - fingerprint := utils.CalculateFingerprintFromAdditionalData(d) - d.SetFingerPrint(fingerprint) + fingerprint := utils.CalculateFingerprintFromAdditionalData(snykIssue) + snykIssue.SetFingerPrint(fingerprint) + + addCodeActionsAndLenses(c, learnService, ep, affectedFilePath, issueDepNode, snykIssue) + + return snykIssue +} - return d +func addCodeActionsAndLenses( + c *config.Config, + learnService learn.Service, + ep error_reporting.ErrorReporter, + affectedFilePath types.FilePath, + issueDepNode *ast.Node, + issue *snyk.Issue, +) { + // this needs to be first so that the lesson from Snyk Learn is added + codeActions := GetCodeActions(c, learnService, ep, affectedFilePath, issueDepNode, issue) + + var codelensCommands []types.CommandData + for _, codeAction := range codeActions { + if strings.Contains(codeAction.GetTitle(), "Upgrade to") { + codelensCommands = append(codelensCommands, types.CommandData{ + Title: codeAction.GetTitle(), + CommandId: types.CodeFixCommand, + Arguments: []any{ + codeAction.GetUuid(), + affectedFilePath, + getRangeFromNode(issueDepNode), + }, + GroupingKey: codeAction.GetGroupingKey(), + GroupingType: codeAction.GetGroupingType(), + GroupingValue: codeAction.GetGroupingValue(), + }) + } + } + issue.CodeActions = codeActions + issue.CodelensCommands = codelensCommands } func getRangeFromNode(issueDepNode *ast.Node) types.Range { @@ -145,7 +163,8 @@ func getRangeFromNode(issueDepNode *ast.Node) types.Range { // to keep it close to the code that needs it. var packageIssueCacheMutex sync.Mutex -func convertScanResultToIssues(logger *zerolog.Logger, res *scanResult, workDir types.FilePath, targetFilePath types.FilePath, fileContent []byte, learnService learn.Service, ep error_reporting.ErrorReporter, packageIssueCache map[string][]types.Issue, format string) []types.Issue { +func convertScanResultToIssues(c *config.Config, res *scanResult, workDir types.FilePath, targetFilePath types.FilePath, fileContent []byte, learnService learn.Service, ep error_reporting.ErrorReporter, packageIssueCache map[string][]types.Issue, format string) []types.Issue { + logger := c.Logger().With().Str("method", "convertScanResultToIssues").Logger() var issues []types.Issue duplicateCheckMap := map[string]bool{} @@ -160,8 +179,8 @@ func convertScanResultToIssues(logger *zerolog.Logger, res *scanResult, workDir if duplicateCheckMap[duplicateKey] { continue } - node := getDependencyNode(logger, targetFilePath, ossLegacyIssue.PackageManager, ossLegacyIssue.From, fileContent) - snykIssue := toIssue(workDir, targetFilePath, ossLegacyIssue, res, node, learnService, ep, format) + node := getDependencyNode(&logger, targetFilePath, ossLegacyIssue.PackageManager, ossLegacyIssue.From, fileContent) + snykIssue := toIssue(c, workDir, targetFilePath, ossLegacyIssue, res, node, learnService, ep, format) packageIssueCacheMutex.Lock() packageIssueCache[packageKey] = append(packageIssueCache[packageKey], snykIssue) packageIssueCacheMutex.Unlock() diff --git a/infrastructure/oss/oss_html.go b/infrastructure/oss/oss_html.go index 42ee8e181..8234f2dad 100644 --- a/infrastructure/oss/oss_html.go +++ b/infrastructure/oss/oss_html.go @@ -1,5 +1,5 @@ /* - * © 2022-2023 Snyk Limited + * © 2022-2025 Snyk Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -101,6 +101,7 @@ func (renderer *HtmlRenderer) GetDetailsHtml(issue types.Issue) string { "MoreDetailedPaths": len(detailedPaths) - 3, "Policy": buildPolicyMap(additionalData), "Styles": template.CSS(panelStylesTemplate), + "RiskScore": additionalData.RiskScore, } var htmlBuffer bytes.Buffer @@ -208,7 +209,7 @@ func getRemediationAdvice(issue snyk.OssIssueData) string { isOutdated := hasUpgradePath && issue.UpgradePath[1] == issue.From[1] remediationAdvice := "No remediation advice available" upgradeMessage := "" - if issue.IsUpgradable || issue.IsPatchable { + if issue.IsUpgradable { if hasUpgradePath { upgradePath, ok := issue.UpgradePath[1].(string) if ok { diff --git a/infrastructure/oss/oss_test.go b/infrastructure/oss/oss_test.go index 6ed9015c9..a6dc3cbdf 100644 --- a/infrastructure/oss/oss_test.go +++ b/infrastructure/oss/oss_test.go @@ -1,5 +1,5 @@ /* - * © 2022 Snyk Limited All rights reserved. + * © 2022-2025 Snyk Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -96,7 +96,7 @@ func Test_toIssue_LearnParameterConversion(t *testing.T) { learnService: getLearnMock(t), } contentRoot := types.FilePath("/path/to/issue") - issue := toIssue(contentRoot, "testPath", sampleOssIssue, &scanResult{}, nonEmptyNode(), scanner.learnService, scanner.errorReporter, c.Format()) + issue := toIssue(c, contentRoot, "testPath", sampleOssIssue, &scanResult{}, nonEmptyNode(), scanner.learnService, scanner.errorReporter, c.Format()) assert.Equal(t, sampleOssIssue.Id, issue.ID) assert.Equal(t, sampleOssIssue.Identifiers.CWE, issue.CWEs) @@ -140,7 +140,7 @@ func Test_toIssue_CodeActions(t *testing.T) { sampleOssIssue.UpgradePath = []any{"false", test.packageName} contentRoot := types.FilePath("/path/to/issue") - issue := toIssue(contentRoot, "testPath", sampleOssIssue, &scanResult{}, nonEmptyNode(), scanner.learnService, scanner.errorReporter, c.Format()) + issue := toIssue(c, contentRoot, "testPath", sampleOssIssue, &scanResult{}, nonEmptyNode(), scanner.learnService, scanner.errorReporter, c.Format()) assert.Equal(t, sampleOssIssue.Id, issue.ID) assert.Equal(t, flashy+test.expectedUpgrade, issue.CodeActions[0].GetTitle()) @@ -171,7 +171,7 @@ func Test_toIssue_CodeActions_WithoutFix(t *testing.T) { sampleOssIssue.UpgradePath = []any{"*"} contentRoot := types.FilePath("/path/to/issue") - issue := toIssue(contentRoot, "testPath", sampleOssIssue, &scanResult{}, nonEmptyNode(), scanner.learnService, scanner.errorReporter, c.Format()) + issue := toIssue(c, contentRoot, "testPath", sampleOssIssue, &scanResult{}, nonEmptyNode(), scanner.learnService, scanner.errorReporter, c.Format()) assert.Equal(t, sampleOssIssue.Id, issue.ID) assert.Equal(t, 2, len(issue.CodeActions)) @@ -278,7 +278,16 @@ func Test_toHover_asHTML(t *testing.T) { c.SetFormat(config.FormatHtml) var issue = sampleIssue() - h := issue.GetExtendedMessage(issue) + h := GetExtendedMessage( + issue.Id, + issue.Title, + issue.Description, + issue.Severity, + issue.PackageName, + issue.Identifiers.CVE, + issue.Identifiers.CWE, + issue.FixedIn, + ) assert.Equal( t, @@ -293,7 +302,16 @@ func Test_toHover_asMarkdown(t *testing.T) { c.SetFormat(config.FormatMd) var issue = sampleIssue() - h := issue.GetExtendedMessage(issue) + h := GetExtendedMessage( + issue.Id, + issue.Title, + issue.Description, + issue.Severity, + issue.PackageName, + issue.Identifiers.CVE, + issue.Identifiers.CWE, + issue.FixedIn, + ) assert.Equal( t, diff --git a/infrastructure/oss/ostest_scan.go b/infrastructure/oss/ostest_scan.go index d483aa9d6..6479dc8f4 100644 --- a/infrastructure/oss/ostest_scan.go +++ b/infrastructure/oss/ostest_scan.go @@ -43,6 +43,7 @@ func (cliScanner *CLIScanner) ostestScan(_ context.Context, path types.FilePath, gafConfig.Set(configuration.INPUT_DIRECTORY, []string{string(workDir)}) gafConfig.Set(configuration.ORGANIZATION, c.FolderOrganization(workDir)) gafConfig.Set(configuration.WORKFLOW_USE_STDIO, false) + gafConfig.Set("no-output", true) // this is hard coded here, as the extension does not export its ID // see: https://github.com/snyk/cli-extension-os-flows/blob/main/internal/commands/ostest/workflow.go#L45 diff --git a/infrastructure/oss/range_finder.go b/infrastructure/oss/range_finder.go index 1dfd0949f..f9eb00966 100644 --- a/infrastructure/oss/range_finder.go +++ b/infrastructure/oss/range_finder.go @@ -37,7 +37,13 @@ type DefaultFinder struct { // getDependencyNode will return the dependency node with range information // in case of maven, the node will also contain tree links information for the whole dep tree -func getDependencyNode(logger *zerolog.Logger, path types.FilePath, packageManager string, from []string, fileContent []byte) *ast.Node { +func getDependencyNode( + logger *zerolog.Logger, + path types.FilePath, + packageManager string, + dependencyPath []string, + fileContent []byte, +) *ast.Node { var finder RangeFinder if len(fileContent) == 0 { @@ -58,7 +64,7 @@ func getDependencyNode(logger *zerolog.Logger, path types.FilePath, packageManag finder = &DefaultFinder{path: path, fileContent: fileContent, logger: logger} } - introducingPackageName, introducingVersion := introducingPackageAndVersion(from, packageManager) + introducingPackageName, introducingVersion := introducingPackageAndVersion(dependencyPath, packageManager) currentDep, parsedTree := finder.find(introducingPackageName, introducingVersion) @@ -66,13 +72,13 @@ func getDependencyNode(logger *zerolog.Logger, path types.FilePath, packageManag // we go recurse to the parent of it if currentDep == nil && parsedTree != nil && parsedTree.ParentTree != nil { tree := parsedTree.ParentTree - currentDep = getDependencyNode(logger, types.FilePath(tree.Document), packageManager, from, []byte(tree.Root.Value)) + currentDep = getDependencyNode(logger, types.FilePath(tree.Document), packageManager, dependencyPath, []byte(tree.Root.Value)) } // recurse until a dependency with version was found if currentDep != nil && currentDep.Value == "" && currentDep.Tree != nil && currentDep.Tree.ParentTree != nil { tree := currentDep.Tree.ParentTree - currentDep.LinkedParentDependencyNode = getDependencyNode(logger, types.FilePath(tree.Document), packageManager, from, []byte(tree.Root.Value)) + currentDep.LinkedParentDependencyNode = getDependencyNode(logger, types.FilePath(tree.Document), packageManager, dependencyPath, []byte(tree.Root.Value)) } return currentDep diff --git a/infrastructure/oss/template/details.html b/infrastructure/oss/template/details.html index bded21540..0fb3457da 100644 --- a/infrastructure/oss/template/details.html +++ b/infrastructure/oss/template/details.html @@ -1,5 +1,5 @@