internal/{frontend,vuln}: make vuln search case-insensitive
Vulnerability search (by Go ID, CVE or GHSA) is now case-insensitive.
Also fixes the regular expression for GHSAs which was too permissive.
Change-Id: I2d53bfe41a1f8f78f6dd8774d3c9accc4461d101
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/506517
Run-TryBot: Tatiana Bradley <tatianabradley@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/internal/frontend/search.go b/internal/frontend/search.go
index ffd6c30..28f168b 100644
--- a/internal/frontend/search.go
+++ b/internal/frontend/search.go
@@ -351,8 +351,8 @@
if urlSchemeIdx > -1 {
query = query[urlSchemeIdx+3:]
}
- if vulnSupport && vuln.IsGoID(query) {
- return fmt.Sprintf("/vuln/%s?q", query)
+ if id, ok := vuln.CanonicalGoID(query); vulnSupport && ok {
+ return fmt.Sprintf("/vuln/%s?q", id)
}
requestedPath := path.Clean(query)
if !strings.Contains(requestedPath, "/") || mode == searchModeVuln {
@@ -388,14 +388,15 @@
func searchVulnAlias(ctx context.Context, mode, cq string, vc *vuln.Client) (_ *searchAction, err error) {
defer derrors.Wrap(&err, "searchVulnAlias(%q, %q)", mode, cq)
- if mode != searchModeVuln || !vuln.IsAlias(cq) || vc == nil {
+ alias, ok := vuln.CanonicalAlias(cq)
+ if mode != searchModeVuln || !ok || vc == nil {
return nil, nil
}
- id, err := vc.ByAlias(ctx, cq)
+ goID, err := vc.ByAlias(ctx, alias)
if err != nil {
return nil, &serverError{status: derrors.ToStatus(err)}
}
- return &searchAction{redirectURL: "/vuln/" + id}, nil
+ return &searchAction{redirectURL: "/vuln/" + goID}, nil
}
// searchMode reports whether the search performed should be in package or
@@ -413,7 +414,7 @@
case searchModeVuln:
return searchModeVuln
default:
- if vuln.IsAlias(q) {
+ if _, ok := vuln.CanonicalAlias(q); ok {
return searchModeVuln
}
if shouldDefaultToSymbolSearch(q) {
diff --git a/internal/frontend/search_test.go b/internal/frontend/search_test.go
index 0bf49e6..55441e5 100644
--- a/internal/frontend/search_test.go
+++ b/internal/frontend/search_test.go
@@ -108,7 +108,7 @@
// See TestSearchVulnAlias in this file for more tests.
{
name: "vuln alias",
- query: "q=GHSA-aaaa-bbbb-cccc&m=vuln",
+ query: "q=GHSA-cccc-ffff-gggg&m=vuln",
wantRedirect: "/vuln/GO-1990-0001",
},
{
@@ -119,18 +119,18 @@
{
// We turn on vuln mode if the query matches a vuln alias.
name: "vuln alias not vuln mode",
- query: "q=GHSA-aaaa-bbbb-cccc",
+ query: "q=GHSA-cccc-ffff-gggg",
wantRedirect: "/vuln/GO-1990-0001",
},
{
name: "vuln alias with no match",
- query: "q=GHSA-aaaa-bbbb-dddd",
+ query: "q=GHSA-cccc-ffff-xxxx",
wantStatus: http.StatusNotFound,
},
{
// An explicit mode overrides that.
name: "vuln alias symbol mode",
- query: "q=GHSA-aaaa-bbbb-cccc?m=symbol",
+ query: "q=GHSA-cccc-ffff-gggg?m=symbol",
wantTemplate: "search",
},
{
@@ -502,7 +502,7 @@
got := newSearchResult(&test.in, false, pr)
test.want.CommitTime = "unknown"
if diff := cmp.Diff(&test.want, got); diff != "" {
- t.Errorf("mimatch (-want, +got):\n%s", diff)
+ t.Errorf("mismatch (-want, +got):\n%s", diff)
}
})
}
@@ -540,6 +540,7 @@
{"non-existent path does not redirect", "github.com/non-existent", "", ""},
{"trim URL scheme from query", "https://golang.org/x/tools", "/golang.org/x/tools", ""},
{"Go vuln redirects", "GO-1969-0720", "/vuln/GO-1969-0720?q", ""},
+ {"Lower-case Go vuln redirects", "go-1969-0720", "/vuln/GO-1969-0720?q", ""},
{"not a Go vuln", "somepkg/GO-1969-0720", "", ""},
// Just setting the search mode to vuln does not cause a redirect.
{"search mode is vuln", "searchmodevuln", "", searchModeVuln},
@@ -593,7 +594,13 @@
{
name: "one match",
mode: searchModeVuln,
- query: "GHSA-aaaa-bbbb-cccc",
+ query: "GHSA-cccc-ffff-gggg",
+ wantURL: "/vuln/GO-1990-0001",
+ },
+ {
+ name: "one match - case insensitive",
+ mode: searchModeVuln,
+ query: "gHSa-ccCc-fFFF-gGgG",
wantURL: "/vuln/GO-1990-0001",
},
} {
diff --git a/internal/frontend/vulns.go b/internal/frontend/vulns.go
index 7dab33b..93aaf1d 100644
--- a/internal/frontend/vulns.go
+++ b/internal/frontend/vulns.go
@@ -89,8 +89,8 @@
template: "vuln/list",
title: "Vulnerability Reports"}, nil
default: // the path should be "/<ID>", e.g. "/GO-2021-0001".
- id := strings.TrimPrefix(path, "/")
- if !vuln.IsGoID(id) {
+ id, ok := vuln.CanonicalGoID(strings.TrimPrefix(path, "/"))
+ if !ok {
if url.Query().Has("q") {
return nil, derrors.NotFound
}
diff --git a/internal/frontend/vulns_test.go b/internal/frontend/vulns_test.go
index 435b1bb..a9a2a1d 100644
--- a/internal/frontend/vulns_test.go
+++ b/internal/frontend/vulns_test.go
@@ -16,7 +16,7 @@
)
var testEntries = []*osv.Entry{
- {ID: "GO-1990-0001", Details: "a", Aliases: []string{"CVE-2000-1", "GHSA-aaaa-bbbb-cccc"}},
+ {ID: "GO-1990-0001", Details: "a", Aliases: []string{"CVE-2000-1", "GHSA-cccc-ffff-gggg"}},
{ID: "GO-1990-0002", Details: "b", Aliases: []string{"CVE-2000-1", "GHSA-1111-2222-3333"}},
{ID: "GO-1990-0010", Details: "c"},
{ID: "GO-1991-0001", Details: "d"},
@@ -104,6 +104,18 @@
title: "GO-1990-0002",
},
},
+ {
+ name: "vuln entry page - case insensitive",
+ url: "https://pkg.go.dev/vuln/go-1990-0002",
+ want: &vulnPage{
+ page: &VulnEntryPage{
+ Entry: testEntries[1],
+ AliasLinks: aliasLinks(testEntries[1]),
+ },
+ template: "vuln/entry",
+ title: "GO-1990-0002",
+ },
+ },
}
for _, tc := range tcs {
diff --git a/internal/vuln/regexp.go b/internal/vuln/regexp.go
index 418bd65..5986075 100644
--- a/internal/vuln/regexp.go
+++ b/internal/vuln/regexp.go
@@ -4,21 +4,48 @@
package vuln
-import "regexp"
-
-var (
- goRegexp = regexp.MustCompile("^GO-[0-9]{4}-[0-9]{4,}$")
- cveRegexp = regexp.MustCompile("^CVE-[0-9]{4}-[0-9]+$")
- ghsaRegexp = regexp.MustCompile("^GHSA-.{4}-.{4}-.{4}$")
+import (
+ "regexp"
+ "strings"
)
-// IsGoID returns whether s is a valid Go vulnerability ID.
-func IsGoID(s string) bool {
- return goRegexp.MatchString(s)
+const (
+ ci = "(?i)" // case-insensitive
+ goRE = "^GO-[0-9]{4}-[0-9]{4,}$"
+ cveRE = "^CVE-[0-9]{4}-[0-9]+$"
+ // Regexp adapted from https://github.com/github/advisory-database.
+ ghsaRE = "^(GHSA)((-[23456789cfghjmpqrvwx]{4}){3})$"
+)
+
+// Case-insensitive regexps for vuln IDs/aliases.
+var (
+ goID = regexp.MustCompile(ci + goRE)
+ cveID = regexp.MustCompile(ci + cveRE)
+ ghsaID = regexp.MustCompile(ci + ghsaRE)
+)
+
+// Canonical returns the canonical form of the given Go ID string
+// by correcting the case.
+//
+// If no canonical form can be found, returns false.
+func CanonicalGoID(id string) (_ string, ok bool) {
+ if goID.MatchString(id) {
+ return strings.ToUpper(id), true
+ }
+ return "", false
}
-// IsAlias returns whether s is a valid vulnerability alias
-// (CVE or GHSA).
-func IsAlias(s string) bool {
- return cveRegexp.MatchString(s) || ghsaRegexp.MatchString(s)
+// Canonical returns the canonical form of the given alias ID string
+// (a CVE or GHSA id) by correcting the case.
+//
+// If no canonical form can be found, returns false.
+func CanonicalAlias(id string) (_ string, ok bool) {
+ if cveID.MatchString(id) {
+ return strings.ToUpper(id), true
+ }
+ parts := ghsaID.FindStringSubmatch(id)
+ if len(parts) != 4 {
+ return "", false
+ }
+ return strings.ToUpper(parts[1]) + strings.ToLower(parts[2]), true
}
diff --git a/internal/vuln/regexp_test.go b/internal/vuln/regexp_test.go
index ca66b53..05ed435 100644
--- a/internal/vuln/regexp_test.go
+++ b/internal/vuln/regexp_test.go
@@ -6,65 +6,115 @@
import "testing"
-func TestIsGoID(t *testing.T) {
+func TestCanonicalGoID(t *testing.T) {
tests := []struct {
- id string
- want bool
+ id string
+ wantID string
+ wantOK bool
}{
{
- id: "GO-1999-0001",
- want: true,
+ id: "GO-1999-0001",
+ wantID: "GO-1999-0001",
+ wantOK: true,
},
{
- id: "GO-2023-12345678",
- want: true,
+ id: "GO-1999-000111",
+ wantID: "GO-1999-000111",
+ wantOK: true,
},
{
- id: "GO-2023-123",
- want: false,
+ id: "go-1999-0001",
+ wantID: "GO-1999-0001",
+ wantOK: true,
},
{
- id: "GO-abcd-0001",
- want: false,
+ id: "GO-1999",
+ wantID: "",
+ wantOK: false,
},
{
- id: "CVE-1999-0001",
- want: false,
+ id: "GHSA-cfgh-2345-rwxq",
+ wantID: "",
+ wantOK: false,
+ },
+ {
+ id: "CVE-1999-000123",
+ wantID: "",
+ wantOK: false,
+ },
+ {
+ id: "ghsa-Cfgh-2345-Rwxq",
+ wantID: "",
+ wantOK: false,
+ },
+ {
+ id: "cve-1999-000123",
+ wantID: "",
+ wantOK: false,
+ },
+ {
+ id: "cve-ghsa-go",
+ wantID: "",
+ wantOK: false,
},
}
for _, tt := range tests {
t.Run(tt.id, func(t *testing.T) {
- got := IsGoID(tt.id)
- if got != tt.want {
- t.Errorf("IsGoID(%s) = %t, want %t", tt.id, got, tt.want)
+ gotID, gotOK := CanonicalGoID(tt.id)
+ if gotID != tt.wantID || gotOK != tt.wantOK {
+ t.Errorf("CanonicalGoID(%s) = (%s, %t), want (%s, %t)", tt.id, gotID, gotOK, tt.wantID, tt.wantOK)
}
})
}
}
-func TestIsAlias(t *testing.T) {
+func TestCanonicalAlias(t *testing.T) {
tests := []struct {
- id string
- want bool
+ id string
+ wantID string
+ wantOK bool
}{
{
- id: "GO-1999-0001",
- want: false,
+ id: "GO-1999-0001",
+ wantID: "",
+ wantOK: false,
},
{
- id: "GHSA-abcd-1234-efgh",
- want: true,
+ id: "GHSA-cfgh-2345-rwxq",
+ wantID: "GHSA-cfgh-2345-rwxq",
+ wantOK: true,
},
{
- id: "CVE-1999-000123",
- want: true,
+ id: "CVE-1999-000123",
+ wantID: "CVE-1999-000123",
+ wantOK: true,
+ },
+ {
+ id: "go-1999-0001",
+ wantID: "",
+ wantOK: false,
+ },
+ {
+ id: "ghsa-Cfgh-2345-Rwxq",
+ wantID: "GHSA-cfgh-2345-rwxq",
+ wantOK: true,
+ },
+ {
+ id: "cve-1999-000123",
+ wantID: "CVE-1999-000123",
+ wantOK: true,
+ },
+ {
+ id: "abc-CVE-1999-0001",
+ wantID: "",
+ wantOK: false,
},
}
for _, tt := range tests {
t.Run(tt.id, func(t *testing.T) {
- got := IsAlias(tt.id)
- if got != tt.want {
- t.Errorf("IsAlias(%s) = %t, want %t", tt.id, got, tt.want)
+ gotID, gotOK := CanonicalAlias(tt.id)
+ if gotID != tt.wantID || gotOK != tt.wantOK {
+ t.Errorf("CanonicalAlias(%s) = (%s, %t), want (%s, %t)", tt.id, gotID, gotOK, tt.wantID, tt.wantOK)
}
})
}