Skip to content

Commit 0b7985e

Browse files
entleinclaude
andcommitted
feat: add user-defined NetworkNeighborhood support
Add support for user-defined NetworkNeighborhood profiles that allow pre-configured network policies without requiring a learning phase. Changes: - Load user-defined NN in container lifecycle (lifecycle.go) - NN cache support for user-defined profiles (networkneighborhoodcache.go) - SharedContainerData: add UserDefinedNetwork field - Enable R0005 (DNS Anomalies) and R0011 (Unexpected Egress) alert triggers - Component tests: Test_27 (wildcard AP matching) and Test_28 (user-defined NN alerts) - Test resources: known profiles, deployment YAMLs, helper scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: entlein <einentlein@gmail.com>
1 parent 3f8e906 commit 0b7985e

19 files changed

+1909
-5
lines changed

pkg/containerprofilemanager/v1/lifecycle.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,17 @@ func (cpm *ContainerProfileManager) addContainer(container *containercollection.
9292
return fmt.Errorf("failed to get shared data for container %s: %w", containerID, err)
9393
}
9494

95-
// Check if the container should use a user-defined profile
95+
// Check if the container should use a user-defined profile.
96+
// When both an ApplicationProfile and a NetworkNeighborhood are
97+
// user-provided, skip ALL recording — there is nothing to learn.
9698
if sharedData.UserDefinedProfile != "" {
9799
logger.L().Debug("ignoring container with a user-defined profile",
98100
helpers.String("containerID", containerID),
99101
helpers.String("containerName", container.Runtime.ContainerName),
100102
helpers.String("podName", container.K8s.PodName),
101103
helpers.String("namespace", container.K8s.Namespace),
102-
helpers.String("userDefinedProfile", sharedData.UserDefinedProfile))
104+
helpers.String("userDefinedProfile", sharedData.UserDefinedProfile),
105+
helpers.String("userDefinedNetwork", sharedData.UserDefinedNetwork))
103106
// Close ready channel before removing entry
104107
if entry, exists := cpm.getContainerEntry(containerID); exists {
105108
entry.readyOnce.Do(func() {

pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type ContainerInfo struct {
3030
InstanceTemplateHash string
3131
Namespace string
3232
SeenContainerFromTheStart bool // True if container was seen from the start
33+
UserDefinedNetwork string // Non-empty when pod has a user-defined NN label
3334
}
3435

3536
// NetworkNeighborhoodCacheImpl implements the NetworkNeighborhoodCache interface
@@ -204,6 +205,13 @@ func (nnc *NetworkNeighborhoodCacheImpl) updateAllNetworkNeighborhoods(ctx conte
204205
continue
205206
}
206207

208+
// Never overwrite a user-defined network neighborhood with an
209+
// auto-learned one. Check if any container for this workload
210+
// has a user-defined-network label.
211+
if nnc.workloadHasUserDefinedNetwork(workloadID) {
212+
continue
213+
}
214+
207215
// If we have a "new" container (seen from start) and the network neighborhood is partial,
208216
// skip it - we don't want to use partial profiles for containers we're tracking from the start
209217
if hasNewContainer && nn.Annotations[helpersv1.CompletionMetadataKey] == helpersv1.Partial {
@@ -419,11 +427,48 @@ func (nnc *NetworkNeighborhoodCacheImpl) addContainer(container *containercollec
419427
InstanceTemplateHash: sharedData.InstanceID.GetTemplateHash(),
420428
Namespace: container.K8s.Namespace,
421429
SeenContainerFromTheStart: !sharedData.PreRunningContainer,
430+
UserDefinedNetwork: sharedData.UserDefinedNetwork,
422431
}
423432

424433
// Add to container info map
425434
nnc.containerIDToInfo.Set(containerID, containerInfo)
426435

436+
// If the container has a user-defined network neighborhood, load it
437+
// directly into the cache — skip learning entirely for this workload.
438+
if sharedData.UserDefinedNetwork != "" {
439+
fullNN, err := nnc.storageClient.GetNetworkNeighborhood(
440+
container.K8s.Namespace, sharedData.UserDefinedNetwork)
441+
if err != nil {
442+
logger.L().Error("failed to get user-defined network neighborhood",
443+
helpers.String("containerID", containerID),
444+
helpers.String("workloadID", workloadID),
445+
helpers.String("namespace", container.K8s.Namespace),
446+
helpers.String("nnName", sharedData.UserDefinedNetwork),
447+
helpers.Error(err))
448+
profileState := &objectcache.ProfileState{
449+
Error: err,
450+
}
451+
nnc.workloadIDToProfileState.Set(workloadID, profileState)
452+
return nil
453+
}
454+
455+
nnc.workloadIDToNetworkNeighborhood.Set(workloadID, fullNN)
456+
profileState := &objectcache.ProfileState{
457+
Completion: helpersv1.Full,
458+
Status: helpersv1.Completed,
459+
Name: fullNN.Name,
460+
Error: nil,
461+
}
462+
nnc.workloadIDToProfileState.Set(workloadID, profileState)
463+
464+
logger.L().Debug("added user-defined network neighborhood to cache",
465+
helpers.String("containerID", containerID),
466+
helpers.String("workloadID", workloadID),
467+
helpers.String("namespace", container.K8s.Namespace),
468+
helpers.String("nnName", sharedData.UserDefinedNetwork))
469+
return nil
470+
}
471+
427472
// Create workload ID to state mapping
428473
if _, exists := nnc.workloadIDToProfileState.Load(workloadID); !exists {
429474
nnc.workloadIDToProfileState.Set(workloadID, nil)
@@ -718,6 +763,20 @@ func (nnc *NetworkNeighborhoodCacheImpl) mergeNetworkPorts(normalPorts, userPort
718763
return normalPorts
719764
}
720765

766+
// workloadHasUserDefinedNetwork returns true if any container tracked for
767+
// the given workloadID has a user-defined-network label set.
768+
func (nnc *NetworkNeighborhoodCacheImpl) workloadHasUserDefinedNetwork(workloadID string) bool {
769+
found := false
770+
nnc.containerIDToInfo.Range(func(_ string, info *ContainerInfo) bool {
771+
if info.WorkloadID == workloadID && info.UserDefinedNetwork != "" {
772+
found = true
773+
return false // stop iteration
774+
}
775+
return true
776+
})
777+
return found
778+
}
779+
721780
func isUserManagedNN(nn *v1beta1.NetworkNeighborhood) bool {
722781
return nn.Annotations != nil &&
723782
nn.Annotations[helpersv1.ManagedByMetadataKey] == helpersv1.ManagedByUserValue &&

pkg/objectcache/shared_container_data.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import (
2020
"k8s.io/apimachinery/pkg/util/validation"
2121
)
2222

23+
// UserDefinedNetworkMetadataKey is the pod label that references a
24+
// user-provided NetworkNeighborhood resource by name (analogous to
25+
// helpersv1.UserDefinedProfileMetadataKey for ApplicationProfiles).
26+
const UserDefinedNetworkMetadataKey = "kubescape.io/user-defined-network"
27+
2328
type ContainerType int
2429

2530
const (
@@ -82,6 +87,7 @@ type WatchedContainerData struct {
8287
PreviousReportTimestamp time.Time
8388
CurrentReportTimestamp time.Time
8489
UserDefinedProfile string
90+
UserDefinedNetwork string
8591
}
8692

8793
type ContainerInfo struct {
@@ -167,6 +173,16 @@ func (watchedContainer *WatchedContainerData) SetContainerInfo(wl workloadinterf
167173
watchedContainer.UserDefinedProfile = userDefinedProfile
168174
}
169175
}
176+
// check for user defined network neighborhood
177+
if userDefinedNetwork, ok := labels[UserDefinedNetworkMetadataKey]; ok {
178+
if userDefinedNetwork != "" {
179+
logger.L().Info("container has a user defined network neighborhood",
180+
helpers.String("network", userDefinedNetwork),
181+
helpers.String("container", containerName),
182+
helpers.String("workload", wl.GetName()))
183+
watchedContainer.UserDefinedNetwork = userDefinedNetwork
184+
}
185+
}
170186
podSpec, err := wl.GetPodSpec()
171187
if err != nil {
172188
return fmt.Errorf("failed to get pod spec: %w", err)

tests/chart/templates/node-agent/default-rules.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ spec:
122122
profileDependency: 0
123123
severity: 1
124124
supportPolicy: false
125-
isTriggerAlert: false
125+
isTriggerAlert: true
126126
mitreTactic: "TA0011"
127127
mitreTechnique: "T1071.004"
128128
tags:
@@ -245,7 +245,7 @@ spec:
245245
- "anomaly"
246246
- "applicationprofile"
247247
- name: "Unexpected Egress Network Traffic"
248-
enabled: false
248+
enabled: true
249249
id: "R0011"
250250
description: "Detecting unexpected egress network traffic that is not whitelisted by application profile."
251251
expressions:
@@ -257,7 +257,7 @@ spec:
257257
profileDependency: 0
258258
severity: 5 # Medium
259259
supportPolicy: false
260-
isTriggerAlert: false
260+
isTriggerAlert: true
261261
mitreTactic: "TA0010"
262262
mitreTechnique: "T1041"
263263
tags:

0 commit comments

Comments
 (0)