diff --git a/cmd/osv-scanner/scan/source/__snapshots__/command_test.snap b/cmd/osv-scanner/scan/source/__snapshots__/command_test.snap index 0f66a03a52b..e0d474ad4ae 100755 --- a/cmd/osv-scanner/scan/source/__snapshots__/command_test.snap +++ b/cmd/osv-scanner/scan/source/__snapshots__/command_test.snap @@ -549,6 +549,16 @@ Ignored invalid config file at /testdata/config-invalid/osv-scanner-tes --- +[TestCommand/config_file_with_invalid_regex - 1] + +--- + +[TestCommand/config_file_with_invalid_regex - 2] +Failed to read config file: invalid regex "[invalid" in package override: error parsing regexp: missing closing ]: `[invalid$` +invalid regex "[invalid" in package override: error parsing regexp: missing closing ]: `[invalid$` + +--- + [TestCommand/config_files_cannot_have_unknown_keys - 1] --- diff --git a/cmd/osv-scanner/scan/source/command_test.go b/cmd/osv-scanner/scan/source/command_test.go index 3febffb55d7..16c0adb5222 100644 --- a/cmd/osv-scanner/scan/source/command_test.go +++ b/cmd/osv-scanner/scan/source/command_test.go @@ -347,6 +347,12 @@ func TestCommand(t *testing.T) { Args: []string{"", "source", "--config=./testdata/osv-scanner-unknown-config.toml", "./testdata/locks-many"}, Exit: 127, }, + // config file with invalid regex in package override + { + Name: "config_file_with_invalid_regex", + Args: []string{"", "source", "--config=./testdata/osv-scanner-invalid-regex-config.toml", "./testdata/locks-many"}, + Exit: 127, + }, // config file with multiple ignores with the same id { Name: "config_files_should_not_have_multiple_ignores_with_the_same_id", diff --git a/cmd/osv-scanner/scan/source/testdata/osv-scanner-invalid-regex-config.toml b/cmd/osv-scanner/scan/source/testdata/osv-scanner-invalid-regex-config.toml new file mode 100644 index 00000000000..45264b4de43 --- /dev/null +++ b/cmd/osv-scanner/scan/source/testdata/osv-scanner-invalid-regex-config.toml @@ -0,0 +1,5 @@ +[[PackageOverrides]] +name = "[invalid" +nameIsRegex = true +ignore = true +reason = "this regex is invalid" diff --git a/docs/configuration.md b/docs/configuration.md index 98fe1acb6ef..94c6bf2dbda 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -50,6 +50,7 @@ You can specify overrides for particular packages to have them either ignored en [[PackageOverrides]] # One or more fields to match each package against: name = "lib" +# nameIsRegex = true # Optional: treat name as a regular expression pattern version = "1.0.0" ecosystem = "Go" group = "dev" @@ -93,9 +94,18 @@ ecosystem = "npm" group = "dev" ignore = true +# ignore packages matching a regex pattern (e.g. all internal packages) +[[PackageOverrides]] +name = "internal-.*" +nameIsRegex = true +ignore = true +reason = "internal packages should not be checked" + # ... and so on ``` +When `nameIsRegex` is set to `true`, the `name` field is treated as a regular expression pattern. The pattern is automatically anchored to match the full package name (i.e. `^pattern$`). Standard [Go regular expression syntax](https://pkg.go.dev/regexp/syntax) is supported. An invalid regex pattern will cause a config loading error. + ## Go Version Override Use the `GoVersionOverride` key to override the Go version used for scanning. This is useful when the scanner fails to detect the correct Go version or when you want to force a specific version. diff --git a/internal/config/config.go b/internal/config/config.go index ce46e8b20ad..879b74fbeb6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,7 @@ import ( "time" "github.com/google/osv-scalibr/extractor" + "github.com/google/osv-scanner/v2/internal/cachedregexp" "github.com/google/osv-scanner/v2/internal/cmdlogger" "github.com/google/osv-scanner/v2/internal/imodels" ) @@ -39,6 +40,7 @@ type PackageOverrideEntry struct { Version string `toml:"version"` Ecosystem string `toml:"ecosystem"` Group string `toml:"group"` + NameIsRegex bool `toml:"nameIsRegex"` Ignore bool `toml:"ignore"` Vulnerability Vulnerability `toml:"vulnerability"` License License `toml:"license"` @@ -47,8 +49,19 @@ type PackageOverrideEntry struct { } func (e PackageOverrideEntry) matches(pkg *extractor.Package) bool { - if e.Name != "" && e.Name != imodels.Name(pkg) { - return false + if e.Name != "" { + if e.NameIsRegex { + re, err := cachedregexp.Compile("^" + e.Name + "$") + if err != nil { + // This should not happen as regex is validated at config load time + return false + } + if !re.MatchString(imodels.Name(pkg)) { + return false + } + } else if e.Name != imodels.Name(pkg) { + return false + } } if e.Version != "" && e.Version != imodels.Version(pkg) { return false diff --git a/internal/config/config_internal_test.go b/internal/config/config_internal_test.go index 751d4aa3eb0..fd61eae8e9b 100644 --- a/internal/config/config_internal_test.go +++ b/internal/config/config_internal_test.go @@ -170,6 +170,14 @@ func Test_tryLoadConfig(t *testing.T) { }, wantErr: true, }, + { + name: "config_with_invalid_regex_in_package_override", + args: args{ + configPath: "./testdata/testdatainner/osv-scanner-invalid-regex.toml", + }, + want: Config{}, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -867,6 +875,210 @@ func TestConfig_ShouldIgnorePackage(t *testing.T) { } } +func TestConfig_ShouldIgnorePackage_NameIsRegex(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + config Config + args *extractor.Package + wantOk bool + wantEntry PackageOverrideEntry + }{ + { + name: "Regex_matches_package_name", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "lib.*", + NameIsRegex: true, + Ignore: true, + Reason: "internal packages", + }, + }, + }, + args: &extractor.Package{ + Name: "lib1", + Version: "1.0.0", + PURLType: purl.TypeGolang, + }, + wantOk: true, + wantEntry: PackageOverrideEntry{ + Name: "lib.*", + NameIsRegex: true, + Ignore: true, + Reason: "internal packages", + }, + }, + { + name: "Regex_does_not_match_package_name", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "lib.*", + NameIsRegex: true, + Ignore: true, + Reason: "internal packages", + }, + }, + }, + args: &extractor.Package{ + Name: "other-pkg", + Version: "1.0.0", + PURLType: purl.TypeGolang, + }, + wantOk: false, + wantEntry: PackageOverrideEntry{}, + }, + { + name: "Invalid_regex_does_not_match", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "[invalid", + NameIsRegex: true, + Ignore: true, + Reason: "bad regex", + }, + }, + }, + args: &extractor.Package{ + Name: "anything", + Version: "1.0.0", + PURLType: purl.TypeGolang, + }, + wantOk: false, + wantEntry: PackageOverrideEntry{}, + }, + { + name: "NameIsRegex_false_uses_exact_match", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "lib.*", + Ignore: true, + Reason: "exact match", + }, + }, + }, + args: &extractor.Package{ + Name: "lib1", + Version: "1.0.0", + PURLType: purl.TypeGolang, + }, + wantOk: false, + wantEntry: PackageOverrideEntry{}, + }, + { + name: "Regex_with_empty_name_matches_all", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "", + NameIsRegex: true, + Ignore: true, + Reason: "match all", + }, + }, + }, + args: &extractor.Package{ + Name: "any-package", + Version: "1.0.0", + PURLType: purl.TypeGolang, + }, + wantOk: true, + wantEntry: PackageOverrideEntry{ + Name: "", + NameIsRegex: true, + Ignore: true, + Reason: "match all", + }, + }, + { + name: "Regex_is_anchored_and_does_not_partial_match", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "lib", + NameIsRegex: true, + Ignore: true, + Reason: "anchored", + }, + }, + }, + args: &extractor.Package{ + Name: "lib1", + Version: "1.0.0", + PURLType: purl.TypeGolang, + }, + wantOk: false, + wantEntry: PackageOverrideEntry{}, + }, + { + name: "Regex_combined_with_ecosystem_filter", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "internal-.*", + NameIsRegex: true, + Ecosystem: "Go", + Ignore: true, + Reason: "internal Go packages", + }, + }, + }, + args: &extractor.Package{ + Name: "internal-lib", + Version: "1.0.0", + PURLType: purl.TypeGolang, + }, + wantOk: true, + wantEntry: PackageOverrideEntry{ + Name: "internal-.*", + NameIsRegex: true, + Ecosystem: "Go", + Ignore: true, + Reason: "internal Go packages", + }, + }, + { + name: "Regex_matches_but_ecosystem_does_not", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "internal-.*", + NameIsRegex: true, + Ecosystem: "Go", + Ignore: true, + Reason: "internal Go packages", + }, + }, + }, + args: &extractor.Package{ + Name: "internal-lib", + Version: "1.0.0", + PURLType: "npm", + }, + wantOk: false, + wantEntry: PackageOverrideEntry{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + gotOk, gotEntry := tt.config.ShouldIgnorePackage(tt.args) + if gotOk != tt.wantOk { + t.Errorf("ShouldIgnorePackage() gotOk = %v, wantOk %v", gotOk, tt.wantOk) + } + if !reflect.DeepEqual(gotEntry, tt.wantEntry) { + t.Errorf("ShouldIgnorePackage() gotEntry = %v, wantEntry %v", gotEntry, tt.wantEntry) + } + }) + } +} + func TestConfig_ShouldIgnorePackageVulnerabilities(t *testing.T) { t.Parallel() diff --git a/internal/config/manager.go b/internal/config/manager.go index 6b5e0acfc5d..4294ebcf4f9 100644 --- a/internal/config/manager.go +++ b/internal/config/manager.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/BurntSushi/toml" + "github.com/google/osv-scanner/v2/internal/cachedregexp" "github.com/google/osv-scanner/v2/internal/cmdlogger" ) @@ -127,6 +128,14 @@ func tryLoadConfig(configPath string) (Config, error) { config.LoadPath = configPath config.warnAboutDuplicates() + + for _, override := range config.PackageOverrides { + if override.NameIsRegex && override.Name != "" { + if _, err := cachedregexp.Compile("^" + override.Name + "$"); err != nil { + return Config{}, fmt.Errorf("invalid regex %q in package override: %w", override.Name, err) + } + } + } } return config, err diff --git a/internal/config/testdata/testdatainner/osv-scanner-invalid-regex.toml b/internal/config/testdata/testdatainner/osv-scanner-invalid-regex.toml new file mode 100644 index 00000000000..45264b4de43 --- /dev/null +++ b/internal/config/testdata/testdatainner/osv-scanner-invalid-regex.toml @@ -0,0 +1,5 @@ +[[PackageOverrides]] +name = "[invalid" +nameIsRegex = true +ignore = true +reason = "this regex is invalid"