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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 572 additions and 81 deletions

View File

@ -862,6 +862,8 @@ components:
$ref: '#/components/schemas/OSVersionCheck' $ref: '#/components/schemas/OSVersionCheck'
geo_location_check: geo_location_check:
$ref: '#/components/schemas/GeoLocationCheck' $ref: '#/components/schemas/GeoLocationCheck'
private_network_check:
$ref: '#/components/schemas/PrivateNetworkCheck'
NBVersionCheck: NBVersionCheck:
description: Posture check for the version of NetBird description: Posture check for the version of NetBird
type: object type: object
@ -932,6 +934,24 @@ components:
required: required:
- locations - locations
- action - action
PrivateNetworkCheck:
description: Posture check for allow or deny private network
type: object
properties:
ranges:
description: List of private network ranges in CIDR notation
type: array
items:
type: string
example: ["192.168.1.0/24", "10.0.0.0/8"]
action:
description: Action to take upon policy match
type: string
enum: [ "allow", "deny" ]
example: "allow"
required:
- ranges
- action
Location: Location:
description: Describe geographical location information description: Describe geographical location information
type: object type: object

View File

@ -116,6 +116,12 @@ const (
PolicyRuleUpdateProtocolUdp PolicyRuleUpdateProtocol = "udp" PolicyRuleUpdateProtocolUdp PolicyRuleUpdateProtocol = "udp"
) )
// Defines values for PrivateNetworkCheckAction.
const (
PrivateNetworkCheckActionAllow PrivateNetworkCheckAction = "allow"
PrivateNetworkCheckActionDeny PrivateNetworkCheckAction = "deny"
)
// Defines values for UserStatus. // Defines values for UserStatus.
const ( const (
UserStatusActive UserStatus = "active" UserStatusActive UserStatus = "active"
@ -186,10 +192,15 @@ type AccountSettings struct {
type Checks struct { type Checks struct {
// GeoLocationCheck Posture check for geo location // GeoLocationCheck Posture check for geo location
GeoLocationCheck *GeoLocationCheck `json:"geo_location_check,omitempty"` GeoLocationCheck *GeoLocationCheck `json:"geo_location_check,omitempty"`
NbVersionCheck *NBVersionCheck `json:"nb_version_check,omitempty"`
// NbVersionCheck Posture check for the version of operating system
NbVersionCheck *NBVersionCheck `json:"nb_version_check,omitempty"`
// OsVersionCheck Posture check for the version of operating system // OsVersionCheck Posture check for the version of operating system
OsVersionCheck *OSVersionCheck `json:"os_version_check,omitempty"` OsVersionCheck *OSVersionCheck `json:"os_version_check,omitempty"`
// PrivateNetworkCheck Posture check for allow or deny private network
PrivateNetworkCheck *PrivateNetworkCheck `json:"private_network_check,omitempty"`
} }
// City Describe city geographical location information // City Describe city geographical location information
@ -324,13 +335,13 @@ type MinKernelVersionCheck struct {
MinKernelVersion string `json:"min_kernel_version"` MinKernelVersion string `json:"min_kernel_version"`
} }
// MinVersionCheck defines model for MinVersionCheck. // MinVersionCheck Posture check for the version of operating system
type MinVersionCheck struct { type MinVersionCheck struct {
// MinVersion Minimum acceptable version // MinVersion Minimum acceptable version
MinVersion string `json:"min_version"` MinVersion string `json:"min_version"`
} }
// NBVersionCheck defines model for NBVersionCheck. // NBVersionCheck Posture check for the version of operating system
type NBVersionCheck = MinVersionCheck type NBVersionCheck = MinVersionCheck
// Nameserver defines model for Nameserver. // Nameserver defines model for Nameserver.
@ -407,9 +418,14 @@ type NameserverGroupRequest struct {
// OSVersionCheck Posture check for the version of operating system // OSVersionCheck Posture check for the version of operating system
type OSVersionCheck struct { type OSVersionCheck struct {
// Android Posture check for the version of operating system
Android *MinVersionCheck `json:"android,omitempty"` Android *MinVersionCheck `json:"android,omitempty"`
Darwin *MinVersionCheck `json:"darwin,omitempty"`
Ios *MinVersionCheck `json:"ios,omitempty"` // Darwin Posture check for the version of operating system
Darwin *MinVersionCheck `json:"darwin,omitempty"`
// Ios Posture check for the version of operating system
Ios *MinVersionCheck `json:"ios,omitempty"`
// Linux Posture check with the kernel version // Linux Posture check with the kernel version
Linux *MinKernelVersionCheck `json:"linux,omitempty"` Linux *MinKernelVersionCheck `json:"linux,omitempty"`
@ -427,22 +443,22 @@ type Peer struct {
ApprovalRequired *bool `json:"approval_required,omitempty"` ApprovalRequired *bool `json:"approval_required,omitempty"`
// CityName Commonly used English name of the city // CityName Commonly used English name of the city
CityName *CityName `json:"city_name,omitempty"` CityName CityName `json:"city_name"`
// Connected Peer to Management connection status // Connected Peer to Management connection status
Connected bool `json:"connected"` Connected bool `json:"connected"`
// ConnectionIp Peer's public connection IP address // ConnectionIp Peer's public connection IP address
ConnectionIp *string `json:"connection_ip,omitempty"` ConnectionIp string `json:"connection_ip"`
// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country // CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country
CountryCode *CountryCode `json:"country_code,omitempty"` CountryCode CountryCode `json:"country_code"`
// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
DnsLabel string `json:"dns_label"` DnsLabel string `json:"dns_label"`
// GeonameId Unique identifier from the GeoNames database for a specific geographical location. // GeonameId Unique identifier from the GeoNames database for a specific geographical location.
GeonameId *int `json:"geoname_id,omitempty"` GeonameId int `json:"geoname_id"`
// Groups Groups that the peer belongs to // Groups Groups that the peer belongs to
Groups []GroupMinimum `json:"groups"` Groups []GroupMinimum `json:"groups"`
@ -457,7 +473,7 @@ type Peer struct {
Ip string `json:"ip"` Ip string `json:"ip"`
// KernelVersion Peer's operating system kernel version // KernelVersion Peer's operating system kernel version
KernelVersion *string `json:"kernel_version,omitempty"` KernelVersion string `json:"kernel_version"`
// LastLogin Last time this peer performed log in (authentication). E.g., user authenticated. // LastLogin Last time this peer performed log in (authentication). E.g., user authenticated.
LastLogin time.Time `json:"last_login"` LastLogin time.Time `json:"last_login"`
@ -481,10 +497,10 @@ type Peer struct {
SshEnabled bool `json:"ssh_enabled"` SshEnabled bool `json:"ssh_enabled"`
// UiVersion Peer's desktop UI version // UiVersion Peer's desktop UI version
UiVersion *string `json:"ui_version,omitempty"` UiVersion string `json:"ui_version"`
// UserId User ID of the user that enrolled this peer // UserId User ID of the user that enrolled this peer
UserId *string `json:"user_id,omitempty"` UserId string `json:"user_id"`
// Version Peer's daemon or cli version // Version Peer's daemon or cli version
Version string `json:"version"` Version string `json:"version"`
@ -496,22 +512,22 @@ type PeerBase struct {
ApprovalRequired *bool `json:"approval_required,omitempty"` ApprovalRequired *bool `json:"approval_required,omitempty"`
// CityName Commonly used English name of the city // CityName Commonly used English name of the city
CityName *CityName `json:"city_name,omitempty"` CityName CityName `json:"city_name"`
// Connected Peer to Management connection status // Connected Peer to Management connection status
Connected bool `json:"connected"` Connected bool `json:"connected"`
// ConnectionIp Peer's public connection IP address // ConnectionIp Peer's public connection IP address
ConnectionIp *string `json:"connection_ip,omitempty"` ConnectionIp string `json:"connection_ip"`
// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country // CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country
CountryCode *CountryCode `json:"country_code,omitempty"` CountryCode CountryCode `json:"country_code"`
// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
DnsLabel string `json:"dns_label"` DnsLabel string `json:"dns_label"`
// GeonameId Unique identifier from the GeoNames database for a specific geographical location. // GeonameId Unique identifier from the GeoNames database for a specific geographical location.
GeonameId *int `json:"geoname_id,omitempty"` GeonameId int `json:"geoname_id"`
// Groups Groups that the peer belongs to // Groups Groups that the peer belongs to
Groups []GroupMinimum `json:"groups"` Groups []GroupMinimum `json:"groups"`
@ -526,7 +542,7 @@ type PeerBase struct {
Ip string `json:"ip"` Ip string `json:"ip"`
// KernelVersion Peer's operating system kernel version // KernelVersion Peer's operating system kernel version
KernelVersion *string `json:"kernel_version,omitempty"` KernelVersion string `json:"kernel_version"`
// LastLogin Last time this peer performed log in (authentication). E.g., user authenticated. // LastLogin Last time this peer performed log in (authentication). E.g., user authenticated.
LastLogin time.Time `json:"last_login"` LastLogin time.Time `json:"last_login"`
@ -550,10 +566,10 @@ type PeerBase struct {
SshEnabled bool `json:"ssh_enabled"` SshEnabled bool `json:"ssh_enabled"`
// UiVersion Peer's desktop UI version // UiVersion Peer's desktop UI version
UiVersion *string `json:"ui_version,omitempty"` UiVersion string `json:"ui_version"`
// UserId User ID of the user that enrolled this peer // UserId User ID of the user that enrolled this peer
UserId *string `json:"user_id,omitempty"` UserId string `json:"user_id"`
// Version Peer's daemon or cli version // Version Peer's daemon or cli version
Version string `json:"version"` Version string `json:"version"`
@ -568,22 +584,22 @@ type PeerBatch struct {
ApprovalRequired *bool `json:"approval_required,omitempty"` ApprovalRequired *bool `json:"approval_required,omitempty"`
// CityName Commonly used English name of the city // CityName Commonly used English name of the city
CityName *CityName `json:"city_name,omitempty"` CityName CityName `json:"city_name"`
// Connected Peer to Management connection status // Connected Peer to Management connection status
Connected bool `json:"connected"` Connected bool `json:"connected"`
// ConnectionIp Peer's public connection IP address // ConnectionIp Peer's public connection IP address
ConnectionIp *string `json:"connection_ip,omitempty"` ConnectionIp string `json:"connection_ip"`
// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country // CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country
CountryCode *CountryCode `json:"country_code,omitempty"` CountryCode CountryCode `json:"country_code"`
// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
DnsLabel string `json:"dns_label"` DnsLabel string `json:"dns_label"`
// GeonameId Unique identifier from the GeoNames database for a specific geographical location. // GeonameId Unique identifier from the GeoNames database for a specific geographical location.
GeonameId *int `json:"geoname_id,omitempty"` GeonameId int `json:"geoname_id"`
// Groups Groups that the peer belongs to // Groups Groups that the peer belongs to
Groups []GroupMinimum `json:"groups"` Groups []GroupMinimum `json:"groups"`
@ -598,7 +614,7 @@ type PeerBatch struct {
Ip string `json:"ip"` Ip string `json:"ip"`
// KernelVersion Peer's operating system kernel version // KernelVersion Peer's operating system kernel version
KernelVersion *string `json:"kernel_version,omitempty"` KernelVersion string `json:"kernel_version"`
// LastLogin Last time this peer performed log in (authentication). E.g., user authenticated. // LastLogin Last time this peer performed log in (authentication). E.g., user authenticated.
LastLogin time.Time `json:"last_login"` LastLogin time.Time `json:"last_login"`
@ -622,10 +638,10 @@ type PeerBatch struct {
SshEnabled bool `json:"ssh_enabled"` SshEnabled bool `json:"ssh_enabled"`
// UiVersion Peer's desktop UI version // UiVersion Peer's desktop UI version
UiVersion *string `json:"ui_version,omitempty"` UiVersion string `json:"ui_version"`
// UserId User ID of the user that enrolled this peer // UserId User ID of the user that enrolled this peer
UserId *string `json:"user_id,omitempty"` UserId string `json:"user_id"`
// Version Peer's daemon or cli version // Version Peer's daemon or cli version
Version string `json:"version"` Version string `json:"version"`
@ -882,6 +898,18 @@ type PostureCheckUpdate struct {
Name string `json:"name"` Name string `json:"name"`
} }
// PrivateNetworkCheck Posture check for allow or deny private network
type PrivateNetworkCheck struct {
// Action Action to take upon policy match
Action PrivateNetworkCheckAction `json:"action"`
// Ranges List of private network ranges in CIDR notation
Ranges []string `json:"ranges"`
}
// PrivateNetworkCheckAction Action to take upon policy match
type PrivateNetworkCheckAction string
// Route defines model for Route. // Route defines model for Route.
type Route struct { type Route struct {
// Description Route description // Description Route description

View File

@ -3,7 +3,6 @@ package http
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -231,44 +230,36 @@ func toGroupsInfo(groups map[string]*server.Group, peerID string) []api.GroupMin
return groupsInfo return groupsInfo
} }
func connectionIPoString(ip net.IP) *string {
publicIP := ""
if ip != nil {
publicIP = ip.String()
}
return &publicIP
}
func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsDomain string, accessiblePeer []api.AccessiblePeer) *api.Peer { func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsDomain string, accessiblePeer []api.AccessiblePeer) *api.Peer {
osVersion := peer.Meta.OSVersion osVersion := peer.Meta.OSVersion
if osVersion == "" { if osVersion == "" {
osVersion = peer.Meta.Core osVersion = peer.Meta.Core
} }
geonameID := int(peer.Location.GeoNameID)
return &api.Peer{ return &api.Peer{
Id: peer.ID, Id: peer.ID,
Name: peer.Name, Name: peer.Name,
Ip: peer.IP.String(), Ip: peer.IP.String(),
ConnectionIp: connectionIPoString(peer.Location.ConnectionIP), ConnectionIp: peer.Location.ConnectionIP.String(),
Connected: peer.Status.Connected, Connected: peer.Status.Connected,
LastSeen: peer.Status.LastSeen, LastSeen: peer.Status.LastSeen,
Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion), Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion),
KernelVersion: &peer.Meta.KernelVersion, KernelVersion: peer.Meta.KernelVersion,
GeonameId: &geonameID, GeonameId: int(peer.Location.GeoNameID),
Version: peer.Meta.WtVersion, Version: peer.Meta.WtVersion,
Groups: groupsInfo, Groups: groupsInfo,
SshEnabled: peer.SSHEnabled, SshEnabled: peer.SSHEnabled,
Hostname: peer.Meta.Hostname, Hostname: peer.Meta.Hostname,
UserId: &peer.UserID, UserId: peer.UserID,
UiVersion: &peer.Meta.UIVersion, UiVersion: peer.Meta.UIVersion,
DnsLabel: fqdn(peer, dnsDomain), DnsLabel: fqdn(peer, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled, LoginExpirationEnabled: peer.LoginExpirationEnabled,
LastLogin: peer.LastLogin, LastLogin: peer.LastLogin,
LoginExpired: peer.Status.LoginExpired, LoginExpired: peer.Status.LoginExpired,
AccessiblePeers: accessiblePeer, AccessiblePeers: accessiblePeer,
ApprovalRequired: &peer.Status.RequiresApproval, ApprovalRequired: &peer.Status.RequiresApproval,
CountryCode: &peer.Location.CountryCode, CountryCode: peer.Location.CountryCode,
CityName: &peer.Location.CityName, CityName: peer.Location.CityName,
} }
} }
@ -277,31 +268,31 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn
if osVersion == "" { if osVersion == "" {
osVersion = peer.Meta.Core osVersion = peer.Meta.Core
} }
geonameID := int(peer.Location.GeoNameID)
return &api.PeerBatch{ return &api.PeerBatch{
Id: peer.ID, Id: peer.ID,
Name: peer.Name, Name: peer.Name,
Ip: peer.IP.String(), Ip: peer.IP.String(),
ConnectionIp: connectionIPoString(peer.Location.ConnectionIP), ConnectionIp: peer.Location.ConnectionIP.String(),
Connected: peer.Status.Connected, Connected: peer.Status.Connected,
LastSeen: peer.Status.LastSeen, LastSeen: peer.Status.LastSeen,
Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion), Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion),
KernelVersion: &peer.Meta.KernelVersion, KernelVersion: peer.Meta.KernelVersion,
GeonameId: &geonameID, GeonameId: int(peer.Location.GeoNameID),
Version: peer.Meta.WtVersion, Version: peer.Meta.WtVersion,
Groups: groupsInfo, Groups: groupsInfo,
SshEnabled: peer.SSHEnabled, SshEnabled: peer.SSHEnabled,
Hostname: peer.Meta.Hostname, Hostname: peer.Meta.Hostname,
UserId: &peer.UserID, UserId: peer.UserID,
UiVersion: &peer.Meta.UIVersion, UiVersion: peer.Meta.UIVersion,
DnsLabel: fqdn(peer, dnsDomain), DnsLabel: fqdn(peer, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled, LoginExpirationEnabled: peer.LoginExpirationEnabled,
LastLogin: peer.LastLogin, LastLogin: peer.LastLogin,
LoginExpired: peer.Status.LoginExpired, LoginExpired: peer.Status.LoginExpired,
AccessiblePeersCount: accessiblePeersCount, AccessiblePeersCount: accessiblePeersCount,
ApprovalRequired: &peer.Status.RequiresApproval, ApprovalRequired: &peer.Status.RequiresApproval,
CountryCode: &peer.Location.CountryCode, CountryCode: peer.Location.CountryCode,
CityName: &peer.Location.CityName, CityName: peer.Location.CityName,
} }
} }

View File

@ -3,6 +3,7 @@ package http
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/netip"
"regexp" "regexp"
"slices" "slices"
@ -212,6 +213,14 @@ func (p *PostureChecksHandler) savePostureChecks(
postureChecks.Checks.GeoLocationCheck = toPostureGeoLocationCheck(geoLocationCheck) postureChecks.Checks.GeoLocationCheck = toPostureGeoLocationCheck(geoLocationCheck)
} }
if privateNetworkCheck := req.Checks.PrivateNetworkCheck; privateNetworkCheck != nil {
postureChecks.Checks.PrivateNetworkCheck, err = toPrivateNetworkCheck(privateNetworkCheck)
if err != nil {
util.WriteError(status.Errorf(status.InvalidArgument, "invalid network prefix"), w)
return
}
}
if err := p.accountManager.SavePostureChecks(account.Id, user.Id, &postureChecks); err != nil { if err := p.accountManager.SavePostureChecks(account.Id, user.Id, &postureChecks); err != nil {
util.WriteError(err, w) util.WriteError(err, w)
return return
@ -226,7 +235,7 @@ func validatePostureChecksUpdate(req api.PostureCheckUpdate) error {
} }
if req.Checks == nil || (req.Checks.NbVersionCheck == nil && req.Checks.OsVersionCheck == nil && if req.Checks == nil || (req.Checks.NbVersionCheck == nil && req.Checks.OsVersionCheck == nil &&
req.Checks.GeoLocationCheck == nil) { req.Checks.GeoLocationCheck == nil && req.Checks.PrivateNetworkCheck == nil) {
return status.Errorf(status.InvalidArgument, "posture checks shouldn't be empty") return status.Errorf(status.InvalidArgument, "posture checks shouldn't be empty")
} }
@ -267,7 +276,20 @@ func validatePostureChecksUpdate(req api.PostureCheckUpdate) error {
return status.Errorf(status.InvalidArgument, "country code must be 2 letters (ISO 3166-1 alpha-2 format)") return status.Errorf(status.InvalidArgument, "country code must be 2 letters (ISO 3166-1 alpha-2 format)")
} }
} }
}
if privateNetworkCheck := req.Checks.PrivateNetworkCheck; privateNetworkCheck != nil {
if privateNetworkCheck.Action == "" {
return status.Errorf(status.InvalidArgument, "action for private network check shouldn't be empty")
}
allowedActions := []api.PrivateNetworkCheckAction{api.PrivateNetworkCheckActionAllow, api.PrivateNetworkCheckActionDeny}
if !slices.Contains(allowedActions, privateNetworkCheck.Action) {
return status.Errorf(status.InvalidArgument, "action for private network check is not valid value")
}
if len(privateNetworkCheck.Ranges) == 0 {
return status.Errorf(status.InvalidArgument, "network ranges for private network check shouldn't be empty")
}
} }
return nil return nil
@ -296,6 +318,10 @@ func toPostureChecksResponse(postureChecks *posture.Checks) *api.PostureCheck {
checks.GeoLocationCheck = toGeoLocationCheckResponse(postureChecks.Checks.GeoLocationCheck) checks.GeoLocationCheck = toGeoLocationCheckResponse(postureChecks.Checks.GeoLocationCheck)
} }
if postureChecks.Checks.PrivateNetworkCheck != nil {
checks.PrivateNetworkCheck = toPrivateNetworkCheckResponse(postureChecks.Checks.PrivateNetworkCheck)
}
return &api.PostureCheck{ return &api.PostureCheck{
Id: postureChecks.ID, Id: postureChecks.ID,
Name: postureChecks.Name, Name: postureChecks.Name,
@ -342,3 +368,31 @@ func toPostureGeoLocationCheck(apiGeoLocationCheck *api.GeoLocationCheck) *postu
Locations: locations, Locations: locations,
} }
} }
func toPrivateNetworkCheckResponse(check *posture.PrivateNetworkCheck) *api.PrivateNetworkCheck {
netPrefixes := make([]string, 0, len(check.Ranges))
for _, netPrefix := range check.Ranges {
netPrefixes = append(netPrefixes, netPrefix.String())
}
return &api.PrivateNetworkCheck{
Ranges: netPrefixes,
Action: api.PrivateNetworkCheckAction(check.Action),
}
}
func toPrivateNetworkCheck(check *api.PrivateNetworkCheck) (*posture.PrivateNetworkCheck, error) {
prefixes := make([]netip.Prefix, 0)
for _, prefix := range check.Ranges {
parsedPrefix, err := netip.ParsePrefix(prefix)
if err != nil {
return nil, err
}
prefixes = append(prefixes, parsedPrefix)
}
return &posture.PrivateNetworkCheck{
Ranges: prefixes,
Action: string(check.Action),
}, nil
}

