From 4631fe145281df7e927fbcc6b33532ca086eab9c Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Wed, 11 Nov 2020 19:10:04 -0500 Subject: [PATCH 1/3] fixes #401 Valid x.y.z versions of OS posture checks can now be specified by posture check ranges. --- controller/model/posture_check_model_os.go | 121 ++++++- .../model/posture_check_model_os_test.go | 304 ++++++++++++++++++ .../model/posture_check_model_process_test.go | 2 + go.mod | 1 + go.sum | 3 + 5 files changed, 416 insertions(+), 15 deletions(-) create mode 100644 controller/model/posture_check_model_os_test.go diff --git a/controller/model/posture_check_model_os.go b/controller/model/posture_check_model_os.go index 2f687740f..7e819f0ef 100644 --- a/controller/model/posture_check_model_os.go +++ b/controller/model/posture_check_model_os.go @@ -18,8 +18,12 @@ package model import ( "fmt" + "github.com/blang/semver" + "github.com/michaelquigley/pfxlog" "github.com/openziti/edge/controller/persistence" + "github.com/openziti/foundation/validation" "go.etcd.io/bbolt" + "strconv" "strings" ) @@ -32,32 +36,94 @@ func (p *PostureCheckOperatingSystem) Evaluate(pd *PostureData) bool { return false } - validOses := map[string]map[string]struct{}{} - - for _, os := range p.OperatingSystems { - osType := strings.ToLower(os.OsType) - validOses[osType] = map[string]struct{}{} - for _, version := range os.OsVersions { - validOses[osType][strings.ToLower(version)] = struct{}{} - } - } + validOses := getValidOses(p.OperatingSystems) osType := strings.ToLower(pd.Os.Type) - osVersion := strings.ToLower(pd.Os.Version) - if validOs, isValidOs := validOses[osType]; isValidOs { - if len(validOs) == 0 { - //any version + + if validOsVersions, isValidOs := validOses[osType]; isValidOs { + if len(validOsVersions) == 0 { return true } - if _, isValidVersion := validOs[osVersion]; isValidVersion { - return true + dataVersion, err := semver.Make(pd.Os.Version) + if err != nil { + pfxlog.Logger().Errorf("could not parse versions %s: %v", pd.Os.Version, err) + return false + } + + for _, validOsVersion := range validOsVersions { + if (*validOsVersion)(dataVersion) { + return true + } } } return false } +type version struct { + value int64 + orHigher bool + subVersions map[int64]*version +} + +func (version *version) isValid(checkVersions []int64) bool { + if len(checkVersions) == 0 { + return false //not enough versions to check + } + + if checkVersions[0] == version.value { + if len(version.subVersions) == 0 { + return true + } + + for _, subVersion := range version.subVersions { + return subVersion.isValid(checkVersions[1:]) + } + } else if version.orHigher && checkVersions[0] > version.value { + return true + } + + return false +} + +func getValidOses(oses []OperatingSystem) map[string][]*semver.Range { + validOses := map[string][]*semver.Range{} + + for _, os := range oses { + osType := strings.ToLower(os.OsType) + validOses[osType] = []*semver.Range{} //last os definition wins if redeclared + + for _, strVersion := range os.OsVersions { + semVer, err := semver.ParseRange(strVersion) + if err != nil { + pfxlog.Logger().Errorf("could not parse version %s: %v", strVersion, err) + continue + } + validOses[osType] = append(validOses[osType], &semVer) + } + } + + return validOses +} + +func parseVersion(version string) []int64 { + strVersions := strings.Split(version, ".") + var intVersions []int64 + + for _, version := range strVersions { + parsedVersion, err := strconv.ParseInt(version, 10, 64) + if err != nil { + pfxlog.Logger().Errorf("error converting version %s to int: %v", version, err) + break + } else { + intVersions = append(intVersions, parsedVersion) + } + } + + return intVersions +} + type OperatingSystem struct { OsType string OsVersions []string @@ -84,11 +150,28 @@ func (p *PostureCheckOperatingSystem) fillFrom(handler Handler, tx *bbolt.Tx, ch return nil } +func (p *PostureCheckOperatingSystem) validateOsVersions() error { + for _, os := range p.OperatingSystems { + for versionIdx, version := range os.OsVersions { + if _, err := semver.ParseRange(version); err != nil { + msg := fmt.Sprintf("invalid version for os: [%s], version: [%s]: %v ", os, version, err) + return validation.NewFieldError(msg, fmt.Sprintf("operatingSystems[%s][%d]", os, versionIdx), msg) + } + } + } + + return nil +} + func (p *PostureCheckOperatingSystem) toBoltEntityForCreate(tx *bbolt.Tx, handler Handler) (persistence.PostureCheckSubType, error) { ret := &persistence.PostureCheckOperatingSystem{ OperatingSystems: []persistence.OperatingSystem{}, } + if err := p.validateOsVersions(); err != nil { + return nil, err + } + for _, osMatch := range p.OperatingSystems { ret.OperatingSystems = append(ret.OperatingSystems, persistence.OperatingSystem{ OsType: osMatch.OsType, @@ -104,6 +187,10 @@ func (p *PostureCheckOperatingSystem) toBoltEntityForUpdate(tx *bbolt.Tx, handle OperatingSystems: []persistence.OperatingSystem{}, } + if err := p.validateOsVersions(); err != nil { + return nil, err + } + for _, osMatch := range p.OperatingSystems { ret.OperatingSystems = append(ret.OperatingSystems, persistence.OperatingSystem{ OsType: osMatch.OsType, @@ -119,6 +206,10 @@ func (p *PostureCheckOperatingSystem) toBoltEntityForPatch(tx *bbolt.Tx, handler OperatingSystems: []persistence.OperatingSystem{}, } + if err := p.validateOsVersions(); err != nil { + return nil, err + } + for _, osMatch := range p.OperatingSystems { ret.OperatingSystems = append(ret.OperatingSystems, persistence.OperatingSystem{ OsType: osMatch.OsType, diff --git a/controller/model/posture_check_model_os_test.go b/controller/model/posture_check_model_os_test.go new file mode 100644 index 000000000..2918dcc01 --- /dev/null +++ b/controller/model/posture_check_model_os_test.go @@ -0,0 +1,304 @@ +/* + Copyright NetFoundry, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package model + +import ( + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestPostureCheckModelOs_Evaluate(t *testing.T) { + + t.Run("returns true for exactly matching valid os type and version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.True(result) + }) + + t.Run("returns true for valid os type and higher than required major version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + osCheck.OperatingSystems[0].OsVersions[0] = ">=1.0.0" + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.True(result) + }) + + t.Run("returns true for valid os type and higher than required minor version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + osCheck.OperatingSystems[0].OsVersions[0] = ">=10.4.0" + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.True(result) + }) + + t.Run("returns true for valid os type and higher than required patch version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + osCheck.OperatingSystems[0].OsVersions[0] = ">=10.5.0" + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.True(result) + }) + + t.Run("returns false for valid os type and lower than required major version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + osCheck.OperatingSystems[0].OsVersions[0] = ">=11.0.0" //default data has 10.5.19041 + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.False(result) + }) + + t.Run("returns false for valid os type and lower than required minor version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + osCheck.OperatingSystems[0].OsVersions[0] = ">10.6.0" //default data has 10.5.19041 + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.False(result) + }) + + t.Run("returns false for valid os type and lower than required patch version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + osCheck.OperatingSystems[0].OsVersions[0] = ">=10.5.19042" //default data has 10.5.19041 + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.False(result) + }) + + t.Run("returns true for valid os type exactly matching version or higher", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + osCheck.OperatingSystems[0].OsVersions[0] = ">=10.5.19041" //default data has 10.5.19041 + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.True(result) + }) + + t.Run("returns false for invalid os but exactly matching version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + osCheck.OperatingSystems[0].OsType = os2Type + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.False(result) + }) + + t.Run("returns false for valid os and no matching version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + postureData.Os.Type = os2Type + postureData.Os.Version = "" + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.False(result) + }) + + t.Run("returns true for valid os and no required version and no submitted version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + postureData.Os.Type = os3Type + postureData.Os.Version = "" + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.True(result) + }) + + t.Run("returns true for valid os and no required version and submitted version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + postureData.Os.Type = os3Type + postureData.Os.Version = "1.2.3" + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.True(result) + }) + + t.Run("returns true for lower exact version match and later and higher match", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + postureData.Os.Type = os2Type + postureData.Os.Version = os2Version1 + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.True(result) + }) + + t.Run("returns true for higher exact version match and an earlier match", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + postureData.Os.Type = os2Type + postureData.Os.Version = "7.8.9" + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.True(result) + }) + + //poorly formatted posture data + t.Run("returns false for posture data with valid os and partial os major version match", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + postureData.Os.Version = "10" + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.False(result) + }) + + t.Run("returns false for posture data with valid os and partial os major and minor version match", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + postureData.Os.Version = "10.5" + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.False(result) + }) + + t.Run("returns false for posture data with valid os and partial os major version match with dangling period", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + postureData.Os.Version = "10" + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.False(result) + }) + + t.Run("returns false for posture data with valid os and partial os major and minor version match with dangling period", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + postureData.Os.Version = "10.5." + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.False(result) + }) + + t.Run("returns false for posture data with valid os and empty version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + postureData.Os.Version = "" + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.False(result) + }) + + t.Run("returns false for posture data with valid os and partially valid and mangled version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + postureData.Os.Version = "10.this is not a real version" + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.False(result) + }) + + t.Run("returns false for posture data with valid os and fully mangled version", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + postureData.Os.Version = "this is not a real version" + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.False(result) + }) +} + + +const ( + os1Type = "Windows" + os1Version = "10.5.19041" + + os2Type = "Linux" + os2Version1 = "3.4.5" + os2Version2 = ">=7.8.9" + + os3Type = "Android" +) + +// Returns a process check and posture data that will pass with matching os type, version, and build. Can +// be altered to test various pass/fail states +func newMatchingOsCheckAndData() (*PostureCheckOperatingSystem, *PostureData) { + postureCheckId := "30qhj45" + + postureResponse := &PostureResponse{ + PostureCheckId: postureCheckId, + TypeId: PostureCheckTypeOs, + TimedOut: false, + LastUpdatedAt: time.Now(), + } + + postureResponseOs := &PostureResponseOs{ + PostureResponse: nil, + Type: os1Type, + Version: os1Version, + } + + postureResponse.SubType = postureResponseOs + postureResponseOs.PostureResponse = postureResponse + + validPostureData := &PostureData{ + Mac: nil, + Domain: nil, + Os: postureResponseOs, + Processes: nil, + } + + processCheck := &PostureCheckOperatingSystem{ + OperatingSystems: []OperatingSystem{ + { + OsType: os1Type, + OsVersions: []string{os1Version}, + }, + { + OsType: os2Type, + OsVersions: []string{ + os2Version1, + os2Version2, + }, + }, + { + OsType: os3Type, + OsVersions: nil, + }, + }, + } + + return processCheck, validPostureData +} diff --git a/controller/model/posture_check_model_process_test.go b/controller/model/posture_check_model_process_test.go index 716a7707d..23162699b 100644 --- a/controller/model/posture_check_model_process_test.go +++ b/controller/model/posture_check_model_process_test.go @@ -60,7 +60,9 @@ func newMatchingProcessCheckAndData() (*PostureCheckProcess, *PostureData) { OperatingSystem: "Windows", Path: `C:\some\path\some.exe`, Hashes: []string{ + "something that will never match 1", binaryHash, + "something that will never match 2", }, Fingerprint: signerFingerprint, } diff --git a/go.mod b/go.mod index df4abebe0..6eb50e2f8 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/Jeffail/gabs v1.4.0 github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2 github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect + github.com/blang/semver v3.5.1+incompatible github.com/coreos/go-iptables v0.4.5 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa diff --git a/go.sum b/go.sum index 9b78ab4b3..8a5732caa 100644 --- a/go.sum +++ b/go.sum @@ -59,6 +59,9 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/biogo/store v0.0.0-20190426020002-884f370e325d h1:vu2gsANkGtqYaQNXhmAAiJ7b1eKTbX3/aPAvDCasBgE= github.com/biogo/store v0.0.0-20190426020002-884f370e325d/go.mod h1:Iev9Q3MErcn+w3UOJD/DkEzllvugfdx7bGcMOFhvr/4= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blang/semver v1.1.0 h1:ol1rO7QQB5uy7umSNV7VAmLugfLRD+17sYJujRNYPhg= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/cenkalti/backoff/v4 v4.0.2 h1:JIufpQLbh4DkbQoii76ItQIUFzevQSqOLZca4eamEDs= From 48a3f5dc0b3f0bebb68be56c3a42cec223bf34b8 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Wed, 11 Nov 2020 20:39:35 -0500 Subject: [PATCH 2/3] remove unused function --- controller/model/posture_check_model_os.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/controller/model/posture_check_model_os.go b/controller/model/posture_check_model_os.go index 7e819f0ef..09aab972b 100644 --- a/controller/model/posture_check_model_os.go +++ b/controller/model/posture_check_model_os.go @@ -23,7 +23,6 @@ import ( "github.com/openziti/edge/controller/persistence" "github.com/openziti/foundation/validation" "go.etcd.io/bbolt" - "strconv" "strings" ) @@ -107,23 +106,6 @@ func getValidOses(oses []OperatingSystem) map[string][]*semver.Range { return validOses } -func parseVersion(version string) []int64 { - strVersions := strings.Split(version, ".") - var intVersions []int64 - - for _, version := range strVersions { - parsedVersion, err := strconv.ParseInt(version, 10, 64) - if err != nil { - pfxlog.Logger().Errorf("error converting version %s to int: %v", version, err) - break - } else { - intVersions = append(intVersions, parsedVersion) - } - } - - return intVersions -} - type OperatingSystem struct { OsType string OsVersions []string From 108b657044cf0beda3ed63462891ef6cdc1a0ff5 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Wed, 11 Nov 2020 20:51:54 -0500 Subject: [PATCH 3/3] fix missing dangling period in os posture test --- controller/model/posture_check_model_os_test.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/controller/model/posture_check_model_os_test.go b/controller/model/posture_check_model_os_test.go index 2918dcc01..b173fea9f 100644 --- a/controller/model/posture_check_model_os_test.go +++ b/controller/model/posture_check_model_os_test.go @@ -191,7 +191,7 @@ func TestPostureCheckModelOs_Evaluate(t *testing.T) { t.Run("returns false for posture data with valid os and partial os major version match with dangling period", func(t *testing.T) { osCheck, postureData := newMatchingOsCheckAndData() - postureData.Os.Version = "10" + postureData.Os.Version = "10." result := osCheck.Evaluate(postureData) @@ -209,6 +209,17 @@ func TestPostureCheckModelOs_Evaluate(t *testing.T) { req.False(result) }) + + t.Run("returns false for posture data with valid os and partial os major and dangling periods", func(t *testing.T) { + osCheck, postureData := newMatchingOsCheckAndData() + postureData.Os.Version = "10.5." + + result := osCheck.Evaluate(postureData) + + req := require.New(t) + req.False(result) + }) + t.Run("returns false for posture data with valid os and empty version", func(t *testing.T) { osCheck, postureData := newMatchingOsCheckAndData() postureData.Os.Version = ""