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

connlist implementing exposure analysis #296

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
813ee12
connlist implementing exposure analysis
shireenf-ibm Jan 16, 2024
d7542ba
Update pkg/netpol/connlist/connlist.go
shireenf-ibm Jan 17, 2024
fc3e814
fix rep. pod name
shireenf-ibm Jan 17, 2024
6a2215e
Update pkg/netpol/eval/exposure.go
shireenf-ibm Jan 17, 2024
5ab1fb5
Update pkg/netpol/connlist/exposed_peer.go
shireenf-ibm Jan 17, 2024
4b1aefb
Update pkg/netpol/connlist/exposed_peer.go
shireenf-ibm Jan 17, 2024
1b295dc
Update pkg/netpol/connlist/exposure_analysis.go
shireenf-ibm Jan 17, 2024
4237e81
Update pkg/netpol/connlist/exposure_analysis.go
shireenf-ibm Jan 17, 2024
59b0477
Merge branch 'connlist_implementations' of github.com:np-guard/netpol…
shireenf-ibm Jan 17, 2024
63e8f68
add func that updates the protected flag of a pod
shireenf-ibm Jan 17, 2024
bdf6d95
return error values
shireenf-ibm Jan 17, 2024
bb07eac
avoid fields dups among types
shireenf-ibm Jan 17, 2024
cd0743a
update func doc
shireenf-ibm Jan 17, 2024
b419627
getConnectionsBetweenPeers update doc + returns the exposureMap
shireenf-ibm Jan 17, 2024
995be16
move connection interface, avoid code dup, and compare conns using Co…
shireenf-ibm Jan 17, 2024
f979481
make the func an exposureMap func
shireenf-ibm Jan 17, 2024
f9745ba
fixing issue of same string in podsOwnerMap
shireenf-ibm Jan 17, 2024
c210b05
Update pkg/netpol/connlist/exposure_analysis.go
shireenf-ibm Jan 18, 2024
77fbee6
renaming Connection interface + move PortRange
shireenf-ibm Jan 18, 2024
d6c8101
struct embedding
shireenf-ibm Jan 18, 2024
9e154f0
using connectionSet internally + move the refinement to one iter at t…
shireenf-ibm Jan 18, 2024
3f3f080
Update pkg/netpol/connection/connection.go
shireenf-ibm Jan 18, 2024
53c4d72
Update pkg/netpol/connlist/exposure_analysis.go
shireenf-ibm Jan 18, 2024
2aeb9a8
rename AllConnections
shireenf-ibm Jan 18, 2024
4f451bf
verify conversion
shireenf-ibm Jan 18, 2024
36f370d
storing the maximum entire cluster connection
shireenf-ibm Jan 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
adisos marked this conversation as resolved.
Show resolved Hide resolved
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
Loading