Merge pull request #402 from openziti/enhance.os.version.checks

Enhance OS version posture checks
This commit is contained in:
Andrew
2020-11-11 21:30:33 -05:00
committed by GitHub
5 changed files with 409 additions and 15 deletions
+88 -15
View File
@@ -18,7 +18,10 @@ 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"
"strings"
)
@@ -32,32 +35,77 @@ 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
}
type OperatingSystem struct {
OsType string
OsVersions []string
@@ -84,11 +132,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 +169,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 +188,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,
@@ -0,0 +1,315 @@
/*
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 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 = ""
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
}
@@ -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,
}
+1
View File
@@ -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
+3
View File
@@ -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=