Skip to content

Commit

Permalink
Analyze rule block to connectivity (#292)
Browse files Browse the repository at this point in the history
* adding example

* externalIPs

* working version

* ER

* endpoints of scope

* suppressTest

* reorg

* remove getRuleIPBlocks()

* bug fix

* fmt

* reorg ip blocks

* rename getAllRulesIPBlocks()

* fmt
  • Loading branch information
haim-kermany authored Mar 3, 2025
1 parent e2bd28b commit ec60301
Show file tree
Hide file tree
Showing 13 changed files with 179 additions and 213 deletions.
2 changes: 1 addition & 1 deletion pkg/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func computeConnectivity(c *configuration.Config, vmsFilter []string) connectivi
logging.Debugf("compute connectivity on parsed config")
res := connectivity.ConnMap{}
// make sure all vm pairs are in the result, by init with global default
res.InitPairs(false, c.Vms, vmsFilter)
res.InitPairs(false, c.Endpoints(), vmsFilter)
// iterate over all vm pairs in the initialized map at res, get the analysis result per pair
for src, srcMap := range res {
for dst := range srcMap {
Expand Down
9 changes: 6 additions & 3 deletions pkg/analyzer/tests_expected_output/ExampleExternal.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
Analyzed connectivity:
Source |Destination |Permitted connections
A |B |TCP dst-ports: 445
B |C |TCP dst-ports: 443
Source |Destination |Permitted connections
1.2.0.0/24 |A |TCP
1.2.1.0/24 |A |ICMP,TCP
1.2.2.0/24 |A |ICMP
1.2.3.0/24 |A |ICMP,UDP
1.2.4.0/24 |A |UDP

4 changes: 4 additions & 0 deletions pkg/configuration/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func ConfigFromResourcesContainer(resources *collector.ResourcesContainerModel,
// Config captures nsx Config, implements NSXConfig interface
type Config struct {
Vms []topology.Endpoint // list of all Vms
externalIPs []topology.Endpoint // list of all external ips
VmsMap map[string]topology.Endpoint // map from uid to vm objects
Fw *dfw.DFW // currently assuming one DFW only (todo: rename pkg dfw)
Groups []*collector.Group // list of all groups (also these with no Vms)
Expand All @@ -50,6 +51,9 @@ func (c *Config) DFW() *dfw.DFW {
func (c *Config) VMs() []topology.Endpoint {
return c.Vms
}
func (c *Config) Endpoints() []topology.Endpoint {
return append(c.Vms, c.externalIPs...)
}
func (c *Config) VMsMap() map[string]topology.Endpoint {
return c.VmsMap
}
Expand Down
46 changes: 16 additions & 30 deletions pkg/configuration/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ type nsxConfigParser struct {
groupPathsToObjects map[string]*collector.Group
servicePathsToObjects map[string]*collector.Service
topology *nsxTopology
allRuleIPBlocks map[string]*topology.RuleIPBlock // a map from the ip string,to the block
}

func (p *nsxConfigParser) init() {
Expand All @@ -62,17 +61,16 @@ func (p *nsxConfigParser) init() {
p.servicePathsToObjects = map[string]*collector.Service{}
p.groupToVMsListCache = map[*collector.Group][]topology.Endpoint{}
p.servicePathToConnCache = map[string]*netset.TransportSet{}
p.allRuleIPBlocks = map[string]*topology.RuleIPBlock{}
}

func (p *nsxConfigParser) runParser() error {
logging.Debugf("started parsing the given NSX config")
p.init()
p.getVMs() // get vms config
p.getVMs() // get vms config
p.getGroups() // get groups config
if err := p.getTopology(); err != nil {
return err
}
p.getGroups() // get groups config
p.removeVMsWithoutGroups()
p.getDFW() // get distributed firewall config
p.addPathsToDisplayNames()
Expand Down Expand Up @@ -173,7 +171,7 @@ func (p *nsxConfigParser) getDFW() {
// more fields to consider: sequence_number , stateful,tcp_strict, unique_id

// This scope will take precedence over rule level scope.
scope, _, _ := p.getEndpointsFromGroupsPaths(secPolicy.Scope, false)
scope, _ := p.getEndpointsFromScopePaths(secPolicy.Scope)
policyHasScope := !slices.Equal(secPolicy.Scope, []string{anyStr})

rules := secPolicy.Rules
Expand All @@ -183,7 +181,7 @@ func (p *nsxConfigParser) getDFW() {
r.scope = scope // scope from policy
if !policyHasScope {
// if policy scope is not configured, rule's scope takes effect
r.scope, r.scopeGroups, _ = p.getEndpointsFromGroupsPaths(rule.Scope, false)
r.scope, r.scopeGroups = p.getEndpointsFromScopePaths(rule.Scope)
}
r.secPolicyName = *secPolicy.DisplayName
p.addFWRule(r, category, rule)
Expand Down Expand Up @@ -222,7 +220,7 @@ func (p *nsxConfigParser) getDefaultRule(secPolicy *collector.SecurityPolicy) *p
res := &parsedRule{}
// scope - the list of group paths where the rules in this policy will get applied.
scope := secPolicy.Scope
vms, groups, _ := p.getEndpointsFromGroupsPaths(scope, false)
vms, groups := p.getEndpointsFromScopePaths(scope)
// rule applied as any-to-any only for ths VMs in the scope of the SecurityPolicy
res.srcVMs = vms
res.dstVMs = vms
Expand Down Expand Up @@ -292,6 +290,13 @@ func (p *nsxConfigParser) getAllGroups() {
p.allGroupsPaths = groupsPaths
}

func (p *nsxConfigParser) getEndpointsFromScopePaths(groupsPaths []string) ([]topology.Endpoint, []*collector.Group) {
if slices.Contains(groupsPaths, anyStr) {
return append(p.allGroupsVMs, p.configRes.externalIPs...), p.allGroups // all endpoints and groups
}
endPoints, groups, _ := p.getEndpointsFromGroupsPaths(groupsPaths, false)
return endPoints, groups
}
func (p *nsxConfigParser) getEndpointsFromGroupsPaths(
groupsPaths []string, exclude bool) (
[]topology.Endpoint, []*collector.Group, []*topology.RuleIPBlock) {
Expand All @@ -313,9 +318,11 @@ func (p *nsxConfigParser) getEndpointsFromGroupsPaths(
strings.Join(ips, common.CommaSeparator))
}
} else {
ruleBlocks = p.getRuleIPBlocks(ips)
for _, ruleBlock := range ruleBlocks {
for _, ip := range ips {
ruleBlock := p.topology.allRuleIPBlocks[ip]
vms = append(vms, ruleBlock.VMs...)
vms = append(vms, ruleBlock.ExternalIPs...)
ruleBlocks = append(ruleBlocks, ruleBlock)
}
}
groups := make([]*collector.Group, len(groupsPaths))
Expand Down Expand Up @@ -505,27 +512,6 @@ func (p *nsxConfigParser) getGroupVMs(groupPath string) ([]topology.Endpoint, *c
}
return nil, nil // could not find given groupPath (add warning)
}
func (p *nsxConfigParser) getRuleIPBlocks(groupsPaths []string) []*topology.RuleIPBlock {
ips := slices.DeleteFunc(slices.Clone(groupsPaths),
func(path string) bool { return path == anyStr || slices.Contains(p.allGroupsPaths, path) })
res := []*topology.RuleIPBlock{}
for _, ip := range ips {
if _, ok := p.allRuleIPBlocks[ip]; !ok {
block, err := netset.IPBlockFromCidrOrAddress(ip)
if err != nil {
block, err = netset.IPBlockFromIPRangeStr(ip)
}
if err != nil {
logging.Warnf("Fail to parse IP %s, ignoring ip", ip)
continue
}
p.allRuleIPBlocks[ip] = topology.NewRuleIPBlock(ip, block)
// todo - calc VMs of the block
}
res = append(res, p.allRuleIPBlocks[ip])
}
return res
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// comments for later
Expand Down
86 changes: 81 additions & 5 deletions pkg/configuration/topology.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
package configuration

import (
"maps"
"slices"

"github.com/np-guard/models/pkg/netset"
"github.com/np-guard/vmware-analyzer/internal/common"
nsx "github.com/np-guard/vmware-analyzer/pkg/configuration/generated"
"github.com/np-guard/vmware-analyzer/pkg/configuration/topology"
"github.com/np-guard/vmware-analyzer/pkg/logging"
)

type nsxTopology struct {
segments []*topology.Segment
vmSegments map[topology.Endpoint][]*topology.Segment
externalBlock *netset.IPBlock
segments []*topology.Segment
vmSegments map[topology.Endpoint][]*topology.Segment
allRuleIPBlocks map[string]*topology.RuleIPBlock // a map from the ip string,to the block
externalBlock *netset.IPBlock
}

func newTopology() *nsxTopology {
return &nsxTopology{vmSegments: map[topology.Endpoint][]*topology.Segment{}, externalBlock: netset.GetCidrAll()}
return &nsxTopology{
vmSegments: map[topology.Endpoint][]*topology.Segment{},
externalBlock: netset.GetCidrAll(),
allRuleIPBlocks: map[string]*topology.RuleIPBlock{},
}
}

func (p *nsxConfigParser) getTopology() (err error) {
p.topology = newTopology()
if err := p.getSegments(); err != nil {
return err
}
p.getAllRulesIPBlocks()
p.getExternalIPs()
// todo - calc VMs of the block
return nil
}

func (p *nsxConfigParser) getSegments() (err error) {
for i := range p.rc.SegmentList {
segResource := &p.rc.SegmentList[i]
if len(segResource.SegmentPorts) == 0 && len(segResource.Subnets) == 0 {
Expand All @@ -29,7 +48,7 @@ func (p *nsxConfigParser) getTopology() (err error) {
if err != nil {
return err
}
segment := topology.NewSegment(*segResource.DisplayName, block)
segment := topology.NewSegment(*segResource.DisplayName, block, subnetsNetworks)
for pi := range segResource.SegmentPorts {
att := *segResource.SegmentPorts[pi].Attachment.Id
vni := p.rc.GetVirtualNetworkInterfaceByPort(att)
Expand All @@ -43,3 +62,60 @@ func (p *nsxConfigParser) getTopology() (err error) {
}
return nil
}

func (p *nsxConfigParser) getAllRulesIPBlocks() {
allIPs := []string{}
// collect all the paths from the rules:
for i := range p.rc.DomainList {
domainRsc := p.rc.DomainList[i].Resources
for j := range domainRsc.SecurityPolicyList {
secPolicy := &domainRsc.SecurityPolicyList[j]
rules := secPolicy.Rules
for i := range rules {
rule := &rules[i]
allIPs = append(allIPs, rule.DestinationGroups...)
allIPs = append(allIPs, rule.SourceGroups...)
}
}
}
// remove duplications, "ANY" and paths to groups:
slices.Sort(allIPs)
allIPs = slices.Compact(allIPs)
allIPs = slices.DeleteFunc(allIPs, func(path string) bool { return path == anyStr || slices.Contains(p.allGroupsPaths, path) })
// create the blocks:
for _, ip := range allIPs {
block, err := netset.IPBlockFromCidrOrAddress(ip)
if err != nil {
block, err = netset.IPBlockFromIPRangeStr(ip)
}
if err != nil {
logging.Warnf("Fail to parse IP %s, ignoring ip", ip)
continue
}
p.topology.allRuleIPBlocks[ip] = topology.NewRuleIPBlock(ip, block)
}
}

// creating external endpoints
func (p *nsxConfigParser) getExternalIPs() {
// collect all the blocks:
exBlocks := make([]*netset.IPBlock, len(p.topology.allRuleIPBlocks))
for i, ruleBlock := range slices.Collect(maps.Values(p.topology.allRuleIPBlocks)) {
exBlocks[i] = ruleBlock.Block.Intersect(p.topology.externalBlock)
}
// creating disjoint blocks:
disjointBlocks := netset.DisjointIPBlocks(exBlocks, nil)
p.configRes.externalIPs = make([]topology.Endpoint, len(netset.DisjointIPBlocks(exBlocks, nil)))
// create external IP per disjoint block:
for i, disjointBlock := range disjointBlocks {
p.configRes.externalIPs[i] = topology.NewExternalIP(disjointBlock)
}
// keep the external ips of each block:
for _, ruleBlock := range p.topology.allRuleIPBlocks {
for _, externalIP := range p.configRes.externalIPs {
if externalIP.(*topology.ExternalIP).Block.IsSubset(ruleBlock.Block) {
ruleBlock.ExternalIPs = append(ruleBlock.ExternalIPs, externalIP)
}
}
}
}
23 changes: 23 additions & 0 deletions pkg/configuration/topology/external_ip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package topology

import (
"github.com/np-guard/models/pkg/netset"
)

type ExternalIP struct {
ipBlock
}

func NewExternalIP(block *netset.IPBlock) *ExternalIP {
e := &ExternalIP{ipBlock: ipBlock{Block: block, originalIP: block.String()}}
return e
}

func (ip *ExternalIP) Name() string { return ip.originalIP }
func (ip *ExternalIP) String() string { return ip.originalIP }
func (ip *ExternalIP) Kind() string { return "external IP" }
func (ip *ExternalIP) ID() string { return ip.originalIP }
func (ip *ExternalIP) InfoStr() []string {
return []string{ip.Name(), ip.ID(), ip.Name()}
}
func (ip *ExternalIP) Tags() []string { return nil }
16 changes: 10 additions & 6 deletions pkg/configuration/topology/ip_blocks.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
package topology

import (
"strings"

"github.com/np-guard/models/pkg/netset"
"github.com/np-guard/vmware-analyzer/internal/common"
)

// a base struct to represent external endpoints, segments and rule block
type ipBlock struct {
Block *netset.IPBlock
Block *netset.IPBlock
originalIP string
}
type RuleIPBlock struct {
ipBlock
origIP string
VMs []Endpoint
VMs []Endpoint
ExternalIPs []Endpoint
}

func NewRuleIPBlock(ip string, block *netset.IPBlock) *RuleIPBlock {
return &RuleIPBlock{origIP: ip, ipBlock: ipBlock{Block: block}}
return &RuleIPBlock{ipBlock: ipBlock{Block: block, originalIP: ip}}
}

type Segment struct {
Expand All @@ -24,6 +28,6 @@ type Segment struct {
VMs []Endpoint
}

func NewSegment(name string, block *netset.IPBlock) *Segment {
return &Segment{name: name, ipBlock: ipBlock{Block: block}}
func NewSegment(name string, block *netset.IPBlock, subnetsNetworks []string) *Segment {
return &Segment{name: name, ipBlock: ipBlock{Block: block, originalIP: strings.Join(subnetsNetworks, common.CommaSeparator)}}
}
44 changes: 23 additions & 21 deletions pkg/data/examples.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,37 +174,39 @@ var Example1d = Example{
}

var Example1External = Example{
Name: "Example1External",
VMs: Example1d.VMs,
GroupsByVMs: Example1d.GroupsByVMs,
Name: "Example1External",
VMs: []string{"A"},
GroupsByVMs: map[string][]string{
"frontend": {"A"},
},
Policies: []Category{
{
Name: "app-x",
CategoryType: "Application",
Rules: []Rule{
{
Name: "allow_http_from_123",
ID: 1004,
Source: "1.2.3.0/8",
Dest: "frontend",
Services: []string{"/infra/services/HTTP"},
Action: Allow,
Name: "allow_tcp_0_1",
ID: 1004,
Source: "1.2.0.0-1.2.1.255",
Dest: "frontend",
Conn: netset.AllTCPTransport(),
Action: Allow,
},
{
Name: "allow_smb_incoming",
ID: 1005,
Source: "frontend",
Dest: "backend",
Services: []string{"/infra/services/SMB"},
Action: Allow,
Name: "allow_udp_3_4",
ID: 1005,
Source: "1.2.3.0-1.2.4.255",
Dest: "frontend",
Conn: netset.AllUDPTransport(),
Action: Allow,
},
{
Name: "allow_https_db_incoming",
ID: 1006,
Source: "backend",
Dest: "db",
Services: []string{"/infra/services/HTTPS"},
Action: Allow,
Name: "allow_icmp_1_3",
ID: 1006,
Source: "1.2.1.0-1.2.3.255",
Dest: "frontend",
Conn: netset.AllICMPTransport(),
Action: Allow,
},
DefaultDenyRule(denyRuleIDApp),
},
Expand Down
Loading

0 comments on commit ec60301

Please sign in to comment.