Skip to content

fix(networkstream): key stream entities by container ID outside Kubernetes - #887

Merged
matthyx merged 1 commit into
kubescape:mainfrom
AlonLiwsky:fix/networkstream-ecs-entity-keying
Aug 12, 2026
Merged

fix(networkstream): key stream entities by container ID outside Kubernetes#887
matthyx merged 1 commit into
kubescape:mainfrom
AlonLiwsky:fix/networkstream-ecs-entity-keying

Conversation

@AlonLiwsky

@AlonLiwsky AlonLiwsky commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Outside Kubernetes the network stream folded every container's traffic onto the single node entity, so no per-container entity existed and per-workload attribution was impossible. This keys entities by container ID instead — collapsing onto the node only for traffic that is not a container's — and creates the entity on first sight where nothing announces containers. The Kubernetes path is unchanged.

Overview

handleNetworkEvent and handleDnsEvent decide which stream entity a connection belongs to. Today the fallback onto the node entity fires whenever ns.k8sObjectCache == nil:

if entityId == "" || ns.k8sObjectCache == nil {
    entityId = ns.nodeName
}

That cache is a Kubernetes API cache. In this repo's binary it is always present, but embedders that run the sensor outside a cluster construct the stream with a nil cache — and there every event, including every container's, is folded onto the single node entity. No per-container entity is ever created, so every container on the machine is indistinguishable in the stream and per-workload attribution downstream is impossible.

After this change the collapse happens only for traffic that is not a container's: an empty container ID, or the "host" sentinel that procfs.go assigns to processes outside every tracked mount namespace. Everything else keys by container ID.

Keying alone is not enough, because an event for an entity that does not exist is dropped, and outside Kubernetes nothing subscribes this package's ContainerCallback — so no container entity is ever announced. When kubernetesMode is off, the entity is therefore created on the container's first event, and pruned once it goes an interval without traffic, so a machine with container churn does not accumulate dead ones.

In Kubernetes it keeps dropping, and that is deliberate. A missing entity in a cluster does not mean "never announced"; it means the container was removed or is ignored. Removal is not quiet, either: the container collection publishes EventTypeRemoveContainer before the container leaves the collection and then holds it in a 2-second cache for enrichers, while the watcher dispatches this callback onto a worker pool. So events for a container that is already gone keep arriving for a while afterwards. Creating entities for those would put a container carrying no pod or workload identity into the payload, for a pod that no longer exists, on every pod termination.

Two smaller fixes ride along:

  • Traffic recorded before a container is announced is no longer thrown away. containerCallback submits its subscribers to a worker pool while events flow straight through, so a container's first outbound connection routinely lands before its AddContainer reaches this package. The announcement now fills identity in over the event maps already there instead of replacing them. The node entity is excluded — it is not announced, so it has no such window, and excluding it keeps that path byte-identical.
  • The k8sInventory lookups in buildNetworkEvent are nil-guarded. The inventory is only built when kubernetesMode is set, so a pod- or service-kind endpoint arriving anywhere else would dereference a nil interface.

Additional Information

Effect on Kubernetes: none. The removed k8sObjectCache == nil disjunct never fired there, and every new behaviour is either behind !cfg.KubernetesMode or scoped away from the node entity. TestHandleNetworkEvent_KeyingIgnoresK8sObjectCache and TestHandleNetworkEvent_KubernetesStillDropsUnknownEntities pin that.

One known gap is deliberately not addressed here. ContainerCallback deletes a stopped container's entity along with any traffic it had not yet flushed, losing up to one interval — which matters most for a short-lived container that makes a single outbound connection. That behaviour is pre-existing and untouched by this PR: it is neither introduced nor widened here. But per-container entities make it matter more, so I will follow up with a separate PR that retires the entity at the next flush instead of deleting it. I am keeping it separate because that one is a Kubernetes behaviour change and deserves its own review rather than riding inside a change whose point is that Kubernetes is unaffected.

Docs: adds docs/features/network-stream-entity-keying.md. Also drops hostNetworkSensorEnabled and hostMalwareSensorEnabled from docs/CONFIGURATION.md — neither key exists anywhere in the codebase, while hostMonitoringEnabled, which does, was missing from the table.

How to Test

This package does not build on macOS, since inspektor-gadget is Linux-only. On a Mac:

docker run --rm -v "$PWD":/src -w /src -v "$(go env GOMODCACHE)":/go/pkg/mod \
  golang:1.25 bash -c 'go test -race ./pkg/networkstream/...'

On Linux, go test -race ./pkg/networkstream/....

15 new tests (18 cases with subtests) cover the keying, the platform gate, entity creation and pruning, the announcement merge in both arrival orders, node-entity restoration, and the nil-inventory guard. I checked each one earns its place by reverting the corresponding behaviour individually and confirming that test — and only that test — fails. The rest of the unit suite is unaffected.

