internal/frontend: allow searching by module for vulns

Previously, users could only search by package for vulnerabilities.
This meant that queries "stdlib" and "toolchain" showed no results.

This change updates search so that users can search by module as well
as package.

Change-Id: I4ecd248e95ac0d8012883743891c80950053731d
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/484458
Reviewed-by: Julie Qiu <julieqiu@google.com>
Run-TryBot: Tatiana Bradley <tatianabradley@google.com>
Reviewed-by: David Chase <drchase@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
diff --git a/internal/frontend/search.go b/internal/frontend/search.go
index a5b9275..056c610 100644
--- a/internal/frontend/search.go
+++ b/internal/frontend/search.go
@@ -369,7 +369,7 @@
 	return fmt.Sprintf("/%s", requestedPath)
 }
 
-func searchVulnModule(ctx context.Context, mode, cq string, client *vuln.Client) (_ *searchAction, err error) {
+func searchVulnModule(ctx context.Context, mode, query string, client *vuln.Client) (_ *searchAction, err error) {
 	if mode != searchModeVuln || client == nil {
 		return nil, nil
 	}
@@ -377,22 +377,36 @@
 	if err != nil {
 		return nil, err
 	}
-	prefix := cq + "/"
-	var entries []*osv.Entry
-EntryLoop:
-	for _, entry := range allEntries {
-		for _, aff := range entry.Affected {
-			for _, imp := range aff.EcosystemSpecific.Packages {
-				if imp.Path == cq || strings.HasPrefix(imp.Path, prefix) {
-					entries = append(entries, entry)
-					continue EntryLoop
+
+	prefix := query + "/"
+	// Returns whether any of the affected modules or packages of the
+	// entry start with the search query.
+	matchesQuery := func(e *osv.Entry) bool {
+		for _, aff := range e.Affected {
+			if aff.Module.Path == query ||
+				strings.HasPrefix(aff.Module.Path, prefix) {
+				return true
+			}
+			for _, pkg := range aff.EcosystemSpecific.Packages {
+				if pkg.Path == query || strings.HasPrefix(pkg.Path, prefix) {
+					return true
 				}
 			}
 		}
+		return false
 	}
+
+	var entries []*osv.Entry
+	for _, entry := range allEntries {
+		if matchesQuery(entry) {
+			entries = append(entries, entry)
+		}
+	}
+
 	sortVulnEntries(entries)
+
 	return &searchAction{
-		title:    fmt.Sprintf("%s - Vulnerability Reports", cq),
+		title:    fmt.Sprintf("%s - Vulnerability Reports", query),
 		template: "vuln/list",
 		page:     &VulnListPage{Entries: entries},
 	}, nil
diff --git a/internal/frontend/search_test.go b/internal/frontend/search_test.go
index f5e1d9a..a79c5b5 100644
--- a/internal/frontend/search_test.go
+++ b/internal/frontend/search_test.go
@@ -647,17 +647,50 @@
 			wantPage: &VulnListPage{Entries: nil},
 		},
 		{
-			name:  "prefix match",
+			name:  "stdlib module match",
+			mode:  searchModeVuln,
+			query: "stdlib",
+			wantPage: &VulnListPage{Entries: []*osv.Entry{
+				testEntries[5],
+			}},
+		},
+		{
+			name:  "stdlib package match",
+			mode:  searchModeVuln,
+			query: "net/http",
+			wantPage: &VulnListPage{Entries: []*osv.Entry{
+				testEntries[5],
+			}},
+		},
+		{
+			name:  "module prefix match",
 			mode:  searchModeVuln,
 			query: "example.com/org",
 			wantPage: &VulnListPage{Entries: []*osv.Entry{
+				// reverse sorted order
+				testEntries[7], testEntries[6],
+			}},
+		},
+		{
+			name:  "module path match",
+			mode:  searchModeVuln,
+			query: "example.com/org/module",
+			wantPage: &VulnListPage{Entries: []*osv.Entry{
 				testEntries[7],
 			}},
 		},
 		{
-			name:  "path match",
+			name:  "package path match",
 			mode:  searchModeVuln,
-			query: "example.com/org/path",
+			query: "example.com/org/module/a/package",
+			wantPage: &VulnListPage{Entries: []*osv.Entry{
+				testEntries[7],
+			}},
+		},
+		{
+			name:  "package prefix match",
+			mode:  searchModeVuln,
+			query: "example.com/org/module/a",
 			wantPage: &VulnListPage{Entries: []*osv.Entry{
 				testEntries[7],
 			}},
diff --git a/internal/frontend/vulns_test.go b/internal/frontend/vulns_test.go
index db8de84..ca3c844 100644
--- a/internal/frontend/vulns_test.go
+++ b/internal/frontend/vulns_test.go
@@ -20,16 +20,34 @@
 	{ID: "GO-1990-10", Details: "c"},
 	{ID: "GO-1991-01", Details: "d"},
 	{ID: "GO-1991-05", Details: "e"},
-	{ID: "GO-1991-23", Details: "f"},
-	{ID: "GO-1991-30", Details: "g"},
+	{ID: "GO-1991-23", Details: "f",
+		Affected: []osv.Affected{{
+			Module: osv.Module{
+				Path: "stdlib",
+			},
+			EcosystemSpecific: osv.EcosystemSpecific{
+				Packages: []osv.Package{
+					{
+						Path: "net/http",
+					}}}}},
+	},
+	{ID: "GO-1991-30", Details: "g",
+		Affected: []osv.Affected{{
+			Module: osv.Module{
+				Path: "example.com/org/repo",
+			},
+		}}},
 	{
 		ID:      "GO-1991-31",
 		Details: "h",
 		Affected: []osv.Affected{{
+			Module: osv.Module{
+				Path: "example.com/org/module",
+			},
 			EcosystemSpecific: osv.EcosystemSpecific{
 				Packages: []osv.Package{
 					{
-						Path: "example.com/org/path",
+						Path: "example.com/org/module/a/package",
 					},
 				},
 			},