internal/frontend: move vulns utilities to internal/vulns
The tests in internal/frontend require special environment setup
as described in doc/postgres.md. When the environment is
not configured correctly, all tests are quietly skipped.
(See TestMain in internal/frontend/server_test.go)
This makes it difficult to test small utility functions
that do not require database backend. Move vulnerability
page related small utilities to a separate internal package
for easier testing.
Change-Id: Ib46a4549bf5a7bf22a3993a1c40f1033ce6a7ce7
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/429677
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
diff --git a/internal/frontend/vulns.go b/internal/frontend/vulns.go
index 511eddb..e6c9eda 100644
--- a/internal/frontend/vulns.go
+++ b/internal/frontend/vulns.go
@@ -14,7 +14,7 @@
"golang.org/x/mod/semver"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/derrors"
- "golang.org/x/pkgsite/internal/stdlib"
+ "golang.org/x/pkgsite/internal/vulns"
"golang.org/x/sync/errgroup"
vulnc "golang.org/x/vuln/client"
"golang.org/x/vuln/osv"
@@ -81,16 +81,11 @@
type VulnPage struct {
basePage
Entry OSVEntry
- AffectedPackages []*AffectedPackage
+ AffectedPackages []*vulns.AffectedPackage
AliasLinks []link
AdvisoryLinks []link
}
-type AffectedPackage struct {
- PackagePath string
- Versions string
-}
-
// OSVEntry holds an OSV entry and provides additional methods.
type OSVEntry struct {
*osv.Entry
@@ -207,7 +202,7 @@
}
return &VulnPage{
Entry: OSVEntry{entry},
- AffectedPackages: affectedPackages(entry),
+ AffectedPackages: vulns.AffectedPackages(entry),
AliasLinks: aliasLinks(entry),
AdvisoryLinks: advisoryLinks(entry),
}, nil
@@ -246,83 +241,6 @@
return &VulnListPage{Entries: entries}, nil
}
-// A pair is like an osv.Range, but each pair is a self-contained 2-tuple
-// (introduced version, fixed version).
-type pair struct {
- intro, fixed string
-}
-
-// collectRangePairs turns a slice of osv Ranges into a more manageable slice of
-// formatted version pairs.
-func collectRangePairs(a osv.Affected) []pair {
- var (
- ps []pair
- p pair
- prefix string
- )
- if stdlib.Contains(a.Package.Name) {
- prefix = "go"
- } else {
- prefix = "v"
- }
- for _, r := range a.Ranges {
- isSemver := r.Type == osv.TypeSemver
- for _, v := range r.Events {
- if v.Introduced != "" {
- // We expected Introduced and Fixed to alternate, but if
- // p.intro != "", then they they don't.
- // Keep going in that case, ignoring the first Introduced.
- p.intro = v.Introduced
- if p.intro == "0" {
- p.intro = ""
- }
- if isSemver && p.intro != "" {
- p.intro = prefix + p.intro
- }
- }
- if v.Fixed != "" {
- p.fixed = v.Fixed
- if isSemver && p.fixed != "" {
- p.fixed = prefix + p.fixed
- }
- ps = append(ps, p)
- p = pair{}
- }
- }
- }
- return ps
-}
-
-func affectedPackages(e *osv.Entry) []*AffectedPackage {
- var affs []*AffectedPackage
- for _, a := range e.Affected {
- pairs := collectRangePairs(a)
- var vs []string
- for _, p := range pairs {
- var s string
- if p.intro == "" && p.fixed == "" {
- // If neither field is set, the vuln applies to all versions.
- // Leave it blank, the template will render it properly.
- s = ""
- } else if p.intro == "" {
- s = "before " + p.fixed
- } else if p.fixed == "" {
- s = p.intro + " and later"
- } else {
- s = "from " + p.intro + " before " + p.fixed
- }
- vs = append(vs, s)
- }
- for _, p := range a.EcosystemSpecific.Imports {
- affs = append(affs, &AffectedPackage{
- PackagePath: p.Path,
- Versions: strings.Join(vs, ", "),
- })
- }
- }
- return affs
-}
-
// aliasLinks generates links to reference pages for vuln aliases.
func aliasLinks(e *osv.Entry) []link {
var links []link
diff --git a/internal/frontend/vulns_test.go b/internal/frontend/vulns_test.go
index 2fc2fb2..cff7812 100644
--- a/internal/frontend/vulns_test.go
+++ b/internal/frontend/vulns_test.go
@@ -8,7 +8,6 @@
"context"
"errors"
"fmt"
- "reflect"
"testing"
"github.com/google/go-cmp/cmp"
@@ -131,31 +130,6 @@
return ids, nil
}
-func TestCollectRangePairs(t *testing.T) {
- in := osv.Affected{
- Package: osv.Package{Name: "github.com/a/b"},
- Ranges: osv.Affects{
- {Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "", Fixed: "0.5"}}},
- {Type: osv.TypeSemver, Events: []osv.RangeEvent{
- {Introduced: "1.2"}, {Fixed: "1.5"},
- {Introduced: "2.1", Fixed: "2.3"},
- }},
- {Type: osv.TypeGit, Events: []osv.RangeEvent{{Introduced: "a", Fixed: "b"}}},
- },
- }
- got := collectRangePairs(in)
- want := []pair{
- {"", "v0.5"},
- {"v1.2", "v1.5"},
- {"v2.1", "v2.3"},
- {"a", "b"},
- }
- if !reflect.DeepEqual(got, want) {
- t.Errorf("\ngot %+v\nwant %+v", got, want)
- }
-
-}
-
func Test_aliasLinks(t *testing.T) {
type args struct {
e *osv.Entry
@@ -220,57 +194,3 @@
})
}
}
-
-func TestAffectedPackages(t *testing.T) {
- for _, test := range []struct {
- name string
- in []osv.RangeEvent
- want string
- }{
- {
- "no intro or fixed",
- nil,
- "",
- },
- {
- "no intro",
- []osv.RangeEvent{{Fixed: "1.5"}},
- "before v1.5",
- },
- {
- "both",
- []osv.RangeEvent{{Introduced: "1.5"}, {Fixed: "1.10"}},
- "from v1.5 before v1.10",
- },
- {
- "multiple",
- []osv.RangeEvent{
- {Introduced: "1.5", Fixed: "1.10"},
- {Fixed: "2.3"},
- },
- "from v1.5 before v1.10, before v2.3",
- },
- } {
- t.Run(test.name, func(t *testing.T) {
- entry := &osv.Entry{
- Affected: []osv.Affected{{
- Package: osv.Package{Name: "example.com/p"},
- EcosystemSpecific: osv.EcosystemSpecific{
- Imports: []osv.EcosystemSpecificImport{{
- Path: "example.com/p",
- }},
- },
- Ranges: osv.Affects{{
- Type: osv.TypeSemver,
- Events: test.in,
- }},
- }},
- }
- out := affectedPackages(entry)
- got := out[0].Versions
- if got != test.want {
- t.Errorf("got %q, want %q\n", got, test.want)
- }
- })
- }
-}
diff --git a/internal/vulns/vulns.go b/internal/vulns/vulns.go
new file mode 100644
index 0000000..2190269
--- /dev/null
+++ b/internal/vulns/vulns.go
@@ -0,0 +1,210 @@
+// 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 vulns provides utilities to interact with vuln APIs.
+package vulns
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "golang.org/x/mod/semver"
+ "golang.org/x/pkgsite/internal/derrors"
+ "golang.org/x/pkgsite/internal/stdlib"
+ "golang.org/x/vuln/osv"
+)
+
+// A Vuln contains information to display about a vulnerability.
+type Vuln struct {
+ // The vulndb ID.
+ ID string
+ // A description of the vulnerability, or the problem in obtaining it.
+ Details string
+}
+
+type vulnEntriesFunc func(context.Context, string) ([]*osv.Entry, error)
+
+// VulnsForPackage obtains vulnerability information for the given package.
+// If packagePath is empty, it returns all entries for the module at version.
+// The getVulnEntries function should retrieve all entries for the given module path.
+// It is passed to facilitate testing.
+// If there is an error, VulnsForPackage returns a single Vuln that describes the error.
+func VulnsForPackage(ctx context.Context, modulePath, version, packagePath string, getVulnEntries vulnEntriesFunc) []Vuln {
+ vs, err := vulnsForPackage(ctx, modulePath, version, packagePath, getVulnEntries)
+ if err != nil {
+ return []Vuln{{Details: fmt.Sprintf("could not get vulnerability data: %v", err)}}
+ }
+ return vs
+}
+
+func vulnsForPackage(ctx context.Context, modulePath, version, packagePath string, getVulnEntries vulnEntriesFunc) (_ []Vuln, err error) {
+ defer derrors.Wrap(&err, "vulns(%q, %q, %q)", modulePath, version, packagePath)
+
+ if getVulnEntries == nil {
+ return nil, nil
+ }
+ // Get all the vulns for this module.
+ entries, err := getVulnEntries(ctx, modulePath)
+ if err != nil {
+ return nil, err
+ }
+ // Each entry describes a single vuln. Select the ones that apply to this
+ // package at this version.
+ var vulns []Vuln
+ for _, e := range entries {
+ if vuln, ok := entryVuln(e, packagePath, version); ok {
+ vulns = append(vulns, vuln)
+ }
+ }
+ return vulns, nil
+}
+
+// AffectedPackage holds information about a package affected by a certain vulnerability.
+type AffectedPackage struct {
+ PackagePath string
+ Versions string
+}
+
+// OSVEntry holds an OSV entry and provides additional methods.
+type OSVEntry struct {
+ *osv.Entry
+}
+
+// AffectedModulesAndPackages returns a list of names affected by a vuln.
+func (e OSVEntry) AffectedModulesAndPackages() []string {
+ var affected []string
+ for _, a := range e.Affected {
+ switch a.Package.Name {
+ case "stdlib", "toolchain":
+ // Name specific standard library packages and tools.
+ for _, p := range a.EcosystemSpecific.Imports {
+ affected = append(affected, p.Path)
+ }
+ default:
+ // Outside the standard library, name the module.
+ affected = append(affected, a.Package.Name)
+ }
+ }
+ return affected
+}
+
+func entryVuln(e *osv.Entry, packagePath, version string) (Vuln, bool) {
+ for _, a := range e.Affected {
+ if !a.Ranges.AffectsSemver(version) {
+ continue
+ }
+ if packageMatches := func() bool {
+ if packagePath == "" {
+ return true // match module only
+ }
+ if len(a.EcosystemSpecific.Imports) == 0 {
+ return true // no package info available, so match on module
+ }
+ for _, p := range a.EcosystemSpecific.Imports {
+ if packagePath == p.Path {
+ return true // package matches
+ }
+ }
+ return false
+ }(); !packageMatches {
+ continue
+ }
+ // Choose the latest fixed version, if any.
+ var fixed string
+ for _, r := range a.Ranges {
+ if r.Type == osv.TypeGit {
+ continue
+ }
+ for _, re := range r.Events {
+ if re.Fixed != "" && (fixed == "" || semver.Compare(re.Fixed, fixed) > 0) {
+ fixed = re.Fixed
+ }
+ }
+ }
+ return Vuln{
+ ID: e.ID,
+ Details: e.Details,
+ }, true
+ }
+ return Vuln{}, false
+}
+
+// A pair is like an osv.Range, but each pair is a self-contained 2-tuple
+// (introduced version, fixed version).
+type pair struct {
+ intro, fixed string
+}
+
+// collectRangePairs turns a slice of osv Ranges into a more manageable slice of
+// formatted version pairs.
+func collectRangePairs(a osv.Affected) []pair {
+ var (
+ ps []pair
+ p pair
+ prefix string
+ )
+ if stdlib.Contains(a.Package.Name) {
+ prefix = "go"
+ } else {
+ prefix = "v"
+ }
+ for _, r := range a.Ranges {
+ isSemver := r.Type == osv.TypeSemver
+ for _, v := range r.Events {
+ if v.Introduced != "" {
+ // We expected Introduced and Fixed to alternate, but if
+ // p.intro != "", then they they don't.
+ // Keep going in that case, ignoring the first Introduced.
+ p.intro = v.Introduced
+ if p.intro == "0" {
+ p.intro = ""
+ }
+ if isSemver && p.intro != "" {
+ p.intro = prefix + p.intro
+ }
+ }
+ if v.Fixed != "" {
+ p.fixed = v.Fixed
+ if isSemver && p.fixed != "" {
+ p.fixed = prefix + p.fixed
+ }
+ ps = append(ps, p)
+ p = pair{}
+ }
+ }
+ }
+ return ps
+}
+
+// AffectedPackages extracts information about affected packages from the given osv.Entry.
+func AffectedPackages(e *osv.Entry) []*AffectedPackage {
+ var affs []*AffectedPackage
+ for _, a := range e.Affected {
+ pairs := collectRangePairs(a)
+ var vs []string
+ for _, p := range pairs {
+ var s string
+ if p.intro == "" && p.fixed == "" {
+ // If neither field is set, the vuln applies to all versions.
+ // Leave it blank, the template will render it properly.
+ s = ""
+ } else if p.intro == "" {
+ s = "before " + p.fixed
+ } else if p.fixed == "" {
+ s = p.intro + " and later"
+ } else {
+ s = "from " + p.intro + " before " + p.fixed
+ }
+ vs = append(vs, s)
+ }
+ for _, p := range a.EcosystemSpecific.Imports {
+ affs = append(affs, &AffectedPackage{
+ PackagePath: p.Path,
+ Versions: strings.Join(vs, ", "),
+ })
+ }
+ }
+ return affs
+}
diff --git a/internal/vulns/vulns_test.go b/internal/vulns/vulns_test.go
new file mode 100644
index 0000000..2b3d249
--- /dev/null
+++ b/internal/vulns/vulns_test.go
@@ -0,0 +1,141 @@
+// 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 vulns
+
+import (
+ "context"
+ "fmt"
+ "reflect"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/vuln/osv"
+)
+
+func TestVulnsForPackage(t *testing.T) {
+ ctx := context.Background()
+ e := osv.Entry{
+ Details: "bad",
+ Affected: []osv.Affected{{
+ Package: osv.Package{Name: "bad.com"},
+ Ranges: []osv.AffectsRange{{
+ Type: osv.TypeSemver,
+ Events: []osv.RangeEvent{{Introduced: "0"}, {Fixed: "1.2.3"}},
+ }},
+ EcosystemSpecific: osv.EcosystemSpecific{
+ Imports: []osv.EcosystemSpecificImport{{
+ Path: "bad.com",
+ }},
+ },
+ }},
+ }
+
+ get := func(_ context.Context, modulePath string) ([]*osv.Entry, error) {
+ switch modulePath {
+ case "good.com":
+ return nil, nil
+ case "bad.com":
+ return []*osv.Entry{&e}, nil
+ default:
+ return nil, fmt.Errorf("unknown module %q", modulePath)
+ }
+ }
+
+ got := VulnsForPackage(ctx, "good.com", "v1.0.0", "good.com", get)
+ if got != nil {
+ t.Errorf("got %v, want nil", got)
+ }
+ got = VulnsForPackage(ctx, "bad.com", "v1.0.0", "bad.com", get)
+ want := []Vuln{{
+ Details: "bad",
+ }}
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("mismatch (-want, +got):\n%s", diff)
+ }
+
+ got = VulnsForPackage(ctx, "bad.com", "v1.3.0", "bad.com", get)
+ if got != nil {
+ t.Errorf("got %v, want nil", got)
+ }
+}
+
+func TestCollectRangePairs(t *testing.T) {
+ in := osv.Affected{
+ Package: osv.Package{Name: "github.com/a/b"},
+ Ranges: osv.Affects{
+ {Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "", Fixed: "0.5"}}},
+ {Type: osv.TypeSemver, Events: []osv.RangeEvent{
+ {Introduced: "1.2"}, {Fixed: "1.5"},
+ {Introduced: "2.1", Fixed: "2.3"},
+ }},
+ {Type: osv.TypeGit, Events: []osv.RangeEvent{{Introduced: "a", Fixed: "b"}}},
+ },
+ }
+ got := collectRangePairs(in)
+ want := []pair{
+ {"", "v0.5"},
+ {"v1.2", "v1.5"},
+ {"v2.1", "v2.3"},
+ {"a", "b"},
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("\ngot %+v\nwant %+v", got, want)
+ }
+
+}
+
+func TestAffectedPackages(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ in []osv.RangeEvent
+ want string
+ }{
+ {
+ "no intro or fixed",
+ nil,
+ "",
+ },
+ {
+ "no intro",
+ []osv.RangeEvent{{Fixed: "1.5"}},
+ "before v1.5",
+ },
+ {
+ "both",
+ []osv.RangeEvent{{Introduced: "1.5"}, {Fixed: "1.10"}},
+ "from v1.5 before v1.10",
+ },
+ {
+ "multiple",
+ []osv.RangeEvent{
+ {Introduced: "1.5", Fixed: "1.10"},
+ {Fixed: "2.3"},
+ },
+ "from v1.5 before v1.10, before v2.3",
+ },
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ entry := &osv.Entry{
+ Affected: []osv.Affected{{
+ Package: osv.Package{Name: "example.com/p"},
+ EcosystemSpecific: osv.EcosystemSpecific{
+ Imports: []osv.EcosystemSpecificImport{{
+ Path: "example.com/p",
+ }},
+ },
+ Ranges: osv.Affects{{
+ Type: osv.TypeSemver,
+ Events: test.in,
+ }},
+ }},
+ }
+ out := AffectedPackages(entry)
+ got := out[0].Versions
+ if got != test.want {
+ t.Errorf("got %q, want %q\n", got, test.want)
+ }
+ })
+ }
+}