Summary by CodeRabbit

  • New Features

    • Network and DNS activity is now attributed to separate container entities.
    • Non-Kubernetes environments can create temporary container entities from observed traffic.
    • Host activity is consistently associated with the node entity.
    • Container metadata and activity recorded before announcement are preserved.
    • Network endpoint enrichment safely supports pod and service details when inventory is available.
  • Bug Fixes

    • Unknown Kubernetes entities continue to be excluded.
    • Idle, unannounced container entities are automatically removed.
    • Snapshot and shutdown handling now safely isolate and flush collected data.
  • Documentation

    • Updated the host monitoring configuration reference.
    • Added documentation for network-stream entity keying and lifecycle behavior.

@AlonLiwsky AlonLiwsky added the ai-assisted Created through Armosec AI tooling (armosec-shared-rules plugin) label Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The network stream now keys events by container ID, maps host traffic to the node entity, creates temporary non-Kubernetes entities, preserves pre-announcement traffic, prunes idle unannounced entities, and guards endpoint enrichment when Kubernetes inventory is unavailable.

Changes

Network stream entity keying

Layer / File(s) Summary
Event entity resolution
pkg/networkstream/v1/network_stream.go, pkg/networkstream/v1/network_stream_test.go, docs/features/network-stream-entity-keying.md
Network and DNS events use shared container-aware resolution. Host IDs map to the node entity. Non-Kubernetes mode creates temporary entities. Kubernetes mode drops unknown entities. Endpoint enrichment handles missing inventory.
Entity lifecycle and snapshot handling
pkg/networkstream/v1/network_stream.go, pkg/networkstream/v1/network_stream_test.go, docs/features/network-stream-entity-keying.md, docs/CONFIGURATION.md
Container announcements preserve earlier traffic and remove unannounced tracking. Removals clear tracking. Snapshots prune idle unannounced entities and recreate the host entity. Configuration documents hostMonitoringEnabled.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NetworkEvent
  participant NetworkStream
  participant EntityResolver
  participant KubernetesInventory
  participant ContainerCallback
  NetworkEvent->>NetworkStream: submit network or DNS event
  NetworkStream->>EntityResolver: resolve container ID
  EntityResolver->>KubernetesInventory: inspect inventory when available
  EntityResolver-->>NetworkStream: return node, existing, new, or no entity
  ContainerCallback->>NetworkStream: announce container
  NetworkStream-->>ContainerCallback: preserve and enrich recorded traffic
Loading

Suggested reviewers: matthyx

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: network stream entities are keyed by container ID outside Kubernetes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@matthyx matthyx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Went through entityForEventLocked, the unannouncedEntities prune path, the ContainerCallback merge, and the k8sInventory nil-guard in buildNetworkEvent, plus the doc changes. Checked out fix/networkstream-ecs-entity-keying locally and ran the package suite (go test -race -v ./pkg/networkstream/...): all 15 new tests + the existing suite pass, no races. go build ./..., gofmt -l, and golangci-lint run ./pkg/networkstream/... are clean on the touched files (the only lint hits are pre-existing SA1019 deprecation warnings in wire.go/wire_test.go, untouched by this PR).

Logic checks out:

  • The node-collapse condition (entityID == "" || entityID == armotypes.HostContainerID) correctly drops the k8sObjectCache == nil disjunct, and cfg.KubernetesMode is the right signal to gate creation-on-first-sight vs. drop-on-miss — it's the same flag that already gates building k8sInventory in the constructor, so it's consistent with an existing invariant rather than a new assumption.
  • unannouncedEntities bookkeeping is correct: cleared on announce (both directions of the announce/first-event race), cleared on remove, and the prune loop deletes from the map it's ranging over, which Go permits.
  • entityForEventLocked restoring the node entity as Host (not inventing a container keyed by the node name) closes a latent gap in the old code too — previously, if the host container's RemoveContainer fired, host traffic was silently dropped until the next flush interval recreated the node entity.
  • docs/CONFIGURATION.md: confirmed hostMalwareSensorEnabled/hostNetworkSensorEnabled don't exist anywhere in the tree, and hostMonitoringEnabled does (pkg/config/config.go, pkg/containerwatcher/v2/container_watcher_collection.go), so that table edit is accurate.

No code-correctness blockers found. Two things stand in the way of merging, unrelated to the change itself:

  1. DCO check is failing (action_required) — the commit needs a Signed-off-by trailer (git commit --amend -s).
  2. This PR is still a draft and build-and-push-image is red — the log shows Username and password required on the quay.io login step, which looks like registry credentials not being available to a fork-sourced workflow run rather than anything introduced by this diff.

