internal/report: fix bug in CVE5 generation
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 <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/data/cve/v5/GO-2023-2328.json b/data/cve/v5/GO-2023-2328.json
index 8df4e2a..5679738 100644
--- a/data/cve/v5/GO-2023-2328.json
+++ b/data/cve/v5/GO-2023-2328.json
@@ -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"
}
],
@@ -65,7 +65,7 @@
"name": "Request.Send"
}
],
- "defaultStatus": "unaffected"
+ "defaultStatus": "affected"
}
],
"problemTypes": [
diff --git a/internal/report/cve5.go b/internal/report/cve5.go
index 28fa5cb..1fda9bc 100644
--- a/internal/report/cve5.go
+++ b/internal/report/cve5.go
@@ -65,12 +65,7 @@
}
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),
@@ -121,22 +116,60 @@
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
}
diff --git a/internal/report/cve5_test.go b/internal/report/cve5_test.go
index 4a306b2..bd415aa 100644
--- a/internal/report/cve5_test.go
+++ b/internal/report/cve5_test.go
@@ -6,6 +6,7 @@
import (
"path/filepath"
+ "reflect"
"testing"
"github.com/google/go-cmp/cmp"
@@ -282,3 +283,165 @@
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)
+ }
+ })
+ }
+}