Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
14a3283
allow networkpolicy to be a cel selector for internal/external traffi…
entlein Aug 18, 2026
e6b7fab
Allow alert from unexpected Ports, allow Port=0 as intentional wildcard
entlein Aug 18, 2026
456d267
Allow alert from unexpected Ports, allow Port=0 as intentional wildcard
entlein Aug 18, 2026
ffb22be
remove explicit wildcard, declare port as non-mandatory, keep the ale…
entlein Aug 20, 2026
283098d
feat(cel/network): serviceRef/serviceSelector/host neighbor resolution
entlein Aug 23, 2026
6f30d65
feat(cel/network): real-Flux component test, RBAC + perf fixes for se…
entlein Aug 23, 2026
7d294ca
test(component): scope the internal-egress rule to the serviceRef suite
entlein Aug 23, 2026
8d5a019
test(chart): make network service resolution a value
entlein Aug 24, 2026
369fbe6
fix(cel): invalidate cached results when a profile is re-resolved
entlein Aug 24, 2026
e240448
feat(rules): R0012 unexpected internal egress
entlein Aug 24, 2026
9537cf0
feat(rules): R0011/R0012 symmetric egress/ingress, no IP-class gate
entlein Aug 24, 2026
b9328c0
Merge remote-tracking branch 'origin/upstream-pr/portalerts' into fea…
entlein Aug 24, 2026
8a74230
Merge remote-tracking branch 'origin/upstream-pr/serviceref-network' …
entlein Aug 24, 2026
9c272fa
Merge branch 'feat/rules-internal-egress' into feat/network-v2
entlein Aug 24, 2026
9a5dca6
dedup selector engine after portalerts merge
entlein Aug 24, 2026
60962b8
pin storage to k8sstormcenter/storage@3844202a (dnsNames + deflate fi…
entlein Aug 24, 2026
70ebf6d
test(component): Test_50 asserts shipped R0011; Test_51 ingress R0012…
entlein Aug 24, 2026
20791f1
chart(kubescape-rules): standalone chart shipping the R0011/R0012 rul…
entlein Aug 24, 2026
eb2f79e
Merge branch 'feat/rules-internal-egress' into feat/network-v2
entlein Aug 24, 2026
5a17ba0
rules: consume the peer-selector engine in R0011/R0012
entlein Aug 24, 2026
4280eae
review: address maintainer blockers on network-v2
entlein Aug 25, 2026
91c6dec
fix(cel/network): label-only peer matching, empty selector fails closed
entlein Aug 25, 2026
daad334
test(networkpeer): characterize excludeNamespaces × selector resolution
entlein Aug 25, 2026
3c45703
test(cel/network): eval-level selector truth tables, matchAddrPort gr…
entlein Aug 26, 2026
bc4abc6
test: pin default-posture R0011/R0012 behavior on learned profiles (T…
entlein Aug 26, 2026
7e4b47e
fix(learn): record service ClusterIP so learned profiles don't false-…
entlein Aug 26, 2026
96cb0b9
feat(network): silence node/host peers by default; expose loopback to…
entlein Aug 26, 2026
9a08bc9
build: pin inspektor-gadget fork to kubescape (matches upstream #925)
entlein Aug 26, 2026
14f912f
Revert the standalone kubescape-rules chart
entlein Aug 26, 2026
36ae810
fix(network): address review — port-aware selector matching + CR nits
entlein Aug 26, 2026
1001913
test(testutils): use a YAML document reader for multi-doc apply (Code…
entlein Aug 26, 2026
7cdd5fb
perf(projection): don't mark every profile resolution-dependent for t…
entlein Aug 27, 2026
7aa43ae
test(component): make Test_20/30/36 deterministic — kill the flakes
entlein Aug 27, 2026
a19e064
fix(projection): keep host-injected profiles resolution-dependent (ma…
entlein Aug 27, 2026
54041a5
test(component): Test_54 — peer allowlisting is namespace-invariant (…
entlein Aug 27, 2026
ba27b23
Revert "test(component): Test_54 — peer allowlisting is namespace-inv…
entlein Aug 27, 2026
a370aba
feat(network): commit to namespace-agnostic peer matching, drop dead …
entlein Aug 28, 2026
652e98f
test(network): name the namespace-disambiguation collision case expli…
entlein Aug 28, 2026
6741015
feat(network): NetworkPolicy namespaceSelector semantics — omitted=sa…
entlein Aug 28, 2026
f4ea9a6
feat(network): unify serviceSelector namespace semantics with the pod…
entlein Aug 28, 2026
db256c4
pinning sotrage to kubescape
entlein Aug 31, 2026
2707b4a
pinning sotrage to kubescape part 2
entlein Aug 31, 2026
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
5 changes: 4 additions & 1 deletion .github/workflows/component-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ jobs:
Test_36_MultiContainerPerContainerBinding,
Test_43_RelativeOpenPathResolution,
Test_48_MultiSubtypeGroupedProfileDocument,
Test_49_EphemeralContainerFullTreatment
Test_49_EphemeralContainerFullTreatment,
Test_50_ServiceRefNetworkNeighbor,
Test_51_ServiceRefIngressR0012,
Test_53_DefaultLearnedNetworkFalsePositives
]
steps:
- name: Checkout code
Expand Down
58 changes: 58 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ import (
"github.com/kubescape/node-agent/pkg/malwaremanager"
malwaremanagerv1 "github.com/kubescape/node-agent/pkg/malwaremanager/v1"
otelmetrics "github.com/kubescape/node-agent/pkg/metricsmanager/otel"
"github.com/kubescape/node-agent/pkg/networkpeer"
"github.com/kubescape/node-agent/pkg/networkstream"
networkstreamv1 "github.com/kubescape/node-agent/pkg/networkstream/v1"
"github.com/kubescape/node-agent/pkg/nodeprofilemanager"
nodeprofilemanagerv1 "github.com/kubescape/node-agent/pkg/nodeprofilemanager/v1"
"github.com/kubescape/node-agent/pkg/objectcache"

"github.com/kubescape/node-agent/pkg/objectcache/containerprofilecache"
"github.com/kubescape/node-agent/pkg/objectcache/dnscache"
"github.com/kubescape/node-agent/pkg/objectcache/k8scache"
Expand Down Expand Up @@ -72,6 +74,8 @@ import (
"github.com/kubescape/node-agent/pkg/watcher/seccompprofilewatcher"
goruntime "go.opentelemetry.io/contrib/instrumentation/runtime"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/informers"
toolscache "k8s.io/client-go/tools/cache"
)

func main() {
Expand Down Expand Up @@ -321,6 +325,60 @@ func main() {
ruleBindingCache.AddNotifier(&ruleBindingNotify)

cpc := containerprofilecache.NewContainerProfileCache(cfg, storageClient, k8sObjectCache, metricsProvider)
// Resolve serviceRef/serviceSelector/entity network neighbors against live
// cluster state (Service ClusterIPs + endpoints, Node IPs + CNI gateway) at
// projection time. Gated behind networkServiceResolutionEnabled: the
// cluster-wide Service+EndpointSlice list+watch (one per DaemonSet node) is
// only paid where profiles actually use the feature. Services and
// EndpointSlices are watched cluster-wide (a profile may reference any
// namespace's Service); the Node informer is field-selected to this agent's
// own node — the "host" entity is local, and a cluster-wide Node watch on
// every DaemonSet pod is O(nodes^2) traffic for no benefit. A TransformFunc
// strips managedFields/annotations (and per-endpoint fields beyond
// Addresses) before objects enter the cache to keep its footprint small.
if cfg.EnableNetworkServiceResolution {
svcInformers := informers.NewSharedInformerFactory(k8sClient.GetKubernetesClient(), 0)
nodeInformers := informers.NewSharedInformerFactoryWithOptions(
k8sClient.GetKubernetesClient(), 0,
informers.WithTweakListOptions(func(o *metav1.ListOptions) {
o.FieldSelector = "metadata.name=" + cfg.NodeName
}),
)
svcInformer := svcInformers.Core().V1().Services().Informer()
sliceInformer := svcInformers.Discovery().V1().EndpointSlices().Informer()
_ = svcInformer.SetTransform(networkpeer.TrimService)
_ = sliceInformer.SetTransform(networkpeer.TrimEndpointSlice)
serviceLister := networkpeer.NewInformerLister(
svcInformers.Core().V1().Services().Lister(),
svcInformers.Discovery().V1().EndpointSlices().Lister(),
nodeInformers.Core().V1().Nodes().Lister(),
cfg.NodeName,
)
// Advance the lister generation on any Service/EndpointSlice/Node
// change, so the reconciler re-projects serviceRef/entity profiles when
// the cluster view moves (endpoint churn, or caches that fill after
// startup). Per-event cost is a single atomic increment; the
// re-projection itself is coalesced onto the reconcile tick, and only
// serviceRef-using profiles are eligible.
bump := toolscache.ResourceEventHandlerFuncs{
AddFunc: func(interface{}) { serviceLister.Bump() },
UpdateFunc: func(_, _ interface{}) { serviceLister.Bump() },
DeleteFunc: func(interface{}) { serviceLister.Bump() },
}
_, _ = svcInformer.AddEventHandler(bump)
_, _ = sliceInformer.AddEventHandler(bump)
_, _ = nodeInformers.Core().V1().Nodes().Informer().AddEventHandler(bump)
// Start the informers and hand the lister over WITHOUT blocking on
// cache sync: node-agent's core startup (container watcher, profiling)
// must not wait on these, and a bounded wait here previously delayed
// learning enough to trip the tight completion budget of Test_22. The
// caches fill in the background; serviceRef/entity neighbors resolve on
// the next reconcile once populated. nil-until-set is a no-op in
// projection.
svcInformers.Start(ctx.Done())
nodeInformers.Start(ctx.Done())
cpc.SetServiceLister(serviceLister)
}
cpc.Start(ctx)
if cpm, ok := containerProfileManager.(*containerprofilemanagerv1.ContainerProfileManager); ok {
cpm.SetCompletionNotifier(cpc)
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ require (
github.com/kubescape/backend v0.0.39
github.com/kubescape/go-logger v0.0.32
github.com/kubescape/k8s-interface v0.0.214
github.com/kubescape/storage v0.0.303
github.com/kubescape/storage v0.0.320
github.com/kubescape/workerpool v0.0.0-20250526074519-0e4a4e7f44cf
github.com/moby/sys/mountinfo v0.7.2
github.com/oleiade/lane/v2 v2.0.0
Expand Down Expand Up @@ -472,7 +472,7 @@ require (
zombiezen.com/go/sqlite v1.4.0 // indirect
)

replace github.com/inspektor-gadget/inspektor-gadget => github.com/matthyx/inspektor-gadget v0.0.0-20260819074828-9494a925bd43
replace github.com/inspektor-gadget/inspektor-gadget => github.com/kubescape/inspektor-gadget v0.0.0-20260826074832-06b0d12baca0

replace github.com/anchore/syft => github.com/kubescape/syft v1.32.0-ks.2

Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -891,10 +891,12 @@ github.com/kubescape/backend v0.0.39 h1:B1QRfKCSFlzuE+jWOnk/l7EpH71/Q3n14KKq0QSn
github.com/kubescape/backend v0.0.39/go.mod h1:cMEGP8cXUZgY89YU4GRBGIla9HZW7grZsUtlCwvZgAE=
github.com/kubescape/go-logger v0.0.32 h1:4mI+XJOV8VFCMewrEE9VIFEIOhzXokYT3nFpNfXf4fM=
github.com/kubescape/go-logger v0.0.32/go.mod h1:Alj7JBQ8/WCxbXe8Ura6ZheSRK45E0p21M3xeqedX90=
github.com/kubescape/inspektor-gadget v0.0.0-20260826074832-06b0d12baca0 h1:kJzq1CnGP4kqAdyVWrb9TGlRpniwsIsVVMfo61OuzCQ=
github.com/kubescape/inspektor-gadget v0.0.0-20260826074832-06b0d12baca0/go.mod h1:cwCFczq1LJ6Frpur0Vr5Ncic77a9ihki07Xpwzy+ItI=
github.com/kubescape/k8s-interface v0.0.214 h1:j7KP0/5VvYOoQdBGV2+gRM3qnR8PWLAGF8RM/k/DmJ0=
github.com/kubescape/k8s-interface v0.0.214/go.mod h1:WNYUG93aZ5kDmuaRKFLtVhp18Yc6EfaHdD1gLYtVTN4=
github.com/kubescape/storage v0.0.303 h1:0nXI6E07lbWsg7iEH04vR4kwiekj//uCQl/La+8j4aM=
github.com/kubescape/storage v0.0.303/go.mod h1:d/1hqWPda2clsjx2wmQgysnB5dThIo3rDKP7RWx+v+M=
github.com/kubescape/storage v0.0.320 h1:uKqc6SL9phBMbjiN20uTK1SVOINHuNrklWaxb2U09Ag=
github.com/kubescape/storage v0.0.320/go.mod h1:d/1hqWPda2clsjx2wmQgysnB5dThIo3rDKP7RWx+v+M=
github.com/kubescape/syft v1.32.0-ks.2 h1:xdUksUmKEyyVKsTfJDYW8Z5HawVJtelsUolPOsWtDx0=
github.com/kubescape/syft v1.32.0-ks.2/go.mod h1:E6Kd4iBM2ljUOUQvSt7hVK6vBwaHkMXwcvBZmGMSY5o=
github.com/kubescape/workerpool v0.0.0-20250526074519-0e4a4e7f44cf h1:hI0jVwrB6fT4GJWvuUjzObfci1CUknrZdRHfnRVtKM0=
Expand All @@ -919,8 +921,6 @@ github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/matthyx/inspektor-gadget v0.0.0-20260819074828-9494a925bd43 h1:RNn7KJswYAsnFW+gZRIBNRrcQ3BSsyW5dpXR28aRkDE=
github.com/matthyx/inspektor-gadget v0.0.0-20260819074828-9494a925bd43/go.mod h1:cwCFczq1LJ6Frpur0Vr5Ncic77a9ihki07Xpwzy+ItI=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
Expand Down
4 changes: 4 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ type Config struct {
EnableMalwareDetection bool `mapstructure:"malwareDetectionEnabled"`
EnableNetworkStreaming bool `mapstructure:"networkStreamingEnabled"`
EnableNetworkTracing bool `mapstructure:"networkServiceEnabled"`
EnableNetworkServiceResolution bool `mapstructure:"networkServiceResolutionEnabled"`
AlertOnHostPeers bool `mapstructure:"alertOnHostPeers"`
EnableNodeProfile bool `mapstructure:"nodeProfileServiceEnabled"`
EnablePartialProfileGeneration bool `mapstructure:"partialProfileGenerationEnabled"`
EnableMetricsExporter bool `mapstructure:"prometheusExporterEnabled"`
Expand Down Expand Up @@ -185,6 +187,8 @@ func LoadConfigOptional(path string, errNotFound bool) (Config, error) {
viper.SetDefault("podName", os.Getenv(PodNameEnvVar))
viper.SetDefault("fimEnabled", false)
viper.SetDefault("networkStreamingEnabled", false)
viper.SetDefault("networkServiceResolutionEnabled", true)
viper.SetDefault("alertOnHostPeers", false)
viper.SetDefault("kubernetesMode", true)
viper.SetDefault("networkStreamingInterval", 2*time.Minute)
viper.SetDefault("workerPoolSize", 3000)
Expand Down
1 change: 1 addition & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestLoadConfig(t *testing.T) {
EnableHttpDetection: false,
EnableFIM: true,
EnableNetworkStreaming: false,
EnableNetworkServiceResolution: true,
EnableEmbeddedSboms: false,
EnableHostSensor: true,
HostSensorInterval: 1 * time.Minute,
Expand Down
16 changes: 4 additions & 12 deletions pkg/containerprofilemanager/v1/container_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ func (cd *containerData) createNetworkNeighbor(networkEvent NetworkEvent, namesp
}

} else if networkEvent.Destination.Kind == EndpointKindService {
// For service, we need to retrieve it and use its selector
svc, err := k8sClient.GetWorkload(networkEvent.Destination.Namespace, "Service", networkEvent.Destination.Name) // TODO: use IG inventory as this can generate a lot of API calls.
if err != nil {
logger.L().Warning("failed to get service",
Expand All @@ -231,19 +230,16 @@ func (cd *containerData) createNetworkNeighbor(networkEvent NetworkEvent, namesp
return nil
}

// The ClusterIP is stable, so record it: detection matches it directly when the event carries the pre-DNAT address, covering selectorless services and the learn/detect label asymmetry.
neighborEntry.IPAddress = networkEvent.Destination.IPAddress

var selector map[string]string
if svc.GetName() == "kubernetes" && svc.GetNamespace() == "default" {
// The default service has no selectors, in addition, we want to save the default service address
selector = svc.GetLabels()
neighborEntry.IPAddress = networkEvent.Destination.IPAddress
} else {
selector = svc.GetServiceSelector()
}

if len(selector) == 0 {
// TODO: check if we need to handle services with no selectors
return nil
} else {
if len(selector) > 0 {
neighborEntry.PodSelector = &metav1.LabelSelector{
MatchLabels: selector,
}
Expand All @@ -255,10 +251,6 @@ func (cd *containerData) createNetworkNeighbor(networkEvent NetworkEvent, namesp
}

} else {
if networkEvent.Destination.IPAddress == "127.0.0.1" {
// No need to generate for localhost
return nil
}
neighborEntry.IPAddress = networkEvent.Destination.IPAddress

if dnsResolverClient != nil {
Expand Down
98 changes: 98 additions & 0 deletions pkg/containerprofilemanager/v1/container_data_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package containerprofilemanager

import (
"testing"

"github.com/kubescape/k8s-interface/k8sinterface"
"github.com/kubescape/k8s-interface/workloadinterface"
"github.com/kubescape/node-agent/pkg/objectcache"
"github.com/kubescape/node-agent/pkg/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
)

type fakeServiceClient struct {
selector map[string]interface{}
labels map[string]interface{}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func (f *fakeServiceClient) GetWorkload(namespace, _, name string) (k8sinterface.IWorkload, error) {
meta := map[string]interface{}{"name": name, "namespace": namespace}
if f.labels != nil {
meta["labels"] = f.labels
}
spec := map[string]interface{}{}
if f.selector != nil {
spec["selector"] = f.selector
}
return workloadinterface.NewWorkloadObj(map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": meta,
"spec": spec,
}), nil
}

func (f *fakeServiceClient) CalculateWorkloadParentRecursive(w k8sinterface.IWorkload) (string, string, error) {
return w.GetKind(), w.GetName(), nil
}
func (f *fakeServiceClient) GetKubernetesClient() kubernetes.Interface { return nil }
func (f *fakeServiceClient) GetDynamicClient() dynamic.Interface { return nil }

// A service neighbor records its stable ClusterIP so detection matches the peer
// by address (the learned selector alone misses on CNIs that keep the pre-DNAT
// ClusterIP on the wire, since IG stamps the service's metadata labels while the
// learned selector holds the pod selector), and a selectorless service is kept.
func TestCreateNetworkNeighbor_ServiceRecordsClusterIP(t *testing.T) {
const clusterIP = "10.43.12.34"
cd := &containerData{watchedContainerData: &objectcache.WatchedContainerData{Namespace: "default"}}
ev := NetworkEvent{
Port: 80,
Protocol: "tcp",
PktType: utils.OutgoingPktType,
Destination: Destination{
Kind: EndpointKindService,
Namespace: "default",
Name: "nginx",
IPAddress: clusterIP,
},
}

withSel := &fakeServiceClient{selector: map[string]interface{}{"app": "nginx"}}
n := cd.createNetworkNeighbor(ev, "default", withSel, nil)
require.NotNil(t, n)
assert.Equal(t, clusterIP, n.IPAddress, "a service neighbor must record its stable ClusterIP")
require.NotNil(t, n.PodSelector, "a service with a selector keeps its pod selector")
assert.Equal(t, "nginx", n.PodSelector.MatchLabels["app"])

noSel := &fakeServiceClient{}
n2 := cd.createNetworkNeighbor(ev, "default", noSel, nil)
require.NotNil(t, n2, "a selectorless service must not be dropped — the ClusterIP identifies it")
assert.Equal(t, clusterIP, n2.IPAddress)
assert.Nil(t, n2.PodSelector, "no selector to learn when the service defines none")
}

// With the 127.0.0.1 learn-drop removed, loopback and its aliases are learned
// as address neighbors (subject to R0011/R0012) instead of being silently
// dropped — loopback is a real attack surface (localhost admin panels, sidecar
// pivots). Only the literal 127.0.0.1 changed behavior; the others were never
// dropped, and this pins that they all remain learnable.
func TestCreateNetworkNeighbor_LoopbackAliasesLearned(t *testing.T) {
cd := &containerData{watchedContainerData: &objectcache.WatchedContainerData{Namespace: "default"}}
for _, ip := range []string{"127.0.0.1", "127.0.0.53", "::1", "0.0.0.0"} {
ev := NetworkEvent{
Port: 8080,
Protocol: "tcp",
PktType: utils.OutgoingPktType,
Destination: Destination{
Kind: EndpointKindRaw,
IPAddress: ip,
},
}
n := cd.createNetworkNeighbor(ev, "default", nil, nil)
require.NotNil(t, n, "loopback/localhost %s must be learned (guard removed), not dropped", ip)
assert.Equal(t, ip, n.IPAddress, "loopback %s recorded as an address neighbor", ip)
}
}
Loading
Loading