View File

@ -6,6 +6,7 @@ import (
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip"
"strings" "strings"
"testing" "testing"
@ -122,7 +123,19 @@ func TestGetPostureCheck(t *testing.T) {
CityName: "Berlin", CityName: "Berlin",
}, },
}, },
Action: posture.GeoLocationActionAllow, Action: posture.CheckActionAllow,
},
},
}
privateNetworkCheck := &posture.Checks{
ID: "privateNetworkPostureCheck",
Name: "privateNetwork",
Checks: posture.ChecksDefinition{
PrivateNetworkCheck: &posture.PrivateNetworkCheck{
Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"),
},
Action: posture.CheckActionAllow,
}, },
}, },
} }
@ -156,6 +169,13 @@ func TestGetPostureCheck(t *testing.T) {
checkName: geoPostureCheck.Name, checkName: geoPostureCheck.Name,
expectedStatus: http.StatusOK, expectedStatus: http.StatusOK,
}, },
{
name: "GetPostureCheck PrivateNetwork OK",
expectedBody: true,
id: privateNetworkCheck.ID,
checkName: privateNetworkCheck.Name,
expectedStatus: http.StatusOK,
},
{ {
name: "GetPostureCheck Not Found", name: "GetPostureCheck Not Found",
id: "not-exists", id: "not-exists",
@ -163,7 +183,7 @@ func TestGetPostureCheck(t *testing.T) {
}, },
} }
p := initPostureChecksTestData(postureCheck, osPostureCheck, geoPostureCheck) p := initPostureChecksTestData(postureCheck, osPostureCheck, geoPostureCheck, privateNetworkCheck)
for _, tc := range tt { for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@ -354,6 +374,39 @@ func TestPostureCheckUpdate(t *testing.T) {
}, },
}, },
}, },
{
name: "Create Posture Checks Private Network",
requestType: http.MethodPost,
requestPath: "/api/posture-checks",
requestBody: bytes.NewBuffer(
[]byte(`{
"name": "default",
"description": "default",
"checks": {
"private_network_check": {
"action": "allow",
"ranges": [
"10.0.0.0/8"
]
}
}
}`)),
expectedStatus: http.StatusOK,
expectedBody: true,
expectedPostureCheck: &api.PostureCheck{
Id: "postureCheck",
Name: "default",
Description: str("default"),
Checks: api.Checks{
PrivateNetworkCheck: &api.PrivateNetworkCheck{
Ranges: []string{
"10.0.0.0/8",
},
Action: api.PrivateNetworkCheckActionAllow,
},
},
},
},
{ {
name: "Create Posture Checks Geo Location with No geolocation DB", name: "Create Posture Checks Geo Location with No geolocation DB",
requestType: http.MethodPost, requestType: http.MethodPost,
@ -661,6 +714,38 @@ func TestPostureCheckUpdate(t *testing.T) {
expectedStatus: http.StatusBadRequest, expectedStatus: http.StatusBadRequest,
expectedBody: false, expectedBody: false,
}, },
{
name: "Update Posture Checks Private Network",
requestType: http.MethodPut,
requestPath: "/api/posture-checks/privateNetworkPostureCheck",
requestBody: bytes.NewBuffer(
[]byte(`{
"name": "default",
"checks": {
"private_network_check": {
"action": "deny",
"ranges": [
"192.168.1.0/24"
]
}
}
}`)),
expectedStatus: http.StatusOK,
expectedBody: true,
expectedPostureCheck: &api.PostureCheck{
Id: "postureCheck",
Name: "default",
Description: str(""),
Checks: api.Checks{
PrivateNetworkCheck: &api.PrivateNetworkCheck{
Ranges: []string{
"192.168.1.0/24",
},
Action: api.PrivateNetworkCheckActionDeny,
},
},
},
},
} }
p := initPostureChecksTestData(&posture.Checks{ p := initPostureChecksTestData(&posture.Checks{
@ -694,7 +779,19 @@ func TestPostureCheckUpdate(t *testing.T) {
CityName: "Berlin", CityName: "Berlin",
}, },
}, },
Action: posture.GeoLocationActionDeny, Action: posture.CheckActionDeny,
},
},
},
&posture.Checks{
ID: "privateNetworkPostureCheck",
Name: "privateNetwork",
Checks: posture.ChecksDefinition{
PrivateNetworkCheck: &posture.PrivateNetworkCheck{
Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"),
},
Action: posture.CheckActionAllow,
}, },
}, },
}, },
@ -793,4 +890,30 @@ func TestPostureCheck_validatePostureChecksUpdate(t *testing.T) {
} }
err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}}) err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}})
assert.NoError(t, err) assert.NoError(t, err)
// valid private network check
privateNetworkCheck := api.PrivateNetworkCheck{
Action: api.PrivateNetworkCheckActionAllow,
Ranges: []string{
"192.168.1.0/24", "10.0.0.0/8",
},
}
err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{PrivateNetworkCheck: &privateNetworkCheck}})
assert.NoError(t, err)
// invalid private network check
privateNetworkCheck = api.PrivateNetworkCheck{
Action: api.PrivateNetworkCheckActionDeny,
Ranges: []string{},
}
err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{PrivateNetworkCheck: &privateNetworkCheck}})
assert.Error(t, err)
// invalid private network check
privateNetworkCheck = api.PrivateNetworkCheck{
Action: "unknownAction",
Ranges: []string{},
}
err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{PrivateNetworkCheck: &privateNetworkCheck}})
assert.Error(t, err)
} }

