osv: use new multi-package format
An implementation of the specification change proposed by
https://github.com/ossf/osv-schema/pull/1. The significant change here
is that instead of generating multiple entries for reports with
multiple packages (in the additional_packages section), we instead
generate a single entry that covers all of the packages, and write the
same entry for each module path.
Change-Id: Ia9d8e0a82081ab7f5becd20c6adf976f4d6966db
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/340210
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Trust: Roland Shoemaker <roland@golang.org>
Run-TryBot: Roland Shoemaker <roland@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Vulndb-Deploy: Roland Shoemaker <bracewell@google.com>
diff --git a/cmd/gendb/main.go b/cmd/gendb/main.go
index e7f25e1..27c8ec6 100644
--- a/cmd/gendb/main.go
+++ b/cmd/gendb/main.go
@@ -77,8 +77,9 @@
// TODO(rolandshoemaker): once the HTML representation is ready this should be
// the link to the HTML page.
linkName := fmt.Sprintf("%s%s.yaml", dbURL, name)
- for path, e := range osv.Generate(name, linkName, vuln) {
- jsonVulns[path] = append(jsonVulns[path], e...)
+ entry, paths := osv.Generate(name, linkName, vuln)
+ for _, path := range paths {
+ jsonVulns[path] = append(jsonVulns[path], entry)
}
}
diff --git a/osv/json.go b/osv/json.go
index 590b230..fa7e5bd 100644
--- a/osv/json.go
+++ b/osv/json.go
@@ -2,23 +2,13 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// Package osv implements the <name-pending> shared vulnerability
-// format, with the Go specific extensions, as defined by
-// https://tinyurl.com/vuln-json.
+// 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).
-//
-// The format of the Go specific "extra" JSON object is as follows:
-//
-// {
-// "symbols": [ string ],
-// "goos": [ string ],
-// "goarch": [ string ],
-// "url": string
-// }
package osv
import (
@@ -53,10 +43,14 @@
Ecosystem Ecosystem `json:"ecosystem"`
}
+type RangeEvent struct {
+ Introduced string `json:"introduced,omitempty"`
+ Fixed string `json:"fixed,omitempty"`
+}
+
type AffectsRange struct {
- Type AffectsRangeType `json:"type"`
- Introduced string `json:"introduced,omitempty"`
- Fixed string `json:"fixed,omitempty"`
+ Type AffectsRangeType `json:"type"`
+ Events []RangeEvent `json:"events"`
}
// addSemverPrefix adds a 'v' prefix to s if it isn't already prefixed
@@ -73,18 +67,27 @@
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))
- return (ar.Introduced == "" || semver.Compare(v, addSemverPrefix(ar.Introduced)) >= 0) &&
- (ar.Fixed == "" || semver.Compare(v, addSemverPrefix(ar.Fixed)) < 0)
+ 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 struct {
- Ranges []AffectsRange `json:"ranges,omitempty"`
-}
+type Affects []AffectsRange
// removeSemverPrefix removes the 'v' or 'go' prefixes from go-style
// SEMVER strings, for usage in the public vulnerability format.
@@ -94,31 +97,29 @@
return s
}
-func generateAffects(versions []report.VersionRange) Affects {
- a := Affects{}
+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 {
- a.Ranges = append(a.Ranges, AffectsRange{
- Type: TypeSemver,
- Introduced: removeSemverPrefix(v.Introduced),
- Fixed: removeSemverPrefix(v.Fixed),
- })
+ 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)})
+ }
}
- if len(a.Ranges) == 0 {
- // If all versions are affected, as indicated by an empty versions slice,
- // we need to include an empty TypeSemver AffectsRange in the JSON
- // output.
- a.Ranges = append(a.Ranges, AffectsRange{Type: TypeSemver})
- }
- return a
+ return Affects{a}
}
func (a Affects) AffectsSemver(v string) bool {
- if len(a.Ranges) == 0 {
+ if len(a) == 0 {
// No ranges implies all versions are affected
return true
}
var semverRangePresent bool
- for _, r := range a.Ranges {
+ for _, r := range a {
if r.Type != TypeSemver {
continue
}
@@ -134,38 +135,68 @@
return !semverRangePresent
}
-type GoSpecific struct {
- Symbols []string `json:"symbols,omitempty"`
- GOOS []string `json:"goos,omitempty"`
- GOARCH []string `json:"goarch,omitempty"`
- URL string `json:"url"`
-}
-
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"`
- Package Package `json:"package"`
- Details string `json:"details"`
- Affects Affects `json:"affects"`
- References []Reference `json:"references,omitempty"`
- EcosystemSpecific GoSpecific `json:"ecosystem_specific"`
+ 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 Generate(id string, url string, r report.Report) map[string][]Entry {
+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
@@ -175,18 +206,19 @@
Published: r.Published,
Modified: lastModified,
Withdrawn: r.Withdrawn,
- Package: Package{
- Name: importPath,
- Ecosystem: GoEcosystem,
- },
- Details: r.Description,
- Affects: generateAffects(r.Versions),
- EcosystemSpecific: GoSpecific{
- Symbols: r.Symbols,
- GOOS: r.OS,
- GOARCH: r.Arch,
- URL: url,
- },
+ 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 != "" {
@@ -203,30 +235,10 @@
entry.Aliases = []string{r.CVE}
}
- entries := map[string][]Entry{}
- modulePath := r.Module
- if r.Stdlib {
- modulePath = "stdlib"
- }
- entries[modulePath] = []Entry{entry}
-
- // It would be better if this was just a recursive thing maybe?
- for _, additional := range r.AdditionalPackages {
- entryCopy := entry
- additionalImportPath := additional.Module
- if additional.Package != "" {
- additionalImportPath = additional.Package
- }
- entryCopy.Package.Name = additionalImportPath
- entryCopy.EcosystemSpecific.Symbols = additional.Symbols
- entryCopy.Affects = generateAffects(additional.Versions)
-
- modulePath := additional.Module
- if r.Stdlib {
- modulePath = "stdlib"
- }
- entries[modulePath] = append(entries[modulePath], entryCopy)
+ var modules []string
+ for module := range moduleMap {
+ modules = append(modules, module)
}
- return entries
+ return entry, modules
}
diff --git a/osv/json_test.go b/osv/json_test.go
index 20c3026..d55d5ca 100644
--- a/osv/json_test.go
+++ b/osv/json_test.go
@@ -5,6 +5,8 @@
package osv
import (
+ "reflect"
+ "sort"
"testing"
"time"
@@ -26,6 +28,14 @@
{Introduced: "v2.5.0"},
},
},
+ {
+ Module: "example.com/also-vulnerable",
+ Package: "example.com/also-vulnerable/package",
+ Symbols: []string{"z"},
+ Versions: []report.VersionRange{
+ {Fixed: "v2.1.1"},
+ },
+ },
},
Versions: []report.VersionRange{
{Fixed: "v2.1.1"},
@@ -45,92 +55,123 @@
},
}
- want := map[string][]Entry{
- "example.com/vulnerable/v2": []Entry{
+ url := "https://vulns.golang.org/GO-1991-0001.html"
+ wantEntry := Entry{
+ ID: "GO-1991-0001",
+ Details: "It's a real bad one, I'll tell you that",
+ References: []Reference{
+ {Type: "FIX", URL: "pr"},
+ {Type: "FIX", URL: "commit"},
+ {Type: "WEB", URL: "issue-a"},
+ {Type: "WEB", URL: "issue-b"},
+ },
+ Aliases: []string{"CVE-0000-0000"},
+ Affected: []Affected{
{
- ID: "GO-1991-0001",
Package: Package{
Name: "example.com/vulnerable/v2",
Ecosystem: "Go",
},
- Details: "It's a real bad one, I'll tell you that",
- Affects: Affects{
- Ranges: []AffectsRange{
- {
- Type: TypeSemver,
- Fixed: "2.1.1",
- },
- {
- Type: TypeSemver,
- Introduced: "2.3.4",
- Fixed: "2.3.5",
- },
- {
- Type: TypeSemver,
- Introduced: "2.5.0",
+ Ranges: []AffectsRange{
+ {
+ Type: TypeSemver,
+ Events: []RangeEvent{
+ {
+ Introduced: "0",
+ },
+ {
+ Fixed: "2.1.1",
+ },
+ {
+ Introduced: "2.3.4",
+ },
+ {
+ Fixed: "2.3.5",
+ },
+ {
+ Introduced: "2.5.0",
+ },
},
},
},
- References: []Reference{
- Reference{Type: "FIX", URL: "pr"},
- Reference{Type: "FIX", URL: "commit"},
- Reference{Type: "WEB", URL: "issue-a"},
- Reference{Type: "WEB", URL: "issue-b"},
- },
- Aliases: []string{"CVE-0000-0000"},
- EcosystemSpecific: GoSpecific{
+ DatabaseSpecific: DatabaseSpecific{URL: url},
+ EcosystemSpecific: EcosystemSpecific{
Symbols: []string{"A", "B.b"},
GOOS: []string{"windows"},
GOARCH: []string{"arm64"},
- URL: "https://vulns.golang.org/GO-1991-0001.html",
},
},
- },
- "vanity.host/vulnerable": []Entry{
{
-
- ID: "GO-1991-0001",
Package: Package{
Name: "vanity.host/vulnerable/package",
Ecosystem: "Go",
},
- Details: "It's a real bad one, I'll tell you that",
- Affects: Affects{
- Ranges: []AffectsRange{
- {
- Type: TypeSemver,
- Fixed: "2.1.1",
- },
- {
- Type: TypeSemver,
- Introduced: "2.3.4",
- Fixed: "2.3.5",
- },
- {
- Type: TypeSemver,
- Introduced: "2.5.0",
+ Ranges: []AffectsRange{
+ {
+ Type: TypeSemver,
+ Events: []RangeEvent{
+ {
+ Introduced: "0",
+ },
+ {
+ Fixed: "2.1.1",
+ },
+ {
+ Introduced: "2.3.4",
+ },
+ {
+ Fixed: "2.3.5",
+ },
+ {
+ Introduced: "2.5.0",
+ },
},
},
},
- References: []Reference{
- Reference{Type: "FIX", URL: "pr"},
- Reference{Type: "FIX", URL: "commit"},
- Reference{Type: "WEB", URL: "issue-a"},
- Reference{Type: "WEB", URL: "issue-b"},
- },
- Aliases: []string{"CVE-0000-0000"},
- EcosystemSpecific: GoSpecific{
+ DatabaseSpecific: DatabaseSpecific{URL: url},
+ EcosystemSpecific: EcosystemSpecific{
Symbols: []string{"b", "A.b"},
GOOS: []string{"windows"},
GOARCH: []string{"arm64"},
- URL: "https://vulns.golang.org/GO-1991-0001.html",
+ },
+ },
+ {
+ Package: Package{
+ Name: "example.com/also-vulnerable/package",
+ Ecosystem: "Go",
+ },
+ Ranges: []AffectsRange{
+ {
+ Type: TypeSemver,
+ Events: []RangeEvent{
+ {
+ Introduced: "0",
+ },
+ {
+ Fixed: "2.1.1",
+ },
+ },
+ },
+ },
+ DatabaseSpecific: DatabaseSpecific{URL: url},
+ EcosystemSpecific: EcosystemSpecific{
+ Symbols: []string{"z"},
+ GOOS: []string{"windows"},
+ GOARCH: []string{"arm64"},
},
},
},
}
- got := Generate("GO-1991-0001", "https://vulns.golang.org/GO-1991-0001.html", r)
- if diff := cmp.Diff(want, got, cmp.Comparer(func(_, _ time.Time) bool { return true })); diff != "" {
- t.Errorf("Generate returned unexpected result (-want +got):\n%s", diff)
+ wantModules := []string{"example.com/vulnerable/v2", "vanity.host/vulnerable", "example.com/also-vulnerable"}
+ sort.Strings(wantModules)
+
+ gotEntry, gotModules := Generate("GO-1991-0001", url, r)
+ if diff := cmp.Diff(wantEntry, gotEntry, cmp.Comparer(func(a, b time.Time) bool { return a.Equal(b) })); diff != "" {
+ t.Errorf("Generate returned unexpected entry (-want +got):\n%s", diff)
+ }
+ sort.Strings(gotModules)
+ if !reflect.DeepEqual(gotModules, wantModules) {
+ t.Errorf("Generate returned unexpected modules: got %v, want %v", gotModules, wantModules)
}
}
@@ -149,119 +190,87 @@
{
// Affects containing an empty SEMVER range also indicates
// everything is affected
- affects: Affects{Ranges: []AffectsRange{{Type: TypeSemver}}},
+ affects: []AffectsRange{{Type: TypeSemver}},
+ version: "v0.0.0",
+ want: true,
+ },
+ {
+ // Affects containing a SEMVER range with only an "introduced":"0"
+ // also indicates everything is affected
+ affects: []AffectsRange{{Type: TypeSemver, Events: []RangeEvent{{Introduced: "0"}}}},
version: "v0.0.0",
want: true,
},
{
// v1.0.0 < v2.0.0
- affects: Affects{
- Ranges: []AffectsRange{
- {Type: TypeSemver, Fixed: "2.0.0"},
- },
- },
+ affects: []AffectsRange{{Type: TypeSemver, Events: []RangeEvent{{Introduced: "0"}, {Fixed: "2.0.0"}}}},
version: "v1.0.0",
want: true,
},
{
// v0.0.1 <= v1.0.0
- affects: Affects{
- Ranges: []AffectsRange{
- {Type: TypeSemver, Introduced: "0.0.1"},
- },
- },
+ affects: []AffectsRange{{Type: TypeSemver, Events: []RangeEvent{{Introduced: "0.0.1"}}}},
version: "v1.0.0",
want: true,
},
{
// v1.0.0 <= v1.0.0
- affects: Affects{
- Ranges: []AffectsRange{
- {Type: TypeSemver, Introduced: "1.0.0"},
- },
- },
+ affects: []AffectsRange{{Type: TypeSemver, Events: []RangeEvent{{Introduced: "1.0.0"}}}},
version: "v1.0.0",
want: true,
},
{
// v1.0.0 <= v1.0.0 < v2.0.0
- affects: Affects{
- Ranges: []AffectsRange{
- {Type: TypeSemver, Introduced: "1.0.0", Fixed: "2.0.0"},
- },
- },
+ affects: []AffectsRange{{Type: TypeSemver, Events: []RangeEvent{{Introduced: "1.0.0"}, {Fixed: "2.0.0"}}}},
version: "v1.0.0",
want: true,
},
{
// v0.0.1 <= v1.0.0 < v2.0.0
- affects: Affects{
- Ranges: []AffectsRange{
- {Type: TypeSemver, Introduced: "0.0.1", Fixed: "2.0.0"},
- },
- },
+ affects: []AffectsRange{{Type: TypeSemver, Events: []RangeEvent{{Introduced: "0.0.1"}, {Fixed: "2.0.0"}}}},
version: "v1.0.0",
want: true,
},
{
// v2.0.0 < v3.0.0
- affects: Affects{
- Ranges: []AffectsRange{
- {Type: TypeSemver, Introduced: "1.0.0", Fixed: "2.0.0"},
- },
- },
+ affects: []AffectsRange{{Type: TypeSemver, Events: []RangeEvent{{Introduced: "1.0.0"}, {Fixed: "2.0.0"}}}},
version: "v3.0.0",
want: false,
},
{
// Multiple ranges
- affects: Affects{
- Ranges: []AffectsRange{
- {Type: TypeSemver, Introduced: "1.0.0", Fixed: "2.0.0"},
- {Type: TypeSemver, Introduced: "3.0.0"},
- },
- },
+ affects: []AffectsRange{{Type: TypeSemver, Events: []RangeEvent{{Introduced: "1.0.0"}, {Fixed: "2.0.0"}, {Introduced: "3.0.0"}}}},
version: "v3.0.0",
want: true,
},
{
// Wrong type range
- affects: Affects{
- Ranges: []AffectsRange{
- {Type: TypeUnspecified, Introduced: "3.0.0"},
- },
- },
+ affects: []AffectsRange{{Type: TypeUnspecified, Events: []RangeEvent{{Introduced: "3.0.0"}}}},
version: "v3.0.0",
want: true,
},
{
// Semver ranges don't match
- affects: Affects{
- Ranges: []AffectsRange{
- {Type: TypeUnspecified, Introduced: "3.0.0"},
- {Type: TypeSemver, Introduced: "4.0.0"},
- },
+ affects: []AffectsRange{
+ {Type: TypeUnspecified, Events: []RangeEvent{{Introduced: "3.0.0"}}},
+ {Type: TypeSemver, Events: []RangeEvent{{Introduced: "4.0.0"}}},
},
version: "v3.0.0",
want: false,
},
{
// Semver ranges do match
- affects: Affects{
- Ranges: []AffectsRange{
- {Type: TypeUnspecified, Introduced: "3.0.0"},
- {Type: TypeSemver, Introduced: "3.0.0"},
- },
+ affects: []AffectsRange{
+ {Type: TypeUnspecified, Events: []RangeEvent{{Introduced: "3.0.0"}}},
+ {Type: TypeSemver, Events: []RangeEvent{{Introduced: "3.0.0"}}},
},
version: "v3.0.0",
want: true,
},
{
// Semver ranges match (go prefix)
- affects: Affects{
- Ranges: []AffectsRange{
- {Type: TypeSemver, Introduced: "3.0.0"},
- },
+ affects: []AffectsRange{
+ {Type: TypeSemver, Events: []RangeEvent{{Introduced: "3.0.0"}}},
},
version: "go3.0.1",
want: true,