From 05ecc9d429b2ff5b2f84ed10248004ecc5ee0399 Mon Sep 17 00:00:00 2001 From: cx-leonardo-fontes <204389152+cx-leonardo-fontes@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:59:38 +0000 Subject: [PATCH 1/3] ignore findings with secret or line with whitespace-only and add context to the parameters of scan interface to allow cancel --- engine/detect/utils.go | 6 +- engine/detect/utils_test.go | 138 ++++++++++++++++++++++++++++++++++++ pkg/scan.go | 37 ++++++++-- pkg/scan_test.go | 39 +++++----- pkg/scanner.go | 8 ++- 5 files changed, 197 insertions(+), 31 deletions(-) create mode 100644 engine/detect/utils_test.go diff --git a/engine/detect/utils.go b/engine/detect/utils.go index 2de61cc7..f98412ba 100644 --- a/engine/detect/utils.go +++ b/engine/detect/utils.go @@ -135,13 +135,13 @@ func shannonEntropy(data string) (entropy float64) { return entropy } -// filter will dedupe, redact, and remove empty secret findings +// filter will dedupe, redact, and remove empty secret/line findings func filter(findings []report.Finding, redact uint) []report.Finding { var retFindings []report.Finding for i := range findings { f := &findings[i] - // Skip findings with empty secrets - if f.Secret == "" { + // Skip findings with empty/whitespace-only secrets or lines + if strings.TrimSpace(f.Secret) == "" || strings.TrimSpace(f.Line) == "" { continue } include := true diff --git a/engine/detect/utils_test.go b/engine/detect/utils_test.go new file mode 100644 index 00000000..36122b7e --- /dev/null +++ b/engine/detect/utils_test.go @@ -0,0 +1,138 @@ +package detect + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/zricethezav/gitleaks/v8/report" +) + +func TestFilter(t *testing.T) { + tests := []struct { + name string + findings []report.Finding + redact uint + expectedCount int + expectedSecret string // for single finding tests + }{ + { + name: "valid finding passes through", + findings: []report.Finding{ + {Secret: "my-secret", Line: "password=my-secret", RuleID: "test-rule"}, + }, + redact: 0, + expectedCount: 1, + expectedSecret: "my-secret", + }, + { + name: "empty secret is filtered out", + findings: []report.Finding{ + {Secret: "", Line: "some line content", RuleID: "test-rule"}, + }, + redact: 0, + expectedCount: 0, + }, + { + name: "empty line is filtered out", + findings: []report.Finding{ + {Secret: "my-secret", Line: "", RuleID: "test-rule"}, + }, + redact: 0, + expectedCount: 0, + }, + { + name: "whitespace-only secret is filtered out", + findings: []report.Finding{ + {Secret: " ", Line: "some line content", RuleID: "test-rule"}, + }, + redact: 0, + expectedCount: 0, + }, + { + name: "whitespace-only line is filtered out", + findings: []report.Finding{ + {Secret: "my-secret", Line: " ", RuleID: "test-rule"}, + }, + redact: 0, + expectedCount: 0, + }, + { + name: "newline-only line is filtered out", + findings: []report.Finding{ + {Secret: "my-secret", Line: "\n", RuleID: "test-rule"}, + }, + redact: 0, + expectedCount: 0, + }, + { + name: "carriage-return-only line is filtered out", + findings: []report.Finding{ + {Secret: "my-secret", Line: "\r", RuleID: "test-rule"}, + }, + redact: 0, + expectedCount: 0, + }, + { + name: "newline and carriage return line is filtered out", + findings: []report.Finding{ + {Secret: "my-secret", Line: "\r\n", RuleID: "test-rule"}, + }, + redact: 0, + expectedCount: 0, + }, + { + name: "tab-only line is filtered out", + findings: []report.Finding{ + {Secret: "my-secret", Line: "\t", RuleID: "test-rule"}, + }, + redact: 0, + expectedCount: 0, + }, + { + name: "mixed whitespace line is filtered out", + findings: []report.Finding{ + {Secret: "my-secret", Line: " \t\n\r ", RuleID: "test-rule"}, + }, + redact: 0, + expectedCount: 0, + }, + { + name: "newline-only secret is filtered out", + findings: []report.Finding{ + {Secret: "\n", Line: "some line content", RuleID: "test-rule"}, + }, + redact: 0, + expectedCount: 0, + }, + { + name: "mixed valid and invalid findings", + findings: []report.Finding{ + {Secret: "valid-secret", Line: "password=valid-secret", RuleID: "test-rule"}, + {Secret: "", Line: "some line", RuleID: "test-rule"}, + {Secret: "another-secret", Line: "", RuleID: "test-rule"}, + {Secret: " ", Line: "line", RuleID: "test-rule"}, + {Secret: "good-secret", Line: "api_key=good-secret", RuleID: "test-rule"}, + }, + redact: 0, + expectedCount: 2, + }, + { + name: "empty findings list", + findings: []report.Finding{}, + redact: 0, + expectedCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := filter(tt.findings, tt.redact) + assert.Equal(t, tt.expectedCount, len(result), "unexpected number of findings") + + if tt.expectedCount == 1 && tt.expectedSecret != "" { + assert.Equal(t, tt.expectedSecret, result[0].Secret, "unexpected secret value") + } + }) + } +} + diff --git a/pkg/scan.go b/pkg/scan.go index ab35914e..e9b41269 100644 --- a/pkg/scan.go +++ b/pkg/scan.go @@ -1,6 +1,7 @@ package scanner import ( + "context" "errors" "fmt" "sync" @@ -56,7 +57,7 @@ func (s *scanner) Reset(scanConfig *ScanConfig, opts ...engine.EngineOption) err return nil } -func (s *scanner) Scan(scanItems []ScanItem, scanConfig *ScanConfig, opts ...engine.EngineOption) (reporting.IReport, error) { +func (s *scanner) Scan(ctx context.Context, scanItems []ScanItem, scanConfig *ScanConfig, opts ...engine.EngineOption) (reporting.IReport, error) { var wg conc.WaitGroup err := s.Reset(scanConfig, opts...) if err != nil { @@ -83,7 +84,11 @@ func (s *scanner) Scan(scanItems []ScanItem, scanConfig *ScanConfig, opts ...eng defer close(s.engineInstance.GetPluginChannels().GetItemsCh()) for _, item := range scanItems { - s.engineInstance.GetPluginChannels().GetItemsCh() <- item + select { + case <-ctx.Done(): + return + case s.engineInstance.GetPluginChannels().GetItemsCh() <- item: + } } }) @@ -95,6 +100,10 @@ func (s *scanner) Scan(scanItems []ScanItem, scanConfig *ScanConfig, opts ...eng close(s.engineInstance.GetErrorsCh()) + if ctx.Err() != nil { + return s.engineInstance.GetReport(), ctx.Err() + } + var errs []error for err = range bufferedErrors { errs = append(errs, err) @@ -107,6 +116,7 @@ func (s *scanner) Scan(scanItems []ScanItem, scanConfig *ScanConfig, opts ...eng } func (s *scanner) ScanDynamic( + ctx context.Context, itemsIn <-chan ScanItem, scanConfig *ScanConfig, opts ...engine.EngineOption, @@ -123,11 +133,22 @@ func (s *scanner) ScanDynamic( wg.Go(func() { defer close(channels.GetItemsCh()) - for item := range itemsIn { - channels.GetItemsCh() <- item + for { + select { + case <-ctx.Done(): + return + case item, ok := <-itemsIn: + if !ok { + log.Info().Msg("scan dynamic finished sending items to engine") + return + } + select { + case <-ctx.Done(): + return + case channels.GetItemsCh() <- item: + } + } } - - log.Info().Msg("scan dynamic finished sending items to engine") }) bufferedErrors := make(chan error, 2) @@ -148,6 +169,10 @@ func (s *scanner) ScanDynamic( close(s.engineInstance.GetErrorsCh()) + if ctx.Err() != nil { + return s.engineInstance.GetReport(), ctx.Err() + } + var errs []error for err = range bufferedErrors { errs = append(errs, err) diff --git a/pkg/scan_test.go b/pkg/scan_test.go index 83fad40d..5d0c39a7 100644 --- a/pkg/scan_test.go +++ b/pkg/scan_test.go @@ -1,6 +1,7 @@ package scanner import ( + "context" "encoding/json" "flag" "fmt" @@ -120,7 +121,7 @@ func TestScan(t *testing.T) { } testScanner := NewScanner() - actualReport, err := testScanner.Scan(scanItems, &ScanConfig{}) + actualReport, err := testScanner.Scan(context.Background(), scanItems, &ScanConfig{}) assert.NoError(t, err, "scanner encountered an error") // Use helper function to either update expected file or compare results @@ -157,7 +158,7 @@ func TestScan(t *testing.T) { } testScanner := NewScanner() - actualReport, err := testScanner.Scan(scanItems, &ScanConfig{ + actualReport, err := testScanner.Scan(context.Background(), scanItems, &ScanConfig{ IgnoreResultIds: []string{ "efc9a9ee89f1d732c7321067eb701b9656e91f15", "c31705d99e835e4ac7bc3f688bd9558309e056ed", @@ -217,7 +218,7 @@ func TestScan(t *testing.T) { } testScanner := NewScanner() - actualReport, err := testScanner.Scan(scanItems, &ScanConfig{ + actualReport, err := testScanner.Scan(context.Background(), scanItems, &ScanConfig{ IgnoreRules: []string{ "github-pat", }, @@ -270,7 +271,7 @@ func TestScan(t *testing.T) { errorsCh <- fmt.Errorf("mock processing error 1") errorsCh <- fmt.Errorf("mock processing error 2") }() - report, err := testScanner.Scan(scanItems, &ScanConfig{}, engine.WithPluginChannels(pluginChannels)) + report, err := testScanner.Scan(context.Background(), scanItems, &ScanConfig{}, engine.WithPluginChannels(pluginChannels)) assert.Equal(t, 0, report.GetTotalItemsScanned()) assert.Equal(t, 0, report.GetTotalSecretsFound()) @@ -281,7 +282,7 @@ func TestScan(t *testing.T) { }) t.Run("scan with scanItems empty", func(t *testing.T) { testScanner := NewScanner() - actualReport, err := testScanner.Scan([]ScanItem{}, &ScanConfig{}) + actualReport, err := testScanner.Scan(context.Background(), []ScanItem{}, &ScanConfig{}) assert.NoError(t, err, "scanner encountered an error") assert.Equal(t, 0, actualReport.GetTotalItemsScanned()) assert.Equal(t, 0, actualReport.GetTotalSecretsFound()) @@ -290,7 +291,7 @@ func TestScan(t *testing.T) { }) t.Run("scan with scanItems nil", func(t *testing.T) { testScanner := NewScanner() - actualReport, err := testScanner.Scan(nil, &ScanConfig{}) + actualReport, err := testScanner.Scan(context.Background(), nil, &ScanConfig{}) assert.NoError(t, err, "scanner encountered an error") assert.Equal(t, 0, actualReport.GetTotalItemsScanned()) assert.Equal(t, 0, actualReport.GetTotalSecretsFound()) @@ -328,7 +329,7 @@ func TestScan(t *testing.T) { } testScanner := NewScanner() - actualReport, err := testScanner.Scan(scanItems, &ScanConfig{}) + actualReport, err := testScanner.Scan(context.Background(), scanItems, &ScanConfig{}) assert.NoError(t, err, "scanner encountered an error") // scan 1 @@ -356,7 +357,7 @@ func TestScan(t *testing.T) { assert.EqualValues(t, normalizedExpectedReport, normalizedActualReport) // scan 2 - actualReport, err = testScanner.Scan(scanItems, &ScanConfig{ + actualReport, err = testScanner.Scan(context.Background(), scanItems, &ScanConfig{ IgnoreResultIds: []string{ "efc9a9ee89f1d732c7321067eb701b9656e91f15", "c31705d99e835e4ac7bc3f688bd9558309e056ed", @@ -626,7 +627,7 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { testScanner := NewScanner() // Test scan - actualReport, err := testScanner.Scan(tc.ScanItems, tc.ScanConfig) + actualReport, err := testScanner.Scan(context.Background(), tc.ScanItems, tc.ScanConfig) for _, expectErr := range tc.expectErrors { assert.ErrorContains(t, err, expectErr.Error()) @@ -643,7 +644,7 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { } close(itemsIn) - dynamicActualReport, err := testScanner.ScanDynamic(itemsIn, tc.ScanConfig) + dynamicActualReport, err := testScanner.ScanDynamic(context.Background(), itemsIn, tc.ScanConfig) for _, expectErr := range tc.expectErrors { assert.ErrorContains(t, err, expectErr.Error()) @@ -698,7 +699,7 @@ func TestScanDynamic(t *testing.T) { testScanner := NewScanner() assert.NoError(t, err, "failed to create scanner") - actualReport, err := testScanner.ScanDynamic(itemsIn, &ScanConfig{}) + actualReport, err := testScanner.ScanDynamic(context.Background(), itemsIn, &ScanConfig{}) assert.NoError(t, err, "scanner encountered an error") compareOrUpdateTestData(t, actualReport, expectedReportPath) @@ -742,7 +743,7 @@ func TestScanDynamic(t *testing.T) { testScanner := NewScanner() - actualReport, err := testScanner.ScanDynamic(itemsIn, &ScanConfig{ + actualReport, err := testScanner.ScanDynamic(context.Background(), itemsIn, &ScanConfig{ IgnoreResultIds: []string{ "efc9a9ee89f1d732c7321067eb701b9656e91f15", "c31705d99e835e4ac7bc3f688bd9558309e056ed", @@ -791,7 +792,7 @@ func TestScanDynamic(t *testing.T) { testScanner := NewScanner() assert.NoError(t, err, "failed to create scanner") - actualReport, err := testScanner.ScanDynamic(itemsIn, &ScanConfig{ + actualReport, err := testScanner.ScanDynamic(context.Background(), itemsIn, &ScanConfig{ IgnoreRules: []string{ "github-pat", }, @@ -819,7 +820,7 @@ func TestScanDynamic(t *testing.T) { testScanner := NewScanner() - report, err := testScanner.ScanDynamic(itemsIn, &ScanConfig{IgnoreRules: idOfRules}) + report, err := testScanner.ScanDynamic(context.Background(), itemsIn, &ScanConfig{IgnoreRules: idOfRules}) assert.Error(t, err) assert.ErrorIs(t, err, engine.ErrNoRulesSelected) @@ -869,7 +870,7 @@ func TestScanDynamic(t *testing.T) { testScanner := NewScanner() // scan 2 - actualReport, err := testScanner.ScanDynamic(itemsIn1, &ScanConfig{}) + actualReport, err := testScanner.ScanDynamic(context.Background(), itemsIn1, &ScanConfig{}) assert.NoError(t, err, "scanner encountered an error") expectedReportBytes, err := os.ReadFile(expectedReportPath) @@ -895,7 +896,7 @@ func TestScanDynamic(t *testing.T) { assert.EqualValues(t, normalizedExpectedReport, normalizedActualReport) // scan 2 - actualReport, err = testScanner.ScanDynamic(itemsIn2, &ScanConfig{ + actualReport, err = testScanner.ScanDynamic(context.Background(), itemsIn2, &ScanConfig{ IgnoreResultIds: []string{ "efc9a9ee89f1d732c7321067eb701b9656e91f15", "c31705d99e835e4ac7bc3f688bd9558309e056ed", @@ -957,7 +958,7 @@ func TestScanWithValidation(t *testing.T) { } testScanner := NewScanner() - actualReport, err := testScanner.Scan(scanItems, &ScanConfig{WithValidation: true}) + actualReport, err := testScanner.Scan(context.Background(), scanItems, &ScanConfig{WithValidation: true}) assert.NoError(t, err, "scanner encountered an error") expectedReportBytes, err := os.ReadFile(expectedReportWithValidationPath) @@ -1108,7 +1109,7 @@ func TestScan_LimitSettings(t *testing.T) { } testScanner := NewScanner() - actualReport, err := testScanner.Scan(scanItems, &ScanConfig{ + actualReport, err := testScanner.Scan(context.Background(), scanItems, &ScanConfig{ MaxFindings: tt.maxFindings, MaxRuleMatchesPerFragment: tt.maxRuleMatchesPerFragment, MaxSecretSize: tt.maxSecretSize, @@ -1214,7 +1215,7 @@ func TestScanDynamic_LimitSettings(t *testing.T) { close(itemsIn) testScanner := NewScanner() - actualReport, err := testScanner.ScanDynamic(itemsIn, &ScanConfig{ + actualReport, err := testScanner.ScanDynamic(context.Background(), itemsIn, &ScanConfig{ MaxFindings: tt.maxFindings, MaxRuleMatchesPerFragment: tt.maxRuleMatchesPerFragment, MaxSecretSize: tt.maxSecretSize, diff --git a/pkg/scanner.go b/pkg/scanner.go index 33b22492..c5935713 100644 --- a/pkg/scanner.go +++ b/pkg/scanner.go @@ -1,6 +1,8 @@ package scanner import ( + "context" + "github.com/checkmarx/2ms/v5/engine" "github.com/checkmarx/2ms/v5/engine/rules/ruledefine" "github.com/checkmarx/2ms/v5/lib/reporting" @@ -50,7 +52,7 @@ func (i ScanItem) GetGitInfo() *plugins.GitInfo { type Scanner interface { Reset(scanConfig *ScanConfig, opts ...engine.EngineOption) error - Scan(scanItems []ScanItem, scanConfig *ScanConfig, opts ...engine.EngineOption) (reporting.IReport, error) + Scan(ctx context.Context, scanItems []ScanItem, scanConfig *ScanConfig, opts ...engine.EngineOption) (reporting.IReport, error) // ScanDynamic performs a scans with custom input of items and optional custom plugin channels. // // To provide custom plugin channels, use engine.WithPluginChannels: @@ -58,6 +60,6 @@ type Scanner interface { // pluginChannels := plugins.NewChannels(func(c *plugins.Channels) { // c.Items = make(chan plugins.ISourceItem, 100) // }) - // s.ScanDynamic(ScanConfig{}, engine.WithPluginChannels(pluginChannels)) - ScanDynamic(itemsIn <-chan ScanItem, scanConfig *ScanConfig, opts ...engine.EngineOption) (reporting.IReport, error) + // s.ScanDynamic(ctx, ScanConfig{}, engine.WithPluginChannels(pluginChannels)) + ScanDynamic(ctx context.Context, itemsIn <-chan ScanItem, scanConfig *ScanConfig, opts ...engine.EngineOption) (reporting.IReport, error) } From 9d9ea6e48178bc6cdba0d84a0e57be3bf52af2ef Mon Sep 17 00:00:00 2001 From: cx-leonardo-fontes <204389152+cx-leonardo-fontes@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:05:04 +0000 Subject: [PATCH 2/3] update test --- engine/detect/utils_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/engine/detect/utils_test.go b/engine/detect/utils_test.go index 36122b7e..f386cfde 100644 --- a/engine/detect/utils_test.go +++ b/engine/detect/utils_test.go @@ -128,10 +128,6 @@ func TestFilter(t *testing.T) { t.Run(tt.name, func(t *testing.T) { result := filter(tt.findings, tt.redact) assert.Equal(t, tt.expectedCount, len(result), "unexpected number of findings") - - if tt.expectedCount == 1 && tt.expectedSecret != "" { - assert.Equal(t, tt.expectedSecret, result[0].Secret, "unexpected secret value") - } }) } } From f5810633f17dba756527f689dd362e20dfc91740 Mon Sep 17 00:00:00 2001 From: cx-leonardo-fontes <204389152+cx-leonardo-fontes@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:08:06 +0000 Subject: [PATCH 3/3] fix linter issue --- pkg/scan.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/scan.go b/pkg/scan.go index e9b41269..af88f204 100644 --- a/pkg/scan.go +++ b/pkg/scan.go @@ -57,7 +57,12 @@ func (s *scanner) Reset(scanConfig *ScanConfig, opts ...engine.EngineOption) err return nil } -func (s *scanner) Scan(ctx context.Context, scanItems []ScanItem, scanConfig *ScanConfig, opts ...engine.EngineOption) (reporting.IReport, error) { +func (s *scanner) Scan( + ctx context.Context, + scanItems []ScanItem, + scanConfig *ScanConfig, + opts ...engine.EngineOption, +) (reporting.IReport, error) { var wg conc.WaitGroup err := s.Reset(scanConfig, opts...) if err != nil {