From 6d0e29e53f16a9def566d904ae866c232a8b899a Mon Sep 17 00:00:00 2001 From: josieang <32358891+josieang@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:43:48 +1000 Subject: [PATCH] implement filtering by packages through the config (#944) I'd like feedback on the config yaml schema, the filter message and it's behaviour if the version is empty (it filters any version of that package). This is in response to https://github.com/google/osv-scanner/issues/814 --- fixtures/testdatainner/osv-scanner.toml | 20 +- pkg/config/config.go | 63 +- pkg/config/config_internal_test.go | 257 ++++++++ .../vulnerability_result_internal_test.snap | 586 ++++++++++++++++++ pkg/osvscanner/osvscanner.go | 15 +- pkg/osvscanner/vulnerability_result.go | 39 +- .../vulnerability_result_internal_test.go | 395 ++---------- 7 files changed, 1003 insertions(+), 372 deletions(-) create mode 100755 pkg/osvscanner/__snapshots__/vulnerability_result_internal_test.snap diff --git a/fixtures/testdatainner/osv-scanner.toml b/fixtures/testdatainner/osv-scanner.toml index 1f843b60e6..f9be2c0f2e 100644 --- a/fixtures/testdatainner/osv-scanner.toml +++ b/fixtures/testdatainner/osv-scanner.toml @@ -1,9 +1,25 @@ [[IgnoredVulns]] id = "GO-2022-0968" -# ignore_until = 2022-11-09 +# ignoreUntil = 2022-11-09 # reason = "" # Optional reason [[IgnoredVulns]] id = "GO-2022-1059" -# ignore_until = 2022-11-09 # Optional exception expiry date +# ignoreUntil = 2022-11-09 # Optional exception expiry date # reason = "" # Optional reason + +[[PackageOverrides]] +name = "lib" +version = "1.0.0" +ecosystem = "Go" +ignore = true +# effectiveUntil = 2022-11-09 # Optional exception expiry date +reason = "abc" + +[[PackageOverrides]] +name = "my-pkg" +version = "1.0.0" +ecosystem = "Go" +ignore = true +reason = "abc" +license.override = ["MIT", "0BSD"] diff --git a/pkg/config/config.go b/pkg/config/config.go index ddd660856a..cff4e40b6f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -23,9 +23,10 @@ type ConfigManager struct { } type Config struct { - IgnoredVulns []IgnoreEntry `toml:"IgnoredVulns"` - LoadPath string `toml:"LoadPath"` - GoVersionOverride string `toml:"GoVersionOverride"` + IgnoredVulns []IgnoreEntry `toml:"IgnoredVulns"` + PackageOverrides []PackageOverrideEntry `toml:"PackageOverrides"` + LoadPath string `toml:"LoadPath"` + GoVersionOverride string `toml:"GoVersionOverride"` } type IgnoreEntry struct { @@ -34,19 +35,67 @@ type IgnoreEntry struct { Reason string `toml:"reason"` } +type PackageOverrideEntry struct { + Name string `toml:"name"` + // If the version is empty, the entry applies to all versions. + Version string `toml:"version"` + Ecosystem string `toml:"ecosystem"` + Ignore bool `toml:"ignore"` + License License `toml:"license"` + EffectiveUntil time.Time `toml:"effectiveUntil"` + Reason string `toml:"reason"` +} + +type License struct { + Override []string `toml:"override"` +} + func (c *Config) ShouldIgnore(vulnID string) (bool, IgnoreEntry) { - index := slices.IndexFunc(c.IgnoredVulns, func(elem IgnoreEntry) bool { return elem.ID == vulnID }) + index := slices.IndexFunc(c.IgnoredVulns, func(e IgnoreEntry) bool { return e.ID == vulnID }) if index == -1 { return false, IgnoreEntry{} } ignoredLine := c.IgnoredVulns[index] - if ignoredLine.IgnoreUntil.IsZero() { + + return shouldIgnoreTimestamp(ignoredLine.IgnoreUntil), ignoredLine +} + +func (c *Config) filterPackageVersionEntries(name string, version string, ecosystem string, condition func(PackageOverrideEntry) bool) (bool, PackageOverrideEntry) { + index := slices.IndexFunc(c.PackageOverrides, func(e PackageOverrideEntry) bool { + if ecosystem != e.Ecosystem || name != e.Name { + return false + } + + return (version == e.Version || e.Version == "") && condition(e) + }) + if index == -1 { + return false, PackageOverrideEntry{} + } + ignoredLine := c.PackageOverrides[index] + + return shouldIgnoreTimestamp(ignoredLine.EffectiveUntil), ignoredLine +} + +func (c *Config) ShouldIgnorePackageVersion(name, version, ecosystem string) (bool, PackageOverrideEntry) { + return c.filterPackageVersionEntries(name, version, ecosystem, func(e PackageOverrideEntry) bool { + return e.Ignore + }) +} + +func (c *Config) ShouldOverridePackageVersionLicense(name, version, ecosystem string) (bool, PackageOverrideEntry) { + return c.filterPackageVersionEntries(name, version, ecosystem, func(e PackageOverrideEntry) bool { + return len(e.License.Override) > 0 + }) +} + +func shouldIgnoreTimestamp(ignoreUntil time.Time) bool { + if ignoreUntil.IsZero() { // If IgnoreUntil is not set, should ignore. - return true, ignoredLine + return true } // Should ignore if IgnoreUntil is still after current time // Takes timezone offsets into account if it is specified. otherwise it's using local time - return ignoredLine.IgnoreUntil.After(time.Now()), ignoredLine + return ignoreUntil.After(time.Now()) } // Sets the override config by reading the config file at configPath. diff --git a/pkg/config/config_internal_test.go b/pkg/config/config_internal_test.go index e271543f77..4663933e10 100644 --- a/pkg/config/config_internal_test.go +++ b/pkg/config/config_internal_test.go @@ -27,6 +27,25 @@ func TestTryLoadConfig(t *testing.T) { ID: "GO-2022-1059", }, }, + PackageOverrides: []PackageOverrideEntry{ + { + Name: "lib", + Version: "1.0.0", + Ecosystem: "Go", + Ignore: true, + Reason: "abc", + }, + { + Name: "my-pkg", + Version: "1.0.0", + Ecosystem: "Go", + Reason: "abc", + Ignore: true, + License: License{ + Override: []string{"MIT", "0BSD"}, + }, + }, + }, } testPaths := []testStruct{ { @@ -69,6 +88,9 @@ func TestTryLoadConfig(t *testing.T) { if !cmp.Equal(config.IgnoredVulns, testData.config.IgnoredVulns) { t.Errorf("Configs not equal: %+v != %+v", config, testData.config) } + if !cmp.Equal(config.PackageOverrides, testData.config.PackageOverrides) { + t.Errorf("Configs not equal: %+v != %+v", config, testData.config) + } if testData.configHasErr { if configErr == nil { t.Error("Config error not returned") @@ -191,3 +213,238 @@ func TestConfig_ShouldIgnore(t *testing.T) { }) } } + +func TestConfig_ShouldIgnorePackageVersion(t *testing.T) { + t.Parallel() + + type args struct { + name string + version string + ecosystem string + } + tests := []struct { + name string + config Config + args args + wantOk bool + wantEntry PackageOverrideEntry + }{ + { + name: "Version-level entry exists", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "lib1", + Version: "1.0.0", + Ecosystem: "Go", + Ignore: true, + EffectiveUntil: time.Time{}, + Reason: "abc", + }, + }, + }, + args: args{ + name: "lib1", + version: "1.0.0", + ecosystem: "Go", + }, + wantOk: true, + wantEntry: PackageOverrideEntry{ + Name: "lib1", + Version: "1.0.0", + Ecosystem: "Go", + Ignore: true, + EffectiveUntil: time.Time{}, + Reason: "abc", + }, + }, + { + name: "Package-level entry exists", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "lib1", + Ecosystem: "Go", + Ignore: true, + EffectiveUntil: time.Time{}, + Reason: "abc", + }, + }, + }, + args: args{ + name: "lib1", + version: "1.0.0", + ecosystem: "Go", + }, + wantOk: true, + wantEntry: PackageOverrideEntry{ + Name: "lib1", + Ecosystem: "Go", + Ignore: true, + EffectiveUntil: time.Time{}, + Reason: "abc", + }, + }, + { + name: "Entry doesn't exist", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "lib1", + Version: "2.0.0", + Ecosystem: "Go", + Ignore: false, + EffectiveUntil: time.Time{}, + Reason: "abc", + }, + { + Name: "lib2", + Version: "2.0.0", + Ignore: true, + Ecosystem: "Go", + EffectiveUntil: time.Time{}, + Reason: "abc", + }, + }, + }, + args: args{ + name: "lib1", + version: "2.0.0", + ecosystem: "Go", + }, + wantOk: false, + wantEntry: PackageOverrideEntry{}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + gotOk, gotEntry := tt.config.ShouldIgnorePackageVersion(tt.args.name, tt.args.version, tt.args.ecosystem) + if gotOk != tt.wantOk { + t.Errorf("ShouldIgnorePackageVersion() gotOk = %v, wantOk %v", gotOk, tt.wantOk) + } + if !reflect.DeepEqual(gotEntry, tt.wantEntry) { + t.Errorf("ShouldIgnorePackageVersion() gotEntry = %v, wantEntry %v", gotEntry, tt.wantEntry) + } + }) + } +} + +func TestConfig_ShouldOverridePackageVersionLicense(t *testing.T) { + t.Parallel() + + type args struct { + name string + version string + ecosystem string + } + tests := []struct { + name string + config Config + args args + wantOk bool + wantEntry PackageOverrideEntry + }{ + { + name: "Exact version entry exists", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "lib1", + Version: "1.0.0", + Ecosystem: "Go", + License: License{ + Override: []string{"mit"}, + }, + Reason: "abc", + }, + }, + }, + args: args{ + name: "lib1", + version: "1.0.0", + ecosystem: "Go", + }, + wantOk: true, + wantEntry: PackageOverrideEntry{ + Name: "lib1", + Version: "1.0.0", + Ecosystem: "Go", + License: License{ + Override: []string{"mit"}, + }, + Reason: "abc", + }, + }, + { + name: "Version entry doesn't exist", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "lib1", + Version: "1.0.0", + Ecosystem: "Go", + License: License{ + Override: []string{"mit"}, + }, + Reason: "abc", + }, + }, + }, + args: args{ + name: "lib1", + version: "1.0.1", + ecosystem: "Go", + }, + wantOk: false, + wantEntry: PackageOverrideEntry{}, + }, + { + name: "Name matches", + config: Config{ + PackageOverrides: []PackageOverrideEntry{ + { + Name: "lib1", + Ecosystem: "Go", + License: License{ + Override: []string{"mit"}, + }, + Reason: "abc", + }, + }, + }, + args: args{ + name: "lib1", + version: "1.0.1", + ecosystem: "Go", + }, + wantOk: true, + wantEntry: PackageOverrideEntry{ + Name: "lib1", + Ecosystem: "Go", + License: License{ + Override: []string{"mit"}, + }, + Reason: "abc", + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + gotOk, gotEntry := tt.config.ShouldOverridePackageVersionLicense(tt.args.name, tt.args.version, tt.args.ecosystem) + if gotOk != tt.wantOk { + t.Errorf("ShouldOverridePackageVersionLicense() gotOk = %v, wantOk %v", gotOk, tt.wantOk) + } + if !reflect.DeepEqual(gotEntry, tt.wantEntry) { + t.Errorf("ShouldOverridePackageVersionLicense() gotEntry = %v, wantEntry %v", gotEntry, tt.wantEntry) + } + }) + } +} diff --git a/pkg/osvscanner/__snapshots__/vulnerability_result_internal_test.snap b/pkg/osvscanner/__snapshots__/vulnerability_result_internal_test.snap new file mode 100755 index 0000000000..9d7d7001d5 --- /dev/null +++ b/pkg/osvscanner/__snapshots__/vulnerability_result_internal_test.snap @@ -0,0 +1,586 @@ + +[Test_assembleResult/group_vulnerabilities - 1] +{ + "results": [ + { + "source": { + "path": "dir/package-lock.json", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "pkg-1", + "version": "1.0.0", + "ecosystem": "npm" + }, + "vulnerabilities": [ + { + "modified": "0001-01-01T00:00:00Z", + "id": "GHSA-123", + "aliases": [ + "CVE-123" + ] + }, + { + "modified": "0001-01-01T00:00:00Z", + "id": "CVE-123" + } + ], + "groups": [ + { + "ids": [ + "CVE-123", + "GHSA-123" + ], + "aliases": [ + "CVE-123", + "GHSA-123" + ], + "max_severity": "" + } + ] + } + ] + }, + { + "source": { + "path": "other-dir/package-lock.json", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "pkg-3", + "version": "1.0.0", + "ecosystem": "npm" + }, + "vulnerabilities": [ + { + "modified": "0001-01-01T00:00:00Z", + "id": "GHSA-456" + } + ], + "groups": [ + { + "ids": [ + "GHSA-456" + ], + "aliases": [ + "GHSA-456" + ], + "max_severity": "" + } + ] + } + ] + } + ], + "experimental_config": { + "licenses": { + "summary": false, + "allowlist": null + } + } +} +--- + +[Test_assembleResult/group_vulnerabilities_with_all_packages_included - 1] +{ + "results": [ + { + "source": { + "path": "dir/package-lock.json", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "pkg-1", + "version": "1.0.0", + "ecosystem": "npm" + }, + "vulnerabilities": [ + { + "modified": "0001-01-01T00:00:00Z", + "id": "GHSA-123", + "aliases": [ + "CVE-123" + ] + }, + { + "modified": "0001-01-01T00:00:00Z", + "id": "CVE-123" + } + ], + "groups": [ + { + "ids": [ + "CVE-123", + "GHSA-123" + ], + "aliases": [ + "CVE-123", + "GHSA-123" + ], + "max_severity": "" + } + ] + }, + { + "package": { + "name": "pkg-2", + "version": "1.0.0", + "ecosystem": "npm" + } + } + ] + }, + { + "source": { + "path": "other-dir/package-lock.json", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "pkg-3", + "version": "1.0.0", + "ecosystem": "npm" + }, + "vulnerabilities": [ + { + "modified": "0001-01-01T00:00:00Z", + "id": "GHSA-456" + } + ], + "groups": [ + { + "ids": [ + "GHSA-456" + ], + "aliases": [ + "GHSA-456" + ], + "max_severity": "" + } + ] + } + ] + } + ], + "experimental_config": { + "licenses": { + "summary": false, + "allowlist": null + } + } +} +--- + +[Test_assembleResult/group_vulnerabilities_with_license_allowlist - 1] +{ + "results": [ + { + "source": { + "path": "dir/package-lock.json", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "pkg-1", + "version": "1.0.0", + "ecosystem": "npm" + }, + "vulnerabilities": [ + { + "modified": "0001-01-01T00:00:00Z", + "id": "GHSA-123", + "aliases": [ + "CVE-123" + ] + }, + { + "modified": "0001-01-01T00:00:00Z", + "id": "CVE-123" + } + ], + "groups": [ + { + "ids": [ + "CVE-123", + "GHSA-123" + ], + "aliases": [ + "CVE-123", + "GHSA-123" + ], + "max_severity": "" + } + ], + "licenses": [ + "MIT", + "0BSD" + ] + } + ] + }, + { + "source": { + "path": "other-dir/package-lock.json", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "pkg-3", + "version": "1.0.0", + "ecosystem": "npm" + }, + "vulnerabilities": [ + { + "modified": "0001-01-01T00:00:00Z", + "id": "GHSA-456" + } + ], + "groups": [ + { + "ids": [ + "GHSA-456" + ], + "aliases": [ + "GHSA-456" + ], + "max_severity": "" + } + ], + "licenses": [ + "UNKNOWN" + ], + "license_violations": [ + "UNKNOWN" + ] + } + ] + } + ], + "experimental_config": { + "licenses": { + "summary": false, + "allowlist": [ + "MIT", + "0BSD" + ] + } + } +} +--- + +[Test_assembleResult/group_vulnerabilities_with_license_allowlist_and_all_packages - 1] +{ + "results": [ + { + "source": { + "path": "dir/package-lock.json", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "pkg-1", + "version": "1.0.0", + "ecosystem": "npm" + }, + "vulnerabilities": [ + { + "modified": "0001-01-01T00:00:00Z", + "id": "GHSA-123", + "aliases": [ + "CVE-123" + ] + }, + { + "modified": "0001-01-01T00:00:00Z", + "id": "CVE-123" + } + ], + "groups": [ + { + "ids": [ + "CVE-123", + "GHSA-123" + ], + "aliases": [ + "CVE-123", + "GHSA-123" + ], + "max_severity": "" + } + ], + "licenses": [ + "MIT", + "0BSD" + ] + }, + { + "package": { + "name": "pkg-2", + "version": "1.0.0", + "ecosystem": "npm" + }, + "licenses": [ + "MIT" + ] + } + ] + }, + { + "source": { + "path": "other-dir/package-lock.json", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "pkg-3", + "version": "1.0.0", + "ecosystem": "npm" + }, + "vulnerabilities": [ + { + "modified": "0001-01-01T00:00:00Z", + "id": "GHSA-456" + } + ], + "groups": [ + { + "ids": [ + "GHSA-456" + ], + "aliases": [ + "GHSA-456" + ], + "max_severity": "" + } + ], + "licenses": [ + "UNKNOWN" + ], + "license_violations": [ + "UNKNOWN" + ] + } + ] + } + ], + "experimental_config": { + "licenses": { + "summary": false, + "allowlist": [ + "MIT", + "0BSD" + ] + } + } +} +--- + +[Test_assembleResult/group_vulnerabilities_with_license_allowlist_and_license_override - 1] +{ + "results": [ + { + "source": { + "path": "dir/package-lock.json", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "pkg-1", + "version": "1.0.0", + "ecosystem": "npm" + }, + "vulnerabilities": [ + { + "modified": "0001-01-01T00:00:00Z", + "id": "GHSA-123", + "aliases": [ + "CVE-123" + ] + }, + { + "modified": "0001-01-01T00:00:00Z", + "id": "CVE-123" + } + ], + "groups": [ + { + "ids": [ + "CVE-123", + "GHSA-123" + ], + "aliases": [ + "CVE-123", + "GHSA-123" + ], + "max_severity": "" + } + ], + "licenses": [ + "MIT", + "0BSD" + ] + } + ] + }, + { + "source": { + "path": "other-dir/package-lock.json", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "pkg-3", + "version": "1.0.0", + "ecosystem": "npm" + }, + "vulnerabilities": [ + { + "modified": "0001-01-01T00:00:00Z", + "id": "GHSA-456" + } + ], + "groups": [ + { + "ids": [ + "GHSA-456" + ], + "aliases": [ + "GHSA-456" + ], + "max_severity": "" + } + ], + "licenses": [ + "MIT" + ] + } + ] + } + ], + "experimental_config": { + "licenses": { + "summary": false, + "allowlist": [ + "MIT", + "0BSD" + ] + } + } +} +--- + +[Test_assembleResult/group_vulnerabilities_with_licenses - 1] +{ + "results": [ + { + "source": { + "path": "dir/package-lock.json", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "pkg-1", + "version": "1.0.0", + "ecosystem": "npm" + }, + "vulnerabilities": [ + { + "modified": "0001-01-01T00:00:00Z", + "id": "GHSA-123", + "aliases": [ + "CVE-123" + ] + }, + { + "modified": "0001-01-01T00:00:00Z", + "id": "CVE-123" + } + ], + "groups": [ + { + "ids": [ + "CVE-123", + "GHSA-123" + ], + "aliases": [ + "CVE-123", + "GHSA-123" + ], + "max_severity": "" + } + ], + "licenses": [ + "MIT", + "0BSD" + ] + }, + { + "package": { + "name": "pkg-2", + "version": "1.0.0", + "ecosystem": "npm" + }, + "licenses": [ + "MIT" + ] + } + ] + }, + { + "source": { + "path": "other-dir/package-lock.json", + "type": "lockfile" + }, + "packages": [ + { + "package": { + "name": "pkg-3", + "version": "1.0.0", + "ecosystem": "npm" + }, + "vulnerabilities": [ + { + "modified": "0001-01-01T00:00:00Z", + "id": "GHSA-456" + } + ], + "groups": [ + { + "ids": [ + "GHSA-456" + ], + "aliases": [ + "GHSA-456" + ], + "max_severity": "" + } + ], + "licenses": [ + "UNKNOWN" + ] + } + ] + } + ], + "experimental_config": { + "licenses": { + "summary": true, + "allowlist": [] + } + } +} +--- diff --git a/pkg/osvscanner/osvscanner.go b/pkg/osvscanner/osvscanner.go index 26aefd2812..290a50b9ca 100644 --- a/pkg/osvscanner/osvscanner.go +++ b/pkg/osvscanner/osvscanner.go @@ -639,6 +639,19 @@ func filterResults(r reporter.Reporter, results *models.VulnerabilityResults, co // Filters package-grouped vulnerabilities according to config, preserving ordering. Returns filtered package vulnerabilities. func filterPackageVulns(r reporter.Reporter, pkgVulns models.PackageVulns, configToUse config.Config) models.PackageVulns { + if ignore, ignoreLine := configToUse.ShouldIgnorePackageVersion(pkgVulns.Package.Name, pkgVulns.Package.Version, pkgVulns.Package.Ecosystem); ignore { + pkgString := fmt.Sprintf("%s/%s/%s", pkgVulns.Package.Ecosystem, pkgVulns.Package.Name, pkgVulns.Package.Version) + switch len(pkgVulns.Vulnerabilities) { + case 1: + r.Infof("1 vulnerability for the package %s has been filtered out because: %s\n", pkgString, ignoreLine.Reason) + default: + r.Infof("%d vulnerabilities for the package %s have been filtered out because: %s\n", len(pkgVulns.Vulnerabilities), pkgString, ignoreLine.Reason) + } + pkgVulns.Groups = nil + pkgVulns.Vulnerabilities = nil + + return pkgVulns + } ignoredVulns := map[string]struct{}{} // Iterate over groups first to remove all aliases of ignored vulnerabilities. var newGroups []models.GroupInfo @@ -846,7 +859,7 @@ func DoScan(actions ScannerActions, r reporter.Reporter) (models.VulnerabilityRe return models.VulnerabilityResults{}, err } } - results := buildVulnerabilityResults(r, filteredScannedPackages, vulnsResp, licensesResp, actions) + results := buildVulnerabilityResults(r, filteredScannedPackages, vulnsResp, licensesResp, actions, &configManager) filtered := filterResults(r, &results, &configManager, actions.ShowAllPackages) if filtered > 0 { diff --git a/pkg/osvscanner/vulnerability_result.go b/pkg/osvscanner/vulnerability_result.go index 7657dbc4c3..88cf7d02fe 100644 --- a/pkg/osvscanner/vulnerability_result.go +++ b/pkg/osvscanner/vulnerability_result.go @@ -1,11 +1,13 @@ package osvscanner import ( + "slices" "sort" "strings" "github.com/google/osv-scanner/internal/output" "github.com/google/osv-scanner/internal/sourceanalysis" + "github.com/google/osv-scanner/pkg/config" "github.com/google/osv-scanner/pkg/grouper" "github.com/google/osv-scanner/pkg/models" "github.com/google/osv-scanner/pkg/osv" @@ -22,6 +24,7 @@ func buildVulnerabilityResults( vulnsResp *osv.HydratedBatchedResponse, licensesResp [][]models.License, actions ScannerActions, + configManager *config.ConfigManager, ) models.VulnerabilityResults { results := models.VulnerabilityResults{ Results: []models.PackageSource{}, @@ -62,24 +65,34 @@ func buildVulnerabilityResults( pkg.Groups[i].MaxSeverity = output.MaxSeverity(group, pkg) } } - if len(actions.ScanLicensesAllowlist) > 0 { - pkg.Licenses = licensesResp[i] - allowlist := make(map[string]bool) - for _, license := range actions.ScanLicensesAllowlist { - allowlist[strings.ToLower(license)] = true + if actions.ScanLicensesSummary || len(actions.ScanLicensesAllowlist) > 0 { + configToUse := configManager.Get(r, rawPkg.Source.Path) + if override, entry := configToUse.ShouldOverridePackageVersionLicense(pkg.Package.Name, pkg.Package.Version, pkg.Package.Ecosystem); override { + overrideLicenses := make([]models.License, len(entry.License.Override)) + for j, license := range entry.License.Override { + overrideLicenses[j] = models.License(license) + } + r.Infof("overriding license for package %s/%s/%s with %s\n", pkg.Package.Ecosystem, pkg.Package.Name, pkg.Package.Version, strings.Join(entry.License.Override, ",")) + licensesResp[i] = overrideLicenses } - for _, license := range pkg.Licenses { - if !allowlist[strings.ToLower(string(license))] { - pkg.LicenseViolations = append(pkg.LicenseViolations, license) + if len(actions.ScanLicensesAllowlist) > 0 { + pkg.Licenses = licensesResp[i] + for _, license := range pkg.Licenses { + lowerLicense := strings.ToLower(string(license)) + if !slices.ContainsFunc(actions.ScanLicensesAllowlist, func(l string) bool { + return strings.ToLower(l) == lowerLicense + }) { + pkg.LicenseViolations = append(pkg.LicenseViolations, license) + } + } + if len(pkg.LicenseViolations) > 0 { + includePackage = true } } - if len(pkg.LicenseViolations) > 0 { - includePackage = true + if actions.ScanLicensesSummary { + pkg.Licenses = licensesResp[i] } } - if actions.ScanLicensesSummary { - pkg.Licenses = licensesResp[i] - } if includePackage { groupedBySource[rawPkg.Source] = append(groupedBySource[rawPkg.Source], pkg) } diff --git a/pkg/osvscanner/vulnerability_result_internal_test.go b/pkg/osvscanner/vulnerability_result_internal_test.go index 5781f4ee2e..a26ceed90f 100644 --- a/pkg/osvscanner/vulnerability_result_internal_test.go +++ b/pkg/osvscanner/vulnerability_result_internal_test.go @@ -1,9 +1,10 @@ package osvscanner import ( - "reflect" "testing" + "github.com/google/osv-scanner/internal/testutility" + "github.com/google/osv-scanner/pkg/config" "github.com/google/osv-scanner/pkg/lockfile" "github.com/google/osv-scanner/pkg/models" "github.com/google/osv-scanner/pkg/osv" @@ -18,6 +19,7 @@ func Test_assembleResult(t *testing.T) { vulnsResp *osv.HydratedBatchedResponse licensesResp [][]models.License actions ScannerActions + config *config.ConfigManager } packages := []scannedPackage{ { @@ -71,20 +73,25 @@ func Test_assembleResult(t *testing.T) { {models.License("MIT")}, {models.License("UNKNOWN")}, } + makeLicensesResp := func() [][]models.License { + cpy := make([][]models.License, len(licensesResp)) + copy(cpy, licensesResp) + + return cpy + } callAnalysisStates := make(map[string]bool) tests := []struct { name string args args - want models.VulnerabilityResults }{{ - name: "group vulnerabilities", + name: "group_vulnerabilities", args: args{ r: &reporter.VoidReporter{}, packages: packages, vulnsResp: vulnsResp, - licensesResp: licensesResp, + licensesResp: makeLicensesResp(), actions: ScannerActions{ ExperimentalScannerActions: ExperimentalScannerActions{ ShowAllPackages: false, @@ -92,72 +99,15 @@ func Test_assembleResult(t *testing.T) { }, CallAnalysisStates: callAnalysisStates, }, - }, - want: models.VulnerabilityResults{ - Results: []models.PackageSource{ - { - Source: models.SourceInfo{ - Path: "dir/package-lock.json", - Type: "lockfile", - }, - Packages: []models.PackageVulns{ - { - Package: models.PackageInfo{ - Name: "pkg-1", - Ecosystem: "npm", - Version: "1.0.0", - }, - Vulnerabilities: []models.Vulnerability{ - { - ID: "GHSA-123", - Aliases: []string{"CVE-123"}, - }, - { - ID: "CVE-123", - }, - }, - Groups: []models.GroupInfo{ - { - IDs: []string{"CVE-123", "GHSA-123"}, - Aliases: []string{"CVE-123", "GHSA-123"}, - }, - }, - }, - }, - }, - { - Source: models.SourceInfo{ - Path: "other-dir/package-lock.json", - Type: "lockfile", - }, - Packages: []models.PackageVulns{ - { - Package: models.PackageInfo{ - Name: "pkg-3", - Ecosystem: "npm", - Version: "1.0.0", - }, - Vulnerabilities: []models.Vulnerability{ - {ID: "GHSA-456"}, - }, - Groups: []models.GroupInfo{ - { - IDs: []string{"GHSA-456"}, - Aliases: []string{"GHSA-456"}, - }, - }, - }, - }, - }, - }, + config: &config.ConfigManager{}, }, }, { - name: "group vulnerabilities, with all packages included", + name: "group_vulnerabilities_with_all_packages_included", args: args{ r: &reporter.VoidReporter{}, packages: packages, vulnsResp: vulnsResp, - licensesResp: licensesResp, + licensesResp: makeLicensesResp(), actions: ScannerActions{ ExperimentalScannerActions: ExperimentalScannerActions{ ShowAllPackages: true, @@ -165,78 +115,15 @@ func Test_assembleResult(t *testing.T) { }, CallAnalysisStates: callAnalysisStates, }, - }, - want: models.VulnerabilityResults{ - Results: []models.PackageSource{ - { - Source: models.SourceInfo{ - Path: "dir/package-lock.json", - Type: "lockfile", - }, - Packages: []models.PackageVulns{ - { - Package: models.PackageInfo{ - Name: "pkg-1", - Ecosystem: "npm", - Version: "1.0.0", - }, - Vulnerabilities: []models.Vulnerability{ - { - ID: "GHSA-123", - Aliases: []string{"CVE-123"}, - }, - { - ID: "CVE-123", - }, - }, - Groups: []models.GroupInfo{ - { - IDs: []string{"CVE-123", "GHSA-123"}, - Aliases: []string{"CVE-123", "GHSA-123"}, - }, - }, - }, { - Package: models.PackageInfo{ - Name: "pkg-2", - Ecosystem: "npm", - Version: "1.0.0", - }, - }, - }, - }, - { - Source: models.SourceInfo{ - Path: "other-dir/package-lock.json", - Type: "lockfile", - }, - Packages: []models.PackageVulns{ - { - Package: models.PackageInfo{ - Name: "pkg-3", - Ecosystem: "npm", - Version: "1.0.0", - }, - Vulnerabilities: []models.Vulnerability{ - {ID: "GHSA-456"}, - }, - Groups: []models.GroupInfo{ - { - IDs: []string{"GHSA-456"}, - Aliases: []string{"GHSA-456"}, - }, - }, - }, - }, - }, - }, + config: &config.ConfigManager{}, }, }, { - name: "group vulnerabilities with licenses", + name: "group_vulnerabilities_with_licenses", args: args{ r: &reporter.VoidReporter{}, packages: packages, vulnsResp: vulnsResp, - licensesResp: licensesResp, + licensesResp: makeLicensesResp(), actions: ScannerActions{ ExperimentalScannerActions: ExperimentalScannerActions{ ShowAllPackages: true, @@ -245,87 +132,15 @@ func Test_assembleResult(t *testing.T) { }, CallAnalysisStates: callAnalysisStates, }, - }, - want: models.VulnerabilityResults{ - ExperimentalAnalysisConfig: models.ExperimentalAnalysisConfig{ - Licenses: models.ExperimentalLicenseConfig{ - Summary: true, - Allowlist: []models.License{}, - }, - }, - Results: []models.PackageSource{ - { - Source: models.SourceInfo{ - Path: "dir/package-lock.json", - Type: "lockfile", - }, - Packages: []models.PackageVulns{ - { - Package: models.PackageInfo{ - Name: "pkg-1", - Ecosystem: "npm", - Version: "1.0.0", - }, - Vulnerabilities: []models.Vulnerability{ - { - ID: "GHSA-123", - Aliases: []string{"CVE-123"}, - }, - { - ID: "CVE-123", - }, - }, - Groups: []models.GroupInfo{ - { - IDs: []string{"CVE-123", "GHSA-123"}, - Aliases: []string{"CVE-123", "GHSA-123"}, - }, - }, - Licenses: makeLicenses([]string{"MIT", "0BSD"}), - }, { - Package: models.PackageInfo{ - Name: "pkg-2", - Ecosystem: "npm", - Version: "1.0.0", - }, - Licenses: makeLicenses([]string{"MIT"}), - }, - }, - }, - { - Source: models.SourceInfo{ - Path: "other-dir/package-lock.json", - Type: "lockfile", - }, - Packages: []models.PackageVulns{ - { - Package: models.PackageInfo{ - Name: "pkg-3", - Ecosystem: "npm", - Version: "1.0.0", - }, - Vulnerabilities: []models.Vulnerability{ - {ID: "GHSA-456"}, - }, - Groups: []models.GroupInfo{ - { - IDs: []string{"GHSA-456"}, - Aliases: []string{"GHSA-456"}, - }, - }, - Licenses: makeLicenses([]string{"UNKNOWN"}), - }, - }, - }, - }, + config: &config.ConfigManager{}, }, }, { - name: "group vulnerabilities with license allowlist", + name: "group_vulnerabilities_with_license_allowlist", args: args{ r: &reporter.VoidReporter{}, packages: packages, vulnsResp: vulnsResp, - licensesResp: licensesResp, + licensesResp: makeLicensesResp(), actions: ScannerActions{ ExperimentalScannerActions: ExperimentalScannerActions{ ShowAllPackages: false, @@ -333,80 +148,44 @@ func Test_assembleResult(t *testing.T) { }, CallAnalysisStates: callAnalysisStates, }, + + config: &config.ConfigManager{}, }, - want: models.VulnerabilityResults{ - ExperimentalAnalysisConfig: models.ExperimentalAnalysisConfig{ - Licenses: models.ExperimentalLicenseConfig{ - Allowlist: []models.License{models.License("MIT"), models.License("0BSD")}, + }, { + name: "group_vulnerabilities_with_license_allowlist_and_license_override", + args: args{ + r: &reporter.VoidReporter{}, + packages: packages, + vulnsResp: vulnsResp, + licensesResp: makeLicensesResp(), + actions: ScannerActions{ + ExperimentalScannerActions: ExperimentalScannerActions{ + ShowAllPackages: false, + ScanLicensesAllowlist: []string{"MIT", "0BSD"}, }, + CallAnalysisStates: callAnalysisStates, }, - Results: []models.PackageSource{ - { - Source: models.SourceInfo{ - Path: "dir/package-lock.json", - Type: "lockfile", - }, - Packages: []models.PackageVulns{ - { - Package: models.PackageInfo{ - Name: "pkg-1", - Ecosystem: "npm", - Version: "1.0.0", - }, - Vulnerabilities: []models.Vulnerability{ - { - ID: "GHSA-123", - Aliases: []string{"CVE-123"}, - }, - { - ID: "CVE-123", - }, - }, - Groups: []models.GroupInfo{ - { - IDs: []string{"CVE-123", "GHSA-123"}, - Aliases: []string{"CVE-123", "GHSA-123"}, - }, - }, - Licenses: makeLicenses([]string{"MIT", "0BSD"}), - }, - }, - }, - { - Source: models.SourceInfo{ - Path: "other-dir/package-lock.json", - Type: "lockfile", - }, - Packages: []models.PackageVulns{ + config: &config.ConfigManager{ + OverrideConfig: &config.Config{ + PackageOverrides: []config.PackageOverrideEntry{ { - Package: models.PackageInfo{ - Name: "pkg-3", - Ecosystem: "npm", - Version: "1.0.0", + Name: "pkg-3", + Ecosystem: "npm", + License: config.License{ + Override: []string{"MIT"}, }, - Vulnerabilities: []models.Vulnerability{ - {ID: "GHSA-456"}, - }, - Groups: []models.GroupInfo{ - { - IDs: []string{"GHSA-456"}, - Aliases: []string{"GHSA-456"}, - }, - }, - Licenses: makeLicenses([]string{"UNKNOWN"}), - LicenseViolations: makeLicenses([]string{"UNKNOWN"}), }, }, }, }, }, }, { - name: "group vulnerabilities, with license allowlist and all packages", + name: "group_vulnerabilities_with_license_allowlist_and_all_packages", args: args{ r: &reporter.VoidReporter{}, packages: packages, vulnsResp: vulnsResp, - licensesResp: licensesResp, + licensesResp: makeLicensesResp(), actions: ScannerActions{ ExperimentalScannerActions: ExperimentalScannerActions{ ShowAllPackages: true, @@ -414,97 +193,15 @@ func Test_assembleResult(t *testing.T) { }, CallAnalysisStates: callAnalysisStates, }, - }, - want: models.VulnerabilityResults{ - ExperimentalAnalysisConfig: models.ExperimentalAnalysisConfig{ - Licenses: models.ExperimentalLicenseConfig{ - Allowlist: []models.License{models.License("MIT"), models.License("0BSD")}, - }, - }, - Results: []models.PackageSource{ - { - Source: models.SourceInfo{ - Path: "dir/package-lock.json", - Type: "lockfile", - }, - Packages: []models.PackageVulns{ - { - Package: models.PackageInfo{ - Name: "pkg-1", - Ecosystem: "npm", - Version: "1.0.0", - }, - Vulnerabilities: []models.Vulnerability{ - { - ID: "GHSA-123", - Aliases: []string{"CVE-123"}, - }, - { - ID: "CVE-123", - }, - }, - Groups: []models.GroupInfo{ - { - IDs: []string{"CVE-123", "GHSA-123"}, - Aliases: []string{"CVE-123", "GHSA-123"}, - }, - }, - Licenses: makeLicenses([]string{"MIT", "0BSD"}), - }, { - Package: models.PackageInfo{ - Name: "pkg-2", - Ecosystem: "npm", - Version: "1.0.0", - }, - Licenses: makeLicenses([]string{"MIT"}), - }, - }, - }, - { - Source: models.SourceInfo{ - Path: "other-dir/package-lock.json", - Type: "lockfile", - }, - Packages: []models.PackageVulns{ - { - Package: models.PackageInfo{ - Name: "pkg-3", - Ecosystem: "npm", - Version: "1.0.0", - }, - Vulnerabilities: []models.Vulnerability{ - {ID: "GHSA-456"}, - }, - Groups: []models.GroupInfo{ - { - IDs: []string{"GHSA-456"}, - Aliases: []string{"GHSA-456"}, - }, - }, - Licenses: makeLicenses([]string{"UNKNOWN"}), - LicenseViolations: makeLicenses([]string{"UNKNOWN"}), - }, - }, - }, - }, + config: &config.ConfigManager{}, }, }} for _, tt := range tests { tt := tt // Reinitialize for t.Parallel() t.Run(tt.name, func(t *testing.T) { t.Parallel() - if got := buildVulnerabilityResults(tt.args.r, tt.args.packages, tt.args.vulnsResp, tt.args.licensesResp, tt.args.actions); !reflect.DeepEqual(got, tt.want) { - t.Errorf("buildVulnerabilityResults() = %v,\nwant %v", got, tt.want) - } + got := buildVulnerabilityResults(tt.args.r, tt.args.packages, tt.args.vulnsResp, tt.args.licensesResp, tt.args.actions, tt.args.config) + testutility.NewSnapshot().MatchJSON(t, got) }) } } - -func makeLicenses(strLicenses []string) []models.License { - licenses := make([]models.License, len(strLicenses)) - for i, l := range strLicenses { - licenses[i] = models.License(l) - } - - return licenses -}