Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Analyze rule block to connectivity #292

Merged
merged 14 commits into from
Mar 3, 2025
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