Skip to content

Commit

Permalink
connlist implementing exposure analysis (#296)
Browse files Browse the repository at this point in the history
* connlist implementing exposure analysis

* Update pkg/netpol/connlist/connlist.go

Co-authored-by: Adi Sosnovich <[email protected]>

* fix rep. pod name

* Update pkg/netpol/eval/exposure.go

Co-authored-by: Adi Sosnovich <[email protected]>

* Update pkg/netpol/connlist/exposed_peer.go

Co-authored-by: Adi Sosnovich <[email protected]>

* Update pkg/netpol/connlist/exposed_peer.go

Co-authored-by: Adi Sosnovich <[email protected]>

* Update pkg/netpol/connlist/exposure_analysis.go

Co-authored-by: Adi Sosnovich <[email protected]>

* Update pkg/netpol/connlist/exposure_analysis.go

Co-authored-by: Adi Sosnovich <[email protected]>

* add func that updates the protected flag of a pod

* return error values

* avoid fields dups among types

* update func doc

* getConnectionsBetweenPeers update doc + returns the exposureMap

* move connection interface, avoid code dup, and compare conns using ConnectionSet

* make the func an exposureMap func

* fixing issue of same string in podsOwnerMap

* Update pkg/netpol/connlist/exposure_analysis.go

Co-authored-by: Adi Sosnovich <[email protected]>

* renaming Connection interface + move PortRange

* struct embedding

* using connectionSet internally + move the refinement to one iter at the end

* Update pkg/netpol/connection/connection.go

Co-authored-by: Adi Sosnovich <[email protected]>

* Update pkg/netpol/connlist/exposure_analysis.go

Co-authored-by: Adi Sosnovich <[email protected]>

* rename AllConnections

* verify conversion

* storing the maximum entire cluster connection

---------

Co-authored-by: Adi Sosnovich <[email protected]>
  • Loading branch information
shireenf-ibm and adisos authored Jan 18, 2024
1 parent 6e94e6c commit 15b25ea
Show file tree
Hide file tree
Showing 18 changed files with 408 additions and 126 deletions.
3 changes: 2 additions & 1 deletion pkg/internal/netpolerrors/netpol_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ const (
NoAllowedConnsWarning = "Connectivity analysis found no allowed connectivity between pairs from the configured workloads or" +
" external IP-blocks"

ErrGettingResInfoFromDir = "Error getting resourceInfos from dir path"
ErrGettingResInfoFromDir = "Error getting resourceInfos from dir path"
ConversionToConnectionSetErr = "failed conversion from AllowedSet to ConnectionSet"

// eval errors
NoSourceDefinedErr = "no source defined, source pod and namespace or external IP required"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package common
package connection

import (
v1 "k8s.io/api/core/v1"
)

// Connection represents a set of allowed connections between two peers
type Connection interface {
// AllowedSet represents a set of allowed connections between two peers
type AllowedSet interface {
// ProtocolsAndPortsMap returns the set of allowed connections
ProtocolsAndPortsMap() map[v1.Protocol][]PortRange
// AllConnections returns true if all ports are allowed for all protocols
AllConnections() bool
// IsAllConnections returns true if all ports are allowed for all protocols
IsAllConnections() bool
// IsEmpty returns true if no connection is allowed
IsEmpty() bool
}
Expand Down
169 changes: 135 additions & 34 deletions pkg/netpol/connlist/connlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/np-guard/netpol-analyzer/pkg/logger"
"github.com/np-guard/netpol-analyzer/pkg/manifests/fsscanner"
"github.com/np-guard/netpol-analyzer/pkg/manifests/parser"
conn "github.com/np-guard/netpol-analyzer/pkg/netpol/connection"
"github.com/np-guard/netpol-analyzer/pkg/netpol/connlist/internal/ingressanalyzer"
"github.com/np-guard/netpol-analyzer/pkg/netpol/eval"
"github.com/np-guard/netpol-analyzer/pkg/netpol/internal/common"
Expand All @@ -46,7 +47,9 @@ type ConnlistAnalyzer struct {

// The new interface
// ConnlistFromResourceInfos returns the allowed-connections list from input slice of resource.Info objects,
// and the list of all workloads from the parsed resources
// the list of all workloads from the parsed resources,
// and list of exposed peers in the parsed resources and their potentially allowed connections
// if exposure-analysis option is on, otherwise nil
func (ca *ConnlistAnalyzer) ConnlistFromResourceInfos(info []*resource.Info) ([]Peer2PeerConnection, []Peer, []ExposedPeer, error) {
// convert resource.Info objects to k8s resources, filter irrelevant resources
objs, fpErrs := parser.ResourceInfoListToK8sObjectsList(info, ca.logger, ca.muteErrsAndWarns)
Expand All @@ -55,7 +58,7 @@ func (ca *ConnlistAnalyzer) ConnlistFromResourceInfos(info []*resource.Info) ([]
if err := ca.hasFatalError(); err != nil {
return nil, nil, nil, err
}
return []Peer2PeerConnection{}, []Peer{}, []ExposedPeer{}, nil
return []Peer2PeerConnection{}, []Peer{}, ca.emptyExposedListOrNil(), nil
}
return ca.connslistFromParsedResources(objs)
}
Expand All @@ -66,8 +69,10 @@ func (ca *ConnlistAnalyzer) copyFpErrs(fpErrs []parser.FileProcessingError) {
}
}

// ConnlistFromDirPath returns the allowed connections list from dir path containing k8s resources
// and list of all workloads from the parsed resources
// ConnlistFromDirPath returns the allowed connections list from dir path containing k8s resources,
// list of all workloads from the parsed resources,
// and list of exposed peers in the parsed resources and their potentially allowed connections
// if exposure-analysis option is on, otherwise nil
func (ca *ConnlistAnalyzer) ConnlistFromDirPath(dirPath string) ([]Peer2PeerConnection, []Peer, []ExposedPeer, error) {
rList, errs := fsscanner.GetResourceInfosFromDirPath([]string{dirPath}, true, ca.stopOnError)
// instead of parsing the builder's string error to decide on error type (warning/error/fatal-err)
Expand Down Expand Up @@ -120,13 +125,22 @@ func WithFocusWorkload(workload string) ConnlistAnalyzerOption {
}
}

// WithExposureAnalysis is a functional option to include exposure analysis
// WithExposureAnalysis is a functional option which directs ConnlistAnalyzer to perform exposure analysis
func WithExposureAnalysis() ConnlistAnalyzerOption {
return func(c *ConnlistAnalyzer) {
c.exposureAnalysis = true
}
}

// emptyExposedListOrNil returns an empty ExposedPeer list if the exposure-analysis option is true for
// the connlist analyzer, otherwise returns nil
func (ca *ConnlistAnalyzer) emptyExposedListOrNil() []ExposedPeer {
if ca.exposureAnalysis {
return []ExposedPeer{}
}
return nil
}

// WithOutputFormat is a functional option, allowing user to choose the output format txt/json/dot/csv/md.
func WithOutputFormat(outputFormat string) ConnlistAnalyzerOption {
return func(p *ConnlistAnalyzer) {
Expand Down Expand Up @@ -197,7 +211,9 @@ func (ca *ConnlistAnalyzer) connslistFromParsedResources(objectsList []parser.K8
return ca.getConnectionsList(pe, ia)
}

// ConnlistFromK8sCluster returns the allowed connections list from k8s cluster resources and a list of all peers names
// ConnlistFromK8sCluster returns the allowed connections list from k8s cluster resources, a list of all peers names,
// and list of exposed peers in the parsed resources and their potentially allowed connections
// if exposure-analysis option is on, otherwise nil
func (ca *ConnlistAnalyzer) ConnlistFromK8sCluster(clientset *kubernetes.Clientset) ([]Peer2PeerConnection, []Peer, []ExposedPeer, error) {
pe := eval.NewPolicyEngine(ca.exposureAnalysis)

Expand Down Expand Up @@ -302,7 +318,7 @@ type connection struct {
src Peer
dst Peer
allConnections bool
protocolsAndPorts map[v1.Protocol][]common.PortRange
protocolsAndPorts map[v1.Protocol][]conn.PortRange
}

func (c *connection) Src() Peer {
Expand All @@ -314,7 +330,7 @@ func (c *connection) Dst() Peer {
func (c *connection) AllProtocolsAndPorts() bool {
return c.allConnections
}
func (c *connection) ProtocolsAndPorts() map[v1.Protocol][]common.PortRange {
func (c *connection) ProtocolsAndPorts() map[v1.Protocol][]conn.PortRange {
return c.protocolsAndPorts
}

Expand Down Expand Up @@ -353,21 +369,21 @@ func (ca *ConnlistAnalyzer) includePairOfWorkloads(src, dst eval.Peer) bool {
return ca.isPeerFocusWorkload(src) || ca.isPeerFocusWorkload(dst)
}

// TBD: should reveal the Fake pod flag in eval.Peer ?
// TODO : enhance this after implementing representative peers
func (ca *ConnlistAnalyzer) hasFakePodsAndIPs(src, dst eval.Peer) bool {
if src.IsPeerIPType() && dst.Name() == common.PodInExposedNs {
if src.IsPeerIPType() && dst.Name() == common.PodInRepNs {
return true
}
if src.Name() == common.PodInExposedNs && dst.IsPeerIPType() {
if src.Name() == common.PodInRepNs && dst.IsPeerIPType() {
return true
}
if src.Name() == common.PodInExposedNs && dst.Name() == common.PodInExposedNs {
if src.Name() == common.PodInRepNs && dst.Name() == common.PodInRepNs {
return true
}
if src.Name() == common.PodInExposedNs && dst.Name() == common.IngressPodName {
if src.Name() == common.PodInRepNs && dst.Name() == common.IngressPodName {
return true
}
if src.Name() == common.IngressPodName && dst.Name() == common.PodInExposedNs {
if src.Name() == common.IngressPodName && dst.Name() == common.PodInRepNs {
return true
}
return false
Expand All @@ -382,11 +398,12 @@ func (ca *ConnlistAnalyzer) isPeerFocusWorkload(peer eval.Peer) bool {
}

// getConnectionsList returns connections list from PolicyEngine and ingressAnalyzer objects
// if the exposure-analysis option is on, also computes and updates the exposure-analysis results
func (ca *ConnlistAnalyzer) getConnectionsList(pe *eval.PolicyEngine, ia *ingressanalyzer.IngressAnalyzer) ([]Peer2PeerConnection,
[]Peer, []ExposedPeer, error) {
connsRes := make([]Peer2PeerConnection, 0)
if !pe.HasPodPeers() {
return connsRes, []Peer{}, []ExposedPeer{}, nil
return connsRes, []Peer{}, ca.emptyExposedListOrNil(), nil
}

// get workload peers and ip blocks
Expand All @@ -412,15 +429,21 @@ func (ca *ConnlistAnalyzer) getConnectionsList(pe *eval.PolicyEngine, ia *ingres
}

// compute connections between peers based on pe analysis of network policies
peersAllowedConns, err := ca.getConnectionsBetweenPeers(pe, peers)
// if exposure-analysis is on, also compute and return the exposures-map
peersAllowedConns, exposuresMap, err := ca.getConnectionsBetweenPeers(pe, peers)
if err != nil {
ca.errors = append(ca.errors, newResourceEvaluationError(err))
return nil, nil, nil, err
}
connsRes = peersAllowedConns

exposedPeers := ca.emptyExposedListOrNil()
if ca.exposureAnalysis {
exposedPeers = buildExposedPeerListFromExposureMap(exposuresMap)
}

if excludeIngressAnalysis {
return connsRes, peers, []ExposedPeer{}, nil
return connsRes, peers, exposedPeers, nil
}

// analyze ingress connections - create connection objects for relevant ingress analyzer connections
Expand All @@ -435,7 +458,7 @@ func (ca *ConnlistAnalyzer) getConnectionsList(pe *eval.PolicyEngine, ia *ingres
ca.logWarning(netpolerrors.NoAllowedConnsWarning)
}

return connsRes, peers, []ExposedPeer{}, nil
return connsRes, peers, exposedPeers, nil
}

// existsFocusWorkload checks if the provided focus workload is ingress-controller
Expand All @@ -460,8 +483,10 @@ func (ca *ConnlistAnalyzer) existsFocusWorkload(peers []Peer, excludeIngressAnal
}

// getConnectionsBetweenPeers returns connections list from PolicyEngine object
func (ca *ConnlistAnalyzer) getConnectionsBetweenPeers(pe *eval.PolicyEngine, peers []Peer) ([]Peer2PeerConnection, error) {
// and exposures-map containing the exposed peers data if the exposure-analysis is on , else empty map
func (ca *ConnlistAnalyzer) getConnectionsBetweenPeers(pe *eval.PolicyEngine, peers []Peer) ([]Peer2PeerConnection, exposureMap, error) {
connsRes := make([]Peer2PeerConnection, 0)
exposuresMap := exposureMap{}
for i := range peers {
srcPeer := peers[i]
for j := range peers {
Expand All @@ -471,23 +496,22 @@ func (ca *ConnlistAnalyzer) getConnectionsBetweenPeers(pe *eval.PolicyEngine, pe
}
allowedConnections, err := pe.AllAllowedConnectionsBetweenWorkloadPeers(srcPeer, dstPeer)
if err != nil {
return nil, err
return nil, nil, err
}
// skip empty connections
if allowedConnections.IsEmpty() {
continue
}
p2pConnection := &connection{
src: srcPeer,
dst: dstPeer,
allConnections: allowedConnections.AllConnections(),
protocolsAndPorts: allowedConnections.ProtocolsAndPortsMap(),
p2pConnection, err := ca.checkIfP2PConnOrExposureConn(pe, allowedConnections, srcPeer, dstPeer, exposuresMap)
if err != nil {
return nil, nil, err
}
if p2pConnection != nil {
connsRes = append(connsRes, p2pConnection)
}
connsRes = append(connsRes, p2pConnection)
}
}

return connsRes, nil
return connsRes, exposuresMap, nil
}

// getIngressAllowedConnections returns connections list from IngressAnalyzer intersected with PolicyEngine's connections
Expand Down Expand Up @@ -519,12 +543,7 @@ func (ca *ConnlistAnalyzer) getIngressAllowedConnections(ia *ingressanalyzer.Ing
ca.warnBlockedIngress(peerStr, peerAndConn.IngressObjects)
continue
}
p2pConnection := &connection{
src: ingressControllerPod,
dst: peerAndConn.Peer,
allConnections: peerAndConn.ConnSet.AllConnections(),
protocolsAndPorts: peerAndConn.ConnSet.ProtocolsAndPortsMap(),
}
p2pConnection := createConnectionObject(peerAndConn.ConnSet, ingressControllerPod, peerAndConn.Peer)
res = append(res, p2pConnection)
}
return res, nil
Expand All @@ -550,3 +569,85 @@ func (ca *ConnlistAnalyzer) logWarning(msg string) {
ca.logger.Warnf(msg)
}
}

// checkIfP2PConnOrExposureConn checks if the given connection is between two peers from the parsed resources, if yes returns it,
// otherwise the connection belongs to exposure-analysis, will be added to the provided map
func (ca *ConnlistAnalyzer) checkIfP2PConnOrExposureConn(pe *eval.PolicyEngine, allowedConnections conn.AllowedSet,
src, dst Peer, exposuresMap exposureMap) (*connection, error) {
if !ca.exposureAnalysis {
// if exposure analysis option is off , the connection is definitely a P2PConnection
return createConnectionObject(allowedConnections, src, dst), nil
}
// else exposure analysis is on
// TODO : enhance this if condition after implementing eval.RepresentativePeer
if src.Name() != common.PodInRepNs && dst.Name() != common.PodInRepNs {
// both src and dst are peers are found in the parsed resources
return createConnectionObject(allowedConnections, src, dst), nil
}
// else: one of the peers is inferred from a netpol-rule , and the other is a peer from the parsed resources
// an exposure analysis connection
var err error
if src.Name() != common.PodInRepNs {
// dst is the inferred from netpol peer, we have an exposed egress for the src peer
err = exposuresMap.addConnToExposureMap(pe, allowedConnections, src, dst, false)
} else {
// src is the inferred from netpol peer, we have an exposed ingress to the dst peer
err = exposuresMap.addConnToExposureMap(pe, allowedConnections, src, dst, true)
}
return nil, err
}

// helper function - returns a connection object from the given fields
func createConnectionObject(allowedConnections conn.AllowedSet, src, dst Peer) *connection {
return &connection{
src: src,
dst: dst,
allConnections: allowedConnections.IsAllConnections(),
protocolsAndPorts: allowedConnections.ProtocolsAndPortsMap(),
}
}

// addConnToExposureMap adds a connection and its data to the exposure-analysis map
func (ex exposureMap) addConnToExposureMap(pe *eval.PolicyEngine, allowedConnections conn.AllowedSet, src, dst Peer, isIngress bool) error {
peer := src // real peer
inferredPeer := dst // inferred from netpol rule
if isIngress {
peer = dst
inferredPeer = src
}
if _, ok := ex[peer]; !ok {
ex[peer] = &peerExposureData{
isIngressProtected: false,
isEgressProtected: false,
ingressExposure: make([]*xgressExposure, 0),
egressExposure: make([]*xgressExposure, 0),
}
}
protected, err := pe.IsPeerProtected(peer, isIngress)
if err != nil {
return err
}
if !protected {
return nil // if the peer is not protected, we don't need to store any connection data
}

allowedConnSet, ok := allowedConnections.(*common.ConnectionSet)
if !ok { // should not get here
return errors.New(netpolerrors.ConversionToConnectionSetErr)
}
// protected peer - store the data
expData := &xgressExposure{
exposedToEntireCluster: inferredPeer.Namespace() == common.AllNamespaces,
namespaceLabels: pe.GetPeerNsLabels(inferredPeer),
podLabels: map[string]string{}, // will be empty since in this branch rules with namespaceSelectors only supported
potentialConn: allowedConnSet,
}
if isIngress {
ex[peer].isIngressProtected = true
ex[peer].ingressExposure = append(ex[peer].ingressExposure, expData)
} else { // egress
ex[peer].isEgressProtected = true
ex[peer].egressExposure = append(ex[peer].egressExposure, expData)
}
return nil
}
17 changes: 12 additions & 5 deletions pkg/netpol/connlist/exposed_peer.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
package connlist

import "github.com/np-guard/netpol-analyzer/pkg/netpol/internal/common"
import (
conn "github.com/np-guard/netpol-analyzer/pkg/netpol/connection"
)

// ExposedPeer captures potential ingress and egress connections data for an exposed Peer
type ExposedPeer interface {
// ExposedPeer is a peer for which the analysis found some potential exposure info
ExposedPeer() Peer
// IsProtectedByIngressNetpols indicates if there are ingress netpols selecting the ExposedPeer
// if peer is not protected, indicates that the peer is exposed on ingress to the whole world
// if peer is not protected by ingress netpols, the IngressExposure list will be empty
IsProtectedByIngressNetpols() bool
// IngressExposure is a list of the potential Ingress connections to the ExposedPeer
IngressExposure() []XgressExposureData
// IsProtectedByEgressNetpols indicates if there are egress netpols selecting the ExposedPeer
// if peer is not protected, indicates that the peer is exposed on egress to the whole world
// if peer is not protected by egress netpols, the EgressExposure list will be empty
IsProtectedByEgressNetpols() bool
// EgressExposure is a list of the potential Egress connections from the ExposedPeer
EgressExposure() []XgressExposureData
}
Expand All @@ -17,15 +27,12 @@ type ExposedPeer interface {
// any pod with labels in any-namespace, or any pod with labels in a namespace with labels, or any pod with labels in a specific namespace
// TODO: add detailed documentation as to which combinations of values represent which kind of "abstract" node in the output
type XgressExposureData interface {
// IsProtectedByNetpols indicates if the exposed peer is protected by any netpol on Ingress/Egress
// if a peer is not protected by xgress netpols, it will be exposed to entire cluster with all allowed connections
IsProtectedByNetpols() bool
// IsExposedToEntireCluster indicates if the peer is exposed to all namespaces in the cluster for the relevant direction
IsExposedToEntireCluster() bool
// NamespaceLabels are matchLabels of potential namespaces which the peer might be exposed to
NamespaceLabels() map[string]string
// PodLabels are matchLabels of potential pods which the peer might be exposed to
PodLabels() map[string]string
// PotentialConnectivity the potential connectivity of the exposure
PotentialConnectivity() common.AllowedConnectivity
PotentialConnectivity() conn.AllowedSet
}
Loading

0 comments on commit 15b25ea

Please sign in to comment.