vulncheck: return the module list

As a convenience, have vulncheck return the list of modules it found
in the binary or source.

When vulcheck is run on source, this information duplicates information
in the requires graph, although in a simpler form.

When it is run on a binary, the requires graph is nil, so this
information is new.

The list of modules and their versions can be found in other ways in
both cases, but it would require duplicating work that vulncheck
already does.

This list will be used by the govulncheck command to display the
versions currently in use.

Change-Id: I35fe10a456ca7d5265340314a2aba402e648af10
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/399934
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
diff --git a/cmd/govulncheck/testdata/json.ct b/cmd/govulncheck/testdata/json.ct
index 90d856f..8cb6727 100644
--- a/cmd/govulncheck/testdata/json.ct
+++ b/cmd/govulncheck/testdata/json.ct
@@ -15,7 +15,21 @@
 		"Modules": {},
 		"Entries": null
 	},
-	"Vulns": null
+	"Vulns": null,
+	"Modules": [
+		{
+			"Path": "golang.org/x/text",
+			"Version": "v0.3.7",
+			"Dir": "",
+			"Replace": null
+		},
+		{
+			"Path": "novuln",
+			"Version": "",
+			"Dir": "",
+			"Replace": null
+		}
+	]
 }
 
 $ cdmodule vuln
@@ -350,5 +364,19 @@
 			"ImportSink": 1,
 			"RequireSink": 1
 		}
+	],
+	"Modules": [
+		{
+			"Path": "golang.org/x/text",
+			"Version": "v0.3.0",
+			"Dir": "",
+			"Replace": null
+		},
+		{
+			"Path": "vuln",
+			"Version": "",
+			"Dir": "",
+			"Replace": null
+		}
 	]
 }
diff --git a/vulncheck/binary.go b/vulncheck/binary.go
index cf04bee..2add537 100644
--- a/vulncheck/binary.go
+++ b/vulncheck/binary.go
@@ -26,12 +26,12 @@
 	if err != nil {
 		return nil, err
 	}
-	modVulns, err := fetchVulnerabilities(ctx, cfg.Client, convertModules(mods))
+	cmods := convertModules(mods)
+	modVulns, err := fetchVulnerabilities(ctx, cfg.Client, cmods)
 	if err != nil {
 		return nil, err
 	}
 	modVulns = modVulns.Filter(lookupEnv("GOOS", runtime.GOOS), lookupEnv("GOARCH", runtime.GOARCH))
-
 	result := &Result{}
 	for pkg, symbols := range packageSymbols {
 		if cfg.ImportsOnly {
@@ -40,6 +40,7 @@
 			addSymbolVulns(pkg, symbols, result, modVulns)
 		}
 	}
+	setModules(result, cmods)
 	return result, nil
 }
 
diff --git a/vulncheck/binary_test.go b/vulncheck/binary_test.go
index 4b6c0c3..2051c34 100644
--- a/vulncheck/binary_test.go
+++ b/vulncheck/binary_test.go
@@ -13,8 +13,11 @@
 	"os/exec"
 	"path/filepath"
 	"runtime"
+	"sort"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 	"golang.org/x/tools/go/packages/packagestest"
 )
 
@@ -129,6 +132,19 @@
 	if len(res.Vulns) != 1 {
 		t.Errorf("expected 1 vuln symbols got %d", len(res.Vulns))
 	}
+
+	// Check that the binary's modules are returned.
+	// The list does not include the module binary itself.
+	wantMods := []*Module{
+		{Path: "golang.org/amod", Version: "v1.1.3"},
+		{Path: "golang.org/bmod", Version: "v0.5.0"},
+		{Path: "golang.org/cmod", Version: "v1.1.3"},
+	}
+	gotMods := res.Modules
+	sort.Slice(gotMods, func(i, j int) bool { return gotMods[i].Path < gotMods[j].Path })
+	if diff := cmp.Diff(wantMods, gotMods, cmpopts.IgnoreFields(Module{}, "Dir")); diff != "" {
+		t.Errorf("modules mismatch (-want, +got):\n%s", diff)
+	}
 }
 
 // hasGoBuild reports whether the current system can build programs with “go build”
