internal/report: FixLink to FixLinks (a slice)

Changes FixLink in the Module struct to FixLinks, a slice of strings.

Change-Id: I0f6e2d8c1184aaf4da145088d996fb116b056099
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/562235
Reviewed-by: Tatiana Bradley <tatianabradley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/data/reports/GO-2024-2497.yaml b/data/reports/GO-2024-2497.yaml
index 61387a2..615cdb3 100644
--- a/data/reports/GO-2024-2497.yaml
+++ b/data/reports/GO-2024-2497.yaml
@@ -30,7 +30,8 @@
             - gatewayFrontend.Solve
             - NewBridgeForwarder
             - llbBridgeForwarder.NewContainer
-      fix_link: https://github.com/moby/buildkit/commit/5026d95aa3336e97cfe46e3764f52d08bac7a10e
+      fix_links:
+        - https://github.com/moby/buildkit/commit/5026d95aa3336e97cfe46e3764f52d08bac7a10e
 summary: Privilege escalation in github.com/moby/buildkit
 description: |-
     BuildKit provides APIs for running interactive containers based on built images.
diff --git a/internal/report/report.go b/internal/report/report.go
index f40b76b..f29800c 100644
--- a/internal/report/report.go
+++ b/internal/report/report.go
@@ -56,7 +56,7 @@
 	Packages             []*Package `yaml:",omitempty"`
 	// Used to automatically determine vulnerable symbols for a given module.
 	// Will be auto-populated from the reports "fix" link if none is specified.
-	FixLink string `yaml:"fix_link,omitempty"`
+	FixLinks []string `yaml:"fix_links,omitempty"`
 }
 
 type Package struct {
@@ -270,6 +270,18 @@
 	return pkgs
 }
 
+// CommitLinks returns all commit fix links in report.References
+func (r *Report) CommitLinks() (links []string) {
+	for _, ref := range r.References {
+		if ref.Type == osv.ReferenceTypeFix {
+			if strings.Contains(ref.URL, "commit") {
+				links = append(links, ref.URL)
+			}
+		}
+	}
+	return links
+}
+
 // Aliases returns all aliases (e.g., CVEs, GHSAs) for a report.
 func (r *Report) Aliases() []string {
 	return append(r.AllCVEs(), r.GHSAs...)
diff --git a/internal/symbols/populate.go b/internal/symbols/populate.go
index fedcf70..c10e88a 100644
--- a/internal/symbols/populate.go
+++ b/internal/symbols/populate.go
@@ -10,7 +10,7 @@
 	"path/filepath"
 	"strings"
 
-	"golang.org/x/vulndb/internal/osv"
+	"golang.org/x/exp/slices"
 	"golang.org/x/vulndb/internal/report"
 )
 
