exp/vulncheck: load vulnerabilities from client to internal structures
This CL introduces internal structures for querying vulnerabilities
provided by a database client. This code is a copy of
exp/vulndb/internal/audit.
Change-Id: I1b94deacab9adc36b27ab66a127fb1fcbc85de1f
Reviewed-on: https://go-review.googlesource.com/c/exp/+/359754
Trust: Zvonimir Pavlinovic <zpavlinovic@google.com>
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/vulncheck/fetch.go b/vulncheck/fetch.go
new file mode 100644
index 0000000..d028513
--- /dev/null
+++ b/vulncheck/fetch.go
@@ -0,0 +1,118 @@
+// Copyright 2021 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 vulncheck
+
+import (
+ "fmt"
+ "go/build"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/vulndb/client"
+)
+
+// modKey creates a unique string identifier for mod.
+func modKey(mod *packages.Module) string {
+ if mod == nil {
+ return ""
+ }
+ return fmt.Sprintf("%s@%s", modPath(mod), modVersion(mod))
+}
+
+func modPath(mod *packages.Module) string {
+ if mod.Replace != nil {
+ return mod.Replace.Path
+ }
+ return mod.Path
+}
+
+func modVersion(mod *packages.Module) string {
+ if mod.Replace != nil {
+ return mod.Replace.Version
+ }
+ return mod.Version
+}
+
+// extractModules collects modules in `pkgs` up to uniqueness of
+// module path and version.
+func extractModules(pkgs []*packages.Package) []*packages.Module {
+ modMap := map[string]*packages.Module{}
+
+ seen := map[*packages.Package]bool{}
+ var extract func(*packages.Package, map[string]*packages.Module)
+ extract = func(pkg *packages.Package, modMap map[string]*packages.Module) {
+ if pkg == nil || seen[pkg] {
+ return
+ }
+ if pkg.Module != nil {
+ modMap[modKey(pkg.Module)] = pkg.Module
+ }
+ seen[pkg] = true
+ for _, imp := range pkg.Imports {
+ extract(imp, modMap)
+ }
+ }
+ for _, pkg := range pkgs {
+ extract(pkg, modMap)
+ }
+
+ modules := []*packages.Module{}
+ for _, mod := range modMap {
+ modules = append(modules, mod)
+ }
+ return modules
+}
+
+// fetchVulnerabilities fetches vulnerabilities that affect the supplied modules.
+func fetchVulnerabilities(client client.Client, modules []*packages.Module) (moduleVulnerabilities, error) {
+ mv := moduleVulnerabilities{}
+ for _, mod := range modules {
+ modPath := mod.Path
+ if mod.Replace != nil {
+ modPath = mod.Replace.Path
+ }
+
+ // skip loading vulns for local imports
+ if isLocal(mod) {
+ // TODO: what if client has its own db
+ // with local vulns?
+ continue
+ }
+ vulns, err := client.GetByModule(modPath)
+ if err != nil {
+ return nil, err
+ }
+ if len(vulns) == 0 {
+ continue
+ }
+ mv = append(mv, modVulns{
+ mod: mod,
+ vulns: vulns,
+ })
+ }
+ return mv, nil
+}
+
+func isLocal(mod *packages.Module) bool {
+ modDir := mod.Dir
+ if mod.Replace != nil {
+ modDir = mod.Replace.Dir
+ }
+ return !strings.HasPrefix(modDir, modCacheDirectory())
+}
+
+func modCacheDirectory() string {
+ var modCacheDir string
+ // TODO: define modCacheDir using something similar to cmd/go/internal/cfg.GOMODCACHE?
+ if modCacheDir = os.Getenv("GOMODCACHE"); modCacheDir == "" {
+ if modCacheDir = os.Getenv("GOPATH"); modCacheDir == "" {
+ modCacheDir = build.Default.GOPATH
+ }
+ modCacheDir = filepath.Join(modCacheDir, "pkg", "mod")
+ }
+ return modCacheDir
+}
diff --git a/vulncheck/fetch_test.go b/vulncheck/fetch_test.go
new file mode 100644
index 0000000..a60edee
--- /dev/null
+++ b/vulncheck/fetch_test.go
@@ -0,0 +1,58 @@
+// Copyright 2021 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 vulncheck
+
+import (
+ "reflect"
+ "testing"
+
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/vulndb/osv"
+)
+
+func TestFetchVulnerabilities(t *testing.T) {
+ mc := &mockClient{
+ ret: map[string][]*osv.Entry{
+ "example.mod/a": {{ID: "a", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/a"}, Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Fixed: "2.0.0"}}}}}}}},
+ "example.mod/b": {{ID: "b", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/b"}, Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Fixed: "1.1.1"}}}}}}}},
+ "example.mod/d": {{ID: "c", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/d"}, Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Fixed: "2.0.0"}}}}}}}},
+ "example.mod/e": {{ID: "e", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/e"}, Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Fixed: "2.2.0"}}}}}}}},
+ },
+ }
+
+ mv, err := fetchVulnerabilities(mc, []*packages.Module{
+ {Path: "example.mod/a", Dir: modCacheDirectory(), Version: "v1.0.0"},
+ {Path: "example.mod/b", Dir: modCacheDirectory(), Version: "v1.0.4"},
+ {Path: "example.mod/c", Replace: &packages.Module{Path: "example.mod/d", Dir: modCacheDirectory(), Version: "v1.0.0"}, Version: "v2.0.0"},
+ {Path: "example.mod/e", Replace: &packages.Module{Path: "../local/example.mod/d", Dir: modCacheDirectory(), Version: "v1.0.1"}, Version: "v2.1.0"},
+ })
+ if err != nil {
+ t.Fatalf("FetchVulnerabilities failed: %s", err)
+ }
+
+ expected := moduleVulnerabilities{
+ {
+ mod: &packages.Module{Path: "example.mod/a", Dir: modCacheDirectory(), Version: "v1.0.0"},
+ vulns: []*osv.Entry{
+ {ID: "a", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/a"}, Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Fixed: "2.0.0"}}}}}}},
+ },
+ },
+ {
+ mod: &packages.Module{Path: "example.mod/b", Dir: modCacheDirectory(), Version: "v1.0.4"},
+ vulns: []*osv.Entry{
+ {ID: "b", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/b"}, Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Fixed: "1.1.1"}}}}}}},
+ },
+ },
+ {
+ mod: &packages.Module{Path: "example.mod/c", Replace: &packages.Module{Path: "example.mod/d", Dir: modCacheDirectory(), Version: "v1.0.0"}, Version: "v2.0.0"},
+ vulns: []*osv.Entry{
+ {ID: "c", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/d"}, Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Fixed: "2.0.0"}}}}}}},
+ },
+ },
+ }
+ if !reflect.DeepEqual(mv, expected) {
+ t.Fatalf("FetchVulnerabilities returned unexpected results, got:\n%s\nwant:\n%s", moduleVulnerabilitiesToString(mv), moduleVulnerabilitiesToString(expected))
+ }
+}
diff --git a/vulncheck/go.mod b/vulncheck/go.mod
index de7dce9..c5c7b59 100644
--- a/vulncheck/go.mod
+++ b/vulncheck/go.mod
@@ -2,9 +2,13 @@
go 1.17
-require golang.org/x/vulndb v0.0.0-20211020161650-d192a0cd6149
+require (
+ golang.org/x/tools v0.0.0-20210910171127-a568412ca0e6
+ golang.org/x/vulndb v0.0.0-20211020161650-d192a0cd6149
+)
require (
- golang.org/x/mod v0.4.1 // indirect
- golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
+ golang.org/x/mod v0.4.2 // indirect
+ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect
+ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)
diff --git a/vulncheck/go.sum b/vulncheck/go.sum
index 4d817b1..978c610 100644
--- a/vulncheck/go.sum
+++ b/vulncheck/go.sum
@@ -42,19 +42,23 @@
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -64,19 +68,26 @@
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20210910171127-a568412ca0e6 h1:O8E8PvYQ1HtZae5hPC9lyLiVuGlqL50yMQW7Y6oRaXQ=
+golang.org/x/tools v0.0.0-20210910171127-a568412ca0e6/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM=
golang.org/x/vulndb v0.0.0-20211020161650-d192a0cd6149 h1:xCvKnKNK/wBCJAR3Xm1N294PIuxhVJDi570Fz6lh2O0=
golang.org/x/vulndb v0.0.0-20211020161650-d192a0cd6149/go.mod h1:dbogqFEsxyrr+RMtOySAr6gAr9NWRrrtJoQLSdq5fLk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/vulncheck/helpers_test.go b/vulncheck/helpers_test.go
new file mode 100644
index 0000000..95eaa24
--- /dev/null
+++ b/vulncheck/helpers_test.go
@@ -0,0 +1,42 @@
+// Copyright 2021 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 vulncheck
+
+import (
+ "fmt"
+
+ "golang.org/x/vulndb/osv"
+)
+
+type mockClient struct {
+ ret map[string][]*osv.Entry
+}
+
+func (mc *mockClient) GetByModule(a string) ([]*osv.Entry, error) {
+ return mc.ret[a], nil
+}
+
+func (mc *mockClient) GetByID(a string) (*osv.Entry, error) {
+ return nil, nil
+}
+
+func moduleVulnerabilitiesToString(mv moduleVulnerabilities) string {
+ var s string
+ for _, m := range mv {
+ s += fmt.Sprintf("mod: %v\n", m.mod)
+ for _, v := range m.vulns {
+ s += fmt.Sprintf("\t%v\n", v)
+ }
+ }
+ return s
+}
+
+func vulnsToString(vulns []*osv.Entry) string {
+ var s string
+ for _, v := range vulns {
+ s += fmt.Sprintf("\t%v\n", v)
+ }
+ return s
+}
diff --git a/vulncheck/vulncheck.go b/vulncheck/vulncheck.go
index 4d8cc90..132ebf9 100644
--- a/vulncheck/vulncheck.go
+++ b/vulncheck/vulncheck.go
@@ -7,8 +7,11 @@
package vulncheck
import (
+ "fmt"
"go/token"
+ "strings"
+ "golang.org/x/tools/go/packages"
"golang.org/x/vulndb/client"
"golang.org/x/vulndb/osv"
)
@@ -148,3 +151,158 @@
// ImportedBy contains IDs of packages directly importing this package.
ImportedBy []int
}
+
+// moduleVulnerabilities is an internal structure for
+// holding and querying vulnerabilities provided by a
+// vulnerability database client.
+type moduleVulnerabilities []modVulns
+
+// modVulns groups vulnerabilities per module.
+type modVulns struct {
+ mod *packages.Module
+ vulns []*osv.Entry
+}
+
+func (mv moduleVulnerabilities) Filter(os, arch string) moduleVulnerabilities {
+ var filteredMod moduleVulnerabilities
+ for _, mod := range mv {
+ module := mod.mod
+ modVersion := module.Version
+ if module.Replace != nil {
+ modVersion = module.Replace.Version
+ }
+ // TODO(https://golang.org/issues/49264): if modVersion == "", try vcs?
+ var filteredVulns []*osv.Entry
+ for _, v := range mod.vulns {
+ var filteredAffected []osv.Affected
+ for _, a := range v.Affected {
+ // A module version is affected if
+ // - it is included in one of the affected version ranges
+ // - and module version is not ""
+ // The latter means the module version is not available, so
+ // we don't want to spam users with potential false alarms.
+ // TODO: issue warning for "" cases above?
+ affected := modVersion != "" && a.Ranges.AffectsSemver(modVersion) && matchesPlatform(os, arch, a.EcosystemSpecific)
+ if affected {
+ filteredAffected = append(filteredAffected, a)
+ }
+ }
+ if len(filteredAffected) == 0 {
+ continue
+ }
+ // save the non-empty vulnerability with only
+ // affected symbols.
+ newV := *v
+ newV.Affected = filteredAffected
+ filteredVulns = append(filteredVulns, &newV)
+ }
+ filteredMod = append(filteredMod, modVulns{
+ mod: module,
+ vulns: filteredVulns,
+ })
+ }
+ return filteredMod
+}
+
+func matchesPlatform(os, arch string, e osv.EcosystemSpecific) bool {
+ matchesOS := len(e.GOOS) == 0
+ matchesArch := len(e.GOARCH) == 0
+ for _, o := range e.GOOS {
+ if os == o {
+ matchesOS = true
+ break
+ }
+ }
+ for _, a := range e.GOARCH {
+ if arch == a {
+ matchesArch = true
+ break
+ }
+ }
+ return matchesOS && matchesArch
+}
+func (mv moduleVulnerabilities) Num() int {
+ var num int
+ for _, m := range mv {
+ num += len(m.vulns)
+ }
+ return num
+}
+
+// VulnsForPackage returns the vulnerabilities for the module which is the most
+// specific prefix of importPath, or nil if there is no matching module with
+// vulnerabilities.
+func (mv moduleVulnerabilities) VulnsForPackage(importPath string) []*osv.Entry {
+ var mostSpecificMod *modVulns
+ for _, mod := range mv {
+ md := mod
+ if strings.HasPrefix(importPath, md.mod.Path) {
+ if mostSpecificMod == nil || len(mostSpecificMod.mod.Path) < len(md.mod.Path) {
+ mostSpecificMod = &md
+ }
+ }
+ }
+
+ if mostSpecificMod == nil {
+ return nil
+ }
+
+ if mostSpecificMod.mod.Replace != nil {
+ importPath = fmt.Sprintf("%s%s", mostSpecificMod.mod.Replace.Path, strings.TrimPrefix(importPath, mostSpecificMod.mod.Path))
+ }
+ vulns := mostSpecificMod.vulns
+ packageVulns := []*osv.Entry{}
+ for _, v := range vulns {
+ for _, a := range v.Affected {
+ if a.Package.Name == importPath {
+ packageVulns = append(packageVulns, v)
+ break
+ }
+ }
+ }
+ return packageVulns
+}
+
+// VulnsForSymbol returns vulnerabilites for `symbol` in `mv.VulnsForPackage(importPath)`.
+func (mv moduleVulnerabilities) VulnsForSymbol(importPath, symbol string) []*osv.Entry {
+ vulns := mv.VulnsForPackage(importPath)
+ if vulns == nil {
+ return nil
+ }
+
+ symbolVulns := []*osv.Entry{}
+ for _, v := range vulns {
+ vulnLoop:
+ for _, a := range v.Affected {
+ if a.Package.Name != importPath {
+ continue
+ }
+ if len(a.EcosystemSpecific.Symbols) == 0 {
+ symbolVulns = append(symbolVulns, v)
+ continue vulnLoop
+ }
+ for _, s := range a.EcosystemSpecific.Symbols {
+ if s == symbol {
+ symbolVulns = append(symbolVulns, v)
+ continue vulnLoop
+ }
+ }
+ }
+ }
+ return symbolVulns
+}
+
+// Vulns returns vulnerabilities for all modules in `mv`.
+func (mv moduleVulnerabilities) Vulns() []*osv.Entry {
+ var vulns []*osv.Entry
+ seen := make(map[string]bool)
+ for _, mv := range mv {
+ for _, v := range mv.vulns {
+ if !seen[v.ID] {
+ vulns = append(vulns, v)
+ seen[v.ID] = true
+ }
+ }
+ }
+ return vulns
+}
diff --git a/vulncheck/vulncheck_test.go b/vulncheck/vulncheck_test.go
new file mode 100644
index 0000000..d4441bc
--- /dev/null
+++ b/vulncheck/vulncheck_test.go
@@ -0,0 +1,218 @@
+// Copyright 2021 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 vulncheck
+
+import (
+ "reflect"
+ "testing"
+
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/vulndb/osv"
+)
+
+func TestFilterVulns(t *testing.T) {
+ mv := moduleVulnerabilities{
+ {
+ mod: &packages.Module{
+ Path: "example.mod/a",
+ Version: "v1.0.0",
+ },
+ vulns: []*osv.Entry{
+ {ID: "a", Affected: []osv.Affected{
+ {Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Fixed: "2.0.0"}}}}},
+ {Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Fixed: "1.0.0"}}}}}, // should be filtered out
+ }},
+ {ID: "b", Affected: []osv.Affected{{Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.0.1"}}}}, EcosystemSpecific: osv.EcosystemSpecific{GOOS: []string{"windows", "linux"}}}}},
+ {ID: "c", Affected: []osv.Affected{{Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.0.1"}, {Fixed: "1.0.1"}}}}, EcosystemSpecific: osv.EcosystemSpecific{GOARCH: []string{"arm64", "amd64"}}}}},
+ {ID: "d", Affected: []osv.Affected{{EcosystemSpecific: osv.EcosystemSpecific{GOOS: []string{"windows"}}}}},
+ },
+ },
+ {
+ mod: &packages.Module{
+ Path: "example.mod/b",
+ Version: "v1.0.0",
+ },
+ vulns: []*osv.Entry{
+ {ID: "e", Affected: []osv.Affected{{EcosystemSpecific: osv.EcosystemSpecific{GOARCH: []string{"arm64"}}}}},
+ {ID: "f", Affected: []osv.Affected{{EcosystemSpecific: osv.EcosystemSpecific{GOOS: []string{"linux"}}}}},
+ {ID: "g", Affected: []osv.Affected{{EcosystemSpecific: osv.EcosystemSpecific{GOARCH: []string{"amd64"}}, Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "0.0.1"}, {Fixed: "2.0.1"}}}}}}},
+ {ID: "h", Affected: []osv.Affected{{EcosystemSpecific: osv.EcosystemSpecific{GOOS: []string{"windows"}, GOARCH: []string{"amd64"}}}}},
+ },
+ },
+ {
+ mod: &packages.Module{
+ Path: "example.mod/c",
+ },
+ vulns: []*osv.Entry{
+ {ID: "i", Affected: []osv.Affected{{EcosystemSpecific: osv.EcosystemSpecific{GOARCH: []string{"amd64"}}, Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "0.0.0"}}}}}}},
+ {ID: "j", Affected: []osv.Affected{{EcosystemSpecific: osv.EcosystemSpecific{GOARCH: []string{"amd64"}}, Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Fixed: "3.0.0"}}}}}}},
+ {ID: "k"},
+ },
+ },
+ {
+ mod: &packages.Module{
+ Path: "example.mod/d",
+ Version: "v1.2.0",
+ },
+ vulns: []*osv.Entry{
+ {ID: "l", Affected: []osv.Affected{
+ {EcosystemSpecific: osv.EcosystemSpecific{GOOS: []string{"windows"}}}, // should be filtered out
+ {EcosystemSpecific: osv.EcosystemSpecific{GOOS: []string{"linux"}}},
+ }},
+ },
+ },
+ }
+
+ expected := moduleVulnerabilities{
+ {
+ mod: &packages.Module{
+ Path: "example.mod/a",
+ Version: "v1.0.0",
+ },
+ vulns: []*osv.Entry{
+ {ID: "a", Affected: []osv.Affected{{Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Fixed: "2.0.0"}}}}}}},
+ {ID: "c", Affected: []osv.Affected{{EcosystemSpecific: osv.EcosystemSpecific{GOARCH: []string{"arm64", "amd64"}}, Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.0.1"}, {Fixed: "1.0.1"}}}}}}},
+ },
+ },
+ {
+ mod: &packages.Module{
+ Path: "example.mod/b",
+ Version: "v1.0.0",
+ },
+ vulns: []*osv.Entry{
+ {ID: "f", Affected: []osv.Affected{{EcosystemSpecific: osv.EcosystemSpecific{GOOS: []string{"linux"}}}}},
+ {ID: "g", Affected: []osv.Affected{{EcosystemSpecific: osv.EcosystemSpecific{GOARCH: []string{"amd64"}}, Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "0.0.1"}, {Fixed: "2.0.1"}}}}}}},
+ },
+ },
+ {
+ mod: &packages.Module{
+ Path: "example.mod/c",
+ },
+ },
+ {
+ mod: &packages.Module{
+ Path: "example.mod/d",
+ Version: "v1.2.0",
+ },
+ vulns: []*osv.Entry{
+ {ID: "l", Affected: []osv.Affected{{EcosystemSpecific: osv.EcosystemSpecific{GOOS: []string{"linux"}}}}},
+ },
+ },
+ }
+
+ filtered := mv.Filter("linux", "amd64")
+ if !reflect.DeepEqual(filtered, expected) {
+ t.Fatalf("Filter returned unexpected results, got:\n%s\nwant:\n%s", moduleVulnerabilitiesToString(filtered), moduleVulnerabilitiesToString(expected))
+ }
+}
+
+func TestVulnsForPackage(t *testing.T) {
+ mv := moduleVulnerabilities{
+ {
+ mod: &packages.Module{
+ Path: "example.mod/a",
+ Version: "v1.0.0",
+ },
+ vulns: []*osv.Entry{
+ {ID: "a", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/a/b/c"}}}},
+ },
+ },
+ {
+ mod: &packages.Module{
+ Path: "example.mod/a/b",
+ Version: "v1.0.0",
+ },
+ vulns: []*osv.Entry{
+ {ID: "b", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/a/b/c"}}}},
+ },
+ },
+ {
+ mod: &packages.Module{
+ Path: "example.mod/d",
+ Version: "v0.0.1",
+ },
+ vulns: []*osv.Entry{
+ {ID: "d", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/d"}}}},
+ },
+ },
+ }
+
+ filtered := mv.VulnsForPackage("example.mod/a/b/c")
+ expected := []*osv.Entry{
+ {ID: "b", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/a/b/c"}}}},
+ }
+
+ if !reflect.DeepEqual(filtered, expected) {
+ t.Fatalf("VulnsForPackage returned unexpected results, got:\n%s\nwant:\n%s", vulnsToString(filtered), vulnsToString(expected))
+ }
+}
+
+func TestVulnsForPackageReplaced(t *testing.T) {
+ mv := moduleVulnerabilities{
+ {
+ mod: &packages.Module{
+ Path: "example.mod/a",
+ Version: "v1.0.0",
+ },
+ vulns: []*osv.Entry{
+ {ID: "a", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/a/b/c"}}}},
+ },
+ },
+ {
+ mod: &packages.Module{
+ Path: "example.mod/a/b",
+ Replace: &packages.Module{
+ Path: "example.mod/b",
+ },
+ Version: "v1.0.0",
+ },
+ vulns: []*osv.Entry{
+ {ID: "c", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/b/c"}}}},
+ },
+ },
+ }
+
+ filtered := mv.VulnsForPackage("example.mod/a/b/c")
+ expected := []*osv.Entry{
+ {ID: "c", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/b/c"}}}},
+ }
+
+ if !reflect.DeepEqual(filtered, expected) {
+ t.Fatalf("VulnsForPackage returned unexpected results, got:\n%s\nwant:\n%s", vulnsToString(filtered), vulnsToString(expected))
+ }
+}
+
+func TestVulnsForSymbol(t *testing.T) {
+ mv := moduleVulnerabilities{
+ {
+ mod: &packages.Module{
+ Path: "example.mod/a",
+ Version: "v1.0.0",
+ },
+ vulns: []*osv.Entry{
+ {ID: "a", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/a/b/c"}}}},
+ },
+ },
+ {
+ mod: &packages.Module{
+ Path: "example.mod/a/b",
+ Version: "v1.0.0",
+ },
+ vulns: []*osv.Entry{
+ {ID: "b", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/a/b/c"}, EcosystemSpecific: osv.EcosystemSpecific{Symbols: []string{"a"}}}}},
+ {ID: "c", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/a/b/c"}, EcosystemSpecific: osv.EcosystemSpecific{Symbols: []string{"b"}}}}},
+ },
+ },
+ }
+
+ filtered := mv.VulnsForSymbol("example.mod/a/b/c", "a")
+ expected := []*osv.Entry{
+ {ID: "b", Affected: []osv.Affected{{Package: osv.Package{Name: "example.mod/a/b/c"}, EcosystemSpecific: osv.EcosystemSpecific{Symbols: []string{"a"}}}}},
+ }
+
+ if !reflect.DeepEqual(filtered, expected) {
+ t.Fatalf("VulnsForPackage returned unexpected results, got:\n%s\nwant:\n%s", vulnsToString(filtered), vulnsToString(expected))
+ }
+}