vulndb/internal/audit: allow "" as a valid module version to check

Top level packages can have module "" version. We want to consider them
as valid versions for which all vulns should be applicable. Using just
semver checking does not work, so this CL addresses that.

Change-Id: I07479909d4c663416d6e91f0e10bb9fe87e413b5
Reviewed-on: https://go-review.googlesource.com/c/exp/+/342552
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Trust: Zvonimir Pavlinovic <zpavlinovic@google.com>
diff --git a/vulndb/internal/audit/detect.go b/vulndb/internal/audit/detect.go
index 5c37973..8e1c0d0 100644
--- a/vulndb/internal/audit/detect.go
+++ b/vulndb/internal/audit/detect.go
@@ -127,14 +127,27 @@
 func (mv ModuleVulnerabilities) Filter(os, arch string) ModuleVulnerabilities {
 	var filteredMod ModuleVulnerabilities
 	for _, mod := range mv {
+		module := mod.mod
+		modVersion := module.Version
+		if module.Replace != nil {
+			modVersion = module.Replace.Version
+		}
 		var filteredVulns []*osv.Entry
 		for _, v := range mod.vulns {
-			if matchesPlatform(os, arch, v.EcosystemSpecific) {
+			// A module version is affected if
+			//  - it is incuded in one of the affected version ranges
+			//  - module version is ""
+			//  The latter means the module version is not available, which
+			//  should happen only for top-level packages for which we want
+			//  to be more conservative.
+			//  TODO: issue warning for "" cases above?
+			affectsVersion := modVersion == "" || v.Affects.AffectsSemver(modVersion)
+			if affectsVersion && matchesPlatform(os, arch, v.EcosystemSpecific) {
 				filteredVulns = append(filteredVulns, v)
 			}
 		}
 		filteredMod = append(filteredMod, modVulns{
-			mod:   mod.mod,
+			mod:   module,
 			vulns: filteredVulns,
 		})
 	}
diff --git a/vulndb/internal/audit/detect_test.go b/vulndb/internal/audit/detect_test.go
index 43db9dc..32cccba 100644
--- a/vulndb/internal/audit/detect_test.go
+++ b/vulndb/internal/audit/detect_test.go
@@ -32,9 +32,9 @@
 				Version: "v1.0.0",
 			},
 			vulns: []*osv.Entry{
-				{ID: "a"},
-				{ID: "b", EcosystemSpecific: osv.GoSpecific{GOOS: []string{"windows", "linux"}}},
-				{ID: "c", EcosystemSpecific: osv.GoSpecific{GOARCH: []string{"arm64", "amd64"}}},
+				{ID: "a", Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "v2.0.0"}}}},
+				{ID: "b", EcosystemSpecific: osv.GoSpecific{GOOS: []string{"windows", "linux"}}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Introduced: "v1.0.1"}}}},
+				{ID: "c", EcosystemSpecific: osv.GoSpecific{GOARCH: []string{"arm64", "amd64"}}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Introduced: "v1.0.0", Fixed: "v1.0.1"}}}},
 				{ID: "d", EcosystemSpecific: osv.GoSpecific{GOOS: []string{"windows"}}},
 			},
 		},
@@ -46,10 +46,20 @@
 			vulns: []*osv.Entry{
 				{ID: "e", EcosystemSpecific: osv.GoSpecific{GOARCH: []string{"arm64"}}},
 				{ID: "f", EcosystemSpecific: osv.GoSpecific{GOOS: []string{"linux"}}},
-				{ID: "g", EcosystemSpecific: osv.GoSpecific{GOARCH: []string{"amd64"}}},
+				{ID: "g", EcosystemSpecific: osv.GoSpecific{GOARCH: []string{"amd64"}}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Introduced: "v0.0.1", Fixed: "v2.0.1"}}}},
 				{ID: "h", EcosystemSpecific: osv.GoSpecific{GOOS: []string{"windows"}, GOARCH: []string{"amd64"}}},
 			},
 		},
+		{
+			mod: &packages.Module{
+				Path: "example.mod/c",
+			},
+			vulns: []*osv.Entry{
+				{ID: "i", EcosystemSpecific: osv.GoSpecific{GOARCH: []string{"amd64"}}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Introduced: "v0.0.0"}}}},
+				{ID: "j", EcosystemSpecific: osv.GoSpecific{GOARCH: []string{"amd64"}}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "v3.0.0"}}}},
+				{ID: "k"},
+			},
+		},
 	}
 
 	filtered := mv.Filter("linux", "amd64")