I'll hold off on approving until it's marked ready for review and the DCO check is green — happy to re-review immediately once those are resolved.

…netes

The node fallback fired whenever the Kubernetes object cache was absent.
That cache is nil on every non-Kubernetes install, so on host and ECS
agents every container's traffic collapsed onto the single node entity,
and no per-container entity existed at all.

Collapse onto the node only for an empty container ID or the host
sentinel. Outside Kubernetes also create a container entity on its first
event -- nothing announces containers there, so those events were dropped
-- and prune such an entity once it goes an interval without traffic, so
a host with container churn does not accumulate dead ones. In Kubernetes
an unknown entity still means removed-or-ignored rather than
never-announced, so it is still dropped and that path is unchanged.

Also in passing:

- Keep traffic recorded before a container is announced. The watcher
  dispatches ContainerCallback to a worker pool while events flow
  straight through, so a container's first egress routinely lands first;
  the announcement now supplies identity over the existing event maps
  instead of replacing them.
- Nil-guard the k8s inventory lookups in buildNetworkEvent, which only
  exist in Kubernetes mode.
- Drop hostNetworkSensorEnabled and hostMalwareSensorEnabled from
  docs/CONFIGURATION.md. Neither key exists anywhere in the codebase;
  hostMonitoringEnabled is the real one and was missing.

Signed-off-by: Alon Liwsky <40373481+AlonLiwsky@users.noreply.github.com>
@AlonLiwsky
AlonLiwsky force-pushed the fix/networkstream-ecs-entity-keying branch from f2b5e96 to bc687e2 Compare August 12, 2026 14:10
@AlonLiwsky
AlonLiwsky marked this pull request as ready for review August 12, 2026 14:11
@AlonLiwsky
AlonLiwsky requested a review from matthyx August 12, 2026 14:11

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/networkstream/v1/network_stream_test.go`:
- Around line 347-355: Isolate each endpoint-kind case in the test loop by
creating a fresh NetworkStream and associated state inside each subtest, or by
assigning distinct endpoint identities. Ensure each invocation of
ns.handleNetworkEvent independently verifies that its event is recorded with an
outbound length of 1, without relying on state from the other case.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 066e6074-085c-4af0-898a-3c023b3a6c1b

📥 Commits

Reviewing files that changed from the base of the PR and between ea83c90 and bc687e2.

📒 Files selected for processing (4)
  • docs/CONFIGURATION.md
  • docs/features/network-stream-entity-keying.md
  • pkg/networkstream/v1/network_stream.go
  • pkg/networkstream/v1/network_stream_test.go

Comment on lines +347 to +355
for _, kind := range []types.EndpointKind{types.EndpointKindPod, types.EndpointKindService} {
t.Run(string(kind), func(t *testing.T) {
event := outboundEvent(101, "1.2.3.4", 443)
event.DstEndpoint = types.L3Endpoint{Addr: "1.2.3.4", Kind: kind}

assert.NotPanics(t, func() { ns.handleNetworkEvent(event, nil) })
assert.Len(t, ns.networkEventsStorage.Entities[testNodeName].Outbound, 1,
"the connection is still recorded, only its Kubernetes enrichment is missing")
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Isolate the pod and service cases.

Both subtests share one NetworkStream and one endpoint identity. The pod case can satisfy the service case's assert.Len(..., 1) assertion. Create the stream inside each subtest, or use distinct endpoint identities. Then each case proves that its event is recorded.

Proposed test isolation
 func TestBuildNetworkEvent_PodEndpointWithoutK8sInventory(t *testing.T) {
-	ns := newTestStream(t, processtree.NewProcessTreeManagerMock())
-	require.Nil(t, ns.k8sInventory)
-
 	for _, kind := range []types.EndpointKind{types.EndpointKindPod, types.EndpointKindService} {
 		t.Run(string(kind), func(t *testing.T) {
+			ns := newTestStream(t, processtree.NewProcessTreeManagerMock())
+			require.Nil(t, ns.k8sInventory)
 			event := outboundEvent(101, "1.2.3.4", 443)
 			event.DstEndpoint = types.L3Endpoint{Addr: "1.2.3.4", Kind: kind}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, kind := range []types.EndpointKind{types.EndpointKindPod, types.EndpointKindService} {
t.Run(string(kind), func(t *testing.T) {
event := outboundEvent(101, "1.2.3.4", 443)
event.DstEndpoint = types.L3Endpoint{Addr: "1.2.3.4", Kind: kind}
assert.NotPanics(t, func() { ns.handleNetworkEvent(event, nil) })
assert.Len(t, ns.networkEventsStorage.Entities[testNodeName].Outbound, 1,
"the connection is still recorded, only its Kubernetes enrichment is missing")
})
for _, kind := range []types.EndpointKind{types.EndpointKindPod, types.EndpointKindService} {
t.Run(string(kind), func(t *testing.T) {
ns := newTestStream(t, processtree.NewProcessTreeManagerMock())
require.Nil(t, ns.k8sInventory)
event := outboundEvent(101, "1.2.3.4", 443)
event.DstEndpoint = types.L3Endpoint{Addr: "1.2.3.4", Kind: kind}
assert.NotPanics(t, func() { ns.handleNetworkEvent(event, nil) })
assert.Len(t, ns.networkEventsStorage.Entities[testNodeName].Outbound, 1,
"the connection is still recorded, only its Kubernetes enrichment is missing")
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/networkstream/v1/network_stream_test.go` around lines 347 - 355, Isolate
each endpoint-kind case in the test loop by creating a fresh NetworkStream and
associated state inside each subtest, or by assigning distinct endpoint
identities. Ensure each invocation of ns.handleNetworkEvent independently
verifies that its event is recorded with an outbound length of 1, without
relying on state from the other case.

