vulncheck: Source returns vulns for all GOOS/GOARCH

vulncheck.Source will return all matching vulns, regardless of the
current values of GOOS and GOARCH.

cmd/govulncheck displays the GOOS/GOARCH values if there are any.

Fixes golang/go#54841.

Change-Id: I98dbb75fe631416d0b53a8d4c851ee01a2d00c6c
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/432356
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/govulncheck/doc.go b/cmd/govulncheck/doc.go
index dc43c59..c65e8cc 100644
--- a/cmd/govulncheck/doc.go
+++ b/cmd/govulncheck/doc.go
@@ -15,12 +15,10 @@
 specification at https://go.dev/security/vuln/database.
 
 Govulncheck looks for vulnerabilities in Go programs using a specific build
-configuration. For analyzing source code, that configuration is the operating
-system, architecture, and Go version specified by GOOS, GOARCH, and the “go”
-command found on the PATH. For binaries, the build configuration is the one
-used to build the binary. Note that different build configurations may have
-different known vulnerabilities. For example, a dependency with a
-Windows-specific vulnerability will not be reported for a Linux build.
+configuration. For analyzing source code, that configuration is the Go version
+specified by the “go” command found on the PATH. For binaries, the build
+configuration is the one used to build the binary. Note that different build
+configurations may have different known vulnerabilities.
 
 Govulncheck must be built with Go version 1.18 or later.
 
@@ -86,13 +84,10 @@
     report false positives for code that is in the binary but unreachable.
   - There is no support for silencing vulnerability findings.
   - Govulncheck only reads binaries compiled with Go 1.18 and later.
-  - Govulncheck only reports vulnerabilities that apply to the current Go build
-    system and configuration (GOOS/GOARCH settings). For example, a
-    vulnerability that only applies to Linux will not be reported if
-    govulncheck is run on a Windows machine. A standard library vulnerability
-    that only applies for Go 1.18 will not be reported if the current Go
-    version is 1.19. See https://go.dev/issue/54841 for updates to this
-    limitation.
+  - Govulncheck only reports vulnerabilities that apply to the current Go
+    version. For example, a standard library vulnerability that only applies for
+    Go 1.18 will not be reported if the current Go version is 1.19. See
+    https://go.dev/issue/54841 for updates to this limitation.
 
 # Feedback
 
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index bd97ff9..240d719 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -17,6 +17,7 @@
 	"sort"
 	"strings"
 
+	"golang.org/x/exp/maps"
 	"golang.org/x/tools/go/buildutil"
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/vuln/client"
@@ -248,7 +249,7 @@
 			b.WriteString(indent("\n\nCall stacks in your code:\n", 2))
 			b.WriteString(indent(stacks, 6))
 		}
-		writeVulnerability(idx+1, id, details, b.String(), found, fixed)
+		writeVulnerability(idx+1, id, details, b.String(), found, fixed, platforms(v0.OSV))
 	}
 	if len(unaffected) > 0 {
 		fmt.Printf(`
@@ -263,21 +264,24 @@
 			found := foundVersion(vuln.ModPath, vuln.PkgPath, ci)
 			fixed := fixedVersion(vuln.PkgPath, vuln.OSV.Affected)
 			fmt.Println()
-			writeVulnerability(idx+1, vuln.OSV.ID, vuln.OSV.Details, "", found, fixed)
+			writeVulnerability(idx+1, vuln.OSV.ID, vuln.OSV.Details, "", found, fixed, platforms(vuln.OSV))
 		}
 	}
 }
 
-func writeVulnerability(idx int, id, details, callstack, found, fixed string) {
+func writeVulnerability(idx int, id, details, callstack, found, fixed, platforms string) {
 	if fixed == "" {
 		fixed = "N/A"
 	}
+	if platforms != "" {
+		platforms = "  Platforms: " + platforms + "\n"
+	}
 	fmt.Printf(`Vulnerability #%d: %s
 %s%s
   Found in: %s
   Fixed in: %s
-  More info: https://pkg.go.dev/vuln/%s
-`, idx, id, indent(details, 2), callstack, found, fixed, id)
+%s  More info: https://pkg.go.dev/vuln/%s
+`, idx, id, indent(details, 2), callstack, found, fixed, platforms, id)
 }
 
 func foundVersion(modulePath, pkgPath string, ci *govulncheck.CallInfo) string {
@@ -344,6 +348,24 @@
 	return b.String()
 }
 
