Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions pkg/apis/softwarecomposition/network_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ type NetworkNeighbor struct {
// Each entry MAY be a literal IP, a CIDR (a.b.c.d/n), or the "*" sentinel.
// See pkg/registry/file/networkmatch for matcher semantics.
IPAddresses []string
// ServiceRefNamespace/ServiceRefName reference a single Service; the
// resolver expands it to that Service's ClusterIP(s) + endpoint IPs.
ServiceRefNamespace string
ServiceRefName string
// ServiceSelector selects Services by label, resolved like ServiceRef.
ServiceSelector *metav1.LabelSelector
// Entity is a reserved peer identity not backed by a Service ("host").
Entity string
}

type NetworkPort struct {
Expand Down
30 changes: 30 additions & 0 deletions pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ func GenerateNetworkPolicy(cp *softwarecomposition.ContainerProfile, knownServer

rule, policyRefs := generateIngressRule(neighbor, knownServers)

if len(rule.From) == 0 && len(rule.Ports) == 0 {
continue
}
if ruleHash, err := hash(rule); err == nil {
if ok := ingressHash[ruleHash]; !ok {
networkPolicy.Spec.Ingress = append(networkPolicy.Spec.Ingress, rule)
Expand All @@ -108,6 +111,9 @@ func GenerateNetworkPolicy(cp *softwarecomposition.ContainerProfile, knownServer

rule, policyRefs := generateEgressRule(neighbor, knownServers)

if len(rule.To) == 0 && len(rule.Ports) == 0 {
continue
}
if ruleHash, err := hash(rule); err == nil {
if ok := egressHash[ruleHash]; !ok {
networkPolicy.Spec.Egress = append(networkPolicy.Spec.Egress, rule)
Expand Down Expand Up @@ -317,10 +323,30 @@ func mergeEgressRulesByPorts(rules []softwarecomposition.NetworkPolicyEgressRule
return mergedRules
}

// unresolvedServiceNeighbor reports whether a neighbor names a Service or host
// entity and carries no peer this package can express. Those selectors are
// resolved against the live cluster by the agent, which this package has no
// view of; emitting the neighbor's ports with no peer would produce a
// NetworkPolicy rule whose empty peer list means every destination. DNSNames
// deliberately do not count as a peer: NetworkPolicy has no DNS peer and
// buildIPAddressesPeers only consumes IPs, so a service neighbor with only
// dnsNames is just as unexpressible as one with nothing.
func unresolvedServiceNeighbor(neighbor softwarecomposition.NetworkNeighbor) bool {
if neighbor.ServiceRefName == "" && neighbor.ServiceSelector == nil && neighbor.Entity == "" {
return false
}
return neighbor.PodSelector == nil && neighbor.NamespaceSelector == nil &&
len(neighbor.IPAddresses) == 0 && neighbor.IPAddress == ""
}

func generateEgressRule(neighbor softwarecomposition.NetworkNeighbor, knownServers softwarecomposition.IKnownServersFinder) (softwarecomposition.NetworkPolicyEgressRule, []softwarecomposition.PolicyRef) {
egressRule := softwarecomposition.NetworkPolicyEgressRule{}
policyRefs := []softwarecomposition.PolicyRef{}

if unresolvedServiceNeighbor(neighbor) {
return egressRule, policyRefs
}

if neighbor.PodSelector != nil {
removeLabels(neighbor.PodSelector.MatchLabels)
egressRule.To = append(egressRule.To, softwarecomposition.NetworkPolicyPeer{
Expand Down Expand Up @@ -413,6 +439,10 @@ func generateIngressRule(neighbor softwarecomposition.NetworkNeighbor, knownServ
ingressRule := softwarecomposition.NetworkPolicyIngressRule{}
policyRefs := []softwarecomposition.PolicyRef{}

if unresolvedServiceNeighbor(neighbor) {
return ingressRule, policyRefs
}

if neighbor.PodSelector != nil {
removeLabels(neighbor.PodSelector.MatchLabels)
ingressRule.From = append(ingressRule.From, softwarecomposition.NetworkPolicyPeer{
Expand Down
206 changes: 206 additions & 0 deletions pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2958,3 +2958,209 @@ func compareEgress(a, b []softwarecomposition.NetworkPolicyEgressRule) error {
}
return nil
}

// TestGenerateNetworkPolicy_UnresolvedServiceNeighborsAreDropped guards the
// blast radius of the serviceRef/serviceSelector/entity selectors: this package
// has no cluster view, so it cannot turn them into peers. A NetworkPolicy rule
// carrying ports but an empty peer list matches EVERY destination, so emitting
// such a neighbor's ports would silently widen the generated policy from "this
// Service" to "anywhere".
func TestGenerateNetworkPolicy_UnresolvedServiceNeighborsAreDropped(t *testing.T) {
timeProvider := metav1.Now()
tcp80 := []softwarecomposition.NetworkPort{
{Port: ptrToInt32(80), Protocol: softwarecomposition.ProtocolTCP, Name: "TCP-80"},
}

for _, tc := range []struct {
name string
neighbor softwarecomposition.NetworkNeighbor
}{
{"serviceRef", softwarecomposition.NetworkNeighbor{Identifier: "svc", ServiceRefNamespace: "default", ServiceRefName: "kubernetes", Ports: tcp80}},
{"serviceSelector", softwarecomposition.NetworkNeighbor{Identifier: "sel", ServiceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"role": "repo"}}, Ports: tcp80}},
{"entity", softwarecomposition.NetworkNeighbor{Identifier: "host", Entity: "host", Ports: tcp80}},
{"serviceRef-noports", softwarecomposition.NetworkNeighbor{Identifier: "svc2", ServiceRefNamespace: "default", ServiceRefName: "kubernetes"}},
} {
t.Run(tc.name, func(t *testing.T) {
cp := &softwarecomposition.ContainerProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "mc",
Namespace: "kubescape",
Annotations: map[string]string{
helpersv1.StatusMetadataKey: helpersv1.Learning,
helpersv1.ContainerTypeMetadataKey: "containers",
},
Labels: map[string]string{
helpersv1.RelatedKindMetadataKey: "Deployment",
helpersv1.RelatedNameMetadataKey: "mc",
helpersv1.ContainerNameMetadataKey: "app",
},
},
Spec: softwarecomposition.ContainerProfileSpec{
LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"app": "mc-app"}},
Egress: []softwarecomposition.NetworkNeighbor{tc.neighbor},
Ingress: []softwarecomposition.NetworkNeighbor{tc.neighbor},
},
}
got, err := GenerateNetworkPolicy(cp, softwarecomposition.NewKnownServersFinderImpl(nil), timeProvider)
assert.NoError(t, err)
for _, r := range got.Spec.Spec.Egress {
assert.NotEmptyf(t, r.To, "egress rule with ports %v has no peer — an empty peer list allows every destination", r.Ports)
}
for _, r := range got.Spec.Spec.Ingress {
assert.NotEmptyf(t, r.From, "ingress rule with ports %v has no peer — an empty peer list allows every source", r.Ports)
}
})
}
}

// serviceNeighborProfile builds a minimal generatable profile carrying the
// given neighbors on both directions, shared by the service-selector tests.
func serviceNeighborProfile(neighbors ...softwarecomposition.NetworkNeighbor) *softwarecomposition.ContainerProfile {
return &softwarecomposition.ContainerProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "mc",
Namespace: "kubescape",
Annotations: map[string]string{
helpersv1.StatusMetadataKey: helpersv1.Learning,
helpersv1.ContainerTypeMetadataKey: "containers",
},
Labels: map[string]string{
helpersv1.RelatedKindMetadataKey: "Deployment",
helpersv1.RelatedNameMetadataKey: "mc",
helpersv1.ContainerNameMetadataKey: "app",
},
},
Spec: softwarecomposition.ContainerProfileSpec{
LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"app": "mc-app"}},
Egress: neighbors,
Ingress: neighbors,
},
}
}

// TestGenerateNetworkPolicy_ServiceNeighborsWithResolvedPeersAreKept is the
// counterpart of the dropped-neighbor test: once the agent (or a user) has
// attached a concrete peer — ipAddresses or a podSelector — alongside a
// serviceRef, the neighbor is resolvable and dropping it would silently strip
// a learned allowlist entry from the generated policy.
func TestGenerateNetworkPolicy_ServiceNeighborsWithResolvedPeersAreKept(t *testing.T) {
timeProvider := metav1.Now()
tcp80 := []softwarecomposition.NetworkPort{
{Port: ptrToInt32(80), Protocol: softwarecomposition.ProtocolTCP, Name: "TCP-80"},
}

t.Run("serviceRef with ipAddresses keeps IP peers", func(t *testing.T) {
cp := serviceNeighborProfile(softwarecomposition.NetworkNeighbor{
Identifier: "svc-ip",
ServiceRefNamespace: "honey",
ServiceRefName: "alertmanager",
IPAddresses: []string{"10.1.2.3"},
Ports: tcp80,
})
got, err := GenerateNetworkPolicy(cp, softwarecomposition.NewKnownServersFinderImpl(nil), timeProvider)
assert.NoError(t, err)

assert.Len(t, got.Spec.Spec.Egress, 1)
assert.Equal(t, []softwarecomposition.NetworkPolicyPeer{
{IPBlock: &softwarecomposition.IPBlock{CIDR: "10.1.2.3/32"}},
}, got.Spec.Spec.Egress[0].To)
assert.Len(t, got.Spec.Spec.Egress[0].Ports, 1)
assert.Equal(t, int32(80), *got.Spec.Spec.Egress[0].Ports[0].Port)

assert.Len(t, got.Spec.Spec.Ingress, 1)
assert.Equal(t, []softwarecomposition.NetworkPolicyPeer{
{IPBlock: &softwarecomposition.IPBlock{CIDR: "10.1.2.3/32"}},
}, got.Spec.Spec.Ingress[0].From)
assert.Len(t, got.Spec.Spec.Ingress[0].Ports, 1)
assert.Equal(t, int32(80), *got.Spec.Spec.Ingress[0].Ports[0].Port)
})

t.Run("serviceSelector with podSelector keeps selector peer", func(t *testing.T) {
podSel := func() *metav1.LabelSelector {
return &metav1.LabelSelector{MatchLabels: map[string]string{"app": "alertmanager"}}
}
cp := serviceNeighborProfile(softwarecomposition.NetworkNeighbor{
Identifier: "svc-sel",
ServiceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"role": "monitoring"}},
PodSelector: podSel(),
Ports: tcp80,
})
got, err := GenerateNetworkPolicy(cp, softwarecomposition.NewKnownServersFinderImpl(nil), timeProvider)
assert.NoError(t, err)

assert.Len(t, got.Spec.Spec.Egress, 1)
assert.Equal(t, []softwarecomposition.NetworkPolicyPeer{{PodSelector: podSel()}}, got.Spec.Spec.Egress[0].To)
assert.Len(t, got.Spec.Spec.Egress[0].Ports, 1)

assert.Len(t, got.Spec.Spec.Ingress, 1)
assert.Equal(t, []softwarecomposition.NetworkPolicyPeer{{PodSelector: podSel()}}, got.Spec.Spec.Ingress[0].From)
assert.Len(t, got.Spec.Spec.Ingress[0].Ports, 1)
})
}

// TestGenerateNetworkPolicy_ServiceNeighborWithOnlyDNSNamesIsDropped pins the
// dnsNames edge: NetworkPolicy has no DNS peer and buildIPAddressesPeers only
// consumes IPs, so dnsNames contribute NO peer here. A serviceRef neighbor
// whose only companion data is dnsNames therefore still has no expressible
// peer; letting it through emits its ports with an empty peer list, which a
// NetworkPolicy interprets as every destination.
func TestGenerateNetworkPolicy_ServiceNeighborWithOnlyDNSNamesIsDropped(t *testing.T) {
timeProvider := metav1.Now()
tcp80 := []softwarecomposition.NetworkPort{
{Port: ptrToInt32(80), Protocol: softwarecomposition.ProtocolTCP, Name: "TCP-80"},
}
cp := serviceNeighborProfile(softwarecomposition.NetworkNeighbor{
Identifier: "svc-dns",
ServiceRefNamespace: "honey",
ServiceRefName: "alertmanager",
DNSNames: []string{"alertmanager.honey.svc.cluster.local."},
Ports: tcp80,
})
got, err := GenerateNetworkPolicy(cp, softwarecomposition.NewKnownServersFinderImpl(nil), timeProvider)
assert.NoError(t, err)
assert.Empty(t, got.Spec.Spec.Egress, "dnsNames produce no peer, so the neighbor is still unresolved and must be dropped")
assert.Empty(t, got.Spec.Spec.Ingress, "dnsNames produce no peer, so the neighbor is still unresolved and must be dropped")
}

// TestGenerateNetworkPolicy_MixedResolvableAndUnresolvedNeighbors guards the
// blast radius of the drop itself: an unresolved serviceRef neighbor must
// vanish alone, not take the profile's resolvable neighbors — or the whole
// policy — with it.
func TestGenerateNetworkPolicy_MixedResolvableAndUnresolvedNeighbors(t *testing.T) {
timeProvider := metav1.Now()
tcp80 := []softwarecomposition.NetworkPort{
{Port: ptrToInt32(80), Protocol: softwarecomposition.ProtocolTCP, Name: "TCP-80"},
}
tcp443 := []softwarecomposition.NetworkPort{
{Port: ptrToInt32(443), Protocol: softwarecomposition.ProtocolTCP, Name: "TCP-443"},
}
cp := serviceNeighborProfile(
softwarecomposition.NetworkNeighbor{
Identifier: "svc-unresolved",
ServiceRefNamespace: "honey",
ServiceRefName: "alertmanager",
Ports: tcp443,
},
softwarecomposition.NetworkNeighbor{
Identifier: "plain-ip",
IPAddresses: []string{"10.1.2.3"},
Ports: tcp80,
},
)
got, err := GenerateNetworkPolicy(cp, softwarecomposition.NewKnownServersFinderImpl(nil), timeProvider)
assert.NoError(t, err)

assert.Len(t, got.Spec.Spec.Egress, 1, "resolvable neighbor must survive the unresolved one being dropped")
assert.Equal(t, []softwarecomposition.NetworkPolicyPeer{
{IPBlock: &softwarecomposition.IPBlock{CIDR: "10.1.2.3/32"}},
}, got.Spec.Spec.Egress[0].To)
assert.Len(t, got.Spec.Spec.Egress[0].Ports, 1)
assert.Equal(t, int32(80), *got.Spec.Spec.Egress[0].Ports[0].Port)

assert.Len(t, got.Spec.Spec.Ingress, 1, "resolvable neighbor must survive the unresolved one being dropped")
assert.Equal(t, []softwarecomposition.NetworkPolicyPeer{
{IPBlock: &softwarecomposition.IPBlock{CIDR: "10.1.2.3/32"}},
}, got.Spec.Spec.Ingress[0].From)
assert.Len(t, got.Spec.Spec.Ingress[0].Ports, 1)
assert.Equal(t, int32(80), *got.Spec.Spec.Ingress[0].Ports[0].Port)
}
Loading
Loading