cmd/govulncheck/internal/govulncheck: canonicalize semvers

semver.Compare works on semvers that are prefixed with "v". We add "v"
if necessary. Otherwise, latest fixed version of a vulnerability can be
miscalculated.

For #54401

Change-Id: Ib653b84080235822c76612bded66eacb1dbc90eb
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/423375
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
diff --git a/cmd/govulncheck/internal/govulncheck/util.go b/cmd/govulncheck/internal/govulncheck/util.go
index 6699236..9bd46b4 100644
--- a/cmd/govulncheck/internal/govulncheck/util.go
+++ b/cmd/govulncheck/internal/govulncheck/util.go
@@ -9,6 +9,7 @@
 	"strings"
 
 	"golang.org/x/mod/semver"
+	isem "golang.org/x/vuln/internal/semver"
 	"golang.org/x/vuln/osv"
 	"golang.org/x/vuln/vulncheck"
 )
@@ -21,7 +22,8 @@
 		for _, r := range a.Ranges {
 			if r.Type == osv.TypeSemver {
 				for _, e := range r.Events {
-					if e.Fixed != "" && (v == "" || semver.Compare(e.Fixed, v) > 0) {
+					if e.Fixed != "" && (v == "" ||
+						semver.Compare(isem.CanonicalizeSemverPrefix(e.Fixed), isem.CanonicalizeSemverPrefix(v)) > 0) {
 						v = e.Fixed
 					}
 				}
diff --git a/cmd/govulncheck/main_test.go b/cmd/govulncheck/main_test.go
index fa07746..608fa68 100644
--- a/cmd/govulncheck/main_test.go
+++ b/cmd/govulncheck/main_test.go
@@ -73,6 +73,30 @@
 			},
 			"v1.5.6",
 		},
+		{
+			"no v prefix",
+			[]osv.Affected{
+				{
+					Ranges: osv.Affects{
+						{
+							Type: osv.TypeSemver,
+							Events: []osv.RangeEvent{
+								{Fixed: "1.17.2"},
+							},
+						}},
+				},
+				{
+					Ranges: osv.Affects{
+						{
+							Type: osv.TypeSemver,
+							Events: []osv.RangeEvent{
+								{Introduced: "1.18.0", Fixed: "1.18.4"},
+							},
+						}},
+				},
+			},
+			"1.18.4",
+		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
 			got := govulncheck.LatestFixed(test.in)
diff --git a/internal/semver/semver.go b/internal/semver/semver.go
new file mode 100644
index 0000000..f93235d
--- /dev/null
+++ b/internal/semver/semver.go
@@ -0,0 +1,35 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package semver provides shared utilities for manipulating
+// Go semantic versions.
+package semver
+
+import "strings"
+
+// addSemverPrefix adds a 'v' prefix to s if it isn't already prefixed
+// with 'v' or 'go'. This allows us to easily test go-style SEMVER
+// strings against normal SEMVER strings.
+func addSemverPrefix(s string) string {
+	if !strings.HasPrefix(s, "v") && !strings.HasPrefix(s, "go") {
+		return "v" + s
+	}
+	return s
+}
+
+// removeSemverPrefix removes the 'v' or 'go' prefixes from go-style
+// SEMVER strings, for usage in the public vulnerability format.
+func removeSemverPrefix(s string) string {
+	s = strings.TrimPrefix(s, "v")
+	s = strings.TrimPrefix(s, "go")
+	return s
+}
+
+// CanonicalizeSemverPrefix turns a SEMVER string into the canonical
+// representation using the 'v' prefix, as used by the OSV format.
+// Input may be a bare SEMVER ("1.2.3"), Go prefixed SEMVER ("go1.2.3"),
+// or already canonical SEMVER ("v1.2.3").
+func CanonicalizeSemverPrefix(s string) string {
+	return addSemverPrefix(removeSemverPrefix(s))
+}
diff --git a/internal/semver/semver_test.go b/internal/semver/semver_test.go
new file mode 100644
index 0000000..8a46228
--- /dev/null
+++ b/internal/semver/semver_test.go
@@ -0,0 +1,25 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package semver
+
+import (
+	"testing"
+)
+
+func TestCanonicalize(t *testing.T) {
+	for _, test := range []struct {
+		v    string
+		want string
+	}{
+		{"v1.2.3", "v1.2.3"},
+		{"1.2.3", "v1.2.3"},
+		{"go1.2.3", "v1.2.3"},
+	} {
+		got := CanonicalizeSemverPrefix(test.v)
+		if got != test.want {
+			t.Errorf("want %s; got %s", test.want, got)
+		}
+	}
+}
diff --git a/osv/json.go b/osv/json.go
index 2289a13..5685198 100644
--- a/osv/json.go
+++ b/osv/json.go
@@ -12,10 +12,10 @@
 package osv
 
 import (
-	"strings"
 	"time"
 
 	"golang.org/x/mod/semver"
+	isem "golang.org/x/vuln/internal/semver"
 )
 
 type AffectsRangeType string
@@ -45,16 +45,6 @@
 	Events []RangeEvent     `json:"events"`
 }
 
-// addSemverPrefix adds a 'v' prefix to s if it isn't already prefixed
-// with 'v' or 'go'. This allows us to easily test go-style SEMVER
-// strings against normal SEMVER strings.
-func addSemverPrefix(s string) string {
-	if !strings.HasPrefix(s, "v") && !strings.HasPrefix(s, "go") {
-		return "v" + s
-	}
-	return s
-}
-
 func (ar AffectsRange) containsSemver(v string) bool {
 	if ar.Type != TypeSemver {
 		return false
@@ -65,14 +55,14 @@
 
 	// Strip and then add the semver prefix so we can support bare versions,
 	// versions prefixed with 'v', and versions prefixed with 'go'.
-	v = canonicalizeSemverPrefix(v)
+	v = isem.CanonicalizeSemverPrefix(v)
 
 	var affected bool
 	for _, e := range ar.Events {
 		if !affected && e.Introduced != "" {
-			affected = e.Introduced == "0" || semver.Compare(v, addSemverPrefix(e.Introduced)) >= 0
+			affected = e.Introduced == "0" || semver.Compare(v, isem.CanonicalizeSemverPrefix(e.Introduced)) >= 0
 		} else if e.Fixed != "" {
-			affected = semver.Compare(v, addSemverPrefix(e.Fixed)) < 0
+			affected = semver.Compare(v, isem.CanonicalizeSemverPrefix(e.Fixed)) < 0
 		}
 	}
 
@@ -160,19 +150,3 @@
 	Affected   []Affected  `json:"affected"`
 	References []Reference `json:"references,omitempty"`
 }
-
-// removeSemverPrefix removes the 'v' or 'go' prefixes from go-style
-// SEMVER strings, for usage in the public vulnerability format.
-func removeSemverPrefix(s string) string {
-	s = strings.TrimPrefix(s, "v")
-	s = strings.TrimPrefix(s, "go")
-	return s
-}
-
-// canonicalizeSemverPrefix turns a SEMVER string into the canonical
-// representation using the 'v' prefix, as used by the OSV format.
-// Input may be a bare SEMVER ("1.2.3"), Go prefixed SEMVER ("go1.2.3"),
-// or already canonical SEMVER ("v1.2.3").
-func canonicalizeSemverPrefix(s string) string {
-	return addSemverPrefix(removeSemverPrefix(s))
-}