vuln/vulncheck: include stdlib in module list

Include the standard library as one of the modules of the binary or
source, so govulncheck can display the version where a vuln was found.

Updates #53948.

Change-Id: Ib0ab43517fc00f6edaa75675089a031bb37b9351
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/418794
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Julie Qiu <julieqiu@google.com>
diff --git a/cmd/govulncheck/main_command_118_test.go b/cmd/govulncheck/main_command_118_test.go
index 2e31a82..e45c797 100644
--- a/cmd/govulncheck/main_command_118_test.go
+++ b/cmd/govulncheck/main_command_118_test.go
@@ -59,6 +59,7 @@
 		cmd.Env = append(os.Environ(), "GOVULNDB=file://"+testDir+"/testdata/vulndb", "GOVERSION=go1.18")
 		out, err := cmd.CombinedOutput()
 		out = filterGoFilePaths(out)
+		out = filterStdlibVersions(out)
 		return out, err
 	}
 
@@ -78,9 +79,12 @@
 	ts.Run(t, *update)
 }
 
-var goFileRegexp = regexp.MustCompile(`[^\s"]*\.go[\s":]`)
+var (
+	goFileRegexp        = regexp.MustCompile(`[^\s"]*\.go[\s":]`)
+	stdlibVersionRegexp = regexp.MustCompile(`("Path": "stdlib",\s*"Version": ")v[^\s]+"`)
+)
 
-// filterGoFilePaths  modifies paths to Go files by replacing their directory with "...".
+// filterGoFilePaths modifies paths to Go files by replacing their directory with "...".
 // For example,/a/b/c.go becomes .../c.go .
 // This makes it possible to compare govulncheck output across systems, because
 // Go filenames include setup-specific paths.
@@ -90,3 +94,11 @@
 		return []byte(fmt.Sprintf(`.../%s%c`, filepath.Base(s[1:len(s)-1]), s[len(s)-1]))
 	})
 }
+
+// filterStdlibVersions removes Go standard library versions from JSON output,
+// since they depend on the system running the test. Some have different
+// versions than others, and on some we are unable to extract a version from
+// the binary so the version is empty.
+func filterStdlibVersions(data []byte) []byte {
+	return stdlibVersionRegexp.ReplaceAll(data, []byte(`${1}"`))
+}
diff --git a/cmd/govulncheck/testdata/json-binary.ct b/cmd/govulncheck/testdata/json-binary.ct
index 30e6e4b..27ea33b 100644
--- a/cmd/govulncheck/testdata/json-binary.ct
+++ b/cmd/govulncheck/testdata/json-binary.ct
@@ -10,6 +10,12 @@
 			"Version": "v0.3.7",
 			"Dir": "",
 			"Replace": null
+		},
+		{
+			"Path": "stdlib",
+			"Version": "",
+			"Dir": "",
+			"Replace": null
 		}
 	]
 }
@@ -86,6 +92,12 @@
 			"Version": "v0.3.0",
 			"Dir": "",
 			"Replace": null
+		},
+		{
+			"Path": "stdlib",
+			"Version": "",
+			"Dir": "",
+			"Replace": null
 		}
 	]
 }
diff --git a/cmd/govulncheck/testdata/json.ct b/cmd/govulncheck/testdata/json.ct
index f93fd2c..57defcf 100644
--- a/cmd/govulncheck/testdata/json.ct
+++ b/cmd/govulncheck/testdata/json.ct
@@ -30,6 +30,12 @@
 			"Version": "v0.3.7",
 			"Dir": "",
 			"Replace": null
+		},
+		{
+			"Path": "stdlib",
+			"Version": "",
+			"Dir": "",
+			"Replace": null
 		}
 	]
 }
@@ -202,6 +208,12 @@
 			"Version": "v0.3.0",
 			"Dir": "",
 			"Replace": null
+		},
+		{
+			"Path": "stdlib",
+			"Version": "",
+			"Dir": "",
+			"Replace": null
 		}
 	]
 }
diff --git a/cmd/govulncheck/testdata/stdlib.ct b/cmd/govulncheck/testdata/stdlib.ct
index 913e73f..743a4c6 100644
--- a/cmd/govulncheck/testdata/stdlib.ct
+++ b/cmd/govulncheck/testdata/stdlib.ct
@@ -13,5 +13,5 @@
 Call stacks in your code:
  golang.org/stdvuln.main calls archive/zip.OpenReader
 
-Found in:  archive/zip
+Found in:  archive/zip@v1.18.0
 More info: https://pkg.go.dev/vuln/STD
diff --git a/vulncheck/binary.go b/vulncheck/binary.go
index f0eac7f..034ba4e 100644
--- a/vulncheck/binary.go
+++ b/vulncheck/binary.go
@@ -32,8 +32,7 @@
 	// set the stdlib version for detection of vulns in the standard library
 	// TODO(#53740): what if Go version is not in semver format?
 	stdlibModule.Version = goTagToSemver(goVersion)
-	// Add "stdlib" module. Even if stdlib is not used, which is unlikely, it
-	// won't appear in vulncheck.Modules nor other results.
+	// Add "stdlib" module.
 	cmods = append(cmods, stdlibModule)
 
 	modVulns, err := fetchVulnerabilities(ctx, cfg.Client, cmods)
diff --git a/vulncheck/binary_test.go b/vulncheck/binary_test.go
index c0316df..0d91820 100644
--- a/vulncheck/binary_test.go
+++ b/vulncheck/binary_test.go
@@ -172,6 +172,7 @@
 		{Path: "golang.org/amod", Version: "v1.1.3"},
 		{Path: "golang.org/bmod", Version: "v0.5.0"},
 		{Path: "golang.org/cmod", Version: "v1.1.3"},
+		stdlibModule,
 	}
 	gotMods := res.Modules
 	sort.Slice(gotMods, func(i, j int) bool { return gotMods[i].Path < gotMods[j].Path })
diff --git a/vulncheck/source.go b/vulncheck/source.go
index 9015c9e..c5d9198 100644
--- a/vulncheck/source.go
+++ b/vulncheck/source.go
@@ -93,13 +93,7 @@
 	}
 	// Sort for determinism.
 	sort.Slice(mods, func(i, j int) bool { return mods[i].Path < mods[j].Path })
-	for _, m := range mods {
-		// stdlib is not a module per se, so we don't
-		// save it as module comprising user code.
-		if m != stdlibModule {
-			r.Modules = append(r.Modules, m)
-		}
-	}
+	r.Modules = append(r.Modules, mods...)
 }
 
 // pkgID is an id counter for nodes of Imports graph.
diff --git a/vulncheck/source_test.go b/vulncheck/source_test.go
index 9229ff7..10f6f73 100644
--- a/vulncheck/source_test.go
+++ b/vulncheck/source_test.go
@@ -175,6 +175,7 @@
 		{Path: "golang.org/entry"},
 		{Path: "golang.org/wmod", Version: "v0.0.0"},
 		{Path: "golang.org/zmod", Version: "v0.0.0"},
+		{Path: "stdlib", Version: "v1.18.0"},
 	}
 	gotMods := result.Modules
 	sort.Slice(gotMods, func(i, j int) bool { return gotMods[i].Path < gotMods[j].Path })
@@ -307,6 +308,7 @@
 		{Path: "golang.org/entry"},
 		{Path: "golang.org/imod1", Version: "v0.0.0"},
 		{Path: "golang.org/imod2", Version: "v0.0.0"},
+		stdlibModule,
 	}
 	gotMods := result.Modules
 	sort.Slice(gotMods, func(i, j int) bool { return gotMods[i].Path < gotMods[j].Path })