diff --git a/vulncheck/source.go b/vulncheck/source.go
index 9ceb993..ed9698e 100644
--- a/vulncheck/source.go
+++ b/vulncheck/source.go
@@ -43,7 +43,8 @@
 		}
 	}
 
-	modVulns, err := fetchVulnerabilities(ctx, cfg.Client, extractModules(pkgs))
+	mods := extractModules(pkgs)
+	modVulns, err := fetchVulnerabilities(ctx, cfg.Client, mods)
 	if err != nil {
 		return nil, err
 	}
@@ -56,6 +57,7 @@
 	}
 
 	vulnPkgModSlice(pkgs, modVulns, result)
+	setModules(result, mods)
 	if cfg.ImportsOnly {
 		return result, nil
 	}
@@ -64,10 +66,23 @@
 	entries := entryPoints(ssaPkgs)
 	cg := callGraph(prog, entries)
 	vulnCallGraphSlice(entries, modVulns, cg, result)
-
 	return result, nil
 }
 
+// Set r.Modules to an adjusted list of modules.
+func setModules(r *Result, mods []*Module) {
+	// Remove Dirs from modules; they aren't needed and complicate testing.
+	for _, m := range mods {
+		m.Dir = ""
+		if m.Replace != nil {
+			m.Replace.Dir = ""
+		}
+	}
+	// Sort for determinism.
+	sort.Slice(mods, func(i, j int) bool { return mods[i].Path < mods[j].Path })
+	r.Modules = mods
+}
+
 // pkgID is an id counter for nodes of Imports graph.
 var pkgID int = 0
 
diff --git a/vulncheck/source_test.go b/vulncheck/source_test.go
index 0ded4dc..ee42e52 100644
--- a/vulncheck/source_test.go
+++ b/vulncheck/source_test.go
@@ -9,8 +9,11 @@
 	"os"
 	"path"
 	"reflect"
+	"sort"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 	"golang.org/x/tools/go/packages/packagestest"
 	"golang.org/x/vuln/osv"
 )
@@ -166,6 +169,20 @@
 	if rgStrMap := reqGraphToStrMap(result.Requires); !reflect.DeepEqual(wantRequires, rgStrMap) {
 		t.Errorf("want %v requires graph; got %v", wantRequires, rgStrMap)
 	}
+
+	// Check that the source's modules are returned.
+	wantMods := []*Module{
+		{Path: "golang.org/amod", Version: "v1.1.3"},
+		{Path: "golang.org/bmod", Version: "v0.5.0"},
+		{Path: "golang.org/entry"},
+		{Path: "golang.org/wmod", Version: "v0.0.0"},
+		{Path: "golang.org/zmod", Version: "v0.0.0"},
+	}
+	gotMods := result.Modules
+	sort.Slice(gotMods, func(i, j int) bool { return gotMods[i].Path < gotMods[j].Path })
+	if diff := cmp.Diff(wantMods, gotMods, cmpopts.IgnoreFields(Module{}, "Dir")); diff != "" {
+		t.Errorf("modules mismatch (-want, +got):\n%s", diff)
+	}
 }
 
 // TestCallGraph checks for call graph vuln slicing correctness.
diff --git a/vulncheck/vulncheck.go b/vulncheck/vulncheck.go
index 8025ca4..ea728a4 100644
--- a/vulncheck/vulncheck.go
+++ b/vulncheck/vulncheck.go
@@ -123,6 +123,9 @@
 	// or whose packages are imported in Imports, or whose modules are required in
 	// Requires, have an entry in Vulns.
 	Vulns []*Vuln
+
+	// Modules are the modules that comprise the user code.
+	Modules []*Module
 }
 
 // Vuln provides information on how a vulnerability is affecting user code by