@@ -21,67 +21,67 @@
 }
 
 func populate(r *report.Report, patched func(string, string, string) (map[string][]string, error)) error {
-	var defaultFixes []string
-
-	for _, ref := range r.References {
-		if ref.Type == osv.ReferenceTypeFix {
-			if filepath.Base(filepath.Dir(ref.URL)) == "commit" {
-				defaultFixes = append(defaultFixes, ref.URL)
-			}
-		}
-	}
-	if len(defaultFixes) == 0 {
-		return fmt.Errorf("no commit fix links found")
-	}
 	var errs []error
 	for _, mod := range r.Modules {
-		hasFixLink := mod.FixLink != ""
-		if hasFixLink {
-			defaultFixes = append(defaultFixes, mod.FixLink)
-		}
-		numFixedSymbols := make([]int, len(defaultFixes))
-		for i, fixLink := range defaultFixes {
-			fixHash := filepath.Base(fixLink)
-			fixRepo := strings.TrimSuffix(fixLink, "/commit/"+fixHash)
-			pkgsToSymbols, err := patched(mod.Module, fixRepo, fixHash)
-			if err != nil {
-				errs = append(errs, err)
+		hasFixLink := len(mod.FixLinks) >= 0
+		fixLinks := mod.FixLinks
+		if len(fixLinks) == 0 {
+			c := r.CommitLinks()
+			if len(c) == 0 {
+				errs = append(errs, fmt.Errorf("no commit fix links found for module %s", mod.Module))
 				continue
 			}
-			packages := mod.AllPackages()
-			for pkg, symbols := range pkgsToSymbols {
-				if _, exists := packages[pkg]; exists {
-					packages[pkg].Symbols = append(packages[pkg].Symbols, symbols...)
-				} else {
-					mod.Packages = append(mod.Packages, &report.Package{
-						Package: pkg,
-						Symbols: symbols,
-					})
-				}
-				numFixedSymbols[i] += len(symbols)
-			}
+			fixLinks = c
 		}
-		// if the module's link field wasn't already populated, populate it with
-		// the link that results in the most symbols
-		if hasFixLink {
-			defaultFixes = defaultFixes[:len(defaultFixes)-1]
-		} else {
-			mod.FixLink = defaultFixes[indexMax(numFixedSymbols)]
+
+		foundSymbols := false
+		for _, fixLink := range fixLinks {
+			found, err := populateFromFixLink(fixLink, mod, patched)
+			if err != nil {
+				errs = append(errs, err)
+			}
+			foundSymbols = foundSymbols || found
+		}
+		if !foundSymbols && fixLinks != nil {
+			errs = append(errs, fmt.Errorf("no vulnerable symbols found for module %s", mod.Module))
+		}
+		// Sort fix links for testing/deterministic output
+		if !hasFixLink {
+			slices.Sort(mod.FixLinks)
 		}
 	}
 
 	return errors.Join(errs...)
 }
 
-// indexMax takes a slice of nonempty ints and returns the index of the maximum value
-func indexMax(s []int) (index int) {
-	maxVal := s[0]
-	index = 0
-	for i, val := range s {
-		if val > maxVal {
-			maxVal = val
-			index = i
+// populateFromFixLink takes a fixLink and a module and returns true if any symbols
+// are found for the given fix/module pair.
+func populateFromFixLink(fixLink string, m *report.Module, patched func(string, string, string) (map[string][]string, error)) (foundSymbols bool, err error) {
+	fixHash := filepath.Base(fixLink)
+	fixRepo := strings.TrimSuffix(fixLink, "/commit/"+fixHash)
+	pkgsToSymbols, err := patched(m.Module, fixRepo, fixHash)
+	if err != nil {
+		return false, err
+	}
+	modPkgs := m.AllPackages()
+	for pkg, symbols := range pkgsToSymbols {
+		foundSymbols = true
+		if modPkg, exists := modPkgs[pkg]; exists {
+			// Ensure there are no duplicate symbols
+			for _, s := range symbols {
+				if !slices.Contains(modPkg.Symbols, s) {
+					modPkg.Symbols = append(modPkg.Symbols, s)
+				}
+			}
+		} else {
+			m.Packages = append(m.Packages, &report.Package{
+				Package: pkg,
+				Symbols: symbols,
+			})
 		}
 	}
-	return index
+	if foundSymbols {
+		m.FixLinks = append(m.FixLinks, fixLink)
+	}
+	return foundSymbols, nil
 }
diff --git a/internal/symbols/populate_test.go b/internal/symbols/populate_test.go
index 57e15e0..46528a8 100644
--- a/internal/symbols/populate_test.go
+++ b/internal/symbols/populate_test.go
@@ -37,7 +37,7 @@
 						Package: "example.com/module/package",
 						Symbols: []string{"symbol1", "symbol2"},
 					}},
-					FixLink: "https://example.com/module/commit/1234",
+					FixLinks: []string{"https://example.com/module/commit/1234"},
 				}},
 				References: []*report.Reference{
 					{
@@ -70,10 +70,10 @@
 					Packages: []*report.Package{{
 						Package: "example.com/module/package",
 						// We don't yet dedupe the symbols.
-						Symbols: []string{"symbol1", "symbol2", "symbol1", "symbol2", "symbol3"},
+						Symbols: []string{"symbol1", "symbol2", "symbol3"},
 					}},
-					// This commit is picked because it results in the most symbols.
-					FixLink: "https://example.com/module/commit/5678",
+					// Both links are added because they both contain vulnerable symbols
+					FixLinks: []string{"https://example.com/module/commit/1234", "https://example.com/module/commit/5678"},
 				}},
 				References: []*report.Reference{
 					{