Repository: G-Rath/osv-detector
Pull request #52 (2022-03-10 18:44:00)

fix: support python packages that have "added support" syntax
I'm pretty sure that technically the advisories are actually wrong as e.g. `twisted[http2]` is technically a different package to `twisted` itself but I can understand that it'd make the number of advisories to maintain grow by a large factor.

At this point I think it's less likely that an advisory will be missed with this change than without it, but if we find later one is being missed, we'll deal with it somehow 🤷 


Patch:
diff --git a/.golangci.yaml b/.golangci.yaml
index d26d102..8393abe 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -32,6 +32,7 @@ issues:
     - path: _test\.go
       linters:
         - goerr113
+        - dupl
     - path: main.go
       linters:
         - gochecknoglobals
diff --git a/detector/parsers/fixtures/pip/with-added-support.txt b/detector/parsers/fixtures/pip/with-added-support.txt
new file mode 100644
index 0000000..8a65dbb
--- /dev/null
+++ b/detector/parsers/fixtures/pip/with-added-support.txt
@@ -0,0 +1,2 @@
+twisted[http2]==20.3.0
+    # via scrapy
diff --git a/detector/parsers/parse-requirements-txt.go b/detector/parsers/parse-requirements-txt.go
index 5231147..288bb75 100644
--- a/detector/parsers/parse-requirements-txt.go
+++ b/detector/parsers/parse-requirements-txt.go
@@ -45,12 +45,16 @@ func parseLine(line string) PackageDetails {
 	}
 
 	return PackageDetails{
-		Name:      name,
+		Name:      cleanupRequirementName(name),
 		Version:   version,
 		Ecosystem: PipEcosystem,
 	}
 }
 
+func cleanupRequirementName(name string) string {
+	return strings.Split(name, "[")[0]
+}
+
 func removeComments(line string) string {
 	var re = regexp.MustCompile(`(^|\s+)#.*$`)
 


Test patch:
diff --git a/detector/database/osv_test.go b/detector/database/osv_test.go
index 8ae3870..d03db3a 100644
--- a/detector/database/osv_test.go
+++ b/detector/database/osv_test.go
@@ -1,11 +1,41 @@
 package database_test
 
 import (
+	"osv-detector/detector"
 	"osv-detector/detector/database"
+	"osv-detector/detector/parsers"
 	"testing"
 	"time"
 )
 
+func expectIsAffected(t *testing.T, osv database.OSV, version string, expectAffected bool) {
+	t.Helper()
+
+	pkg := detector.PackageDetails{Name: "my-package", Version: version, Ecosystem: parsers.NpmEcosystem}
+
+	if osv.IsAffected(pkg) != expectAffected {
+		if expectAffected {
+			t.Errorf("Expected OSV to affect package version %s but it did not", version)
+		} else {
+			t.Errorf("Expected OSV not to affect package version %s but it did", version)
+		}
+	}
+}
+
+func buildOSVWithAffected(affected ...database.Affected) database.OSV {
+	return database.OSV{
+		ID:        "1",
+		Published: time.Time{},
+		Modified:  time.Time{},
+		Details:   "This is an open source vulnerability!",
+		Affected:  affected,
+	}
+}
+
+func buildEcosystemAffectsRange(events ...database.RangeEvent) database.AffectsRange {
+	return database.AffectsRange{Type: database.TypeEcosystem, Events: events}
+}
+
 func TestOSV_AffectsEcosystem(t *testing.T) {
 	t.Parallel()
 
@@ -80,3 +110,133 @@ func TestOSV_AffectsEcosystem(t *testing.T) {
 		)
 	}
 }
+
+func TestOSV_IsAffected_AffectsWithEcosystem_DifferentEcosystem(t *testing.T) {
+	t.Parallel()
+
+	osv := buildOSVWithAffected(
+		database.Affected{
+			Package: database.Package{Ecosystem: parsers.PipEcosystem, Name: "my-package"},
+			Ranges: []database.AffectsRange{
+				buildEcosystemAffectsRange(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_AffectsWithEcosystem_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{
+				buildEcosystemAffectsRange(database.RangeEvent{Introduced: "0"}),
+			},
+		},
+	)
+
+	for _, v := range []string{"1.0.0", "1.1.1", "2.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{
+				buildEcosystemAffectsRange(
+					database.RangeEvent{Introduced: "0"},
+					database.RangeEvent{Fixed: "1"},
+				),
+			},
+		},
+	)
+
+	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{
+				buildEcosystemAffectsRange(
+					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_AffectsWithEcosystem_MultipleAffected(t *testing.T) {
+	t.Parallel()
+
+	osv := buildOSVWithAffected(
+		database.Affected{
+			Package: database.Package{Ecosystem: parsers.NpmEcosystem, Name: "my-package"},
+			Ranges: []database.AffectsRange{
+				buildEcosystemAffectsRange(
+					database.RangeEvent{Introduced: "0"},
+					database.RangeEvent{Fixed: "1"},
+				),
+			},
+		},
+		database.Affected{
+			Package: database.Package{Ecosystem: parsers.NpmEcosystem, Name: "my-package"},
+			Ranges: []database.AffectsRange{
+				buildEcosystemAffectsRange(
+					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)
+	}
+}
diff --git a/detector/parsers/parse-requirements-txt_test.go b/detector/parsers/parse-requirements-txt_test.go
index 28a7e2f..23d0a27 100644
--- a/detector/parsers/parse-requirements-txt_test.go
+++ b/detector/parsers/parse-requirements-txt_test.go
@@ -262,3 +262,21 @@ func TestParseRequirementsTxt_FileFormatExample(t *testing.T) {
 		},
 	})
 }
+
+func TestParseRequirementsTxt_WithAddedSupport(t *testing.T) {
+	t.Parallel()
+
+	packages, err := parsers.ParseRequirementsTxt("fixtures/pip/with-added-support.txt")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []parsers.PackageDetails{
+		{
+			Name:      "twisted",
+			Version:   "20.3.0",
+			Ecosystem: parsers.PipEcosystem,
+		},
+	})
+}