blob: 172169a67a524bec187f45ecbb271fc81280cd41 [file] [log] [blame]
// Copyright 2023 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 genericosv
import (
"fmt"
"sort"
"strings"
osvschema "github.com/google/osv-scanner/pkg/models"
"golang.org/x/exp/slices"
"golang.org/x/mod/module"
"golang.org/x/vulndb/internal/cveschema5"
"golang.org/x/vulndb/internal/ghsa"
"golang.org/x/vulndb/internal/osv"
"golang.org/x/vulndb/internal/proxy"
"golang.org/x/vulndb/internal/report"
"golang.org/x/vulndb/internal/version"
)
// ToReport converts OSV into a Go Report with the given ID.
func (osv *Entry) ToReport(goID string, pc *proxy.Client) *report.Report {
r := &report.Report{
ID: goID,
Summary: osv.Summary,
Description: osv.Details,
}
addNote := func(note string) {
r.Notes = append(r.Notes, note)
}
addAlias := func(alias string) {
switch {
case cveschema5.IsCVE(alias):
r.CVEs = append(r.CVEs, alias)
case ghsa.IsGHSA(alias):
r.GHSAs = append(r.GHSAs, alias)
default:
addNote(fmt.Sprintf("create: found alias %s that is not a GHSA or CVE", alias))
}
}
addAlias(osv.ID)
for _, alias := range osv.Aliases {
addAlias(alias)
}
for _, ref := range osv.References {
r.References = append(r.References, convertRef(ref))
}
r.Modules = affectedToModules(osv.Affected, addNote, pc)
r.Credits = convertCredits(osv.Credits)
r.Fix(pc)
if lints := r.Lint(pc); len(lints) > 0 {
slices.Sort(lints)
for _, lint := range lints {
addNote(fmt.Sprintf("lint: %s", lint))
}
}
return r
}
type addNoteFunc func(string)
func affectedToModules(as []osvschema.Affected, addNote addNoteFunc, pc *proxy.Client) []*report.Module {
var modules []*report.Module
for _, a := range as {
if a.Package.Ecosystem != osvschema.EcosystemGo {
continue
}
modules = append(modules, &report.Module{
Module: a.Package.Name,
Versions: convertVersions(a.Ranges, addNote),
})
}
for _, m := range modules {
extractImportPath(m, pc)
if ok := fixMajorVersion(m, pc); !ok {
addIncompatible(m, pc)
}
canonicalize(m, pc)
m.FixVersions(pc)
}
sortModules(modules)
return modules
}
// extractImportPath checks if the module m's "module" path is actually
// an import path. If so, it adds the import path to the packages list
// and fixes the module path. Modifies m.
//
// Does nothing if the module path is already correct, or isn't recognized
// by the proxy at all.
func extractImportPath(m *report.Module, pc *proxy.Client) {
path := m.Module
modulePath, err := pc.FindModule(m.Module)
if err != nil || // path doesn't contain a module, needs human review
path == modulePath { // path is already a module, no action needed
return
}
m.Module = modulePath
m.Packages = append(m.Packages, &report.Package{Package: path})
}
// fixMajorVersion corrects the major version prefix of the module
// path if possible.
// Returns true if the major version was already correct or could be
// fixed.
// For now, it gives up if it encounters various problems and
// special cases (see comments inline).
func fixMajorVersion(m *report.Module, pc *proxy.Client) (ok bool) {
if strings.HasPrefix(m.Module, "gopkg.in/") {
return false // don't attempt to fix gopkg.in modules
}
// If there is no "introduced" version, don't attempt to fix
// major version.
// Example: example.com/module is fixed at 2.2.2. This likely means
// that example.com/module is vulnerable at all versions and
// example.com/module/v2 is vulnerable up to 2.2.2.
// Changing example.com/module to example.com/module/v2 would lose
// information.
hasIntroduced := func(m *report.Module) bool {
for _, vr := range m.Versions {
if vr.Introduced != "" {
return true
}
}
return false
}
if !hasIntroduced(m) {
return false
}
wantMajor, ok := commonMajor(m.Versions)
if !ok { // inconsistent major version, don't attempt to fix
return false
}
prefix, major, ok := module.SplitPathVersion(m.Module)
if !ok { // couldn't parse module path, don't attempt to fix
return false
}
if major == wantMajor {
return true // nothing to do
}
fixed := prefix + wantMajor
if !pc.ModuleExists(fixed) {
return false // attempted fixed module doesn't exist, give up
}
m.Module = fixed
return true
}
const (
v0 = "v0"
v1 = "v1"
v0v1 = "v0 or v1"
)
func major(v string) string {
m := version.Major(v)
if m == v0 || m == v1 {
return v0v1
}
return m
}
// commonMajor returns the major version path suffix (e.g. "/v2") common
// to all versions in the version range, or ("", false) if not all versions
// have the same major version.
// Returns ("", true) if the major version is 0 or 1.
func commonMajor(vs []report.VersionRange) (_ string, ok bool) {
maj := major(first(vs))
for _, vr := range vs {
for _, v := range []string{vr.Introduced, vr.Fixed} {
if v == "" {
continue
}
current := major(v)
if current != maj {
return "", false
}
}
}
if maj == v0v1 {
return "", true
}
return "/" + maj, true
}
// canonicalize attempts to canonicalize the module path,
// and updates the module path and packages list if successful.
// Modifies m.
//
// Does nothing if the module path is already canonical, or isn't recognized
// by the proxy at all.
func canonicalize(m *report.Module, pc *proxy.Client) {
if len(m.Versions) == 0 {
return // no versions, don't attempt to fix
}
canonical, err := commonCanonical(m, pc)
if err != nil {
return // no consistent canonical version found, don't attempt to fix
}
original := m.Module
m.Module = canonical
// Fix any package paths.
for _, p := range m.Packages {
if strings.HasPrefix(p.Package, original) {
p.Package = canonical + strings.TrimPrefix(p.Package, original)
}
}
}
func commonCanonical(m *report.Module, pc *proxy.Client) (string, error) {
canonical, err := pc.CanonicalModulePath(m.Module, first(m.Versions))
if err != nil {
return "", err
}
for _, vr := range m.Versions {
for _, v := range []string{vr.Introduced, vr.Fixed} {
if v == "" {
continue
}
current, err := pc.CanonicalModulePath(m.Module, v)
if err != nil {
return "", err
}
if current != canonical {
return "", fmt.Errorf("inconsistent canonical module paths: %s and %s", canonical, current)
}
}
}
return canonical, nil
}
// addIncompatible adds "+incompatible" to all versions where module@version
// does not exist but module@version+incompatible does exist.
// TODO(https://go.dev/issue/61769): Consider making this work for
// non-canonical versions too (example: GHSA-w4xh-w33p-4v29).
func addIncompatible(m *report.Module, pc *proxy.Client) {
tryAdd := func(v string) (string, bool) {
if v == "" {
return "", false
}
if major(v) == v0v1 {
return "", false // +incompatible does not apply for major versions < 2
}
if pc.ModuleExistsAtTaggedVersion(m.Module, v) {
return "", false // module@version is already OK
}
if vi := v + "+incompatible"; pc.ModuleExistsAtTaggedVersion(m.Module, vi) {
return vi, true
}
return "", false // module@version+incompatible doesn't exist
}
for i, vr := range m.Versions {
if vi, ok := tryAdd(vr.Introduced); ok {
m.Versions[i].Introduced = vi
}
if vi, ok := tryAdd(vr.Fixed); ok {
m.Versions[i].Fixed = vi
}
}
}
func sortModules(ms []*report.Module) {
sort.Slice(ms, func(i, j int) bool {
m1, m2 := ms[i], ms[j]
// Break ties by lowest affected version, assuming the version list is sorted.
if m1.Module == m2.Module {
vr1, vr2 := m1.Versions, m2.Versions
if len(vr1) == 0 {
return true
} else if len(vr2) == 0 {
return false
}
return version.Before(first(vr1), first(vr2))
}
return m1.Module < m2.Module
})
}
func first(vrs []report.VersionRange) string {
for _, vr := range vrs {
for _, v := range []string{vr.Introduced, vr.Fixed} {
if v != "" {
return v
}
}
}
return ""
}
func convertVersions(rs []osvschema.Range, addNote addNoteFunc) []report.VersionRange {
var vrs []report.VersionRange
for _, r := range rs {
for _, e := range r.Events {
var vr report.VersionRange
switch {
case e.Introduced == "0":
continue
case e.Introduced != "":
vr.Introduced = e.Introduced
case e.Fixed != "":
vr.Fixed = e.Fixed
default:
addNote(fmt.Sprintf("create: unsupported version range event %#v", e))
continue
}
vrs = append(vrs, vr)
}
}
return vrs
}
func convertRef(ref osvschema.Reference) *report.Reference {
return &report.Reference{
Type: osv.ReferenceType(ref.Type),
URL: ref.URL,
}
}
func convertCredits(cs []osvschema.Credit) []string {
var credits []string
for _, c := range cs {
credit := c.Name
if len(c.Contact) != 0 {
credit = fmt.Sprintf("%s (%s)", c.Name, strings.Join(c.Contact, ","))
}
credits = append(credits, credit)
}
return credits
}