@matthyx matthyx added the release Create release label Aug 12, 2026

@matthyx matthyx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked: DCO now passes and the PR is out of draft. Diffed the current head (bc687e2) against what I reviewed before — identical, so this was just the sign-off amend, no code changes to re-review.

build-and-push-image is still red on the same quay.io login step (Username and password required), but that's a registry-credentials gap that looks generic to fork-sourced PRs in this repo, not something this diff affects — branch protection doesn't require that check, and it's orthogonal to the change under review.

Approving.

@matthyx
matthyx merged commit cac1668 into kubescape:main Aug 12, 2026
8 of 13 checks passed
@matthyx matthyx moved this to To Archive in KS PRs tracking Aug 13, 2026
entlein pushed a commit to k8sstormcenter/node-agent that referenced this pull request Aug 15, 2026
…netes (kubescape#887)

The node fallback fired whenever the Kubernetes object cache was absent.
That cache is nil on every non-Kubernetes install, so on host and ECS
agents every container's traffic collapsed onto the single node entity,
and no per-container entity existed at all.

Collapse onto the node only for an empty container ID or the host
sentinel. Outside Kubernetes also create a container entity on its first
event -- nothing announces containers there, so those events were dropped
-- and prune such an entity once it goes an interval without traffic, so
a host with container churn does not accumulate dead ones. In Kubernetes
an unknown entity still means removed-or-ignored rather than
never-announced, so it is still dropped and that path is unchanged.

Also in passing:

- Keep traffic recorded before a container is announced. The watcher
  dispatches ContainerCallback to a worker pool while events flow
  straight through, so a container's first egress routinely lands first;
  the announcement now supplies identity over the existing event maps
  instead of replacing them.
- Nil-guard the k8s inventory lookups in buildNetworkEvent, which only
  exist in Kubernetes mode.
- Drop hostNetworkSensorEnabled and hostMalwareSensorEnabled from
  docs/CONFIGURATION.md. Neither key exists anywhere in the codebase;
  hostMonitoringEnabled is the real one and was missing.

Signed-off-by: Alon Liwsky <40373481+AlonLiwsky@users.noreply.github.com>
Signed-off-by: entlein <einentlein@gmail.com>
entlein pushed a commit to k8sstormcenter/node-agent that referenced this pull request Aug 26, 2026
…netes (kubescape#887)

The node fallback fired whenever the Kubernetes object cache was absent.
That cache is nil on every non-Kubernetes install, so on host and ECS
agents every container's traffic collapsed onto the single node entity,
and no per-container entity existed at all.

Collapse onto the node only for an empty container ID or the host
sentinel. Outside Kubernetes also create a container entity on its first
event -- nothing announces containers there, so those events were dropped
-- and prune such an entity once it goes an interval without traffic, so
a host with container churn does not accumulate dead ones. In Kubernetes
an unknown entity still means removed-or-ignored rather than
never-announced, so it is still dropped and that path is unchanged.

Also in passing:

- Keep traffic recorded before a container is announced. The watcher
  dispatches ContainerCallback to a worker pool while events flow
  straight through, so a container's first egress routinely lands first;
  the announcement now supplies identity over the existing event maps
  instead of replacing them.
- Nil-guard the k8s inventory lookups in buildNetworkEvent, which only
  exist in Kubernetes mode.
- Drop hostNetworkSensorEnabled and hostMalwareSensorEnabled from
  docs/CONFIGURATION.md. Neither key exists anywhere in the codebase;
  hostMonitoringEnabled is the real one and was missing.

Signed-off-by: Alon Liwsky <40373481+AlonLiwsky@users.noreply.github.com>
Signed-off-by: entlein <einentlein@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-assisted Created through Armosec AI tooling (armosec-shared-rules plugin) release Create release

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants