diff --git a/internal/resources/scanner.go b/internal/resources/scanner.go index b427ca5f..04985250 100644 --- a/internal/resources/scanner.go +++ b/internal/resources/scanner.go @@ -9,4 +9,9 @@ type ScanConfig struct { CustomRules []*ruledefine.Rule WithValidation bool PluginName string + + // Limit settings + MaxFindings uint64 // Total findings limit across entire scan (0 = no limit) + MaxRuleMatchesPerFragment uint64 // Regex matches limit per rule per fragment (0 = no limit) + MaxSecretSize uint64 // Maximum secret size in bytes (0 = no limit) } diff --git a/pkg/scan.go b/pkg/scan.go index 16ba7aa7..f5d63a23 100644 --- a/pkg/scan.go +++ b/pkg/scan.go @@ -33,28 +33,31 @@ func NewScanner() Scanner { return &scanner{} } -func (s *scanner) Reset(scanConfig resources.ScanConfig, opts ...engine.EngineOption) error { +func (s *scanner) Reset(scanConfig *resources.ScanConfig, opts ...engine.EngineOption) error { s.mu.Lock() defer s.mu.Unlock() engineInstance, err := engine.Init(&engine.EngineConfig{ - IgnoredIds: scanConfig.IgnoreResultIds, - SelectedList: scanConfig.SelectRules, - CustomRules: scanConfig.CustomRules, - IgnoreList: scanConfig.IgnoreRules, - ScanConfig: scanConfig, + IgnoredIds: scanConfig.IgnoreResultIds, + SelectedList: scanConfig.SelectRules, + CustomRules: scanConfig.CustomRules, + IgnoreList: scanConfig.IgnoreRules, + MaxFindings: scanConfig.MaxFindings, + MaxRuleMatchesPerFragment: scanConfig.MaxRuleMatchesPerFragment, + MaxSecretSize: scanConfig.MaxSecretSize, + ScanConfig: *scanConfig, }, opts...) if err != nil { return fmt.Errorf("error initializing engine: %w", err) } s.engineInstance = engineInstance - s.scanConfig = scanConfig + s.scanConfig = *scanConfig return nil } -func (s *scanner) Scan(scanItems []ScanItem, scanConfig resources.ScanConfig, opts ...engine.EngineOption) (reporting.IReport, error) { +func (s *scanner) Scan(scanItems []ScanItem, scanConfig *resources.ScanConfig, opts ...engine.EngineOption) (reporting.IReport, error) { var wg conc.WaitGroup err := s.Reset(scanConfig, opts...) if err != nil { @@ -106,7 +109,7 @@ func (s *scanner) Scan(scanItems []ScanItem, scanConfig resources.ScanConfig, op func (s *scanner) ScanDynamic( itemsIn <-chan ScanItem, - scanConfig resources.ScanConfig, + scanConfig *resources.ScanConfig, opts ...engine.EngineOption, ) (reporting.IReport, error) { var wg conc.WaitGroup diff --git a/pkg/scan_test.go b/pkg/scan_test.go index f065c87a..1d9b6bf5 100644 --- a/pkg/scan_test.go +++ b/pkg/scan_test.go @@ -121,7 +121,7 @@ func TestScan(t *testing.T) { } testScanner := NewScanner() - actualReport, err := testScanner.Scan(scanItems, resources.ScanConfig{}) + actualReport, err := testScanner.Scan(scanItems, &resources.ScanConfig{}) assert.NoError(t, err, "scanner encountered an error") // Use helper function to either update expected file or compare results @@ -158,7 +158,7 @@ func TestScan(t *testing.T) { } testScanner := NewScanner() - actualReport, err := testScanner.Scan(scanItems, resources.ScanConfig{ + actualReport, err := testScanner.Scan(scanItems, &resources.ScanConfig{ IgnoreResultIds: []string{ "efc9a9ee89f1d732c7321067eb701b9656e91f15", "c31705d99e835e4ac7bc3f688bd9558309e056ed", @@ -218,7 +218,7 @@ func TestScan(t *testing.T) { } testScanner := NewScanner() - actualReport, err := testScanner.Scan(scanItems, resources.ScanConfig{ + actualReport, err := testScanner.Scan(scanItems, &resources.ScanConfig{ IgnoreRules: []string{ "github-pat", }, @@ -271,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, resources.ScanConfig{}, engine.WithPluginChannels(pluginChannels)) + report, err := testScanner.Scan(scanItems, &resources.ScanConfig{}, engine.WithPluginChannels(pluginChannels)) assert.Equal(t, 0, report.GetTotalItemsScanned()) assert.Equal(t, 0, report.GetTotalSecretsFound()) @@ -282,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{}, resources.ScanConfig{}) + actualReport, err := testScanner.Scan([]ScanItem{}, &resources.ScanConfig{}) assert.NoError(t, err, "scanner encountered an error") assert.Equal(t, 0, actualReport.GetTotalItemsScanned()) assert.Equal(t, 0, actualReport.GetTotalSecretsFound()) @@ -291,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, resources.ScanConfig{}) + actualReport, err := testScanner.Scan(nil, &resources.ScanConfig{}) assert.NoError(t, err, "scanner encountered an error") assert.Equal(t, 0, actualReport.GetTotalItemsScanned()) assert.Equal(t, 0, actualReport.GetTotalSecretsFound()) @@ -329,7 +329,7 @@ func TestScan(t *testing.T) { } testScanner := NewScanner() - actualReport, err := testScanner.Scan(scanItems, resources.ScanConfig{}) + actualReport, err := testScanner.Scan(scanItems, &resources.ScanConfig{}) assert.NoError(t, err, "scanner encountered an error") // scan 1 @@ -357,7 +357,7 @@ func TestScan(t *testing.T) { assert.EqualValues(t, normalizedExpectedReport, normalizedActualReport) // scan 2 - actualReport, err = testScanner.Scan(scanItems, resources.ScanConfig{ + actualReport, err = testScanner.Scan(scanItems, &resources.ScanConfig{ IgnoreResultIds: []string{ "efc9a9ee89f1d732c7321067eb701b9656e91f15", "c31705d99e835e4ac7bc3f688bd9558309e056ed", @@ -427,14 +427,14 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { tests := []struct { Name string - ScanConfig resources.ScanConfig + ScanConfig *resources.ScanConfig ScanItems []ScanItem ExpectedReportPath string expectErrors []error }{ { Name: "Run all default + custom rules", - ScanConfig: resources.ScanConfig{ + ScanConfig: &resources.ScanConfig{ CustomRules: cloneRules(customRules), WithValidation: true, }, @@ -444,7 +444,7 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { }, { Name: "Run only custom rules", - ScanConfig: resources.ScanConfig{ + ScanConfig: &resources.ScanConfig{ CustomRules: cloneRules(customRules), WithValidation: true, SelectRules: []string{"custom"}, @@ -455,7 +455,7 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { }, { Name: "Run only custom override rules", - ScanConfig: resources.ScanConfig{ + ScanConfig: &resources.ScanConfig{ CustomRules: cloneRules(customRules), WithValidation: true, SelectRules: []string{"override"}, @@ -466,7 +466,7 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { }, { Name: "Run default + non override rules", - ScanConfig: resources.ScanConfig{ + ScanConfig: &resources.ScanConfig{ CustomRules: cloneRules(customRules), WithValidation: true, IgnoreRules: []string{"override"}, @@ -477,7 +477,7 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { }, { Name: "Run only custom rules and ignore overrides", - ScanConfig: resources.ScanConfig{ + ScanConfig: &resources.ScanConfig{ CustomRules: cloneRules(customRules), WithValidation: true, SelectRules: []string{"custom"}, @@ -489,7 +489,7 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { }, { Name: "Run only default rules by ignoring custom rules", - ScanConfig: resources.ScanConfig{ + ScanConfig: &resources.ScanConfig{ CustomRules: cloneRules(customRules), WithValidation: true, IgnoreRules: []string{"custom"}, @@ -500,7 +500,7 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { }, { Name: "Run only custom rules by ignoring custom rules by id", - ScanConfig: resources.ScanConfig{ + ScanConfig: &resources.ScanConfig{ CustomRules: cloneRules(customRules), WithValidation: true, SelectRules: []string{"custom"}, @@ -515,7 +515,7 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { }, { Name: "Run only custom rules by ignoring custom rules by name", - ScanConfig: resources.ScanConfig{ + ScanConfig: &resources.ScanConfig{ CustomRules: cloneRules(customRules), WithValidation: true, SelectRules: []string{"custom"}, @@ -530,7 +530,7 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { }, { Name: "Run only custom rules by ignoring override result Ids", - ScanConfig: resources.ScanConfig{ + ScanConfig: &resources.ScanConfig{ CustomRules: cloneRules(customRules), WithValidation: true, SelectRules: []string{"custom"}, @@ -546,7 +546,7 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { }, { Name: "Rule name, id, regex missing", - ScanConfig: resources.ScanConfig{ + ScanConfig: &resources.ScanConfig{ CustomRules: []*ruledefine.Rule{ { Description: "Match passwords", @@ -568,7 +568,7 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { }, { Name: "Regex, severity and score parameters invalid", - ScanConfig: resources.ScanConfig{ + ScanConfig: &resources.ScanConfig{ CustomRules: []*ruledefine.Rule{ { RuleID: "db18ccf1-4fbf-49f6-aec1-939a2e5464c0", @@ -600,7 +600,7 @@ func TestScanAndScanDynamicWithCustomRules(t *testing.T) { }, { Name: "Rule id missing", - ScanConfig: resources.ScanConfig{ + ScanConfig: &resources.ScanConfig{ CustomRules: []*ruledefine.Rule{ { RuleName: "mock-rule", @@ -699,7 +699,7 @@ func TestScanDynamic(t *testing.T) { testScanner := NewScanner() assert.NoError(t, err, "failed to create scanner") - actualReport, err := testScanner.ScanDynamic(itemsIn, resources.ScanConfig{}) + actualReport, err := testScanner.ScanDynamic(itemsIn, &resources.ScanConfig{}) assert.NoError(t, err, "scanner encountered an error") compareOrUpdateTestData(t, actualReport, expectedReportPath) @@ -743,7 +743,7 @@ func TestScanDynamic(t *testing.T) { testScanner := NewScanner() - actualReport, err := testScanner.ScanDynamic(itemsIn, resources.ScanConfig{ + actualReport, err := testScanner.ScanDynamic(itemsIn, &resources.ScanConfig{ IgnoreResultIds: []string{ "efc9a9ee89f1d732c7321067eb701b9656e91f15", "c31705d99e835e4ac7bc3f688bd9558309e056ed", @@ -792,7 +792,7 @@ func TestScanDynamic(t *testing.T) { testScanner := NewScanner() assert.NoError(t, err, "failed to create scanner") - actualReport, err := testScanner.ScanDynamic(itemsIn, resources.ScanConfig{ + actualReport, err := testScanner.ScanDynamic(itemsIn, &resources.ScanConfig{ IgnoreRules: []string{ "github-pat", }, @@ -820,7 +820,7 @@ func TestScanDynamic(t *testing.T) { testScanner := NewScanner() - report, err := testScanner.ScanDynamic(itemsIn, resources.ScanConfig{IgnoreRules: idOfRules}) + report, err := testScanner.ScanDynamic(itemsIn, &resources.ScanConfig{IgnoreRules: idOfRules}) assert.Error(t, err) assert.ErrorIs(t, err, engine.ErrNoRulesSelected) @@ -870,7 +870,7 @@ func TestScanDynamic(t *testing.T) { testScanner := NewScanner() // scan 2 - actualReport, err := testScanner.ScanDynamic(itemsIn1, resources.ScanConfig{}) + actualReport, err := testScanner.ScanDynamic(itemsIn1, &resources.ScanConfig{}) assert.NoError(t, err, "scanner encountered an error") expectedReportBytes, err := os.ReadFile(expectedReportPath) @@ -896,7 +896,7 @@ func TestScanDynamic(t *testing.T) { assert.EqualValues(t, normalizedExpectedReport, normalizedActualReport) // scan 2 - actualReport, err = testScanner.ScanDynamic(itemsIn2, resources.ScanConfig{ + actualReport, err = testScanner.ScanDynamic(itemsIn2, &resources.ScanConfig{ IgnoreResultIds: []string{ "efc9a9ee89f1d732c7321067eb701b9656e91f15", "c31705d99e835e4ac7bc3f688bd9558309e056ed", @@ -958,7 +958,7 @@ func TestScanWithValidation(t *testing.T) { } testScanner := NewScanner() - actualReport, err := testScanner.Scan(scanItems, resources.ScanConfig{WithValidation: true}) + actualReport, err := testScanner.Scan(scanItems, &resources.ScanConfig{WithValidation: true}) assert.NoError(t, err, "scanner encountered an error") expectedReportBytes, err := os.ReadFile(expectedReportWithValidationPath) @@ -1029,6 +1029,212 @@ func compareOrUpdateTestData(t *testing.T, actualReport reporting.IReport, expec assert.EqualValues(t, normalizedExpectedReport, normalizedActualReport) } +func TestScan_LimitSettings(t *testing.T) { + // Load test data once for all subtests + githubPatBytes, err := os.ReadFile(githubPatPath) + assert.NoError(t, err, "failed to read github-pat file") + githubPatContent := string(githubPatBytes) + + jwtBytes, err := os.ReadFile(jwtPath) + assert.NoError(t, err, "failed to read jwt file") + jwtContent := string(jwtBytes) + + genericKeysBytes, err := os.ReadFile(genericKeysPath) + assert.NoError(t, err, "failed to read generic-api-keys file") + genericKeysContent := string(genericKeysBytes) + + tests := []struct { + name string + filePaths []string + maxFindings uint64 + maxRuleMatchesPerFragment uint64 + maxSecretSize uint64 + expectedFindings int + expectedMaxSecretSize int + }{ + { + name: "MaxFindings limits total secrets found", + filePaths: []string{githubPatPath, jwtPath, genericKeysPath}, + maxFindings: 2, + maxRuleMatchesPerFragment: 0, + maxSecretSize: 0, + expectedFindings: 2, + expectedMaxSecretSize: 1000, + }, + { + name: "MaxRuleMatchesPerFragment limits matches per rule per fragment", + filePaths: []string{githubPatPath}, + maxFindings: 0, + maxRuleMatchesPerFragment: 1, + maxSecretSize: 0, + expectedFindings: 1, + expectedMaxSecretSize: 1000, + }, + { + name: "MaxSecretSize filters out large secrets", + filePaths: []string{jwtPath, githubPatPath}, + maxFindings: 0, + maxRuleMatchesPerFragment: 0, + maxSecretSize: 50, + expectedFindings: 3, + expectedMaxSecretSize: 50, + }, + { + name: "All limit settings work together", + filePaths: []string{githubPatPath, jwtPath, genericKeysPath}, + maxFindings: 3, + maxRuleMatchesPerFragment: 1, + maxSecretSize: 100, + expectedFindings: 2, + expectedMaxSecretSize: 100, + }, + } + + // Map file paths to content + contentMap := map[string]*string{ + githubPatPath: &githubPatContent, + jwtPath: &jwtContent, + genericKeysPath: &genericKeysContent, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scanItems := make([]ScanItem, 0, len(tt.filePaths)) + for _, path := range tt.filePaths { + scanItems = append(scanItems, ScanItem{ + Content: contentMap[path], + ID: fmt.Sprintf("mock-%s", path), + Source: path, + }) + } + + testScanner := NewScanner() + actualReport, err := testScanner.Scan(scanItems, &resources.ScanConfig{ + MaxFindings: tt.maxFindings, + MaxRuleMatchesPerFragment: tt.maxRuleMatchesPerFragment, + MaxSecretSize: tt.maxSecretSize, + }) + assert.NoError(t, err, "scanner encountered an error") + + totalSecrets := actualReport.GetTotalSecretsFound() + assert.Equal(t, tt.expectedFindings, totalSecrets, "total secrets should match expected findings") + + results := actualReport.GetResults() + for _, secretsList := range results { + for _, secret := range secretsList { + assert.LessOrEqual(t, len(secret.Value), tt.expectedMaxSecretSize, "secret value exceeds max size") + } + } + }) + } +} + +func TestScanDynamic_LimitSettings(t *testing.T) { + // Load test data once for all subtests + githubPatBytes, err := os.ReadFile(githubPatPath) + assert.NoError(t, err, "failed to read github-pat file") + githubPatContent := string(githubPatBytes) + + jwtBytes, err := os.ReadFile(jwtPath) + assert.NoError(t, err, "failed to read jwt file") + jwtContent := string(jwtBytes) + + genericKeysBytes, err := os.ReadFile(genericKeysPath) + assert.NoError(t, err, "failed to read generic-api-keys file") + genericKeysContent := string(genericKeysBytes) + + tests := []struct { + name string + filePaths []string + maxFindings uint64 + maxRuleMatchesPerFragment uint64 + maxSecretSize uint64 + expectedFindings int + expectedMaxSecretSize int + }{ + { + name: "MaxFindings limits total secrets found", + filePaths: []string{githubPatPath, jwtPath, genericKeysPath}, + maxFindings: 2, + maxRuleMatchesPerFragment: 0, + maxSecretSize: 0, + expectedFindings: 2, + expectedMaxSecretSize: 1000, + }, + { + name: "MaxRuleMatchesPerFragment limits matches per rule per fragment", + filePaths: []string{githubPatPath}, + maxFindings: 0, + maxRuleMatchesPerFragment: 1, + maxSecretSize: 0, + expectedFindings: 1, + expectedMaxSecretSize: 1000, + }, + { + name: "MaxSecretSize filters out large secrets", + filePaths: []string{jwtPath, githubPatPath}, + maxFindings: 0, + maxRuleMatchesPerFragment: 0, + maxSecretSize: 50, + expectedFindings: 3, + expectedMaxSecretSize: 50, + }, + { + name: "All limit settings work together", + filePaths: []string{githubPatPath, jwtPath, genericKeysPath}, + maxFindings: 3, + maxRuleMatchesPerFragment: 1, + maxSecretSize: 100, + expectedFindings: 2, + expectedMaxSecretSize: 100, + }, + } + + // Map file paths to content + contentMap := map[string]*string{ + githubPatPath: &githubPatContent, + jwtPath: &jwtContent, + genericKeysPath: &genericKeysContent, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scanItems := make([]ScanItem, 0, len(tt.filePaths)) + for _, path := range tt.filePaths { + scanItems = append(scanItems, ScanItem{ + Content: contentMap[path], + ID: fmt.Sprintf("mock-%s", path), + Source: path, + }) + } + + itemsIn := make(chan ScanItem, len(scanItems)) + for _, item := range scanItems { + itemsIn <- item + } + close(itemsIn) + + testScanner := NewScanner() + actualReport, err := testScanner.ScanDynamic(itemsIn, &resources.ScanConfig{ + MaxFindings: tt.maxFindings, + MaxRuleMatchesPerFragment: tt.maxRuleMatchesPerFragment, + MaxSecretSize: tt.maxSecretSize, + }) + assert.NoError(t, err, "scanner encountered an error") + + totalSecrets := actualReport.GetTotalSecretsFound() + assert.Equal(t, tt.expectedFindings, totalSecrets, "total secrets should match expected findings") + + results := actualReport.GetResults() + for _, secretsList := range results { + for _, secret := range secretsList { + assert.LessOrEqual(t, len(secret.Value), tt.expectedMaxSecretSize, "secret value exceeds max size") + } + } + }) + } +} + func cloneRules(rulesToClone []*ruledefine.Rule) []*ruledefine.Rule { clonedRules := make([]*ruledefine.Rule, 0, len(rulesToClone)) for _, rule := range rulesToClone { diff --git a/pkg/scanner.go b/pkg/scanner.go index 9cec4740..a0e6b977 100644 --- a/pkg/scanner.go +++ b/pkg/scanner.go @@ -34,8 +34,8 @@ func (i ScanItem) GetGitInfo() *plugins.GitInfo { } type Scanner interface { - Reset(scanConfig resources.ScanConfig, opts ...engine.EngineOption) error - Scan(scanItems []ScanItem, scanConfig resources.ScanConfig, opts ...engine.EngineOption) (reporting.IReport, error) + Reset(scanConfig *resources.ScanConfig, opts ...engine.EngineOption) error + Scan(scanItems []ScanItem, scanConfig *resources.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: @@ -44,5 +44,5 @@ type Scanner interface { // c.Items = make(chan plugins.ISourceItem, 100) // }) // s.ScanDynamic(ScanConfig{}, engine.WithPluginChannels(pluginChannels)) - ScanDynamic(itemsIn <-chan ScanItem, scanConfig resources.ScanConfig, opts ...engine.EngineOption) (reporting.IReport, error) + ScanDynamic(itemsIn <-chan ScanItem, scanConfig *resources.ScanConfig, opts ...engine.EngineOption) (reporting.IReport, error) }