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,