vulncheck: add module path to vulns in binary mode

When analyzing a binary, populate Vuln.ModPath with a module path that
corresponds to the package path.

Change-Id: Iea3405a66bd9f8b2cbc58e8d3809bc2911c23559
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/400117
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
diff --git a/vulncheck/binary.go b/vulncheck/binary.go
index 0d1d331..273a3fd 100644
--- a/vulncheck/binary.go
+++ b/vulncheck/binary.go
@@ -11,6 +11,7 @@
 	"context"
 	"io"
 	"runtime"
+	"strings"
 
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/vuln/internal/derrors"
@@ -34,10 +35,11 @@
 	modVulns = modVulns.filter(lookupEnv("GOOS", runtime.GOOS), lookupEnv("GOARCH", runtime.GOARCH))
 	result := &Result{}
 	for pkg, symbols := range packageSymbols {
+		mod := findPackageModule(pkg, cmods)
 		if cfg.ImportsOnly {
-			addImportsOnlyVulns(pkg, symbols, result, modVulns)
+			addImportsOnlyVulns(pkg, mod, symbols, result, modVulns)
 		} else {
-			addSymbolVulns(pkg, symbols, result, modVulns)
+			addSymbolVulns(pkg, mod, symbols, result, modVulns)
 		}
 	}
 	setModules(result, cmods)
@@ -46,7 +48,7 @@
 
 // addImportsOnlyVulns adds Vuln entries to result in imports only mode, i.e., for each vulnerable symbol
 // of pkg.
-func addImportsOnlyVulns(pkg string, symbols []string, result *Result, modVulns moduleVulnerabilities) {
+func addImportsOnlyVulns(pkg, mod string, symbols []string, result *Result, modVulns moduleVulnerabilities) {
 	for _, osv := range modVulns.vulnsForPackage(pkg) {
 		for _, affected := range osv.Affected {
 			if affected.Package.Name != pkg {
@@ -70,7 +72,7 @@
 					OSV:     osv,
 					Symbol:  symbol,
 					PkgPath: pkg,
-					// TODO(zpavlinovic): infer mod path from PkgPath and modules?
+					ModPath: mod,
 				}
 				result.Vulns = append(result.Vulns, vuln)
 			}
@@ -79,7 +81,7 @@
 }
 
 // addSymbolVulns adds Vuln entries to result for every symbol of pkg in the binary that is vulnerable.
-func addSymbolVulns(pkg string, symbols []string, result *Result, modVulns moduleVulnerabilities) {
+func addSymbolVulns(pkg, mod string, symbols []string, result *Result, modVulns moduleVulnerabilities) {
 	for _, symbol := range symbols {
 		for _, osv := range modVulns.vulnsForSymbol(pkg, symbol) {
 			for _, affected := range osv.Affected {
@@ -90,7 +92,7 @@
 					OSV:     osv,
 					Symbol:  symbol,
 					PkgPath: pkg,
-					// TODO(zpavlinovic): infer mod path from PkgPath and modules?
+					ModPath: mod,
 				}
 				result.Vulns = append(result.Vulns, vuln)
 				break
@@ -102,7 +104,7 @@
 func convertModules(mods []*packages.Module) []*Module {
 	vmods := make([]*Module, len(mods))
 	// TODO(github.com/golang/go/issues/50030): should we share unique
-	// modules? Not needed nowas module info is not returned by Binary.
+	// modules? Not needed now as module info is not returned by Binary.
 	for i, mod := range mods {
 		vmods[i] = &Module{
 			Path:    mod.Path,
@@ -117,3 +119,16 @@
 	}
 	return vmods
 }
+
+// findPackageModule returns the path of a module that could contain the import
+// path pkg. It uses paths only. It is possible but unlikely for a package path
+// to match two or more different module paths. We just take the first one.
+// If no module path matches, findPackageModule returns the empty string.
+func findPackageModule(pkg string, mods []*Module) string {
+	for _, m := range mods {
+		if pkg == m.Path || strings.HasPrefix(pkg, m.Path+"/") {
+			return m.Path
+		}
+	}
+	return ""
+}
diff --git a/vulncheck/binary_test.go b/vulncheck/binary_test.go
index 2051c34..c610b1c 100644
--- a/vulncheck/binary_test.go
+++ b/vulncheck/binary_test.go
@@ -118,8 +118,16 @@
 	// In importsOnly mode, all three vulnerable symbols
 	// {avuln.VulnData.Vuln1, avuln.VulnData.Vuln2, bvuln.Vuln}
 	// should be detected.
-	if len(res.Vulns) != 3 {
-		t.Errorf("expected 3 vuln symbols; got %d", len(res.Vulns))
+	wantVulns := []*Vuln{
+		{Symbol: "Vuln", PkgPath: "golang.org/bmod/bvuln", ModPath: "golang.org/bmod"},
+		{Symbol: "VulnData.Vuln1", PkgPath: "golang.org/amod/avuln", ModPath: "golang.org/amod"},
+		{Symbol: "VulnData.Vuln2", PkgPath: "golang.org/amod/avuln", ModPath: "golang.org/amod"},
+	}
+	diff := cmp.Diff(wantVulns, res.Vulns,
+		cmpopts.IgnoreFields(Vuln{}, "OSV"),
+		cmpopts.SortSlices(func(v1, v2 *Vuln) bool { return v1.Symbol < v2.Symbol }))
+	if diff != "" {
+		t.Errorf("vulns mismatch (-want, +got)\n%s", diff)
 	}
 
 	// Test the symbols (non-import mode)