blob: c06aecb85ccf3d5f7f524b4acb79cfcfc8749f2e [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)
fixMajorVersion(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.
// For now, it gives up if it encounters various problems and
// special cases (see comments inline).
func fixMajorVersion(m *report.Module, pc *proxy.Client) {
if strings.HasPrefix(m.Module, "gopkg.in/") {
return // 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
}
wantMajor, ok := commonMajor(m.Versions)
if !ok { // inconsistent major version, don't attempt to fix
return
}
prefix, major, ok := module.SplitPathVersion(m.Module)
if !ok { // couldn't parse module path, don't attempt to fix
return
}
if major == wantMajor {
return // nothing to do
}
fixed := prefix + wantMajor
if !pc.ModuleExists(fixed) {
return // attempted fixed module doesn't exist, give up
}
m.Module = fixed
}
// 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) {
const (
v0 = "v0"
v1 = "v1"
v0v1 = "v0 or v1"
)
getMajor := func(v string) string {
m := version.Major(v)
if m == v0 || m == v1 {
return v0v1
}
return m
}
major := getMajor(first(vs))
for _, vr := range vs {
for _, v := range []string{vr.Introduced, vr.Fixed} {
if v == "" {
continue
}
current := getMajor(v)
if current != major {
return "", false
}
}
}
if major == v0v1 {
return "", true
}
return "/" + major, true
}
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
}