@@ -61,9 +71,8 @@
 				Version: "v1.0.0",
 			},
 			vulns: []*osv.Entry{
-				{ID: "a"},
-				{ID: "b", EcosystemSpecific: osv.GoSpecific{GOOS: []string{"windows", "linux"}}},
-				{ID: "c", EcosystemSpecific: osv.GoSpecific{GOARCH: []string{"arm64", "amd64"}}},
+				{ID: "a", Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "v2.0.0"}}}},
+				{ID: "c", EcosystemSpecific: osv.GoSpecific{GOARCH: []string{"arm64", "amd64"}}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Introduced: "v1.0.0", Fixed: "v1.0.1"}}}},
 			},
 		},
 		{
@@ -73,9 +82,18 @@
 			},
 			vulns: []*osv.Entry{
 				{ID: "f", EcosystemSpecific: osv.GoSpecific{GOOS: []string{"linux"}}},
-				{ID: "g", EcosystemSpecific: osv.GoSpecific{GOARCH: []string{"amd64"}}},
+				{ID: "g", EcosystemSpecific: osv.GoSpecific{GOARCH: []string{"amd64"}}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Introduced: "v0.0.1", Fixed: "v2.0.1"}}}},
 			},
 		},
+		{
+			mod: &packages.Module{
+				Path: "example.mod/c",
+			},
+			vulns: []*osv.Entry{
+				{ID: "i", EcosystemSpecific: osv.GoSpecific{GOARCH: []string{"amd64"}}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Introduced: "v0.0.0"}}}},
+				{ID: "j", EcosystemSpecific: osv.GoSpecific{GOARCH: []string{"amd64"}}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "v3.0.0"}}}},
+				{ID: "k"}},
+		},
 	}
 	if !reflect.DeepEqual(filtered, expected) {
 		t.Fatalf("Filter returned unexpected results, got:\n%s\nwant:\n%s", moduleVulnerabilitiesToString(filtered), moduleVulnerabilitiesToString(expected))
diff --git a/vulndb/internal/audit/vulnerability.go b/vulndb/internal/audit/vulnerability.go
index 142bb57..def73d1 100644
--- a/vulndb/internal/audit/vulnerability.go
+++ b/vulndb/internal/audit/vulnerability.go
@@ -18,29 +18,19 @@
 	mv := ModuleVulnerabilities{}
 	for _, mod := range modules {
 		modPath := mod.Path
-		modVersion := mod.Version
 		if mod.Replace != nil {
 			modPath = mod.Replace.Path
-			modVersion = mod.Replace.Version
 		}
 		vulns, err := client.Get([]string{modPath})
 		if err != nil {
 			return nil, err
 		}
-		// TODO(rolandshoemaker): we may want to consider moving this functionality into
-		// ModuleVulnerabilities.Filter, consolidating the filtering logic in one place.
-		var filteredVulns []*osv.Entry
-		for _, v := range vulns {
-			if v.Affects.AffectsSemver(modVersion) {
-				filteredVulns = append(filteredVulns, v)
-			}
-		}
-		if len(filteredVulns) == 0 {
+		if len(vulns) == 0 {
 			continue
 		}
 		mv = append(mv, modVulns{
 			mod:   mod,
-			vulns: filteredVulns,
+			vulns: vulns,
 		})
 	}
 	return mv, nil
diff --git a/vulndb/internal/audit/vulnerability_test.go b/vulndb/internal/audit/vulnerability_test.go
index 79a1189..06783e7 100644
--- a/vulndb/internal/audit/vulnerability_test.go
+++ b/vulndb/internal/audit/vulnerability_test.go
@@ -23,18 +23,15 @@
 func TestFetchVulnerabilities(t *testing.T) {
 	mc := &mockClient{
 		ret: map[string][]*osv.Entry{
-			"example.mod/a": {
-				{ID: "a", Package: osv.Package{Name: "example.mod/a"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "v2.0.0"}}}},
-				{ID: "b", Package: osv.Package{Name: "example.mod/a"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "v1.0.0"}}}},
-			},
-			"example.mod/b": {{ID: "c", Package: osv.Package{Name: "example.mod/b"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "v1.0.0"}}}}},
+			"example.mod/a": {{ID: "a", Package: osv.Package{Name: "example.mod/a"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "v2.0.0"}}}}},
+			"example.mod/b": {{ID: "b", Package: osv.Package{Name: "example.mod/b"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "v1.1.1"}}}}},
 			"example.mod/d": {{ID: "c", Package: osv.Package{Name: "example.mod/d"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "v2.0.0"}}}}},
 		},
 	}
 
 	mv, err := FetchVulnerabilities(mc, []*packages.Module{
 		{Path: "example.mod/a", Version: "v1.0.0"},
-		{Path: "example.mod/b", Version: "v1.0.0"},
+		{Path: "example.mod/b", Version: "v1.0.4"},
 		{Path: "example.mod/c", Replace: &packages.Module{Path: "example.mod/d", Version: "v1.0.0"}, Version: "v2.0.0"},
 	})
 	if err != nil {
@@ -49,6 +46,12 @@
 			},
 		},
 		{
+			mod: &packages.Module{Path: "example.mod/b", Version: "v1.0.4"},
+			vulns: []*osv.Entry{
+				{ID: "b", Package: osv.Package{Name: "example.mod/b"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "v1.1.1"}}}},
+			},
+		},
+		{
 			mod: &packages.Module{Path: "example.mod/c", Replace: &packages.Module{Path: "example.mod/d", Version: "v1.0.0"}, Version: "v2.0.0"},
 			vulns: []*osv.Entry{
 				{ID: "c", Package: osv.Package{Name: "example.mod/d"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "v2.0.0"}}}},