Skip to content

Commit

Permalink
internal/report: fix bug in CVE5 generation
Browse files Browse the repository at this point in the history
Fixes a bug in which incorrect version ranges were sometimes generated
when converting reports to CVE5. The bug happens when operating on a report
with no fixed version.

The problem is that the CVE JSON 5.0 format only allows version ranges
of the form "versions X to Y are affected", "versions X to Y are NOT affected"
or "version X is affected".

It does not directly allow the statement "version X and above are affected" - this must be expressed as "version 0 through X are unaffected, all others are affected". This change allows that to be expressed.

This bug became clear when we published GO-2023-2328. The CVE for that report
is also re-generated as a part of this change.

Change-Id: I0c61168581d65b13850d3a763a3300c04594b84c
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/545295
LUCI-TryBot-Result: Go LUCI <[email protected]>
Reviewed-by: Damien Neil <[email protected]>
  • Loading branch information
tatianab committed Nov 27, 2023
1 parent edf27d2 commit a33350d
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 14 deletions.
8 changes: 4 additions & 4 deletions data/cve/v5/GO-2023-2328.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
"packageName": "github.com/go-resty/resty/v2",
"versions": [
{
"version": "2.10.0",
"lessThan": "",
"status": "affected",
"version": "0",
"lessThan": "2.10.0",
"status": "unaffected",
"versionType": "semver"
}
],
Expand Down Expand Up @@ -65,7 +65,7 @@
"name": "Request.Send"
}
],
"defaultStatus": "unaffected"
"defaultStatus": "affected"
}
],
"problemTypes": [
Expand Down
53 changes: 43 additions & 10 deletions internal/report/cve5.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,7 @@ func (r *Report) ToCVE5() (_ *cveschema5.CVERecord, err error) {
}

for _, m := range r.Modules {
versions := versionRangeToVersionRange(m.Versions)
defaultStatus := cveschema5.StatusUnaffected
if len(versions) == 0 {
// If there are no recorded versions affected, we assume all versions are affected.
defaultStatus = cveschema5.StatusAffected
}
versions, defaultStatus := versionRangeToVersionRange(m.Versions)
for _, p := range m.Packages {
affected := cveschema5.Affected{
Vendor: vendor(m.Module),
Expand Down Expand Up @@ -121,22 +116,60 @@ func (r *Report) CVEFilename() string {
return filepath.Join(cve5Dir, r.ID+".json")
}

func versionRangeToVersionRange(versions []VersionRange) []cveschema5.VersionRange {
const (
typeSemver = "semver"
versionZero = "0"
)

func versionRangeToVersionRange(versions []VersionRange) ([]cveschema5.VersionRange, cveschema5.VersionStatus) {
if len(versions) == 0 {
// If there are no recorded versions affected, we assume all versions are affected.
return nil, cveschema5.StatusAffected
}

var cveVRs []cveschema5.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 versions[len(versions)-1].Fixed == "" {
current := &cveschema5.VersionRange{}
for _, vr := range versions {
if vr.Introduced != "" {
if current.Introduced == "" {
current.Introduced = versionZero
}
current.Fixed = cveschema5.Version(vr.Introduced)
current.Status = cveschema5.StatusUnaffected
current.VersionType = typeSemver
cveVRs = append(cveVRs, *current)
current = &cveschema5.VersionRange{}
}
if vr.Fixed != "" {
current.Introduced = cveschema5.Version(vr.Fixed)
}
}
return cveVRs, cveschema5.StatusAffected
}

// Otherwise, express the version ranges normally as affected ranges,
// with a default status of "unaffected".
for _, vr := range versions {
cveVR := cveschema5.VersionRange{
Status: cveschema5.StatusAffected,
VersionType: "semver",
VersionType: typeSemver,
}
if vr.Introduced != "" {
cveVR.Introduced = cveschema5.Version(vr.Introduced)
} else {
cveVR.Introduced = "0"
cveVR.Introduced = versionZero
}
if vr.Fixed != "" {
cveVR.Fixed = cveschema5.Version(vr.Fixed)
}
cveVRs = append(cveVRs, cveVR)
}
return cveVRs

return cveVRs, cveschema5.StatusUnaffected
}
163 changes: 163 additions & 0 deletions internal/report/cve5_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package report

import (
"path/filepath"
"reflect"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -282,3 +283,165 @@ func TestCVEFilename(t *testing.T) {
t.Errorf("got %s, want %s", got, want)
}
}

func TestVersionRangeToVersionRange(t *testing.T) {
tests := []struct {
name string
versions []VersionRange
wantRange []cveschema5.VersionRange
wantDefault cveschema5.VersionStatus
}{
{
name: "nil",
versions: nil,
wantRange: nil,
wantDefault: cveschema5.StatusAffected,
},
{
name: "empty",
versions: []VersionRange{},
wantRange: nil,
wantDefault: cveschema5.StatusAffected,
},
{
name: "basic",
versions: []VersionRange{
{
Introduced: "1.0.0",
Fixed: "1.0.1",
},
{
Introduced: "1.2.0",
Fixed: "1.2.3",
},
},
wantRange: []cveschema5.VersionRange{
{
Introduced: "1.0.0",
Fixed: "1.0.1",
Status: cveschema5.StatusAffected,
VersionType: typeSemver,
},
{
Introduced: "1.2.0",
Fixed: "1.2.3",
Status: cveschema5.StatusAffected,
VersionType: typeSemver,
},
},
wantDefault: cveschema5.StatusUnaffected,
},
{
name: "no initial introduced",
versions: []VersionRange{
{
Fixed: "1.0.1",
},
{
Introduced: "1.2.0",
Fixed: "1.2.3",
},
},
wantRange: []cveschema5.VersionRange{
{
Introduced: "0",
Fixed: "1.0.1",
Status: cveschema5.StatusAffected,
VersionType: typeSemver,
},
{
Introduced: "1.2.0",
Fixed: "1.2.3",
Status: cveschema5.StatusAffected,
VersionType: typeSemver,
},
},
wantDefault: cveschema5.StatusUnaffected,
},
{
name: "no fix",
versions: []VersionRange{
{
Introduced: "1.0.0",
},
},
wantRange: []cveschema5.VersionRange{
{
Introduced: "0",
Fixed: "1.0.0",
Status: cveschema5.StatusUnaffected,
VersionType: typeSemver,
},
},
wantDefault: cveschema5.StatusAffected,
},
{
name: "no final fix",
versions: []VersionRange{
{
Introduced: "1.0.0",
Fixed: "1.0.3",
},
{
Introduced: "1.1.0",
},
},
wantRange: []cveschema5.VersionRange{
{
Introduced: "0",
Fixed: "1.0.0",
Status: cveschema5.StatusUnaffected,
VersionType: typeSemver,
},
{
Introduced: "1.0.3",
Fixed: "1.1.0",
Status: cveschema5.StatusUnaffected,
VersionType: typeSemver,
},
},
wantDefault: cveschema5.StatusAffected,
},
{
name: "no initial introduced and no final fix",
versions: []VersionRange{
{
Fixed: "1.0.3",
},
{
Introduced: "1.0.5",
Fixed: "1.0.7",
},
{
Introduced: "1.1.0",
},
},
wantRange: []cveschema5.VersionRange{
{
Introduced: "1.0.3",
Fixed: "1.0.5",
Status: cveschema5.StatusUnaffected,
VersionType: typeSemver,
},
{
Introduced: "1.0.7",
Fixed: "1.1.0",
Status: cveschema5.StatusUnaffected,
VersionType: typeSemver,
},
},
wantDefault: cveschema5.StatusAffected,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotRange, gotStatus := versionRangeToVersionRange(tt.versions)
if !reflect.DeepEqual(gotRange, tt.wantRange) {
t.Errorf("versionRangeToVersionRange() got version range = %v, want %v", gotRange, tt.wantRange)
}
if !reflect.DeepEqual(gotStatus, tt.wantDefault) {
t.Errorf("versionRangeToVersionRange() got default status = %v, want %v", gotStatus, tt.wantDefault)
}
})
}
}

0 comments on commit a33350d

Please sign in to comment.