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
+20
View File
@@ -862,6 +862,8 @@ components:
$ref: '#/components/schemas/OSVersionCheck'
geo_location_check:
$ref: '#/components/schemas/GeoLocationCheck'
private_network_check:
$ref: '#/components/schemas/PrivateNetworkCheck'
NBVersionCheck:
description: Posture check for the version of NetBird
type: object
@@ -932,6 +934,24 @@ components:
required:
- locations
- 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:
description: Describe geographical location information
type: object
+54 -26
View File
@@ -116,6 +116,12 @@ const (
PolicyRuleUpdateProtocolUdp PolicyRuleUpdateProtocol = "udp"
)
// Defines values for PrivateNetworkCheckAction.
const (
PrivateNetworkCheckActionAllow PrivateNetworkCheckAction = "allow"
PrivateNetworkCheckActionDeny PrivateNetworkCheckAction = "deny"
)
// Defines values for UserStatus.
const (
UserStatusActive UserStatus = "active"
@@ -186,10 +192,15 @@ type AccountSettings struct {
type Checks struct {
// GeoLocationCheck Posture check for geo location
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 *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
@@ -324,13 +335,13 @@ type MinKernelVersionCheck struct {
MinKernelVersion string `json:"min_kernel_version"`
}
// MinVersionCheck defines model for MinVersionCheck.
// MinVersionCheck Posture check for the version of operating system
type MinVersionCheck struct {
// MinVersion Minimum acceptable version
MinVersion string `json:"min_version"`
}
// NBVersionCheck defines model for NBVersionCheck.
// NBVersionCheck Posture check for the version of operating system
type NBVersionCheck = MinVersionCheck
// Nameserver defines model for Nameserver.
@@ -407,9 +418,14 @@ type NameserverGroupRequest struct {
// OSVersionCheck Posture check for the version of operating system
type OSVersionCheck struct {
// Android Posture check for the version of operating system
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 *MinKernelVersionCheck `json:"linux,omitempty"`
@@ -427,22 +443,22 @@ type Peer struct {
ApprovalRequired *bool `json:"approval_required,omitempty"`
// 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 bool `json:"connected"`
// 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 *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 string `json:"dns_label"`
// 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 []GroupMinimum `json:"groups"`
@@ -457,7 +473,7 @@ type Peer struct {
Ip string `json:"ip"`
// 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 time.Time `json:"last_login"`
@@ -481,10 +497,10 @@ type Peer struct {
SshEnabled bool `json:"ssh_enabled"`
// 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 *string `json:"user_id,omitempty"`
UserId string `json:"user_id"`
// Version Peer's daemon or cli version
Version string `json:"version"`
@@ -496,22 +512,22 @@ type PeerBase struct {
ApprovalRequired *bool `json:"approval_required,omitempty"`
// 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 bool `json:"connected"`
// 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 *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 string `json:"dns_label"`
// 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 []GroupMinimum `json:"groups"`
@@ -526,7 +542,7 @@ type PeerBase struct {
Ip string `json:"ip"`
// 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 time.Time `json:"last_login"`
@@ -550,10 +566,10 @@ type PeerBase struct {
SshEnabled bool `json:"ssh_enabled"`
// 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 *string `json:"user_id,omitempty"`
UserId string `json:"user_id"`
// Version Peer's daemon or cli version
Version string `json:"version"`
@@ -568,22 +584,22 @@ type PeerBatch struct {
ApprovalRequired *bool `json:"approval_required,omitempty"`
// 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 bool `json:"connected"`
// 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 *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 string `json:"dns_label"`
// 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 []GroupMinimum `json:"groups"`
@@ -598,7 +614,7 @@ type PeerBatch struct {
Ip string `json:"ip"`
// 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 time.Time `json:"last_login"`
@@ -622,10 +638,10 @@ type PeerBatch struct {
SshEnabled bool `json:"ssh_enabled"`
// 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 *string `json:"user_id,omitempty"`
UserId string `json:"user_id"`
// Version Peer's daemon or cli version
Version string `json:"version"`
@@ -882,6 +898,18 @@ type PostureCheckUpdate struct {
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.
type Route struct {
// Description Route description
+16 -25
View File
@@ -3,7 +3,6 @@ package http
import (
"encoding/json"
"fmt"
"net"
"net/http"
"github.com/gorilla/mux"
@@ -231,44 +230,36 @@ func toGroupsInfo(groups map[string]*server.Group, peerID string) []api.GroupMin
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 {
osVersion := peer.Meta.OSVersion
if osVersion == "" {
osVersion = peer.Meta.Core
}
geonameID := int(peer.Location.GeoNameID)
return &api.Peer{
Id: peer.ID,
Name: peer.Name,
Ip: peer.IP.String(),
ConnectionIp: connectionIPoString(peer.Location.ConnectionIP),
ConnectionIp: peer.Location.ConnectionIP.String(),
Connected: peer.Status.Connected,
LastSeen: peer.Status.LastSeen,
Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion),
KernelVersion: &peer.Meta.KernelVersion,
GeonameId: &geonameID,
KernelVersion: peer.Meta.KernelVersion,
GeonameId: int(peer.Location.GeoNameID),
Version: peer.Meta.WtVersion,
Groups: groupsInfo,
SshEnabled: peer.SSHEnabled,
Hostname: peer.Meta.Hostname,
UserId: &peer.UserID,
UiVersion: &peer.Meta.UIVersion,
UserId: peer.UserID,
UiVersion: peer.Meta.UIVersion,
DnsLabel: fqdn(peer, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled,
LastLogin: peer.LastLogin,
LoginExpired: peer.Status.LoginExpired,
AccessiblePeers: accessiblePeer,
ApprovalRequired: &peer.Status.RequiresApproval,
CountryCode: &peer.Location.CountryCode,
CityName: &peer.Location.CityName,
CountryCode: peer.Location.CountryCode,
CityName: peer.Location.CityName,
}
}
@@ -277,31 +268,31 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn
if osVersion == "" {
osVersion = peer.Meta.Core
}
geonameID := int(peer.Location.GeoNameID)
return &api.PeerBatch{
Id: peer.ID,
Name: peer.Name,
Ip: peer.IP.String(),
ConnectionIp: connectionIPoString(peer.Location.ConnectionIP),
ConnectionIp: peer.Location.ConnectionIP.String(),
Connected: peer.Status.Connected,
LastSeen: peer.Status.LastSeen,
Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion),
KernelVersion: &peer.Meta.KernelVersion,
GeonameId: &geonameID,
KernelVersion: peer.Meta.KernelVersion,
GeonameId: int(peer.Location.GeoNameID),
Version: peer.Meta.WtVersion,
Groups: groupsInfo,
SshEnabled: peer.SSHEnabled,
Hostname: peer.Meta.Hostname,
UserId: &peer.UserID,
UiVersion: &peer.Meta.UIVersion,
UserId: peer.UserID,
UiVersion: peer.Meta.UIVersion,
DnsLabel: fqdn(peer, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled,
LastLogin: peer.LastLogin,
LoginExpired: peer.Status.LoginExpired,
AccessiblePeersCount: accessiblePeersCount,
ApprovalRequired: &peer.Status.RequiresApproval,
CountryCode: &peer.Location.CountryCode,
CityName: &peer.Location.CityName,
CountryCode: peer.Location.CountryCode,
CityName: peer.Location.CityName,
}
}
@@ -3,6 +3,7 @@ package http
import (
"encoding/json"
"net/http"
"net/netip"
"regexp"
"slices"
@@ -212,6 +213,14 @@ func (p *PostureChecksHandler) savePostureChecks(
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 {
util.WriteError(err, w)
return
@@ -226,7 +235,7 @@ func validatePostureChecksUpdate(req api.PostureCheckUpdate) error {
}
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")
}
@@ -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)")
}
}
}
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
@@ -296,6 +318,10 @@ func toPostureChecksResponse(postureChecks *posture.Checks) *api.PostureCheck {
checks.GeoLocationCheck = toGeoLocationCheckResponse(postureChecks.Checks.GeoLocationCheck)
}
if postureChecks.Checks.PrivateNetworkCheck != nil {
checks.PrivateNetworkCheck = toPrivateNetworkCheckResponse(postureChecks.Checks.PrivateNetworkCheck)
}
return &api.PostureCheck{
Id: postureChecks.ID,
Name: postureChecks.Name,
@@ -342,3 +368,31 @@ func toPostureGeoLocationCheck(apiGeoLocationCheck *api.GeoLocationCheck) *postu
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
}
@@ -6,6 +6,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"net/netip"
"strings"
"testing"
@@ -122,7 +123,19 @@ func TestGetPostureCheck(t *testing.T) {
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,
expectedStatus: http.StatusOK,
},
{
name: "GetPostureCheck PrivateNetwork OK",
expectedBody: true,
id: privateNetworkCheck.ID,
checkName: privateNetworkCheck.Name,
expectedStatus: http.StatusOK,
},
{
name: "GetPostureCheck Not Found",
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 {
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",
requestType: http.MethodPost,
@@ -661,6 +714,38 @@ func TestPostureCheckUpdate(t *testing.T) {
expectedStatus: http.StatusBadRequest,
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{
@@ -694,7 +779,19 @@ func TestPostureCheckUpdate(t *testing.T) {
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}})
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)
}