blob: fa7e5bd804df6a6d716c1c6abed2c9253c9901e5 [file] [log] [blame]
// 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 osv implements the OSV shared vulnerability
// format, as defined by https://github.com/ossf/osv-schema.
//
// As this package is intended for use with the Go vulnerability
// database, only the subset of features which are used by that
// database are implemented (for instance, only the SEMVER affected
// range type is implemented).
package osv
import (
"strings"
"time"
"golang.org/x/mod/semver"
"golang.org/x/vulndb/report"
)
// DBIndex contains a mapping of vulnerable packages to the
// last time a new vulnerability was added to the database.
// TODO: this is probably not the correct place to put this
// type, since it's not really an OSV/CVF thing, but rather
// vulndb implementatiion detail.
type DBIndex map[string]time.Time
type AffectsRangeType string
const (
TypeUnspecified AffectsRangeType = "UNSPECIFIED"
TypeGit AffectsRangeType = "GIT"
TypeSemver AffectsRangeType = "SEMVER"
)
type Ecosystem string
const GoEcosystem Ecosystem = "Go"
type Package struct {
Name string `json:"name"`
Ecosystem Ecosystem `json:"ecosystem"`
}
type RangeEvent struct {
Introduced string `json:"introduced,omitempty"`
Fixed string `json:"fixed,omitempty"`
}
type AffectsRange struct {
Type AffectsRangeType `json:"type"`
Events []RangeEvent `json:"events"`
}
// addSemverPrefix adds a 'v' prefix to s if it isn't already prefixed
// with 'v' or 'go'. This allows us to easily test go-style SEMVER
// strings against normal SEMVER strings.
func addSemverPrefix(s string) string {
if !strings.HasPrefix(s, "v") && !strings.HasPrefix(s, "go") {
return "v" + s
}
return s
}
func (ar AffectsRange) containsSemver(v string) bool {
if ar.Type != TypeSemver {
return false
}
if len(ar.Events) == 0 {
return true
}
// Strip and then add the semver prefix so we can support bare versions,
// versions prefixed with 'v', and versions prefixed with 'go'.
v = addSemverPrefix(removeSemverPrefix(v))
var affected bool
for _, e := range ar.Events {
if !affected && e.Introduced != "" {
affected = e.Introduced == "0" || semver.Compare(v, addSemverPrefix(e.Introduced)) >= 0
} else if e.Fixed != "" {
affected = semver.Compare(v, addSemverPrefix(e.Fixed)) < 0
}
}
return affected
}
type Affects []AffectsRange
// removeSemverPrefix removes the 'v' or 'go' prefixes from go-style
// SEMVER strings, for usage in the public vulnerability format.
func removeSemverPrefix(s string) string {
s = strings.TrimPrefix(s, "v")
s = strings.TrimPrefix(s, "go")
return s
}
func generateAffectedRanges(versions []report.VersionRange) Affects {
a := AffectsRange{Type: TypeSemver}
if len(versions) == 0 || versions[0].Introduced == "" {
a.Events = append(a.Events, RangeEvent{Introduced: "0"})
}
for _, v := range versions {
if v.Introduced != "" {
a.Events = append(a.Events, RangeEvent{Introduced: removeSemverPrefix(v.Introduced)})
}
if v.Fixed != "" {
a.Events = append(a.Events, RangeEvent{Fixed: removeSemverPrefix(v.Fixed)})
}
}
return Affects{a}
}
func (a Affects) AffectsSemver(v string) bool {
if len(a) == 0 {
// No ranges implies all versions are affected
return true
}
var semverRangePresent bool
for _, r := range a {
if r.Type != TypeSemver {
continue
}
semverRangePresent = true
if r.containsSemver(v) {
return true
}
}
// If there were no semver ranges present we
// assume that all semvers are affected, similarly
// to how to we assume all semvers are affected
// if there are no ranges at all.
return !semverRangePresent
}
type Reference struct {
Type string `json:"type"`
URL string `json:"url"`
}
type Affected struct {
Package Package `json:"package"`
Ranges Affects `json:"ranges,omitempty"`
DatabaseSpecific DatabaseSpecific `json:"database_specific"`
EcosystemSpecific EcosystemSpecific `json:"ecosystem_specific"`
}
type DatabaseSpecific struct {
URL string `json:"url"`
}
type EcosystemSpecific struct {
Symbols []string `json:"symbols,omitempty"`
GOOS []string `json:"goos,omitempty"`
GOARCH []string `json:"goarch,omitempty"`
}
// Entry represents a OSV style JSON vulnerability database
// entry
type Entry struct {
ID string `json:"id"`
Published time.Time `json:"published"`
Modified time.Time `json:"modified"`
Withdrawn *time.Time `json:"withdrawn,omitempty"`
Aliases []string `json:"aliases,omitempty"`
Details string `json:"details"`
Affected []Affected `json:"affected"`
References []Reference `json:"references,omitempty"`
}
func generateAffected(importPath string, versions []report.VersionRange, goos, goarch, symbols []string, url string) Affected {
return Affected{
Package: Package{
Name: importPath,
Ecosystem: GoEcosystem,
},
Ranges: generateAffectedRanges(versions),
DatabaseSpecific: DatabaseSpecific{URL: url},
EcosystemSpecific: EcosystemSpecific{
GOOS: goos,
GOARCH: goarch,
Symbols: symbols,
},
}
}
func Generate(id string, url string, r report.Report) (Entry, []string) {
importPath := r.Module
if r.Package != "" {
importPath = r.Package
}
moduleMap := make(map[string]bool)
if r.Stdlib {
moduleMap["stdlib"] = true
} else {
moduleMap[r.Module] = true
}
lastModified := r.Published
if r.LastModified != nil {
lastModified = *r.LastModified
}
entry := Entry{
ID: id,
Published: r.Published,
Modified: lastModified,
Withdrawn: r.Withdrawn,
Details: r.Description,
Affected: []Affected{generateAffected(importPath, r.Versions, r.OS, r.Arch, r.Symbols, url)},
}
for _, additional := range r.AdditionalPackages {
additionalPath := additional.Module
if additional.Package != "" {
additionalPath = additional.Package
}
if !r.Stdlib {
moduleMap[additional.Module] = true
}
entry.Affected = append(entry.Affected, generateAffected(additionalPath, additional.Versions, r.OS, r.Arch, additional.Symbols, url))
}
if r.Links.PR != "" {
entry.References = append(entry.References, Reference{Type: "FIX", URL: r.Links.PR})
}
if r.Links.Commit != "" {
entry.References = append(entry.References, Reference{Type: "FIX", URL: r.Links.Commit})
}
for _, link := range r.Links.Context {
entry.References = append(entry.References, Reference{Type: "WEB", URL: link})
}
if r.CVE != "" {
entry.Aliases = []string{r.CVE}
}
var modules []string
for module := range moduleMap {
modules = append(modules, module)
}
return entry, modules
}