internal/{frontend,vuln}: move logic to search by module to vuln client
Moves the logic for finding entries based on a module/package prefix
to the vuln client, so it is easier to test the core functionality.
Also modifies the functionality slightly, so that a search query
with a trailing "/" will show results.
Change-Id: Ief191a62bcf7badbc6e70112d9fcedae3dd03275
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/486456
Run-TryBot: Tatiana Bradley <tatianabradley@google.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Tatiana Bradley <tatianabradley@google.com>
diff --git a/internal/frontend/search.go b/internal/frontend/search.go
index 766d5a2..ffd6c30 100644
--- a/internal/frontend/search.go
+++ b/internal/frontend/search.go
@@ -23,7 +23,6 @@
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/middleware"
- "golang.org/x/pkgsite/internal/osv"
"golang.org/x/pkgsite/internal/postgres"
"golang.org/x/pkgsite/internal/stdlib"
"golang.org/x/pkgsite/internal/version"
@@ -373,36 +372,12 @@
if mode != searchModeVuln || client == nil {
return nil, nil
}
- allEntries, err := client.Entries(ctx, -1)
+
+ entries, err := client.ByPackagePrefix(ctx, query)
if err != nil {
return nil, err
}
- 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)
- }
- }
-
return &searchAction{
title: fmt.Sprintf("%s - Vulnerability Reports", query),
template: "vuln/list",
diff --git a/internal/vuln/client.go b/internal/vuln/client.go
index 1b43079..d5f60f6 100644
--- a/internal/vuln/client.go
+++ b/internal/vuln/client.go
@@ -11,6 +11,7 @@
"fmt"
"path/filepath"
"sort"
+ "strings"
"sync"
"golang.org/x/pkgsite/internal/derrors"
@@ -238,6 +239,52 @@
return c.byIDs(ctx, ids)
}
+// ByPackagePrefix returns all the OSV entries that match the given
+// package prefix, in descending order by ID, or (nil, nil) if there
+// are none.
+//
+// An entry matches a prefix if:
+// - Any affected module or package equals the given prefix, OR
+// - Any affected module or package's path begins with the given prefix
+// interpreted as a full path. (E.g. "example.com/module/package" matches
+// the prefix "example.com/module" but not "example.com/mod")
+func (c *Client) ByPackagePrefix(ctx context.Context, prefix string) (_ []*osv.Entry, err error) {
+ allEntries, err := c.Entries(ctx, -1)
+ if err != nil {
+ return nil, err
+ }
+
+ prefix = strings.TrimSuffix(prefix, "/")
+ match := func(s string) bool {
+ return s == prefix || strings.HasPrefix(s, prefix+"/")
+ }
+
+ // Returns whether any of the affected modules or packages of the
+ // entry start with the prefix.
+ matchesQuery := func(e *osv.Entry) bool {
+ for _, aff := range e.Affected {
+ if match(aff.Module.Path) {
+ return true
+ }
+ for _, pkg := range aff.EcosystemSpecific.Packages {
+ if match(pkg.Path) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ var entries []*osv.Entry
+ for _, entry := range allEntries {
+ if matchesQuery(entry) {
+ entries = append(entries, entry)
+ }
+ }
+
+ return entries, nil
+}
+
func (c *Client) byIDs(ctx context.Context, ids []string) (_ []*osv.Entry, err error) {
entries := make([]*osv.Entry, len(ids))
g, gctx := errgroup.WithContext(ctx)
diff --git a/internal/vuln/client_test.go b/internal/vuln/client_test.go
index be630b6..e48cb14 100644
--- a/internal/vuln/client_test.go
+++ b/internal/vuln/client_test.go
@@ -14,6 +14,7 @@
"testing"
"time"
+ "github.com/google/go-cmp/cmp"
"golang.org/x/pkgsite/internal/osv"
"golang.org/x/tools/txtar"
)
@@ -386,6 +387,150 @@
}
}
+func TestByPackagePrefix(t *testing.T) {
+ stdlibNet := &osv.Entry{
+ ID: "1-STDLIB-NET",
+ Affected: []osv.Affected{
+ {
+ Module: osv.Module{
+ Path: "golang.org/x/example",
+ },
+ },
+ {
+ Module: osv.Module{
+ Path: "stdlib",
+ },
+ EcosystemSpecific: osv.EcosystemSpecific{
+ Packages: []osv.Package{
+ {
+ Path: "net/http/httputil",
+ },
+ },
+ },
+ },
+ },
+ }
+ stdlibCrypto := &osv.Entry{
+ ID: "2-STDLIB-CRYPTO",
+ Affected: []osv.Affected{
+ {
+ Module: osv.Module{
+ Path: "golang.org/x/example",
+ },
+ },
+ {
+ Module: osv.Module{
+ Path: "stdlib",
+ },
+ EcosystemSpecific: osv.EcosystemSpecific{
+ Packages: []osv.Package{
+ {
+ Path: "crypto/tls",
+ },
+ },
+ },
+ },
+ },
+ }
+ thirdParty := &osv.Entry{
+ ID: "3-EXAMPLE-COM",
+ Affected: []osv.Affected{
+ {
+ Module: osv.Module{
+ Path: "golang.org/x/example",
+ },
+ },
+ {
+
+ Module: osv.Module{
+ Path: "example.com/org/module",
+ },
+ EcosystemSpecific: osv.EcosystemSpecific{
+ Packages: []osv.Package{
+ {
+ Path: "example.com/org/module/somepkg",
+ },
+ {
+ Path: "example.com/org/module/package/inner",
+ },
+ },
+ },
+ },
+ },
+ }
+ vc, err := NewInMemoryClient([]*osv.Entry{stdlibCrypto, stdlibNet, thirdParty})
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, tc := range []struct {
+ name string
+ query string
+ want []*osv.Entry
+ }{
+ {
+ name: "no match",
+ query: "net/htt",
+ want: nil,
+ },
+ {
+ name: "stdlib module exact match",
+ query: "stdlib",
+ want: []*osv.Entry{stdlibCrypto, stdlibNet},
+ },
+ {
+ name: "stdlib package exact match",
+ query: "net/http/httputil",
+ want: []*osv.Entry{stdlibNet},
+ },
+ {
+ name: "stdlib package prefix match",
+ query: "net/http",
+ want: []*osv.Entry{stdlibNet},
+ },
+ {
+ name: "3p module exact match",
+ query: "example.com/org/module",
+ want: []*osv.Entry{thirdParty},
+ },
+ {
+ name: "3p module prefix match",
+ query: "example.com/org",
+ want: []*osv.Entry{thirdParty},
+ },
+ {
+ name: "3p package exact match",
+ query: "example.com/org/module/package/inner",
+ want: []*osv.Entry{thirdParty},
+ },
+ {
+ name: "3p package prefix match",
+ query: "example.com/org/module/package",
+ want: []*osv.Entry{thirdParty},
+ },
+ {
+ name: "prefix with trailing slash",
+ query: "example.com/org/module/package/",
+ want: []*osv.Entry{thirdParty},
+ },
+ {
+ name: "descending order by ID",
+ query: "golang.org/x",
+ want: []*osv.Entry{thirdParty, stdlibCrypto, stdlibNet},
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ got, err := vc.ByPackagePrefix(context.Background(), tc.query)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !cmp.Equal(got, tc.want) {
+ t.Errorf("ByPackagePrefix(%s) = %v, want %v", tc.query, got, tc.want)
+ }
+ })
+ }
+}
+
// newTestClientFromTxtar creates an in-memory client for use in tests.
// It reads test data from a txtar file which must follow the
// v1 database schema.