From 6eebed33b2009471a1608891d0df77cd4090548c Mon Sep 17 00:00:00 2001 From: rahul2393 Date: Sun, 19 Jul 2020 20:33:56 +0530 Subject: [PATCH] improve ruby comparison version check. (#552) * Implemented ruby comparison version check. * Added semver package to validate and check version * Added more tests * Replaced go-version with semver * Removing go-version from dependency * Added check for ruby gem version format * Updated semver model and patch rewrite process * Refactoring --- go.mod | 2 +- go.sum | 4 +- pkg/detector/library/bundler/advisory.go | 4 +- pkg/detector/library/bundler/advisory_test.go | 5 +- pkg/detector/library/cargo/advisory.go | 4 +- pkg/detector/library/composer/advisory.go | 5 +- pkg/detector/library/detect.go | 5 +- pkg/detector/library/driver.go | 6 +- pkg/detector/library/ghsa/advisory.go | 4 +- pkg/detector/library/node/advisory.go | 5 +- pkg/detector/library/python/advisory.go | 4 +- pkg/scanner/utils/utils.go | 49 +++++-- pkg/scanner/utils/utils_test.go | 129 ++++++++++++++++++ 13 files changed, 196 insertions(+), 30 deletions(-) create mode 100644 pkg/scanner/utils/utils_test.go diff --git a/go.mod b/go.mod index ff33362293..c490e5ffa2 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/aquasecurity/trivy go 1.13 require ( + github.com/Masterminds/semver/v3 v3.1.0 github.com/aquasecurity/fanal v0.0.0-20200528202907-79693bf4a058 github.com/aquasecurity/go-dep-parser v0.0.0-20190819075924-ea223f0ef24b github.com/aquasecurity/trivy-db v0.0.0-20200715174849-fa5a3ca24b16 @@ -18,7 +19,6 @@ require ( github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936 - github.com/knqyf263/go-version v1.1.1 github.com/kylelemons/godebug v1.1.0 github.com/mattn/go-colorable v0.1.4 // indirect github.com/olekukonko/tablewriter v0.0.2-0.20190607075207-195002e6e56a diff --git a/go.sum b/go.sum index 2a76104717..f5d59b9198 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/GoogleCloudPlatform/docker-credential-gcr v1.5.0 h1:wykTgKwhVr2t2qs+xI020s6W5dt614QqCHV+7W9dg64= github.com/GoogleCloudPlatform/docker-credential-gcr v1.5.0/go.mod h1:BB1eHdMLYEFuFdBlRMb0N7YGVdM5s6Pt0njxgvfbGGs= github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= +github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk= +github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA= @@ -279,8 +281,6 @@ github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d/go.mod h1: github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936 h1:HDjRqotkViMNcGMGicb7cgxklx8OwnjtCBmyWEqrRvM= github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0= github.com/knqyf263/go-rpmdb v0.0.0-20190501070121-10a1c42a10dc/go.mod h1:MrSSvdMpTSymaQWk1yFr9sxFSyQmKMj6jkbvGrchBV8= -github.com/knqyf263/go-version v1.1.1 h1:+MpcBC9b7rk5ihag8Y/FLG8get1H2GjniwKQ+9DxI2o= -github.com/knqyf263/go-version v1.1.1/go.mod h1:0tBvHvOBSf5TqGNcY+/ih9o8qo3R16iZCpB9rP0D3VM= github.com/knqyf263/nested v0.0.1 h1:Sv26CegUMhjt19zqbBKntjwESdxe5hxVPSk0+AKjdUc= github.com/knqyf263/nested v0.0.1/go.mod h1:zwhsIhMkBg90DTOJQvxPkKIypEHPYkgWHs4gybdlUmk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= diff --git a/pkg/detector/library/bundler/advisory.go b/pkg/detector/library/bundler/advisory.go index a0ad070023..1fd016e35b 100644 --- a/pkg/detector/library/bundler/advisory.go +++ b/pkg/detector/library/bundler/advisory.go @@ -3,7 +3,7 @@ package bundler import ( "strings" - "github.com/knqyf263/go-version" + "github.com/Masterminds/semver/v3" "golang.org/x/xerrors" bundlerSrc "github.com/aquasecurity/trivy-db/pkg/vulnsrc/bundler" @@ -44,7 +44,7 @@ func NewAdvisory() *Advisory { } } -func (a *Advisory) DetectVulnerabilities(pkgName string, pkgVer *version.Version) ([]types.DetectedVulnerability, error) { +func (a *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { advisories, err := a.vs.Get(pkgName) if err != nil { return nil, xerrors.Errorf("failed to get bundler advisories: %w", err) diff --git a/pkg/detector/library/bundler/advisory_test.go b/pkg/detector/library/bundler/advisory_test.go index 5104e88f61..b70587b0c8 100644 --- a/pkg/detector/library/bundler/advisory_test.go +++ b/pkg/detector/library/bundler/advisory_test.go @@ -5,8 +5,9 @@ import ( "github.com/aquasecurity/trivy/pkg/log" + "github.com/Masterminds/semver/v3" + bundlerSrc "github.com/aquasecurity/trivy-db/pkg/vulnsrc/bundler" - "github.com/knqyf263/go-version" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -53,7 +54,7 @@ func TestScanner_Detect(t *testing.T) { versionStr := "1.9.25-x64-mingw32" versionStr = platformReplacer.Replace(versionStr) - v, _ := version.NewVersion(versionStr) + v, _ := semver.NewVersion(versionStr) vulns, err := s.DetectVulnerabilities("ffi", v) diff --git a/pkg/detector/library/cargo/advisory.go b/pkg/detector/library/cargo/advisory.go index 416fa80bdd..b331fa4724 100644 --- a/pkg/detector/library/cargo/advisory.go +++ b/pkg/detector/library/cargo/advisory.go @@ -5,9 +5,9 @@ import ( "github.com/aquasecurity/trivy/pkg/types" + "github.com/Masterminds/semver/v3" cargoSrc "github.com/aquasecurity/trivy-db/pkg/vulnsrc/cargo" "github.com/aquasecurity/trivy/pkg/scanner/utils" - "github.com/knqyf263/go-version" "golang.org/x/xerrors" ) @@ -21,7 +21,7 @@ func NewAdvisory() *Advisory { } } -func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *version.Version) ([]types.DetectedVulnerability, error) { +func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { advisories, err := s.vs.Get(pkgName) if err != nil { return nil, xerrors.Errorf("failed to get cargo advisories: %w", err) diff --git a/pkg/detector/library/composer/advisory.go b/pkg/detector/library/composer/advisory.go index 90575cb5a8..6d08ef7e7d 100644 --- a/pkg/detector/library/composer/advisory.go +++ b/pkg/detector/library/composer/advisory.go @@ -8,9 +8,10 @@ import ( "golang.org/x/xerrors" + "github.com/Masterminds/semver/v3" + composerSrc "github.com/aquasecurity/trivy-db/pkg/vulnsrc/composer" "github.com/aquasecurity/trivy/pkg/scanner/utils" - "github.com/knqyf263/go-version" ) type Advisory struct { @@ -23,7 +24,7 @@ func NewAdvisory() *Advisory { } } -func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *version.Version) ([]types.DetectedVulnerability, error) { +func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { ref := fmt.Sprintf("composer://%s", pkgName) advisories, err := s.vs.Get(ref) if err != nil { diff --git a/pkg/detector/library/detect.go b/pkg/detector/library/detect.go index e99339ab5f..3503ae729c 100644 --- a/pkg/detector/library/detect.go +++ b/pkg/detector/library/detect.go @@ -8,11 +8,12 @@ import ( "github.com/google/wire" + "github.com/Masterminds/semver/v3" "github.com/aquasecurity/trivy/pkg/log" - "github.com/knqyf263/go-version" "golang.org/x/xerrors" + "github.com/aquasecurity/trivy/pkg/scanner/utils" "github.com/aquasecurity/trivy/pkg/types" ) @@ -54,7 +55,7 @@ func detect(driver Driver, libs []ftypes.LibraryInfo) ([]types.DetectedVulnerabi log.Logger.Infof("Detecting %s vulnerabilities...", driver.Type()) var vulnerabilities []types.DetectedVulnerability for _, lib := range libs { - v, err := version.NewVersion(lib.Library.Version) + v, err := semver.NewVersion(utils.FormatPatchVersion(lib.Library.Version)) if err != nil { log.Logger.Debugf("invalid version, library: %s, version: %s, error: %s\n", lib.Library.Name, lib.Library.Version, err) diff --git a/pkg/detector/library/driver.go b/pkg/detector/library/driver.go index 10824617b5..e3aa30597b 100644 --- a/pkg/detector/library/driver.go +++ b/pkg/detector/library/driver.go @@ -3,6 +3,7 @@ package library import ( "fmt" + "github.com/Masterminds/semver/v3" "github.com/aquasecurity/fanal/analyzer/library" ecosystem "github.com/aquasecurity/trivy-db/pkg/vulnsrc/ghsa" "github.com/aquasecurity/trivy/pkg/detector/library/bundler" @@ -12,7 +13,6 @@ import ( "github.com/aquasecurity/trivy/pkg/detector/library/node" "github.com/aquasecurity/trivy/pkg/detector/library/python" "github.com/aquasecurity/trivy/pkg/types" - "github.com/knqyf263/go-version" "golang.org/x/xerrors" ) @@ -21,7 +21,7 @@ type Factory interface { } type advisory interface { - DetectVulnerabilities(string, *version.Version) ([]types.DetectedVulnerability, error) + DetectVulnerabilities(string, *semver.Version) ([]types.DetectedVulnerability, error) } type DriverFactory struct{} @@ -59,7 +59,7 @@ func NewDriver(p string, advisories ...advisory) Driver { return Driver{pkgManager: p, advisories: advisories} } -func (driver *Driver) Detect(pkgName string, pkgVer *version.Version) ([]types.DetectedVulnerability, error) { +func (driver *Driver) Detect(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { var detectedVulnerabilities []types.DetectedVulnerability uniqVulnIdMap := make(map[string]struct{}) for _, d := range driver.advisories { diff --git a/pkg/detector/library/ghsa/advisory.go b/pkg/detector/library/ghsa/advisory.go index f894668068..cb481e40cb 100644 --- a/pkg/detector/library/ghsa/advisory.go +++ b/pkg/detector/library/ghsa/advisory.go @@ -3,7 +3,7 @@ package ghsa import ( "strings" - "github.com/knqyf263/go-version" + "github.com/Masterminds/semver/v3" "golang.org/x/xerrors" "github.com/aquasecurity/trivy-db/pkg/vulnsrc/ghsa" @@ -25,7 +25,7 @@ func NewAdvisory(ecosystem ghsa.Ecosystem) *Advisory { } } -func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *version.Version) ([]types.DetectedVulnerability, error) { +func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { advisories, err := s.vs.Get(pkgName) if err != nil { return nil, xerrors.Errorf("failed to get ghsa advisories: %w", err) diff --git a/pkg/detector/library/node/advisory.go b/pkg/detector/library/node/advisory.go index 3aad2c0a71..5476c9c731 100644 --- a/pkg/detector/library/node/advisory.go +++ b/pkg/detector/library/node/advisory.go @@ -3,9 +3,10 @@ package node import ( "strings" - version "github.com/knqyf263/go-version" "golang.org/x/xerrors" + "github.com/Masterminds/semver/v3" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/node" "github.com/aquasecurity/trivy/pkg/scanner/utils" "github.com/aquasecurity/trivy/pkg/types" @@ -21,7 +22,7 @@ func NewAdvisory() *Advisory { } } -func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *version.Version) ([]types.DetectedVulnerability, error) { +func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { replacer := strings.NewReplacer(".alpha", "-alpha", ".beta", "-beta", ".rc", "-rc", " <", ", <", " >", ", >") advisories, err := s.vs.Get(pkgName) if err != nil { diff --git a/pkg/detector/library/python/advisory.go b/pkg/detector/library/python/advisory.go index fd4da6e752..21bf1d87b2 100644 --- a/pkg/detector/library/python/advisory.go +++ b/pkg/detector/library/python/advisory.go @@ -8,8 +8,8 @@ import ( "golang.org/x/xerrors" + "github.com/Masterminds/semver/v3" "github.com/aquasecurity/trivy/pkg/scanner/utils" - "github.com/knqyf263/go-version" ) type Advisory struct { @@ -22,7 +22,7 @@ func NewAdvisory() *Advisory { } } -func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *version.Version) ([]types.DetectedVulnerability, error) { +func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { advisories, err := s.vs.Get(pkgName) if err != nil { return nil, xerrors.Errorf("failed to get python advisories: %w", err) diff --git a/pkg/scanner/utils/utils.go b/pkg/scanner/utils/utils.go index 9dce7ae2e3..7fe794b72e 100644 --- a/pkg/scanner/utils/utils.go +++ b/pkg/scanner/utils/utils.go @@ -2,27 +2,50 @@ package utils import ( "fmt" + "strconv" "strings" + "github.com/Masterminds/semver/v3" + "github.com/aquasecurity/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" - "github.com/knqyf263/go-version" ) var ( replacer = strings.NewReplacer(".alpha", "-alpha", ".beta", "-beta", ".rc", "-rc") ) -func MatchVersions(currentVersion *version.Version, rangeVersions []string) bool { - for _, p := range rangeVersions { - c, err := version.NewConstraint(replacer.Replace(p)) - if err != nil { - log.Logger.Debug("NewConstraint", "error", err) - return false +func MatchVersions(currentVersion *semver.Version, rangeVersions []string) bool { + for _, v := range rangeVersions { + v = replacer.Replace(v) + constraintParts := strings.Split(v, ",") + for j := range constraintParts { + constraintParts[j] = FormatPatchVersion(constraintParts[j]) } - if c.Check(currentVersion) { + v = strings.Join(constraintParts, ",") + c, err := semver.NewConstraint(v) + if err != nil { + log.Logger.Error("NewConstraint", "error", err) + continue + } + // Validate a version against a constraint. + valid, msgs := c.Validate(currentVersion) + if valid { return true } + for _, m := range msgs { + // re-validate after removing the patch version + if strings.HasSuffix(m.Error(), "is a prerelease version and the constraint is only looking for release versions") { + v2, err := semver.NewVersion(fmt.Sprintf("%v.%v.%v", currentVersion.Major(), currentVersion.Minor(), currentVersion.Patch())) + if err == nil { + valid, _ = c.Validate(v2) + if valid { + return true + } + } + } + } } return false } @@ -35,6 +58,16 @@ func FormatSrcVersion(pkg types.Package) string { return formatVersion(pkg.SrcEpoch, pkg.SrcVersion, pkg.SrcRelease) } +func FormatPatchVersion(version string) string { + part := strings.Split(version, ".") + if len(part) > 3 { + if _, err := strconv.Atoi(part[2]); err == nil { + version = strings.Join(part[:3], ".") + "-" + strings.Join(part[3:], ".") + } + } + return version +} + func formatVersion(epoch int, version, release string) string { v := version if release != "" { diff --git a/pkg/scanner/utils/utils_test.go b/pkg/scanner/utils/utils_test.go new file mode 100644 index 0000000000..e69c854bd5 --- /dev/null +++ b/pkg/scanner/utils/utils_test.go @@ -0,0 +1,129 @@ +package utils + +import ( + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMatchVersions(t *testing.T) { + testCases := []struct { + name string + currentVersion string + rangeVersion []string + expectedCheck bool + }{ + { + name: "pass: expect true when os/machine is in version string", + currentVersion: "1.9.25-x86-mingw32", + rangeVersion: []string{`>= 1.9.24`}, + expectedCheck: true, + }, + { + name: "pass: expect true when language is in version string", + currentVersion: "1.8.6-java", + rangeVersion: []string{`~> 1.5.5`, `~> 1.6.8`, `>= 1.7.7`}, + expectedCheck: true, + }, + { + name: "expect false", + currentVersion: "1.9.23-x86-mingw32", + rangeVersion: []string{`>= 1.9.24`}, + expectedCheck: false, + }, + { + // passes if (>= 1.2.3, < 2.0.0) + name: "expect false", + currentVersion: "1.2.4", + rangeVersion: []string{`^1.2.3`}, + expectedCheck: true, + }, + { + // passes if (>= 1.2.3, < 2.0.0) + name: "expect false", + currentVersion: "2.0.0", + rangeVersion: []string{`^1.2.3`}, + expectedCheck: false, + }, + { + // passes if (>= 2.0.18, < 3.0.0) || (>= 3.1.16, < 4.0.0) || (>= 4.0.8, < 5.0.0) || ( >=5.0.0,<6.0.0) + name: "expect false", + currentVersion: "3.1.16", + rangeVersion: []string{`^2.0.18 || ^3.1.6 || ^4.0.8 || ^5.0.0-beta.5`}, + expectedCheck: true, + }, + { + // passes if (>= 2.0.18, < 3.0.0) || (>= 3.1.16, < 4.0.0) || (>= 4.0.8, < 5.0.0) || ( >=5.0.0,<6.0.0) + name: "expect false", + currentVersion: "6.0.0", + rangeVersion: []string{`^2.0.18 || ^3.1.6 || ^4.0.8 || ^5.0.0-beta.5`}, + expectedCheck: false, + }, + { + // passes if (>= 2.0.18, < 3.0.0) || (>= 3.1.16, < 4.0.0) || (>= 4.0.8, < 5.0.0) || ( >=5.0.0,<6.0.0) + name: "expect false", + currentVersion: "5.0.0-beta.5", + rangeVersion: []string{`^2.0.18 || ^3.1.6 || ^4.0.8 || ^5.0.0-beta.5`}, + expectedCheck: true, + }, + { + // Ruby GEM with more dots + name: "expect false", + currentVersion: "1.10.9-java", + rangeVersion: []string{`>= 1.6.7.1`}, + expectedCheck: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + v, err := semver.NewVersion(tc.currentVersion) + require.NoError(t, err) + match := MatchVersions(v, tc.rangeVersion) + assert.Equal(t, tc.expectedCheck, match) + }) + } +} + +func TestFormatPatchVersio(t *testing.T) { + testCases := []struct { + name string + currentVersion string + expectedVersion string + }{ + { + name: "patch with no dots should return version should be unchanged", + currentVersion: "1.2.3-beta", + expectedVersion: "1.2.3-beta", + }, + { + name: "patch with dots after non-integer patch version should be unchanged", + currentVersion: "1.2.3-beta.1", + expectedVersion: "1.2.3-beta.1", + }, + { + name: "patch with dots after integer patch version should append dash and join rest versions parts", + currentVersion: "1.2.3.4", + expectedVersion: "1.2.3-4", + }, + { + name: "patch with dots after integer patch version should append dash and join extra versions parts", + currentVersion: "1.2.3.4.5", + expectedVersion: "1.2.3-4.5", + }, + { + name: "unchanged case", + currentVersion: "1.2.3.4-5", + expectedVersion: "1.2.3-4-5", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := FormatPatchVersion(tc.currentVersion) + assert.Equal(t, tc.expectedVersion, got) + }) + } +}