blob: ba08f3fc68893feaf1213d068ba0017ef53b1571 [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 cve5
import (
"errors"
"fmt"
"regexp"
"slices"
"strings"
"golang.org/x/vulndb/internal/derrors"
"golang.org/x/vulndb/internal/idstr"
"golang.org/x/vulndb/internal/osv"
"golang.org/x/vulndb/internal/proxy"
"golang.org/x/vulndb/internal/report"
"golang.org/x/vulndb/internal/stdlib"
"golang.org/x/vulndb/internal/version"
)
var (
// The universal unique identifier for the Go Project CNA, which
// needs to be included CVE JSON 5.0 records.
GoOrgUUID = "1bb62c36-49e3-4200-9d77-64a1400537cc"
)
// FromReport creates a CVE in 5.0 format from a YAML report file.
func FromReport(r *report.Report) (_ *CVERecord, err error) {
defer derrors.Wrap(&err, "FromReport(%q)", r.ID)
if r.CVEMetadata == nil {
return nil, errors.New("report missing cve_metadata section")
}
if r.CVEMetadata.ID == "" {
return nil, errors.New("report missing CVE ID")
}
description := r.CVEMetadata.Description
if description == "" {
description = r.Description.String()
}
if r.CVEMetadata.CWE == "" {
return nil, errors.New("report missing CWE")
}
c := &CNAPublishedContainer{
ProviderMetadata: ProviderMetadata{
OrgID: GoOrgUUID,
},
Title: report.RemoveNewlines(r.Summary.String()),
Descriptions: []Description{
{
Lang: "en",
Value: report.RemoveNewlines(description),
},
},
ProblemTypes: []ProblemType{
{
Descriptions: []ProblemTypeDescription{
{
Lang: "en",
Description: r.CVEMetadata.CWE,
},
},
},
},
}
for _, m := range r.Modules {
versions, defaultStatus := versionsToVersionRanges(m.Versions)
for _, p := range m.Packages {
affected := Affected{
Vendor: report.Vendor(m.Module),
Product: p.Package,
CollectionURL: "https://pkg.go.dev",
PackageName: p.Package,
Versions: versions,
DefaultStatus: defaultStatus,
Platforms: p.GOOS,
}
for _, symbol := range p.AllSymbols() {
affected.ProgramRoutines = append(affected.ProgramRoutines, ProgramRoutine{Name: symbol})
}
c.Affected = append(c.Affected, affected)
}
}
for _, ref := range r.References {
c.References = append(c.References, Reference{URL: ref.URL})
}
c.References = append(c.References, Reference{
URL: idstr.GoAdvisory(r.ID),
})
for _, ref := range r.CVEMetadata.References {
c.References = append(c.References, Reference{URL: ref})
}
for _, credit := range r.Credits {
c.Credits = append(c.Credits, Credit{
Lang: "en",
Value: credit,
})
}
return &CVERecord{
DataType: "CVE_RECORD",
DataVersion: "5.0",
Metadata: Metadata{
ID: r.CVEMetadata.ID,
},
Containers: Containers{
CNAContainer: *c,
},
}, nil
}
const (
typeSemver = "semver"
versionZero = "0"
)
func versionsToVersionRanges(vs report.Versions) ([]VersionRange, VersionStatus) {
if len(vs) == 0 {
// If there are no recorded versions affected, we assume all versions are affected.
return nil, StatusAffected
}
var vrs []VersionRange
// If there is no final fixed version, then the default status is
// "affected" and we express the versions in terms of which ranges
// are *unaffected*. This is due to the fact that the CVE schema
// does not allow us to express a range as "version X.X.X and above are affected".
if vs[len(vs)-1].Type != report.VersionTypeFixed {
current := &VersionRange{}
for _, vr := range vs {
if vr.IsIntroduced() {
if current.Introduced == "" {
current.Introduced = versionZero
}
current.Fixed = Version(vr.Version)
current.Status = StatusUnaffected
current.VersionType = typeSemver
vrs = append(vrs, *current)
current = &VersionRange{}
} else if vr.IsFixed() {
current.Introduced = Version(vr.Version)
}
}
return vrs, StatusAffected
}
// Otherwise, express the version ranges normally as affected ranges,
// with a default status of "unaffected".
var current *VersionRange
for _, vr := range vs {
if vr.IsIntroduced() {
if current == nil {
current = &VersionRange{
Status: StatusAffected,
VersionType: typeSemver,
Introduced: Version(vr.Version),
}
}
}
if vr.IsFixed() {
if current == nil {
current = &VersionRange{
Status: StatusAffected,
VersionType: typeSemver,
Introduced: versionZero,
}
}
current.Fixed = Version(vr.Version)
vrs = append(vrs, *current)
current = nil
}
}
return vrs, StatusUnaffected
}
var _ report.Source = &CVERecord{}
func (c *CVERecord) ToReport(pxc *proxy.Client, modulePath string) *report.Report {
return cve5ToReport(c, pxc, modulePath)
}
func (c *CVERecord) SourceID() string {
return c.Metadata.ID
}
func (c *CVERecord) ReferenceURLs() []string {
var result []string
for _, r := range c.Containers.CNAContainer.References {
result = append(result, r.URL)
}
return result
}
func cve5ToReport(c *CVERecord, pxc *proxy.Client, modulePath string) *report.Report {
cna := c.Containers.CNAContainer
var description report.Description
for _, d := range cna.Descriptions {
if d.Lang == "en" {
description += report.Description(d.Value + "\n")
}
}
var credits []string
for _, c := range cna.Credits {
credits = append(credits, c.Value)
}
var refs []*report.Reference
for _, ref := range c.Containers.CNAContainer.References {
refs = append(refs, convertRef(ref))
}
r := &report.Report{
Modules: affectedToModules(cna.Affected, pxc, modulePath),
Summary: report.Summary(cna.Title),
Description: description,
Credits: credits,
References: refs,
}
r.AddCVE(c.Metadata.ID, getCWE5(&cna), isGoCNA5(&cna))
return r
}
func convertRef(ref Reference) *report.Reference {
if t := typeFromTags(ref.Tags); t != osv.ReferenceTypeWeb {
return &report.Reference{
Type: t,
URL: ref.URL,
}
}
return report.ReferenceFromUrl(ref.URL)
}
const (
refTagIssue = "issue-tracking"
refTagMailingList = "mailing-list"
refTagPatch = "patch"
refTagReleaseNotes = "release-notes"
refTag3PAdvisory = "third-party-advisory"
refTagVendorAdvisory = "vendor-advisory"
refTagVdbEntry = "vdb-entry"
refTagMedia = "media-coverage"
refTagTechnical = "technical-description"
refTagRelated = "related"
refTagGovt = "government resource"
refTagMitigation = "mitigation"
// uncategorized:
// "broken-link"
// "customer-entitlement"
// "not-applicable"
// "permissions-required"
// "product"
// "signature"
)
func tagToType(tag string) osv.ReferenceType {
switch tag {
case refTagVendorAdvisory:
return osv.ReferenceTypeAdvisory
case refTagIssue:
return osv.ReferenceTypeReport
case refTagPatch:
return osv.ReferenceTypeFix
}
return defaultType
}
var order = []osv.ReferenceType{
osv.ReferenceTypeAdvisory,
osv.ReferenceTypeFix,
osv.ReferenceTypeReport,
osv.ReferenceTypeWeb,
}
var defaultType = osv.ReferenceTypeWeb
func bestType(types []osv.ReferenceType) osv.ReferenceType {
if len(types) == 0 {
return defaultType
} else if len(types) == 1 {
return types[0]
}
slices.SortStableFunc(types, func(a, b osv.ReferenceType) int {
if a == b {
return 0
}
for _, t := range order {
if a == t {
return -1
}
if b == t {
return 1
}
}
return 0
})
return types[0]
}
func typeFromTags(tags []string) osv.ReferenceType {
var types []osv.ReferenceType
for _, tag := range tags {
if t := tagToType(tag); t != "" {
types = append(types, t)
}
}
return bestType(types)
}
func getCWE5(c *CNAPublishedContainer) string {
if len(c.ProblemTypes) == 0 || len(c.ProblemTypes[0].Descriptions) == 0 {
return ""
}
return c.ProblemTypes[0].Descriptions[0].Description
}
func isGoCNA5(c *CNAPublishedContainer) bool {
return c.ProviderMetadata.OrgID == GoOrgUUID
}
func affectedToModules(as []Affected, pxc *proxy.Client, fallbackModule string) []*report.Module {
// Use a placeholder module if there is no information on
// modules/packages in the CVE.
if len(as) == 0 {
return []*report.Module{{
Module: fallbackModule,
}}
}
var modules []*report.Module
for _, a := range as {
modules = append(modules, affectedToModule(&a, pxc, fallbackModule))
}
return modules
}
func affectedToModule(a *Affected, pxc *proxy.Client, fallbackModule string) *report.Module {
var pkgPath string
isSet := func(s string) bool {
const na = "n/a"
return s != "" && s != na
}
switch {
case isSet(a.PackageName):
pkgPath = a.PackageName
case isSet(a.Product):
pkgPath = a.Product
case isSet(a.Vendor):
pkgPath = a.Vendor
default:
pkgPath = fallbackModule
}
modulePath := fallbackModule
if stdlib.Contains(modulePath) && stdlib.Contains(pkgPath) {
// Standard library and toolchain
if strings.HasPrefix(pkgPath, stdlib.ToolchainModulePath) {
modulePath = stdlib.ToolchainModulePath
} else {
modulePath = stdlib.ModulePath
}
} else if mp, err := pxc.FindModule(pkgPath); mp != "" && err == nil { // no error
// Recognized third-party package path
modulePath = mp
} else {
// Unrecognized third-party package path
pkgPath = fallbackModule
}
vs, uvs := convertVersions(a.Versions, a.DefaultStatus)
// Add a package if we have any meaningful package-level data.
var pkgs []*report.Package
if pkgPath != modulePath || len(a.ProgramRoutines) != 0 || len(a.Platforms) != 0 {
var symbols []string
for _, s := range a.ProgramRoutines {
symbols = append(symbols, s.Name)
}
pkgs = []*report.Package{
{
Package: pkgPath,
Symbols: symbols,
GOOS: a.Platforms,
},
}
}
return &report.Module{
Module: modulePath,
Versions: vs,
UnsupportedVersions: uvs,
Packages: pkgs,
}
}
func convertVersions(vrs []VersionRange, defaultStatus VersionStatus) (vs report.Versions, uvs report.Versions) {
for _, vr := range vrs {
// Version ranges starting with "n/a" don't have any meaningful data.
if vr.Introduced == "n/a" {
continue
}
v, ok := toVersions(&vr, defaultStatus)
if ok {
vs = append(vs, v...)
continue
}
uvs = append(uvs, toUnsupported(&vr, defaultStatus))
}
return vs, uvs
}
var (
// Regex for matching version strings like "<= X, < Y".
introducedFixedRE = regexp.MustCompile(`^>= (.+), < (.+)$`)
// Regex for matching version strings like "< Y".
fixedRE = regexp.MustCompile(`^< (.+)$`)
)
func toVersions(cvr *VersionRange, defaultStatus VersionStatus) (report.Versions, bool) {
if cvr == nil {
return nil, true
}
intro, fixed := version.TrimPrefix(string(cvr.Introduced)), version.TrimPrefix(string(cvr.Fixed))
if intro == "" && fixed == "" {
return nil, true
}
// Handle special cases where the info is not quite correctly encoded but
// we can still figure out the intent.
// Case one: introduced version is of the form "<= X, < Y".
if m := introducedFixedRE.FindStringSubmatch(intro); len(m) == 3 {
return report.Versions{
report.Introduced(m[1]),
report.Fixed(m[2]),
}, true
}
// Case two: introduced version is of the form "< Y".
if m := fixedRE.FindStringSubmatch(intro); len(m) == 2 {
return report.Versions{
report.Fixed(m[1]),
}, true
}
// For now, don't attempt to fix any other cases we don't understand.
if cvr.VersionType != typeSemver ||
cvr.LessThanOrEqual != "" ||
!version.IsValid(intro) ||
!version.IsValid(fixed) ||
cvr.Status != StatusAffected ||
defaultStatus == StatusAffected {
return nil, false
}
if intro == "0" {
return report.Versions{
report.Fixed(fixed),
}, true
}
return report.Versions{
report.Introduced(intro), report.Fixed(fixed),
}, true
}
func toUnsupported(cvr *VersionRange, defaultStatus VersionStatus) *report.Version {
var version string
switch {
case cvr.Fixed != "":
version = fmt.Sprintf("%s from %s before %s", cvr.Status, cvr.Introduced, cvr.Fixed)
case cvr.LessThanOrEqual != "":
version = fmt.Sprintf("%s from %s to %s", cvr.Status, cvr.Introduced, cvr.LessThanOrEqual)
default:
version = fmt.Sprintf("%s at %s", cvr.Status, cvr.Introduced)
}
if defaultStatus != "" {
version = fmt.Sprintf("%s (default: %s)", version, defaultStatus)
}
return &report.Version{
Version: version,
Type: "cve_version_range",
}
}