+// platforms returns a string describing the GOOS/GOARCH pairs that the vuln affects.
+// If it affects all of them, it returns the empty string.
+func platforms(e *osv.Entry) string {
+	platforms := map[string]bool{}
+	for _, a := range e.Affected {
+		for _, p := range a.EcosystemSpecific.Imports {
+			for _, os := range p.GOOS {
+				for _, arch := range p.GOARCH {
+					platforms[os+"/"+arch] = true
+				}
+			}
+		}
+	}
+	keys := maps.Keys(platforms)
+	sort.Strings(keys)
+	return strings.Join(keys, ", ")
+}
+
 func isFile(path string) bool {
 	s, err := os.Stat(path)
 	if err != nil {
diff --git a/cmd/govulncheck/main_test.go b/cmd/govulncheck/main_test.go
index 0648f71..93095a5 100644
--- a/cmd/govulncheck/main_test.go
+++ b/cmd/govulncheck/main_test.go
@@ -108,6 +108,63 @@
 	}
 }
 
+func TestPlatforms(t *testing.T) {
+	for _, test := range []struct {
+		entry *osv.Entry
+		want  string
+	}{
+		{
+			entry: &osv.Entry{ID: "All"},
+			want:  "",
+		},
+		{
+			entry: &osv.Entry{
+				ID: "one-import",
+				Affected: []osv.Affected{{
+					Package: osv.Package{Name: "golang.org/vmod"},
+					Ranges:  osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.2.0"}}}},
+					EcosystemSpecific: osv.EcosystemSpecific{
+						Imports: []osv.EcosystemSpecificImport{{
+							GOOS:   []string{"windows", "linux"},
+							GOARCH: []string{"amd64", "wasm"},
+						}},
+					},
+				}},
+			},
+			want: "linux/amd64, linux/wasm, windows/amd64, windows/wasm",
+		},
+		{
+			entry: &osv.Entry{
+				ID: "two-imports",
+				Affected: []osv.Affected{{
+					Package: osv.Package{Name: "golang.org/vmod"},
+					Ranges:  osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.2.0"}}}},
+					EcosystemSpecific: osv.EcosystemSpecific{
+						Imports: []osv.EcosystemSpecificImport{
+							{
+								GOOS:   []string{"windows"},
+								GOARCH: []string{"amd64"},
+							},
+							{
+								GOOS:   []string{"linux"},
+								GOARCH: []string{"amd64"},
+							},
+						},
+					},
+				}},
+			},
+			want: "linux/amd64, windows/amd64",
+		},
+	} {
+		t.Run(test.entry.ID, func(t *testing.T) {
+			got := platforms(test.entry)
+			if got != test.want {
+				t.Errorf("got %q, want %q", got, test.want)
+			}
+		})
+	}
+}
+
 func TestIndent(t *testing.T) {
 	for _, test := range []struct {
 		name string
diff --git a/cmd/govulncheck/testdata/default.ct b/cmd/govulncheck/testdata/default.ct
index e86d477..b02c50b 100644
--- a/cmd/govulncheck/testdata/default.ct
+++ b/cmd/govulncheck/testdata/default.ct
@@ -43,4 +43,5 @@
   GJSON allowed a ReDoS (regular expression denial of service) attack.
   Found in: github.com/tidwall/gjson@v1.9.2
   Fixed in: github.com/tidwall/gjson@v1.9.3
+  Platforms: linux/amd64, windows/amd64
   More info: https://pkg.go.dev/vuln/GO-2021-0265
diff --git a/cmd/govulncheck/testdata/two-symbols.ct b/cmd/govulncheck/testdata/two-symbols.ct
index 8e0e087..3fc8095 100644
--- a/cmd/govulncheck/testdata/two-symbols.ct
+++ b/cmd/govulncheck/testdata/two-symbols.ct
@@ -40,4 +40,5 @@
   GJSON allowed a ReDoS (regular expression denial of service) attack.
   Found in: github.com/tidwall/gjson@v1.9.2
   Fixed in: github.com/tidwall/gjson@v1.9.3
+  Platforms: linux/amd64, windows/amd64
   More info: https://pkg.go.dev/vuln/GO-2021-0265
diff --git a/cmd/govulncheck/testdata/verbose.ct b/cmd/govulncheck/testdata/verbose.ct
index dcab77f..5c1e49b 100644
--- a/cmd/govulncheck/testdata/verbose.ct
+++ b/cmd/govulncheck/testdata/verbose.ct
@@ -46,4 +46,5 @@
   GJSON allowed a ReDoS (regular expression denial of service) attack.
   Found in: github.com/tidwall/gjson@v1.9.2
   Fixed in: github.com/tidwall/gjson@v1.9.3
+  Platforms: linux/amd64, windows/amd64
   More info: https://pkg.go.dev/vuln/GO-2021-0265
diff --git a/cmd/govulncheck/testdata/vulndb/github.com/tidwall/gjson.json b/cmd/govulncheck/testdata/vulndb/github.com/tidwall/gjson.json
index b682d88..423378d 100644
--- a/cmd/govulncheck/testdata/vulndb/github.com/tidwall/gjson.json
+++ b/cmd/govulncheck/testdata/vulndb/github.com/tidwall/gjson.json
@@ -157,7 +157,9 @@
                             "path": "github.com/tidwall/gjson",
                             "symbols": [
                                 "match.Match"
-                            ]
+                            ],
+			    "goos": ["linux", "windows"],
+			    "goarch": ["amd64"]
                         }
                     ]
                 }