View File

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

View File

@ -2,6 +2,7 @@ package posture
import ( import (
"encoding/json" "encoding/json"
"net/netip"
"testing" "testing"
"github.com/stretchr/testify/assert" "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" nbpeer "github.com/netbirdio/netbird/management/server/peer"
) )
const (
GeoLocationActionAllow string = "allow"
GeoLocationActionDeny string = "deny"
)
type Location struct { type Location struct {
// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country // CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country
CountryCode string CountryCode string
@ -39,9 +34,9 @@ func (g *GeoLocationCheck) Check(peer nbpeer.Peer) (bool, error) {
if loc.CountryCode == peer.Location.CountryCode { if loc.CountryCode == peer.Location.CountryCode {
if loc.CityName == "" || loc.CityName == peer.Location.CityName { if loc.CityName == "" || loc.CityName == peer.Location.CityName {
switch g.Action { switch g.Action {
case GeoLocationActionDeny: case CheckActionDeny:
return false, nil return false, nil
case GeoLocationActionAllow: case CheckActionAllow:
return true, nil return true, nil
default: default:
return false, fmt.Errorf("invalid geo location action: %s", g.Action) 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 // At this point, no location in the list matches the peer's location
// For action deny and no location match, allow the peer // For action deny and no location match, allow the peer
if g.Action == GeoLocationActionDeny { if g.Action == CheckActionDeny {
return true, nil return true, nil
} }
// For action allow and no location match, deny the peer // For action allow and no location match, deny the peer
if g.Action == GeoLocationActionAllow { if g.Action == CheckActionAllow {
return false, nil return false, nil
} }

View File

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