Repository: G-Rath/osv-detector
Pull request #57 (2022-03-11 03:59:59)

feat: support SEMVER ranges
This is needed for #53 as it uses `SEMVER` for some ecosystems like `npm`.

While we could use the native `semver`, frankly our custom `semver` should be able to handle this and I actually wasn't able to get the native `semver` working 😂 


Patch:
diff --git a/detector/database/osv.go b/detector/database/osv.go
index b2961c4..79f9500 100644
--- a/detector/database/osv.go
+++ b/detector/database/osv.go
@@ -55,8 +55,8 @@ type AffectsRange struct {
 	Events []RangeEvent     `json:"events"`
 }
 
-func (ar AffectsRange) containsEcosystem(v string) bool {
-	if ar.Type != TypeEcosystem {
+func (ar AffectsRange) containsVersion(v string) bool {
+	if ar.Type != TypeEcosystem && ar.Type != TypeSemver {
 		return false
 	}
 	// todo: we should probably warn here
@@ -80,14 +80,14 @@ func (ar AffectsRange) containsEcosystem(v string) bool {
 
 type Affects []AffectsRange
 
-// AffectsEcosystem checks if the given version is within the range
-// specified by the events of any "Ecosystem" type ranges
-func (a Affects) AffectsEcosystem(v string) bool {
+// affectsVersion checks if the given version is within the range
+// specified by the events of any "Ecosystem" or "Semver" type ranges
+func (a Affects) affectsVersion(v string) bool {
 	for _, r := range a {
-		if r.Type != TypeEcosystem {
-			continue
+		if r.Type != TypeEcosystem && r.Type != TypeSemver {
+			return false
 		}
-		if r.containsEcosystem(v) {
+		if r.containsVersion(v) {
 			return true
 		}
 	}
@@ -156,7 +156,7 @@ func (osv *OSV) IsAffected(pkg detector.PackageDetails) bool {
 				continue
 			}
 
-			if affected.Ranges.AffectsEcosystem(pkg.Version) {
+			if affected.Ranges.affectsVersion(pkg.Version) {
 				return true
 			}
 		}


Test patch:
diff --git a/detector/database/osv_test.go b/detector/database/osv_test.go
index a6fa251..b90cfa7 100644
--- a/detector/database/osv_test.go
+++ b/detector/database/osv_test.go
@@ -36,6 +36,10 @@ func buildEcosystemAffectsRange(events ...database.RangeEvent) database.AffectsR
 	return database.AffectsRange{Type: database.TypeEcosystem, Events: events}
 }
 
+func buildSemverAffectsRange(events ...database.RangeEvent) database.AffectsRange {
+	return database.AffectsRange{Type: database.TypeSemver, Events: events}
+}
+
 func TestPackage_NormalizedName(t *testing.T) {
 	t.Parallel()
 
@@ -338,3 +342,133 @@ func TestOSV_IsAffected_AffectsWithEcosystem_PipNamesAreNormalised(t *testing.T)
 		t.Errorf("Expected OSV not to normalize names of non-pip packages, but it did")
 	}
 }
+
+func TestOSV_IsAffected_AffectsWithSemver_DifferentEcosystem(t *testing.T) {
+	t.Parallel()
+
+	osv := buildOSVWithAffected(
+		database.Affected{
+			Package: database.Package{Ecosystem: parsers.PipEcosystem, Name: "my-package"},
+			Ranges: []database.AffectsRange{
+				buildSemverAffectsRange(database.RangeEvent{Introduced: "0"}),
+			},
+		},
+	)
+
+	for _, v := range []string{"1.0.0", "1.1.1", "2.0.0"} {
+		expectIsAffected(t, osv, v, false)
+	}
+}
+
+func TestOSV_IsAffected_AffectsWithSemver_SingleAffected(t *testing.T) {
+	t.Parallel()
+
+	var osv database.OSV
+
+	// "Introduced: 0" means everything is affected
+	osv = buildOSVWithAffected(
+		database.Affected{
+			Package: database.Package{Ecosystem: parsers.NpmEcosystem, Name: "my-package"},
+			Ranges: []database.AffectsRange{
+				buildSemverAffectsRange(database.RangeEvent{Introduced: "0"}),
+			},
+		},
+	)
+
+	for _, v := range []string{"v1.0.0", "v1.1.1", "v2.0.0"} {
+		expectIsAffected(t, osv, v, true)
+	}
+
+	// "Fixed: 1" means all versions after this are not vulnerable
+	osv = buildOSVWithAffected(
+		database.Affected{
+			Package: database.Package{Ecosystem: parsers.NpmEcosystem, Name: "my-package"},
+			Ranges: []database.AffectsRange{
+				buildSemverAffectsRange(
+					database.RangeEvent{Introduced: "0"},
+					database.RangeEvent{Fixed: "1.0.0"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
+		expectIsAffected(t, osv, v, true)
+	}
+
+	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0"} {
+		expectIsAffected(t, osv, v, false)
+	}
+
+	// multiple fixes and introduced
+	osv = buildOSVWithAffected(
+		database.Affected{
+			Package: database.Package{Ecosystem: parsers.NpmEcosystem, Name: "my-package"},
+			Ranges: []database.AffectsRange{
+				buildSemverAffectsRange(
+					database.RangeEvent{Introduced: "0"},
+					database.RangeEvent{Fixed: "1"},
+					database.RangeEvent{Introduced: "2.1.0"},
+					database.RangeEvent{Fixed: "3.2.0"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
+		expectIsAffected(t, osv, v, true)
+	}
+
+	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
+		expectIsAffected(t, osv, v, false)
+	}
+
+	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
+		expectIsAffected(t, osv, v, true)
+	}
+
+	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
+		expectIsAffected(t, osv, v, false)
+	}
+}
+
+func TestOSV_IsAffected_AffectsWithSemver_MultipleAffected(t *testing.T) {
+	t.Parallel()
+
+	osv := buildOSVWithAffected(
+		database.Affected{
+			Package: database.Package{Ecosystem: parsers.NpmEcosystem, Name: "my-package"},
+			Ranges: []database.AffectsRange{
+				buildSemverAffectsRange(
+					database.RangeEvent{Introduced: "0"},
+					database.RangeEvent{Fixed: "1"},
+				),
+			},
+		},
+		database.Affected{
+			Package: database.Package{Ecosystem: parsers.NpmEcosystem, Name: "my-package"},
+			Ranges: []database.AffectsRange{
+				buildSemverAffectsRange(
+					database.RangeEvent{Introduced: "2.1.0"},
+					database.RangeEvent{Fixed: "3.2.0"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
+		expectIsAffected(t, osv, v, true)
+	}
+
+	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
+		expectIsAffected(t, osv, v, false)
+	}
+
+	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
+		expectIsAffected(t, osv, v, true)
+	}
+
+	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
+		expectIsAffected(t, osv, v, false)
+	}
+}