Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 57 additions & 16 deletions domain/ide/workspace/folder.go
Original file line number Diff line number Diff line change
Expand Up @@ -636,48 +636,89 @@ func (f *Folder) FilterIssues(
issues = getIssuePerFileFromFlatList(deltaForAllProducts)
}

codeConsistentIgnoresEnabled := f.featureFlagService.GetFromFolderConfig(f.path, featureflag.SnykCodeConsistentIgnores)
folderConfig := f.c.FolderConfig(f.path)

for path, issueSlice := range issues {
if !f.Contains(path) {
logger.Error().Msg("issue found in cache that does not pertain to folder")
logger.Error().Msg("issues found in cache that do not pertain to folder")
continue
}
for _, issue := range issueSlice {
// Logging here will spam the logs
if isVisibleSeverity(f.c, issue) && (!codeConsistentIgnoresEnabled || isVisibleForIssueViewOptions(f.c, issue)) && supportedIssueTypes[issue.GetFilterableIssueType()] {
if f.isIssueVisible(issue, supportedIssueTypes, folderConfig) {
filteredIssues[path] = append(filteredIssues[path], issue)
}
}
}
return filteredIssues
}

func isVisibleSeverity(c *config.Config, issue types.Issue) bool {
logger := c.Logger().With().Str("method", "isVisibleSeverity").Logger()

filterSeverity := c.FilterSeverity()
logger.Debug().Interface("filterSeverity", filterSeverity).Msg("Filtering issues by severity")
func (f *Folder) isIssueVisible(issue types.Issue, supportedIssueTypes map[product.FilterableIssueType]bool, folderConfig *types.FolderConfig) bool {
if !supportedIssueTypes[issue.GetFilterableIssueType()] {
return false
}
if !f.isVisibleSeverity(issue) {
return false
}
riskScoreInCLIEnabled := folderConfig.FeatureFlags[featureflag.UseExperimentalRiskScoreInCLI]
if riskScoreInCLIEnabled && !f.isVisibleRiskScore(issue, folderConfig.RiskScoreThreshold) {
return false
}
codeConsistentIgnoresEnabled := folderConfig.FeatureFlags[featureflag.SnykCodeConsistentIgnores]
if codeConsistentIgnoresEnabled && !f.isVisibleForIssueViewOptions(issue) {
return false
}
return true
}

func (f *Folder) isVisibleSeverity(issue types.Issue) bool {
switch issue.GetSeverity() {
case types.Critical:
return c.FilterSeverity().Critical
return f.c.FilterSeverity().Critical
case types.High:
return c.FilterSeverity().High
return f.c.FilterSeverity().High
case types.Medium:
return c.FilterSeverity().Medium
return f.c.FilterSeverity().Medium
case types.Low:
return c.FilterSeverity().Low
return f.c.FilterSeverity().Low
}
return false
}

func isVisibleForIssueViewOptions(c *config.Config, issue types.Issue) bool {
logger := c.Logger().With().Str("method", "isVisibleForIssueViewOptions").Logger()
func (f *Folder) isVisibleRiskScore(issue types.Issue, riskScoreThreshold int) bool {
if riskScoreThreshold == 0 {
// Showing all issues because threshold is 0
return true
} else if riskScoreThreshold < 0 {
// Invalid negative risk score threshold. Showing all issues.
return true
} else if riskScoreThreshold > 1000 {
// Invalid high risk score threshold. Showing no issues.
return false
}

// Get risk score from issue's additional data
additionalData := issue.GetAdditionalData()
ossIssueData, ok := additionalData.(snyk.OssIssueData)
if !ok {
// If it's not an OSS issue, don't filter by risk score
return true
}

issueRiskScore := ossIssueData.RiskScore

issueViewOptions := c.IssueViewOptions()
logger.Debug().Interface("issueViewOptions", issueViewOptions).Msg("Filtering issues by issue view options")
// If issue has no risk score (0 means not set for legacy scans), show all issues
// This handles legacy scans that don't provide risk scores if somehow we got here with the risk score feature flag enabled
if issueRiskScore == 0 {
return true
}

// Issue is visible if its risk score meets or exceeds the filter threshold
return issueRiskScore >= uint16(riskScoreThreshold)
}

func (f *Folder) isVisibleForIssueViewOptions(issue types.Issue) bool {
issueViewOptions := f.c.IssueViewOptions()
if issue.GetIsIgnored() {
return issueViewOptions.IgnoredIssues
} else {
Expand Down
126 changes: 119 additions & 7 deletions domain/ide/workspace/folder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,11 +260,18 @@ func TestProcessResults_whenFilteringIssueViewOptions_ProcessesOnlyFilteredIssue
issueViewOptions := types.NewIssueViewOptions(false, true)
c.SetIssueViewOptions(&issueViewOptions)

fakeFeatureFlagService := featureflag.NewFakeService()
fakeFeatureFlagService.Flags[featureflag.SnykCodeConsistentIgnores] = true
folderPath := types.FilePath("dummy")
folderConfig := &types.FolderConfig{
FolderPath: folderPath,
FeatureFlags: map[string]bool{
featureflag.SnykCodeConsistentIgnores: true,
},
}
err := storedconfig.UpdateFolderConfig(c.Engine().GetConfiguration(), folderConfig, c.Logger())
require.NoError(t, err)

notifier := notification.NewNotifier()
f := NewFolder(c, "dummy", "dummy", scanner.NewTestScanner(), hover.NewFakeHoverService(), scanner.NewMockScanNotifier(), notifier, persistence.NewNopScanPersister(), scanstates.NewNoopStateAggregator(), fakeFeatureFlagService)
f := NewFolder(c, folderPath, "dummy", scanner.NewTestScanner(), hover.NewFakeHoverService(), scanner.NewMockScanNotifier(), notifier, persistence.NewNopScanPersister(), scanstates.NewNoopStateAggregator(), featureflag.NewFakeService())

path1 := types.FilePath(filepath.Join(string(f.path), "path1"))
data := types.ScanData{
Expand Down Expand Up @@ -467,6 +474,15 @@ func Test_FilterCachedDiagnostics_filtersIgnoredIssues(t *testing.T) {
// arrange
filePath, folderPath := types.FilePath("test/path"), types.FilePath("test")

folderConfig := &types.FolderConfig{
FolderPath: folderPath,
FeatureFlags: map[string]bool{
featureflag.SnykCodeConsistentIgnores: true,
},
}
err := storedconfig.UpdateFolderConfig(c.Engine().GetConfiguration(), folderConfig, c.Logger())
require.NoError(t, err)

openIssue1 := &snyk.Issue{
AffectedFilePath: filePath,
IsIgnored: false,
Expand Down Expand Up @@ -501,10 +517,7 @@ func Test_FilterCachedDiagnostics_filtersIgnoredIssues(t *testing.T) {
ignoredIssue2,
}

fakeFeatureFlagService := featureflag.NewFakeService()
fakeFeatureFlagService.Flags[featureflag.SnykCodeConsistentIgnores] = true

f := NewFolder(c, folderPath, "Test", scannerRecorder, hover.NewFakeHoverService(), scanner.NewMockScanNotifier(), notification.NewMockNotifier(), persistence.NewNopScanPersister(), scanstates.NewNoopStateAggregator(), fakeFeatureFlagService)
f := NewFolder(c, folderPath, "Test", scannerRecorder, hover.NewFakeHoverService(), scanner.NewMockScanNotifier(), notification.NewMockNotifier(), persistence.NewNopScanPersister(), scanstates.NewNoopStateAggregator(), featureflag.NewFakeService())
ctx := t.Context()

c.SetIssueViewOptions(util.Ptr(types.NewIssueViewOptions(true, false)))
Expand All @@ -519,6 +532,105 @@ func Test_FilterCachedDiagnostics_filtersIgnoredIssues(t *testing.T) {
assert.Contains(t, filteredDiagnostics[filePath], openIssue2)
}

func Test_FilterIssues_RiskScoreThreshold(t *testing.T) {
// Shared setup for all subtests
c := testutil.UnitTest(t)

folderPath := types.FilePath(t.TempDir())
engineConfig := c.Engine().GetConfiguration()
logger := c.Logger()

// Create minimal folder for testing FilterIssues
sc := scanner.NewTestScanner()
folder := NewFolder(c, folderPath, "test-folder", sc, hover.NewFakeHoverService(), scanner.NewMockScanNotifier(), notification.NewMockNotifier(), persistence.NewNopScanPersister(), scanstates.NewNoopStateAggregator(), featureflag.NewFakeService())

filePath := types.FilePath(filepath.Join(string(folderPath), "test.go"))

// Create issues with different risk scores
issue1 := &snyk.Issue{
ID: "issue-1",
AffectedFilePath: filePath,
Severity: types.High,
Product: product.ProductOpenSource,
AdditionalData: snyk.OssIssueData{
Key: "issue-1-key",
RiskScore: 300,
},
}

issue2 := &snyk.Issue{
ID: "issue-2",
AffectedFilePath: filePath,
Severity: types.High,
Product: product.ProductOpenSource,
AdditionalData: snyk.OssIssueData{
Key: "issue-2-key",
RiskScore: 500,
},
}

issue3 := &snyk.Issue{
ID: "issue-3",
AffectedFilePath: filePath,
Severity: types.High,
Product: product.ProductOpenSource,
AdditionalData: snyk.OssIssueData{
Key: "issue-3-key",
RiskScore: 600,
},
}

supportedIssueTypes := map[product.FilterableIssueType]bool{
product.FilterableIssueTypeOpenSource: true,
}

issuesByFile := snyk.IssuesByFile{
filePath: {issue1, issue2, issue3},
}

t.Run("shows all issues when threshold is zero", func(t *testing.T) {
// Set folder config with risk score threshold set to 0 (0 = show all, valid range: 0-1000)
folderConfig := &types.FolderConfig{
FolderPath: folderPath,
RiskScoreThreshold: 0,
FeatureFlags: map[string]bool{
featureflag.UseExperimentalRiskScoreInCLI: true, // The one we actually use.
// featureflag.UseExperimentalRiskScore: true, // Not used in the prod filtering logic.
},
}
err := storedconfig.UpdateFolderConfig(engineConfig, folderConfig, logger)
require.NoError(t, err)

// Verify all issues are visible when threshold is 0
filteredIssues := folder.FilterIssues(issuesByFile, supportedIssueTypes)
require.Contains(t, filteredIssues, filePath)
assert.ElementsMatch(t, filteredIssues[filePath], []types.Issue{issue1, issue2, issue3}, "All issues should be visible when threshold is 0")
})

t.Run("filters issues by threshold", func(t *testing.T) {
// Set folder config with risk score threshold of 400 (valid range: 0-1000)
folderConfig := &types.FolderConfig{
FolderPath: folderPath,
RiskScoreThreshold: 400,
FeatureFlags: map[string]bool{
featureflag.UseExperimentalRiskScoreInCLI: true, // The one we actually use.
// featureflag.UseExperimentalRiskScore: true, // Not used in the prod filtering logic.
},
}
err := storedconfig.UpdateFolderConfig(engineConfig, folderConfig, logger)
require.NoError(t, err)

// Verify filtering works correctly with threshold of 400
filteredIssues := folder.FilterIssues(issuesByFile, supportedIssueTypes)
require.Contains(t, filteredIssues, filePath)

assert.Len(t, filteredIssues[filePath], 2, "Only issues with risk score >= 400 should be visible")
assert.NotContains(t, filteredIssues[filePath], issue1, "Issue 1 (risk score 300) should be filtered out")
assert.Contains(t, filteredIssues[filePath], issue2, "Issue 2 (risk score 500) should be visible")
assert.Contains(t, filteredIssues[filePath], issue3, "Issue 3 (risk score 600) should be visible")
})
}

func Test_ClearDiagnosticsByIssueType(t *testing.T) {
// Arrange
c := testutil.UnitTest(t)
Expand Down
3 changes: 3 additions & 0 deletions internal/types/lsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,8 @@ type FolderConfig struct {
OrgSetByUser bool `json:"orgSetByUser"`
FeatureFlags map[string]bool `json:"featureFlags"`
SastSettings *sast_contract.SastResponse `json:"sastSettings"`
// RiskScoreThreshold filters OSS issues by their risk score. Only issues with score >= threshold are shown. Valid range is 0-1000.
RiskScoreThreshold int `json:"riskScoreThreshold"`
}

func (fc *FolderConfig) Clone() *FolderConfig {
Expand All @@ -579,6 +581,7 @@ func (fc *FolderConfig) Clone() *FolderConfig {
OrgMigratedFromGlobalConfig: fc.OrgMigratedFromGlobalConfig,
OrgSetByUser: fc.OrgSetByUser,
SastSettings: fc.SastSettings,
RiskScoreThreshold: fc.RiskScoreThreshold,
}

if fc.LocalBranches != nil {
Expand Down
Loading