diff --git a/vulncheck/source.go b/vulncheck/source.go
index deba9e7..533c6f2 100644
--- a/vulncheck/source.go
+++ b/vulncheck/source.go
@@ -59,8 +59,7 @@
 	if err != nil {
 		return nil, err
 	}
-	modVulns = modVulns.filter(lookupEnv("GOOS", runtime.GOOS), lookupEnv("GOARCH", runtime.GOARCH))
-
+	modVulns = modVulns.filter(cfg.GOOS, cfg.GOARCH)
 	result := &Result{
 		Imports:  &ImportGraph{Packages: make(map[int]*PkgNode)},
 		Requires: &RequireGraph{Modules: make(map[int]*ModNode)},
diff --git a/vulncheck/source_test.go b/vulncheck/source_test.go
index 77362ce..02cd78a 100644
--- a/vulncheck/source_test.go
+++ b/vulncheck/source_test.go
@@ -610,8 +610,8 @@
 		t.Fatal(err)
 	}
 
-	if len(result.Vulns) != 1 {
-		t.Errorf("want 1 Vuln, got %d", len(result.Vulns))
+	if g, w := len(result.Vulns), 1; g != w {
+		t.Errorf("got %d Vulns, want %d", g, w)
 	}
 
 	os.Setenv("GOOS", "freebsd")
@@ -622,8 +622,9 @@
 		t.Fatal(err)
 	}
 
-	if len(result.Vulns) != 0 {
-		t.Errorf("want 0 Vulns, got %d", len(result.Vulns))
+	// GOOS and GOARCH no longer affect the vulns.
+	if g, w := len(result.Vulns), 1; g != w {
+		t.Errorf("got %d Vulns, want %d", g, w)
 	}
 }
 
diff --git a/vulncheck/utils.go b/vulncheck/utils.go
index 868fa72..878b76c 100644
--- a/vulncheck/utils.go
+++ b/vulncheck/utils.go
@@ -8,7 +8,6 @@
 	"bytes"
 	"go/token"
 	"go/types"
-	"os"
 	"strings"
 
 	"golang.org/x/tools/go/callgraph"
@@ -233,13 +232,6 @@
 	return buf.String()
 }
 
-func lookupEnv(key, defaultValue string) string {
-	if v, ok := os.LookupEnv(key); ok {
-		return v
-	}
-	return defaultValue
-}
-
 // allSymbols returns all top-level functions and methods defined in pkg.
 func allSymbols(pkg *types.Package) []string {
 	var names []string
diff --git a/vulncheck/vulncheck.go b/vulncheck/vulncheck.go
index 3b72f2c..48fc8dc 100644
--- a/vulncheck/vulncheck.go
+++ b/vulncheck/vulncheck.go
@@ -30,6 +30,11 @@
 	// to vulncheck. If not provided, the current underlying Go version
 	// is used to detect vulnerabilities in Go standard library.
 	SourceGoVersion string
+
+	// Consider only vulnerabilities that apply to this OS and architecture.
+	// An empty string means "all" (don't filter).
+	// Applies only to Source.
+	GOOS, GOARCH string
 }
 
 // Package is a Go package for vulncheck analysis. It is a version of
@@ -369,21 +374,23 @@
 }
 
 func matchesPlatform(os, arch string, e osv.EcosystemSpecificImport) bool {
-	matchesOS := len(e.GOOS) == 0
-	matchesArch := len(e.GOARCH) == 0
-	for _, o := range e.GOOS {
-		if os == o {
-			matchesOS = true
-			break
+	return matchesPlatformComponent(os, e.GOOS) &&
+		matchesPlatformComponent(arch, e.GOARCH)
+}
+
+// matchesPlatformComponent reports whether a GOOS (or GOARCH)
+// matches a list of GOOS (or GOARCH) values from an osv.EcosystemSpecificImport.
+func matchesPlatformComponent(s string, ps []string) bool {
+	// An empty input or an empty GOOS or GOARCH list means "matches everything."
+	if s == "" || len(ps) == 0 {
+		return true
+	}
+	for _, p := range ps {
+		if s == p {
+			return true
 		}
 	}
-	for _, a := range e.GOARCH {
-		if arch == a {
-			matchesArch = true
-			break
-		}
-	}
-	return matchesOS && matchesArch
+	return false
 }
 
 // vulnsForPackage returns the vulnerabilities for the module which is the most