mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-06 05:29:07 +02:00
Add initial support of device posture checks (#1540)
This PR implements the following posture checks: * Agent minimum version allowed * OS minimum version allowed * Geo-location based on connection IP For the geo-based location, we rely on GeoLite2 databases which are free IP geolocation databases. MaxMind was tested and we provide a script that easily allows to download of all necessary files, see infrastructure_files/download-geolite2.sh. The OpenAPI spec should extensively cover the life cycle of current version posture checks.
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
package posture
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
)
|
||||
|
||||
const (
|
||||
NBVersionCheckName = "NBVersionCheck"
|
||||
OSVersionCheckName = "OSVersionCheck"
|
||||
GeoLocationCheckName = "GeoLocationCheck"
|
||||
)
|
||||
|
||||
// Check represents an interface for performing a check on a peer.
|
||||
type Check interface {
|
||||
Check(peer nbpeer.Peer) (bool, error)
|
||||
Name() string
|
||||
}
|
||||
|
||||
type Checks struct {
|
||||
// ID of the posture checks
|
||||
ID string `gorm:"primaryKey"`
|
||||
|
||||
// Name of the posture checks
|
||||
Name string
|
||||
|
||||
// Description of the posture checks visible in the UI
|
||||
Description string
|
||||
|
||||
// AccountID is a reference to the Account that this object belongs
|
||||
AccountID string `json:"-" gorm:"index"`
|
||||
|
||||
// Checks is a set of objects that perform the actual checks
|
||||
Checks ChecksDefinition `gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
// ChecksDefinition contains definition of actual check
|
||||
type ChecksDefinition struct {
|
||||
NBVersionCheck *NBVersionCheck `json:",omitempty"`
|
||||
OSVersionCheck *OSVersionCheck `json:",omitempty"`
|
||||
GeoLocationCheck *GeoLocationCheck `json:",omitempty"`
|
||||
}
|
||||
|
||||
// Copy returns a copy of a checks definition.
|
||||
func (cd ChecksDefinition) Copy() ChecksDefinition {
|
||||
var cdCopy ChecksDefinition
|
||||
if cd.NBVersionCheck != nil {
|
||||
cdCopy.NBVersionCheck = &NBVersionCheck{
|
||||
MinVersion: cd.NBVersionCheck.MinVersion,
|
||||
}
|
||||
}
|
||||
if cd.OSVersionCheck != nil {
|
||||
cdCopy.OSVersionCheck = &OSVersionCheck{}
|
||||
osCheck := cdCopy.OSVersionCheck
|
||||
if osCheck.Android != nil {
|
||||
cdCopy.OSVersionCheck.Android = &MinVersionCheck{MinVersion: osCheck.Android.MinVersion}
|
||||
}
|
||||
if osCheck.Darwin != nil {
|
||||
cdCopy.OSVersionCheck.Darwin = &MinVersionCheck{MinVersion: osCheck.Darwin.MinVersion}
|
||||
}
|
||||
if osCheck.Ios != nil {
|
||||
cdCopy.OSVersionCheck.Ios = &MinVersionCheck{MinVersion: osCheck.Ios.MinVersion}
|
||||
}
|
||||
if osCheck.Linux != nil {
|
||||
cdCopy.OSVersionCheck.Linux = &MinKernelVersionCheck{MinKernelVersion: osCheck.Linux.MinKernelVersion}
|
||||
}
|
||||
if osCheck.Windows != nil {
|
||||
cdCopy.OSVersionCheck.Windows = &MinKernelVersionCheck{MinKernelVersion: osCheck.Windows.MinKernelVersion}
|
||||
}
|
||||
}
|
||||
if cd.GeoLocationCheck != nil {
|
||||
geoCheck := cd.GeoLocationCheck
|
||||
cdCopy.GeoLocationCheck = &GeoLocationCheck{
|
||||
Action: geoCheck.Action,
|
||||
Locations: make([]Location, len(geoCheck.Locations)),
|
||||
}
|
||||
copy(cd.GeoLocationCheck.Locations, geoCheck.Locations)
|
||||
}
|
||||
return cdCopy
|
||||
}
|
||||
|
||||
// TableName returns the name of the table for the Checks model in the database.
|
||||
func (*Checks) TableName() string {
|
||||
return "posture_checks"
|
||||
}
|
||||
|
||||
// Copy returns a copy of a posture checks.
|
||||
func (pc *Checks) Copy() *Checks {
|
||||
checks := &Checks{
|
||||
ID: pc.ID,
|
||||
Name: pc.Name,
|
||||
Description: pc.Description,
|
||||
AccountID: pc.AccountID,
|
||||
Checks: pc.Checks.Copy(),
|
||||
}
|
||||
return checks
|
||||
}
|
||||
|
||||
// EventMeta returns activity event meta-related to this posture checks.
|
||||
func (pc *Checks) EventMeta() map[string]any {
|
||||
return map[string]any{"name": pc.Name}
|
||||
}
|
||||
|
||||
// GetChecks returns list of all initialized checks definitions
|
||||
func (pc *Checks) GetChecks() []Check {
|
||||
var checks []Check
|
||||
if pc.Checks.NBVersionCheck != nil {
|
||||
checks = append(checks, pc.Checks.NBVersionCheck)
|
||||
}
|
||||
if pc.Checks.OSVersionCheck != nil {
|
||||
checks = append(checks, pc.Checks.OSVersionCheck)
|
||||
}
|
||||
if pc.Checks.GeoLocationCheck != nil {
|
||||
checks = append(checks, pc.Checks.GeoLocationCheck)
|
||||
}
|
||||
return checks
|
||||
}
|
||||
|
||||
func (pc *Checks) Validate() error {
|
||||
if check := pc.Checks.NBVersionCheck; check != nil {
|
||||
if !isVersionValid(check.MinVersion) {
|
||||
return fmt.Errorf("%s version: %s is not valid", check.Name(), check.MinVersion)
|
||||
}
|
||||
}
|
||||
|
||||
if osCheck := pc.Checks.OSVersionCheck; osCheck != nil {
|
||||
if osCheck.Android != nil {
|
||||
if !isVersionValid(osCheck.Android.MinVersion) {
|
||||
return fmt.Errorf("%s android version: %s is not valid", osCheck.Name(), osCheck.Android.MinVersion)
|
||||
}
|
||||
}
|
||||
|
||||
if osCheck.Ios != nil {
|
||||
if !isVersionValid(osCheck.Ios.MinVersion) {
|
||||
return fmt.Errorf("%s ios version: %s is not valid", osCheck.Name(), osCheck.Ios.MinVersion)
|
||||
}
|
||||
}
|
||||
|
||||
if osCheck.Darwin != nil {
|
||||
if !isVersionValid(osCheck.Darwin.MinVersion) {
|
||||
return fmt.Errorf("%s darwin version: %s is not valid", osCheck.Name(), osCheck.Darwin.MinVersion)
|
||||
}
|
||||
}
|
||||
|
||||
if osCheck.Linux != nil {
|
||||
if !isVersionValid(osCheck.Linux.MinKernelVersion) {
|
||||
return fmt.Errorf("%s linux kernel version: %s is not valid", osCheck.Name(),
|
||||
osCheck.Linux.MinKernelVersion)
|
||||
}
|
||||
}
|
||||
|
||||
if osCheck.Windows != nil {
|
||||
if !isVersionValid(osCheck.Windows.MinKernelVersion) {
|
||||
return fmt.Errorf("%s windows kernel version: %s is not valid", osCheck.Name(),
|
||||
osCheck.Windows.MinKernelVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isVersionValid(ver string) bool {
|
||||
newVersion, err := version.NewVersion(ver)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if newVersion != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package posture
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChecks_MarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
checks *Checks
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid Posture Checks Marshal",
|
||||
checks: &Checks{
|
||||
ID: "id1",
|
||||
Name: "name1",
|
||||
Description: "desc1",
|
||||
AccountID: "acc1",
|
||||
Checks: ChecksDefinition{
|
||||
NBVersionCheck: &NBVersionCheck{
|
||||
MinVersion: "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []byte(`
|
||||
{
|
||||
"ID": "id1",
|
||||
"Name": "name1",
|
||||
"Description": "desc1",
|
||||
"Checks": {
|
||||
"NBVersionCheck": {
|
||||
"MinVersion": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Empty Posture Checks Marshal",
|
||||
checks: &Checks{
|
||||
ID: "",
|
||||
Name: "",
|
||||
Description: "",
|
||||
AccountID: "",
|
||||
Checks: ChecksDefinition{
|
||||
NBVersionCheck: &NBVersionCheck{},
|
||||
},
|
||||
},
|
||||
want: []byte(`
|
||||
{
|
||||
"ID": "",
|
||||
"Name": "",
|
||||
"Description": "",
|
||||
"Checks": {
|
||||
"NBVersionCheck": {
|
||||
"MinVersion": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := json.Marshal(tt.checks)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Checks.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
assert.JSONEq(t, string(got), string(tt.want))
|
||||
assert.Equal(t, tt.checks, tt.checks.Copy(), "original Checks should not be modified")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChecks_UnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
in []byte
|
||||
expected *Checks
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid JSON Posture Checks Unmarshal",
|
||||
in: []byte(`
|
||||
{
|
||||
"ID": "id1",
|
||||
"Name": "name1",
|
||||
"Description": "desc1",
|
||||
"Checks": {
|
||||
"NBVersionCheck": {
|
||||
"MinVersion": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
expected: &Checks{
|
||||
ID: "id1",
|
||||
Name: "name1",
|
||||
Description: "desc1",
|
||||
Checks: ChecksDefinition{
|
||||
NBVersionCheck: &NBVersionCheck{
|
||||
MinVersion: "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON Posture Checks Unmarshal",
|
||||
in: []byte(`{`),
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "Empty JSON Posture Check Unmarshal",
|
||||
in: []byte(`{}`),
|
||||
expected: &Checks{},
|
||||
expectedError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
var checks Checks
|
||||
err := json.Unmarshal(tc.in, &checks)
|
||||
if tc.expectedError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, &checks)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChecks_Validate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
checks Checks
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid checks version",
|
||||
checks: Checks{
|
||||
Checks: ChecksDefinition{
|
||||
NBVersionCheck: &NBVersionCheck{
|
||||
MinVersion: "0.25.0",
|
||||
},
|
||||
OSVersionCheck: &OSVersionCheck{
|
||||
Ios: &MinVersionCheck{
|
||||
MinVersion: "13.0.1",
|
||||
},
|
||||
Linux: &MinKernelVersionCheck{
|
||||
MinKernelVersion: "5.3.3-dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid checks version",
|
||||
checks: Checks{
|
||||
Checks: ChecksDefinition{
|
||||
NBVersionCheck: &NBVersionCheck{
|
||||
MinVersion: "abc",
|
||||
},
|
||||
OSVersionCheck: &OSVersionCheck{
|
||||
Android: &MinVersionCheck{
|
||||
MinVersion: "dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "Combined valid and invalid checks version",
|
||||
checks: Checks{
|
||||
Checks: ChecksDefinition{
|
||||
NBVersionCheck: &NBVersionCheck{
|
||||
MinVersion: "abc",
|
||||
},
|
||||
OSVersionCheck: &OSVersionCheck{
|
||||
Windows: &MinKernelVersionCheck{
|
||||
MinKernelVersion: "10.0.1234",
|
||||
},
|
||||
Darwin: &MinVersionCheck{
|
||||
MinVersion: "13.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.checks.Validate()
|
||||
if tc.expectedError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package posture
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
)
|
||||
|
||||
const (
|
||||
GeoLocationActionAllow string = "allow"
|
||||
GeoLocationActionDeny string = "deny"
|
||||
)
|
||||
|
||||
type Location struct {
|
||||
// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country
|
||||
CountryCode string
|
||||
|
||||
// CityName Commonly used English name of the city
|
||||
CityName string
|
||||
}
|
||||
|
||||
var _ Check = (*GeoLocationCheck)(nil)
|
||||
|
||||
type GeoLocationCheck struct {
|
||||
// Locations list of geolocations, to which the policy applies
|
||||
Locations []Location
|
||||
|
||||
// Action to take upon policy match
|
||||
Action string
|
||||
}
|
||||
|
||||
func (g *GeoLocationCheck) Check(peer nbpeer.Peer) (bool, error) {
|
||||
// deny if the peer location is not evaluated
|
||||
if peer.Location.CountryCode == "" && peer.Location.CityName == "" {
|
||||
return false, fmt.Errorf("peer's location is not set")
|
||||
}
|
||||
|
||||
for _, loc := range g.Locations {
|
||||
if loc.CountryCode == peer.Location.CountryCode {
|
||||
if loc.CityName == "" || loc.CityName == peer.Location.CityName {
|
||||
switch g.Action {
|
||||
case GeoLocationActionDeny:
|
||||
return false, nil
|
||||
case GeoLocationActionAllow:
|
||||
return true, nil
|
||||
default:
|
||||
return false, fmt.Errorf("invalid geo location action: %s", g.Action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// At this point, no location in the list matches the peer's location
|
||||
// For action deny and no location match, allow the peer
|
||||
if g.Action == GeoLocationActionDeny {
|
||||
return true, nil
|
||||
}
|
||||
// For action allow and no location match, deny the peer
|
||||
if g.Action == GeoLocationActionAllow {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("invalid geo location action: %s", g.Action)
|
||||
}
|
||||
|
||||
func (g *GeoLocationCheck) Name() string {
|
||||
return GeoLocationCheckName
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package posture
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/peer"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGeoLocationCheck_Check(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input peer.Peer
|
||||
check GeoLocationCheck
|
||||
wantErr bool
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
name: "Peer location matches the location in the allow sets",
|
||||
input: peer.Peer{
|
||||
Location: peer.Location{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
},
|
||||
},
|
||||
check: GeoLocationCheck{
|
||||
Locations: []Location{
|
||||
{
|
||||
CountryCode: "US",
|
||||
CityName: "Los Angeles",
|
||||
},
|
||||
{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionAllow,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "Peer location matches the location in the allow country only",
|
||||
input: peer.Peer{
|
||||
Location: peer.Location{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
},
|
||||
},
|
||||
check: GeoLocationCheck{
|
||||
Locations: []Location{
|
||||
{
|
||||
CountryCode: "DE",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionAllow,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "Peer location doesn't match the location in the allow sets",
|
||||
input: peer.Peer{
|
||||
Location: peer.Location{
|
||||
CountryCode: "DE",
|
||||
CityName: "Frankfurt am Main",
|
||||
},
|
||||
},
|
||||
check: GeoLocationCheck{
|
||||
Locations: []Location{
|
||||
{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
},
|
||||
{
|
||||
CountryCode: "US",
|
||||
CityName: "Los Angeles",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionAllow,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "Peer location doesn't match the location in the allow country only",
|
||||
input: peer.Peer{
|
||||
Location: peer.Location{
|
||||
CountryCode: "DE",
|
||||
CityName: "Frankfurt am Main",
|
||||
},
|
||||
},
|
||||
check: GeoLocationCheck{
|
||||
Locations: []Location{
|
||||
{
|
||||
CountryCode: "US",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionAllow,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "Peer location matches the location in the deny sets",
|
||||
input: peer.Peer{
|
||||
Location: peer.Location{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
},
|
||||
},
|
||||
check: GeoLocationCheck{
|
||||
Locations: []Location{
|
||||
{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
},
|
||||
{
|
||||
CountryCode: "US",
|
||||
CityName: "Los Angeles",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionDeny,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "Peer location matches the location in the deny country only",
|
||||
input: peer.Peer{
|
||||
Location: peer.Location{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
},
|
||||
},
|
||||
check: GeoLocationCheck{
|
||||
Locations: []Location{
|
||||
{
|
||||
CountryCode: "DE",
|
||||
},
|
||||
{
|
||||
CountryCode: "US",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionDeny,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "Peer location doesn't match the location in the deny sets",
|
||||
input: peer.Peer{
|
||||
Location: peer.Location{
|
||||
CountryCode: "DE",
|
||||
CityName: "Frankfurt am Main",
|
||||
},
|
||||
},
|
||||
check: GeoLocationCheck{
|
||||
Locations: []Location{
|
||||
{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
},
|
||||
{
|
||||
CountryCode: "US",
|
||||
CityName: "Los Angeles",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionDeny,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "Peer location doesn't match the location in the deny country only",
|
||||
input: peer.Peer{
|
||||
Location: peer.Location{
|
||||
CountryCode: "DE",
|
||||
CityName: "Frankfurt am Main",
|
||||
},
|
||||
},
|
||||
check: GeoLocationCheck{
|
||||
Locations: []Location{
|
||||
{
|
||||
CountryCode: "US",
|
||||
CityName: "Los Angeles",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionDeny,
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "Peer with no location in the allow sets",
|
||||
input: peer.Peer{},
|
||||
check: GeoLocationCheck{
|
||||
Locations: []Location{
|
||||
{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionAllow,
|
||||
},
|
||||
wantErr: true,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "Peer with no location in the deny sets",
|
||||
input: peer.Peer{},
|
||||
check: GeoLocationCheck{
|
||||
Locations: []Location{
|
||||
{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
},
|
||||
},
|
||||
Action: GeoLocationActionDeny,
|
||||
},
|
||||
wantErr: true,
|
||||
isValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
isValid, err := tt.check.Check(tt.input)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.isValid, isValid)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package posture
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-version"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
)
|
||||
|
||||
type NBVersionCheck struct {
|
||||
MinVersion string
|
||||
}
|
||||
|
||||
var _ Check = (*NBVersionCheck)(nil)
|
||||
|
||||
func (n *NBVersionCheck) Check(peer nbpeer.Peer) (bool, error) {
|
||||
peerNBVersion, err := version.NewVersion(peer.Meta.WtVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
constraints, err := version.NewConstraint(">= " + n.MinVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if constraints.Check(peerNBVersion) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
log.Debugf("peer %s NB version %s is older than minimum allowed version %s",
|
||||
peer.ID, peer.Meta.WtVersion, n.MinVersion)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (n *NBVersionCheck) Name() string {
|
||||
return NBVersionCheckName
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package posture
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/peer"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNBVersionCheck_Check(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input peer.Peer
|
||||
check NBVersionCheck
|
||||
wantErr bool
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
name: "Valid Peer NB version",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
WtVersion: "1.0.1",
|
||||
},
|
||||
},
|
||||
check: NBVersionCheck{
|
||||
MinVersion: "1.0.0",
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "Valid Peer NB version With No Patch Version 1",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
WtVersion: "2.0.9",
|
||||
},
|
||||
},
|
||||
check: NBVersionCheck{
|
||||
MinVersion: "2.0",
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "Valid Peer NB version With No Patch Version 2",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
WtVersion: "2.0.0",
|
||||
},
|
||||
},
|
||||
check: NBVersionCheck{
|
||||
MinVersion: "2.0",
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "Older Peer NB version",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
WtVersion: "0.9.9",
|
||||
},
|
||||
},
|
||||
check: NBVersionCheck{
|
||||
MinVersion: "1.0.0",
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "Older Peer NB version With Patch Version",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
WtVersion: "0.1.0",
|
||||
},
|
||||
},
|
||||
check: NBVersionCheck{
|
||||
MinVersion: "0.2",
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid Peer NB version",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
WtVersion: "x.y.z",
|
||||
},
|
||||
},
|
||||
check: NBVersionCheck{
|
||||
MinVersion: "1.0.0",
|
||||
},
|
||||
wantErr: true,
|
||||
isValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
isValid, err := tt.check.Check(tt.input)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.isValid, isValid)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package posture
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type MinVersionCheck struct {
|
||||
MinVersion string
|
||||
}
|
||||
|
||||
type MinKernelVersionCheck struct {
|
||||
MinKernelVersion string
|
||||
}
|
||||
|
||||
type OSVersionCheck struct {
|
||||
Android *MinVersionCheck
|
||||
Darwin *MinVersionCheck
|
||||
Ios *MinVersionCheck
|
||||
Linux *MinKernelVersionCheck
|
||||
Windows *MinKernelVersionCheck
|
||||
}
|
||||
|
||||
var _ Check = (*OSVersionCheck)(nil)
|
||||
|
||||
func (c *OSVersionCheck) Check(peer nbpeer.Peer) (bool, error) {
|
||||
peerGoOS := peer.Meta.GoOS
|
||||
switch peerGoOS {
|
||||
case "android":
|
||||
return checkMinVersion(peerGoOS, peer.Meta.OSVersion, c.Android)
|
||||
case "darwin":
|
||||
return checkMinVersion(peerGoOS, peer.Meta.OSVersion, c.Darwin)
|
||||
case "ios":
|
||||
return checkMinVersion(peerGoOS, peer.Meta.OSVersion, c.Ios)
|
||||
case "linux":
|
||||
kernelVersion := strings.Split(peer.Meta.KernelVersion, "-")[0]
|
||||
return checkMinKernelVersion(peerGoOS, kernelVersion, c.Linux)
|
||||
case "windows":
|
||||
return checkMinKernelVersion(peerGoOS, peer.Meta.KernelVersion, c.Windows)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *OSVersionCheck) Name() string {
|
||||
return OSVersionCheckName
|
||||
}
|
||||
|
||||
func checkMinVersion(peerGoOS, peerVersion string, check *MinVersionCheck) (bool, error) {
|
||||
if check == nil {
|
||||
log.Debugf("peer %s OS is not allowed in the check", peerGoOS)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
peerNBVersion, err := version.NewVersion(peerVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
constraints, err := version.NewConstraint(">= " + check.MinVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if constraints.Check(peerNBVersion) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
log.Debugf("peer %s OS version %s is older than minimum allowed version %s", peerGoOS, peerVersion, check.MinVersion)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func checkMinKernelVersion(peerGoOS, peerVersion string, check *MinKernelVersionCheck) (bool, error) {
|
||||
if check == nil {
|
||||
log.Debugf("peer %s OS is not allowed in the check", peerGoOS)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
peerNBVersion, err := version.NewVersion(peerVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
constraints, err := version.NewConstraint(">= " + check.MinKernelVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if constraints.Check(peerNBVersion) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
log.Debugf("peer %s kernel version %s is older than minimum allowed version %s", peerGoOS, peerVersion, check.MinKernelVersion)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package posture
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/peer"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOSVersionCheck_Check(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input peer.Peer
|
||||
check OSVersionCheck
|
||||
wantErr bool
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
name: "Valid Peer Windows Kernel version",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
GoOS: "linux",
|
||||
KernelVersion: "10.0.20348.2227",
|
||||
},
|
||||
},
|
||||
check: OSVersionCheck{
|
||||
Linux: &MinKernelVersionCheck{
|
||||
MinKernelVersion: "10.0.20340.2200",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "Valid Peer Linux Kernel version",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
GoOS: "linux",
|
||||
KernelVersion: "6.1.1",
|
||||
},
|
||||
},
|
||||
check: OSVersionCheck{
|
||||
Linux: &MinKernelVersionCheck{
|
||||
MinKernelVersion: "6.0.0",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "Valid Peer Linux Kernel version with suffix",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
GoOS: "linux",
|
||||
KernelVersion: "6.5.11-linuxkit",
|
||||
},
|
||||
},
|
||||
check: OSVersionCheck{
|
||||
Linux: &MinKernelVersionCheck{
|
||||
MinKernelVersion: "6.0.0",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "Not valid Peer macOS version",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
GoOS: "darwin",
|
||||
OSVersion: "14.2.1",
|
||||
},
|
||||
},
|
||||
check: OSVersionCheck{
|
||||
Darwin: &MinVersionCheck{
|
||||
MinVersion: "15",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "Valid Peer ios version allowed by any rule",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
GoOS: "ios",
|
||||
OSVersion: "17.0.1",
|
||||
},
|
||||
},
|
||||
check: OSVersionCheck{
|
||||
Ios: &MinVersionCheck{
|
||||
MinVersion: "0",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "Valid Peer android version not allowed by rule",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
GoOS: "android",
|
||||
OSVersion: "14",
|
||||
},
|
||||
},
|
||||
check: OSVersionCheck{},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "Valid Peer Linux Kernel version not allowed by rule",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
GoOS: "linux",
|
||||
KernelVersion: "6.1.1",
|
||||
},
|
||||
},
|
||||
check: OSVersionCheck{},
|
||||
wantErr: false,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid Peer Linux kernel version",
|
||||
input: peer.Peer{
|
||||
Meta: peer.PeerSystemMeta{
|
||||
GoOS: "linux",
|
||||
KernelVersion: "x.y.1",
|
||||
},
|
||||
},
|
||||
check: OSVersionCheck{
|
||||
Linux: &MinKernelVersionCheck{
|
||||
MinKernelVersion: "6.0.0",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
isValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
isValid, err := tt.check.Check(tt.input)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.isValid, isValid)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user