blob: cff9a33ec571ece9eaf72ee8ee9a3701be6af84b [file] [log] [blame]
// 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 vuln
import (
"context"
"fmt"
"go/token"
"strings"
"golang.org/x/pkgsite/internal/osv"
"golang.org/x/pkgsite/internal/stdlib"
vers "golang.org/x/pkgsite/internal/version"
)
// 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
}
// VulnsForPackage obtains vulnerability information for the given package.
// If packagePath is empty, it returns all entries for the module at version.
// If there is an error, VulnsForPackage returns a single Vuln that describes the error.
func VulnsForPackage(ctx context.Context, modulePath, version, packagePath string, vc *Client) []Vuln {
if vc == nil {
return nil
}
// Handle special module paths.
if modulePath == stdlib.ModulePath {
// Stdlib pages requested at master will map to a pseudo version
// that puts all vulns in range.
// We can't really tell you're at master so version.IsPseudo
// is the best we can do. The result is vulns won't be reported for a
// pseudoversion that refers to a commit that is in a vulnerable range.
switch {
case vers.IsPseudo(version):
return nil
case strings.HasPrefix(packagePath, "cmd/"):
modulePath = osv.GoCmdModulePath
default:
modulePath = osv.GoStdModulePath
}
}
// Get all the vulns for this package/version.
entries, err := vc.ByPackage(ctx, &PackageRequest{Module: modulePath, Package: packagePath, Version: version})
if err != nil {
return []Vuln{{Details: fmt.Sprintf("could not get vulnerability data: %v", err)}}
}
return toVulns(entries)
}
func toVulns(entries []*osv.Entry) []Vuln {
if len(entries) == 0 {
return nil
}
vulns := make([]Vuln, len(entries))
for i, e := range entries {
vulns[i] = Vuln{
ID: e.ID,
Details: e.Summary,
}
}
return vulns
}
// AffectedComponent holds information about a module/package affected by a certain vulnerability.
type AffectedComponent struct {
Path string
Versions string
// Lists of affected symbols (for packages).
// If both of these lists are empty, all symbols in the package are affected.
ExportedSymbols []string
UnexportedSymbols []string
}
// 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.Module.Path) {
prefix = "go"
} else {
prefix = "v"
}
for _, r := range a.Ranges {
isSemver := r.Type == osv.RangeTypeSemver
for _, v := range r.Events {
if v.Introduced != "" {
// We expected Introduced and Fixed to alternate, but if
// p.intro != "", then 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
}
// AffectedComponents extracts information about affected packages (and // modules, if there are any with no package information) from the given osv.Entry.
func AffectedComponents(e *osv.Entry) (pkgs, modsNoPkgs []*AffectedComponent) {
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)
}
if len(a.EcosystemSpecific.Packages) == 0 {
modsNoPkgs = append(modsNoPkgs, &AffectedComponent{
Path: a.Module.Path,
Versions: strings.Join(vs, ", "),
})
}
for _, p := range a.EcosystemSpecific.Packages {
exported, unexported := affectedSymbols(p.Symbols)
pkgs = append(pkgs, &AffectedComponent{
Path: p.Path,
Versions: strings.Join(vs, ", "),
ExportedSymbols: exported,
UnexportedSymbols: unexported,
// TODO(hyangah): where to place GOOS/GOARCH info
})
}
}
return pkgs, modsNoPkgs
}
func affectedSymbols(in []string) (e, u []string) {
for _, s := range in {
exported := true
for _, part := range strings.Split(s, ".") {
if !token.IsExported(part) {
exported = false // exported only if all parts of the symbol name are exported.
}
}
if exported {
e = append(e, s)
} else {
u = append(u, s)
}
}
return e, u
}