Add private network posture check (#1606)

* wip: Add PrivateNetworkCheck checks interface implementation

* use generic CheckAction constant

* Add private network check to posture checks

* Fix copy function target in posture checks

* Add network check functionality to posture package

* regenerate the openapi specs

* Update Posture Check actions in test file

* Remove unused function

* Refactor network address handling in PrivateNetworkCheck

* Refactor Prefixes to Ranges in private network checks

* Implement private network checks in posture checks handler tests

* Add test for check copy

* Add gorm serializer for network range
This commit is contained in:
Bethuel Mmbaga
2024-02-22 19:22:43 +03:00
committed by GitHub
parent bbea4c3cc3
commit a47c69c472
11 changed files with 572 additions and 81 deletions

View File

@@ -2,6 +2,7 @@ package posture
import (
"fmt"
"net/netip"
"github.com/hashicorp/go-version"
@@ -9,9 +10,13 @@ import (
)
const (
NBVersionCheckName = "NBVersionCheck"
OSVersionCheckName = "OSVersionCheck"
GeoLocationCheckName = "GeoLocationCheck"
NBVersionCheckName = "NBVersionCheck"
OSVersionCheckName = "OSVersionCheck"
GeoLocationCheckName = "GeoLocationCheck"
PrivateNetworkCheckName = "PrivateNetworkCheck"
CheckActionAllow string = "allow"
CheckActionDeny string = "deny"
)
// Check represents an interface for performing a check on a peer.
@@ -39,9 +44,10 @@ type Checks struct {
// ChecksDefinition contains definition of actual check
type ChecksDefinition struct {
NBVersionCheck *NBVersionCheck `json:",omitempty"`
OSVersionCheck *OSVersionCheck `json:",omitempty"`
GeoLocationCheck *GeoLocationCheck `json:",omitempty"`
NBVersionCheck *NBVersionCheck `json:",omitempty"`
OSVersionCheck *OSVersionCheck `json:",omitempty"`
GeoLocationCheck *GeoLocationCheck `json:",omitempty"`
PrivateNetworkCheck *PrivateNetworkCheck `json:",omitempty"`
}
// Copy returns a copy of a checks definition.
@@ -54,7 +60,7 @@ func (cd ChecksDefinition) Copy() ChecksDefinition {
}
if cd.OSVersionCheck != nil {
cdCopy.OSVersionCheck = &OSVersionCheck{}
osCheck := cdCopy.OSVersionCheck
osCheck := cd.OSVersionCheck
if osCheck.Android != nil {
cdCopy.OSVersionCheck.Android = &MinVersionCheck{MinVersion: osCheck.Android.MinVersion}
}
@@ -79,6 +85,14 @@ func (cd ChecksDefinition) Copy() ChecksDefinition {
}
copy(cdCopy.GeoLocationCheck.Locations, geoCheck.Locations)
}
if cd.PrivateNetworkCheck != nil {
privateNetCheck := cd.PrivateNetworkCheck
cdCopy.PrivateNetworkCheck = &PrivateNetworkCheck{
Action: privateNetCheck.Action,
Ranges: make([]netip.Prefix, len(privateNetCheck.Ranges)),
}
copy(cdCopy.PrivateNetworkCheck.Ranges, privateNetCheck.Ranges)
}
return cdCopy
}
@@ -116,6 +130,9 @@ func (pc *Checks) GetChecks() []Check {
if pc.Checks.GeoLocationCheck != nil {
checks = append(checks, pc.Checks.GeoLocationCheck)
}
if pc.Checks.PrivateNetworkCheck != nil {
checks = append(checks, pc.Checks.PrivateNetworkCheck)
}
return checks
}

View File

@@ -2,6 +2,7 @@ package posture
import (
"encoding/json"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
@@ -216,3 +217,62 @@ func TestChecks_Validate(t *testing.T) {
})
}
}
func TestChecks_Copy(t *testing.T) {
check := &Checks{
ID: "1",
Name: "default",
Description: "description",
AccountID: "accountID",
Checks: ChecksDefinition{
NBVersionCheck: &NBVersionCheck{
MinVersion: "0.25.0",
},
OSVersionCheck: &OSVersionCheck{
Android: &MinVersionCheck{
MinVersion: "13",
},
Darwin: &MinVersionCheck{
MinVersion: "14.2.0",
},
Ios: &MinVersionCheck{
MinVersion: "17.3.0",
},
Linux: &MinKernelVersionCheck{
MinKernelVersion: "6.5.11-linuxkit",
},
Windows: &MinKernelVersionCheck{
MinKernelVersion: "10.0.14393",
},
},
GeoLocationCheck: &GeoLocationCheck{
Locations: []Location{
{
CountryCode: "DE",
CityName: "Berlin",
},
},
Action: CheckActionAllow,
},
PrivateNetworkCheck: &PrivateNetworkCheck{
Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("10.0.0.0/8"),
},
Action: CheckActionDeny,
},
},
}
checkCopy := check.Copy()
assert.Equal(t, check.ID, checkCopy.ID)
assert.Equal(t, check.Name, checkCopy.Name)
assert.Equal(t, check.Description, checkCopy.Description)
assert.Equal(t, check.AccountID, checkCopy.AccountID)
assert.Equal(t, check.Checks.Copy(), checkCopy.Checks.Copy())
assert.ElementsMatch(t, check.GetChecks(), checkCopy.GetChecks())
// Updating the original check should not take effect on copy
check.Name = "name"
assert.NotSame(t, check, checkCopy)
}

View File

@@ -6,11 +6,6 @@ import (
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
@@ -39,9 +34,9 @@ func (g *GeoLocationCheck) Check(peer nbpeer.Peer) (bool, error) {
if loc.CountryCode == peer.Location.CountryCode {
if loc.CityName == "" || loc.CityName == peer.Location.CityName {
switch g.Action {
case GeoLocationActionDeny:
case CheckActionDeny:
return false, nil
case GeoLocationActionAllow:
case CheckActionAllow:
return true, nil
default:
return false, fmt.Errorf("invalid geo location action: %s", g.Action)
@@ -51,11 +46,11 @@ func (g *GeoLocationCheck) Check(peer nbpeer.Peer) (bool, error) {
}
// 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 {
if g.Action == CheckActionDeny {
return true, nil
}
// For action allow and no location match, deny the peer
if g.Action == GeoLocationActionAllow {
if g.Action == CheckActionAllow {
return false, nil
}

View File

@@ -35,7 +35,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
CityName: "Berlin",
},
},
Action: GeoLocationActionAllow,
Action: CheckActionAllow,
},
wantErr: false,
isValid: true,
@@ -54,7 +54,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
CountryCode: "DE",
},
},
Action: GeoLocationActionAllow,
Action: CheckActionAllow,
},
wantErr: false,
isValid: true,
@@ -78,7 +78,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
CityName: "Los Angeles",
},
},
Action: GeoLocationActionAllow,
Action: CheckActionAllow,
},
wantErr: false,
isValid: false,
@@ -97,7 +97,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
CountryCode: "US",
},
},
Action: GeoLocationActionAllow,
Action: CheckActionAllow,
},
wantErr: false,
isValid: false,
@@ -121,7 +121,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
CityName: "Los Angeles",
},
},
Action: GeoLocationActionDeny,
Action: CheckActionDeny,
},
wantErr: false,
isValid: false,
@@ -143,7 +143,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
CountryCode: "US",
},
},
Action: GeoLocationActionDeny,
Action: CheckActionDeny,
},
wantErr: false,
isValid: false,
@@ -167,7 +167,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
CityName: "Los Angeles",
},
},
Action: GeoLocationActionDeny,
Action: CheckActionDeny,
},
wantErr: false,
isValid: true,
@@ -187,7 +187,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
CityName: "Los Angeles",
},
},
Action: GeoLocationActionDeny,
Action: CheckActionDeny,
},
wantErr: false,
isValid: true,
@@ -202,7 +202,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
CityName: "Berlin",
},
},
Action: GeoLocationActionAllow,
Action: CheckActionAllow,
},
wantErr: true,
isValid: false,
@@ -217,7 +217,7 @@ func TestGeoLocationCheck_Check(t *testing.T) {
CityName: "Berlin",
},
},
Action: GeoLocationActionDeny,
Action: CheckActionDeny,
},
wantErr: true,
isValid: false,

View File

@@ -0,0 +1,54 @@
package posture
import (
"fmt"
"net/netip"
"slices"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
)
type PrivateNetworkCheck struct {
Action string
Ranges []netip.Prefix `gorm:"serializer:json"`
}
var _ Check = (*PrivateNetworkCheck)(nil)
func (p *PrivateNetworkCheck) Check(peer nbpeer.Peer) (bool, error) {
if len(peer.Meta.NetworkAddresses) == 0 {
return false, fmt.Errorf("peer's does not contain private network addresses")
}
maskedPrefixes := make([]netip.Prefix, 0, len(p.Ranges))
for _, prefix := range p.Ranges {
maskedPrefixes = append(maskedPrefixes, prefix.Masked())
}
for _, peerNetAddr := range peer.Meta.NetworkAddresses {
peerMaskedPrefix := peerNetAddr.NetIP.Masked()
if slices.Contains(maskedPrefixes, peerMaskedPrefix) {
switch p.Action {
case CheckActionDeny:
return false, nil
case CheckActionAllow:
return true, nil
default:
return false, fmt.Errorf("invalid private network check action: %s", p.Action)
}
}
}
if p.Action == CheckActionDeny {
return true, nil
}
if p.Action == CheckActionAllow {
return false, nil
}
return false, fmt.Errorf("invalid private network check action: %s", p.Action)
}
func (p *PrivateNetworkCheck) Name() string {
return PrivateNetworkCheckName
}

View File

@@ -0,0 +1,149 @@
package posture
import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
)
func TestPrivateNetworkCheck_Check(t *testing.T) {
tests := []struct {
name string
check PrivateNetworkCheck
peer nbpeer.Peer
wantErr bool
isValid bool
}{
{
name: "Peer private networks matches the allowed range",
check: PrivateNetworkCheck{
Action: CheckActionAllow,
Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("10.0.0.0/8"),
},
},
peer: nbpeer.Peer{
Meta: nbpeer.PeerSystemMeta{
NetworkAddresses: []nbpeer.NetworkAddress{
{
NetIP: netip.MustParsePrefix("192.168.0.123/24"),
},
{
NetIP: netip.MustParsePrefix("fe80::6089:eaff:fe0c:232f/64"),
},
},
},
},
wantErr: false,
isValid: true,
},
{
name: "Peer private networks doesn't matches the allowed range",
check: PrivateNetworkCheck{
Action: CheckActionAllow,
Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("10.0.0.0/8"),
},
},
peer: nbpeer.Peer{
Meta: nbpeer.PeerSystemMeta{
NetworkAddresses: []nbpeer.NetworkAddress{
{
NetIP: netip.MustParsePrefix("198.19.249.3/24"),
},
},
},
},
wantErr: false,
isValid: false,
},
{
name: "Peer with no privates network in the allow range",
check: PrivateNetworkCheck{
Action: CheckActionAllow,
Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/16"),
netip.MustParsePrefix("10.0.0.0/8"),
},
},
peer: nbpeer.Peer{},
wantErr: true,
isValid: false,
},
{
name: "Peer private networks matches the denied range",
check: PrivateNetworkCheck{
Action: CheckActionDeny,
Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("10.0.0.0/8"),
},
},
peer: nbpeer.Peer{
Meta: nbpeer.PeerSystemMeta{
NetworkAddresses: []nbpeer.NetworkAddress{
{
NetIP: netip.MustParsePrefix("192.168.0.123/24"),
},
{
NetIP: netip.MustParsePrefix("fe80::6089:eaff:fe0c:232f/64"),
},
},
},
},
wantErr: false,
isValid: false,
},
{
name: "Peer private networks doesn't matches the denied range",
check: PrivateNetworkCheck{
Action: CheckActionDeny,
Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("10.0.0.0/8"),
},
},
peer: nbpeer.Peer{
Meta: nbpeer.PeerSystemMeta{
NetworkAddresses: []nbpeer.NetworkAddress{
{
NetIP: netip.MustParsePrefix("198.19.249.3/24"),
},
},
},
},
wantErr: false,
isValid: true,
},
{
name: "Peer with no private networks in the denied range",
check: PrivateNetworkCheck{
Action: CheckActionDeny,
Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/16"),
netip.MustParsePrefix("10.0.0.0/8"),
},
},
peer: nbpeer.Peer{},
wantErr: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isValid, err := tt.check.Check(tt.peer)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.isValid, isValid)
})
}
}