diff --git a/.envguard.yml.example b/.envguard.yml.example index 23772fa..8c02baa 100644 --- a/.envguard.yml.example +++ b/.envguard.yml.example @@ -2,15 +2,16 @@ entropy_threshold: 4.5 min_length: 20 max_file_size_kb: 500 exclude_paths: - - "testdata/**" - "**/*.test.js" - "vendor/**" exclude_extensions: - ".lock" - ".svg" - ".png" +entropy_exclude_paths: + - "testdata/**" + - "fixtures/**" custom_patterns: - name: "Internal Token" pattern: "MYCO_[A-Z0-9]{32}" severity: "HIGH" -allow_test_fixtures: false diff --git a/README.md b/README.md index b62dcc2..88bbf23 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,8 @@ The entropy engine tokenizes each scanned line, measures Shannon entropy, and fl | `max_file_size_kb` | `int` | `500` | Skip files larger than this limit with a warning. | | `exclude_paths` | `[]string` | `["testdata/**","**/*.test.js","vendor/**"]` | Glob patterns excluded from scanning. | | `exclude_extensions` | `[]string` | `[".lock",".svg",".png"]` | File extensions excluded from scanning. | +| `entropy_exclude_paths` | `[]string` | `[]` | Glob patterns that skip entropy scanning only while keeping pattern matching enabled for files that are still included by `exclude_paths`. | | `custom_patterns` | `[]pattern` | `[]` | Extra regex rules added to the built-in pattern library. | -| `allow_test_fixtures` | `bool` | `false` | Skip entropy scanning for files under `testdata/`. | Example: @@ -79,20 +79,24 @@ entropy_threshold: 4.5 min_length: 20 max_file_size_kb: 500 exclude_paths: - - "testdata/**" - "**/*.test.js" - "vendor/**" exclude_extensions: - ".lock" - ".png" - ".svg" +entropy_exclude_paths: + - "testdata/**" + - "fixtures/**" custom_patterns: - name: "Internal Token" pattern: "MYCO_[A-Z0-9]{32}" severity: "HIGH" -allow_test_fixtures: false ``` +Note: +`exclude_paths` is applied before scanning starts. If a path is excluded there, `entropy_exclude_paths` will never see it. To keep pattern matching enabled for `testdata/` while suppressing entropy checks, remove `testdata/**` from `exclude_paths` and add it to `entropy_exclude_paths` instead. + ## CLI Reference ### `envguard check [path]` diff --git a/config/loader_test.go b/config/loader_test.go index b313b75..160ce2b 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -15,7 +15,7 @@ func TestLoadFindsConfigFromParentDirectory(t *testing.T) { require.NoError(t, os.MkdirAll(nestedDir, 0o755)) configPath := filepath.Join(repoRoot, ".envguard.yml") - configBody := []byte("entropy_threshold: 5.1\nmin_length: 12\nallow_test_fixtures: true\n") + configBody := []byte("entropy_threshold: 5.1\nmin_length: 12\nentropy_exclude_paths:\n - fixtures/**\n") require.NoError(t, os.WriteFile(configPath, configBody, 0o644)) cfg, loadedPath, err := Load(nestedDir) @@ -24,5 +24,5 @@ func TestLoadFindsConfigFromParentDirectory(t *testing.T) { assert.Equal(t, configPath, loadedPath) assert.Equal(t, 5.1, cfg.EntropyThreshold) assert.Equal(t, 12, cfg.MinLength) - assert.True(t, cfg.AllowTestFixtures) + assert.Equal(t, []string{"fixtures/**"}, cfg.EntropyExcludePaths) } diff --git a/config/schema.go b/config/schema.go index 5977368..b524fa7 100644 --- a/config/schema.go +++ b/config/schema.go @@ -12,10 +12,10 @@ type Config struct { ExcludePaths []string `json:"exclude_paths" yaml:"exclude_paths"` // ExcludeExtensions contains file extensions that should not be scanned. ExcludeExtensions []string `json:"exclude_extensions" yaml:"exclude_extensions"` + // EntropyExcludePaths contains glob patterns for files or directories that should skip entropy scanning only. + EntropyExcludePaths []string `json:"entropy_exclude_paths" yaml:"entropy_exclude_paths"` // CustomPatterns contains user-defined regex rules appended to the built-in pattern set. CustomPatterns []CustomPattern `json:"custom_patterns" yaml:"custom_patterns"` - // AllowTestFixtures skips entropy scanning for files under testdata when enabled. - AllowTestFixtures bool `json:"allow_test_fixtures" yaml:"allow_test_fixtures"` } // CustomPattern defines a user-provided regex-based secret detection rule. diff --git a/scanner/scanner.go b/scanner/scanner.go index c1aba80..2be6e46 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -183,7 +183,7 @@ func (e *Engine) scanFile(path string) ([]Finding, error) { line := scanner.Text() patternFindings, spans := e.scanPatterns(relative, lineNumber, line) findings = append(findings, patternFindings...) - if e.cfg.AllowTestFixtures && isTestdataPath(relative) { + if e.shouldSkipEntropy(relative) { continue } findings = append(findings, e.scanEntropy(relative, lineNumber, line, spans)...) @@ -339,6 +339,16 @@ func isTestdataPath(path string) bool { return strings.Contains(filepath.ToSlash(path), "/testdata/") || strings.HasPrefix(filepath.ToSlash(path), "testdata/") } +func (e *Engine) shouldSkipEntropy(path string) bool { + normalized := filepath.ToSlash(path) + for _, pattern := range e.cfg.EntropyExcludePaths { + if matchGlob(pattern, normalized) { + return true + } + } + return false +} + func matchGlob(pattern string, target string) bool { pattern = filepath.ToSlash(pattern) target = filepath.ToSlash(target) diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go index 307c6c1..c13bb88 100644 --- a/scanner/scanner_test.go +++ b/scanner/scanner_test.go @@ -88,6 +88,46 @@ func TestEnvFileDetectionCanBeAllowlisted(t *testing.T) { assert.Empty(t, findings) } +func TestEntropyExcludePathsSkipsNonTestdataFixtures(t *testing.T) { + tempDir := t.TempDir() + fixturesDir := filepath.Join(tempDir, "fixtures") + require.NoError(t, os.MkdirAll(fixturesDir, 0o755)) + + target := filepath.Join(fixturesDir, "sample.txt") + require.NoError(t, os.WriteFile(target, []byte("token=abcd1234efgh5678ijkl9012mnop3456\n"), 0o644)) + + cfg := config.Default() + cfg.ExcludePaths = nil + cfg.EntropyExcludePaths = []string{"fixtures/**"} + + engine, err := NewEngineWithRoot(cfg, allowlist.Set{}, tempDir) + require.NoError(t, err) + + findings, err := engine.ScanPaths([]string{target}) + require.NoError(t, err) + assert.Empty(t, findings) +} + +func TestEntropyExcludePathsCanSkipTestdataEntropy(t *testing.T) { + tempDir := t.TempDir() + testdataDir := filepath.Join(tempDir, "testdata") + require.NoError(t, os.MkdirAll(testdataDir, 0o755)) + + target := filepath.Join(testdataDir, "sample.txt") + require.NoError(t, os.WriteFile(target, []byte("token=abcd1234efgh5678ijkl9012mnop3456\n"), 0o644)) + + cfg := config.Default() + cfg.ExcludePaths = nil + cfg.EntropyExcludePaths = []string{"testdata/**"} + + engine, err := NewEngineWithRoot(cfg, allowlist.Set{}, tempDir) + require.NoError(t, err) + + findings, err := engine.ScanPaths([]string{target}) + require.NoError(t, err) + assert.Empty(t, findings) +} + func chdirForTest(t *testing.T, dir string) { t.Helper() wd, err := os.Getwd()