Skip to content

feat(network): symmetric R0011/R0012 with NetworkPolicy-style internal allowlisting and port-aware alerting - #923

Open
entlein wants to merge 31 commits into
kubescape:mainfrom
k8sstormcenter:feat/network-v2
Open

feat(network): symmetric R0011/R0012 with NetworkPolicy-style internal allowlisting and port-aware alerting#923
entlein wants to merge 31 commits into
kubescape:mainfrom
k8sstormcenter:feat/network-v2

Conversation

@entlein

@entlein entlein commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Ports and NW Policies - twin rules for internal/external nw

replacing the partial PRs #902, #905, #915 (storage companion: kubescape/storage#364).

Feature: Ports alert, Internal and External Network Slices can be selected for allowlisting

Rules — symmetric egress/ingress, no IP-class gate

  • R0011 (egress) and new R0012 (ingress) are exact twins: internal and external peers treated alike. Lateral movement to an unlisted internal peer alerts; a serviceCIDR-wide allowlist entry no longer blinds detection.
  • Both consume the port-aware matcher was_address_port_protocol_in_*: an allowed address on a violated port alerts; an entry with no ports means any port, computed per-(port,protocol).

Allowlisting internal traffic, NetworkPolicy-style

  • Peer selectors: podSelector on profile neighbors, matched at event time (was_selector_in_*)
  • Service references: serviceRef{Name}, serviceSelector, entity: host resolve at projection time to ClusterIP + endpoint IPs + the Service FQDN, feeding the ordinary address/DNS surfaces. Unresolvable selectors contribute nothing — never a match-all. Informers are gated behind networkServiceResolutionEnabled (default ON) and strip managedFields/annotations;

measured cost MUST BE REMEASURED

Validation

  • Full component-test matrix must be green

Dependency

go.mod temporarily replaces kubescape/storage with the fork branch of kubescape/storage#364 (schema fields + generated code + loss-guards in collapse/deflate/NetworkPolicy generation). After #364 merges, the replace drops for a pseudo-version pin — no storage release required.

Blocker before merge

must switch the rules OFF by default, even though Test53 is to show how many FalsePositives a user would get if it were turned on.

Difficulties encoutnered

Matching the host - ip turned out to be difficult, so its not used by default (its enabelable)

Not changed

Rule Definitions and severity (and mitre fields) need to be aligned with whenever we update the rulelibrary, for now this feature must be considered depended on a new rulelibrary release, as R0012 does not exist yet.

Summary by CodeRabbit

  • New Features

    • Added Kubernetes service and endpoint resolution for network neighbors, including service references, selectors, DNS names, and host peers.
    • Added port- and protocol-aware ingress and egress matching.
    • Added destination namespace and pod-label data for network rules.
    • Enabled network service resolution by default, with optional host-peer alerts.
    • Added support for service-based network policies and improved loopback traffic learning.
  • Bug Fixes

    • Corrected wildcard and literal port handling, including port 0.
    • Prevented unresolved or invalid selectors from broadening network access.

entlein added 19 commits August 18, 2026 17:37
…c allowlisting

Signed-off-by: entlein <einentlein@gmail.com>
…rt if delcared and violated

Signed-off-by: entlein <einentlein@gmail.com>
Let a ContainerProfile allowlist cluster-infrastructure egress/ingress by
Service name, Service label selector, or host entity instead of a broad
ipAddresses serviceCIDR that blinds R0011/R0012 to lateral movement.

Each serviceRef/serviceSelector/entity neighbor resolves at projection time
to the concrete ClusterIP + backing-endpoint (or node/gateway) IPs it stands
for, carrying its own ports, and is appended as an ordinary selector-free
ipAddresses neighbor. The existing port-sensitive address matcher enforces
it unchanged; unresolved selectors contribute nothing (never a match-all).

- pkg/networkpeer: Resolve/Matches/ResolveIPs + Lister over Service,
  EndpointSlice and Node informers, with a generation counter so a profile
  projected before the informers synced re-projects once the view changes.
- objectcache/reconciler: mark profiles that use service resolution and
  re-project them when the lister generation advances; plain profiles keep
  the identical old fast-skip path.
- cmd/main.go: cluster-wide Service/EndpointSlice informers + a node-scoped
  Node informer, started non-blocking (no WaitForCacheSync on the hot path).
- fail closed on ServiceSelector MatchExpressions / empty matchLabels and on
  any namespaceSelector other than kubernetes.io/metadata.name=<ns>.
- Test_50 component test (serviceRef egress allowed, external egress still
  fires R0011) + resolve/expand/lister unit tests + fixture-lint R-NN-12
  extended to accept the new target fields.

Depends on the storage schema fields ServiceRefNamespace/ServiceRefName/
ServiceSelector/Entity; go.mod pins the fork's storage until the companion
upstream storage PR lands.

Signed-off-by: tanzee <einentlein@gmail.com>
…rviceRef

Component test Test_50 now generates its traffic from a real Flux
source-controller reconciling HelmRepository CRs instead of exec'ing curl,
and its ContainerProfile is network-only (no syscalls/execs, which only add
false-positive surface to a network test).

The profile names every peer as a Kubernetes object: serviceRef
default/kubernetes for the apiserver, serviceRef kube-system/kube-dns for
resolution, and a serviceSelector role=helm-repo fanning across the two repo
Services. The negative is the lateral move a serviceCIDR entry hides: the
HelmRepository URL is repointed at a sibling Service on the same port that
the selector does not cover, and the controller fetches it itself.
Verified on kind: 0 alerts for the named peers, R0011 within 15s for the
sibling.

Fixes found while validating end to end:
- ClusterRole was missing discovery.k8s.io/endpointslices, so the informer
  was forbidden and Service endpoint IPs never resolved — the feature
  silently degraded to ClusterIP-only.
- Service/EndpointSlice informers are now gated behind
  networkServiceResolutionEnabled and strip managedFields/annotations (and
  per-endpoint fields beyond Addresses) via SetTransform, so agents that do
  not use the feature pay no cluster-wide list+watch and the cache stays
  small on those that do.
- serviceRef/serviceSelector now also imply the Service cluster FQDN as a
  dnsName, so a client dialling the Service by name is allowlisted without a
  parallel dnsNames entry.
- specFromNeighbor no longer allocates a discarded port slice for every
  plain ipAddresses neighbor.
- R0011 no longer excludes private destinations: in-cluster lateral movement
  is exactly what this feature exists to expose.

Signed-off-by: tanzee <einentlein@gmail.com>
Relaxing the shipped R0011 to fire on private destinations made kube-dns
egress alert for every workload that does not name it: Test_21 gained a
spurious R0011 and Test_28 lost allowed_fusioncore_no_alert and
mitm_coredns_poisoning. Restore the stock expression and express the
internal-egress predicate as a test-only rule (R9911) bound by podSelector to
this suite's pods, so nothing outside it changes.

Verified on kind: Test_50 passes both phases against the stock ruleset, and
Test_21 + all six Test_28 subtests are green again.

Signed-off-by: tanzee <einentlein@gmail.com>
Hardcoding the flag in the ConfigMap made it impossible to measure the
feature's cost against itself. Expose it as nodeAgent.config.networkServiceResolution
(on in the test chart, so Test_50 still exercises it) so an A/B can toggle
resolution without rebuilding the image.

Signed-off-by: tanzee <einentlein@gmail.com>
The CEL result cache keys on SpecHash + SyncChecksum. Re-projecting a
serviceRef/serviceSelector/entity profile against a moved cluster view changes
neither: SpecHash tracks the rule projection spec, and SyncChecksum comes from a
learned CP annotation an authored profile does not carry at all. So a result
computed before the Service/EndpointSlice informers filled — 'this ClusterIP is
not in egress' — was served from the LRU indefinitely, and the re-projection the
lister generation correctly triggered had no observable effect. Egress to an
allowlisted Service kept alerting.

Carry the resolution generation on the projected profile and include it in the
key, so the cache moves whenever the resolved addresses can have moved.

Signed-off-by: tanzee <einentlein@gmail.com>
R0011 keeps its external-only scope (!is_private_ip); internal traffic gets its
own rule instead of widening R0011 — rewriting R0011's scope broke Test_21/28
(kube-dns FPs) when tried in the fork CT.

R0012 alerts on OUTGOING to private addresses (loopback excluded — is_private_ip
counts 127.0.0.1/::1 as private) not allowlisted by the profile's egress
addresses, which includes serviceRef/serviceSelector-resolved entries. Uses the
port-aware matcher; behaves address-only until the port projection lands, then
becomes port-sensitive with no rules change. A selector clause
(was_selector_in_egress) is added one-line when the peer-selector fields merge.
Same defaults as R0011; uniqueId keyed on addr_port_proto; bound in the default
binding (new rule names are inert until bound).

Signed-off-by: tanzee <einentlein@gmail.com>
Per design review: R0011 (egress) and R0012 (ingress, new) are symmetric twins.
Neither uses is_private_ip — internal and external peers are treated alike, so
lateral movement to unlisted internal peers alerts; only loopback is excluded.
Allowlisting internal traffic is the profile's job (addresses, resolved
serviceRef/serviceSelector entries), not the rule's.

Both use the port-aware matcher (address-only until port projection lands).
On HOST (incoming) events the gadget's dstAddr/dstPort carry the remote peer
and local port. R0011's scope widens to internal egress: component tests whose
profiles do not list kube-dns et al. will alert until their profiles do —
that pressure is the feature.

Signed-off-by: tanzee <einentlein@gmail.com>
…t/network-v2

# Conflicts:
#	pkg/objectcache/projection_types.go
# Conflicts:
#	tests/chart/templates/node-agent/default-rules.yaml
portalerts carried its own copy of the celnetworkselector peer-selector
functions; the merge kept both and the package no longer compiled. One copy
remains.

Signed-off-by: tanzee <einentlein@gmail.com>
…xes)

Signed-off-by: tanzee <einentlein@gmail.com>
… twin

The scoped R9911 rule and its binding are gone — the widened R0011 covers
internal egress, so the decoy pivot asserts the shipped rule. Test_51 mirrors
it for ingress: nginx serves a serviceRef-listed client (flux
source-controller, resolution covers its ClusterIP and pod endpoint IPs) with
zero R0012, then an unlisted k6 client joins and R0012 must fire. Both use
only real controller/loadgen traffic.

Signed-off-by: tanzee <einentlein@gmail.com>
…eset

Deployable over any kubescape install to replace the stock rules; namespace
templated. A drift test pins the chart copy to the CI-validated test-chart
copy so the shipped semantics are always the tested ones.

Signed-off-by: tanzee <einentlein@gmail.com>
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds Kubernetes service, selector, and host-peer resolution. Container-profile projections now retain resolved network data and refresh when cluster state changes. CEL rules enforce address, port, protocol, namespace, and pod-selector constraints. Component fixtures validate the new behavior.

Changes

Network peer resolution

Layer / File(s) Summary
Service, selector, and host resolution
pkg/networkpeer/*
Adds informer-backed service and node lookup, service DNS resolution, selector expansion, host-peer expansion, generation tracking, fail-closed selector handling, and resolver tests and benchmarks.
Container-profile projection and cache refresh
pkg/objectcache/..., pkg/containerprofilemanager/...
Projects peer selectors and address-port groups, resolves dynamic neighbors, injects host peers, preserves literal port 0, and refreshes cached projections when the lister generation changes.
CEL network enforcement
pkg/rulemanager/cel/..., pkg/utils/cel.go
Adds port- and protocol-aware address matching, namespace and pod-selector matching, destination event fields, uncached selector functions, cost coverage, and selector compilation tests.
Runtime wiring and integration fixtures
cmd/main.go, pkg/config/..., tests/chart/..., tests/component_test.go, tests/resources/..., tests/testutils/k8s.go, go.mod, .github/workflows/component-tests.yaml
Enables informer-based resolution, adds EndpointSlice permissions and configuration, updates runtime rules and dependencies, adds service and network fixtures, and registers component tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Merge Risk: 🟡 Moderate · up to 10019

This PR adds symmetric ingress/egress network allowlisting and port-aware alerting, but the current behavior can suppress alerts when selector allowlists ignore port or protocol constraints, enable both rules by default and create excessive alerts, and report loopback traffic contrary to the documented rule behavior. These issues require correction or explicit owner acceptance before merge.

Sequence Diagram(s)

sequenceDiagram
  participant NodeAgent
  participant Informers
  participant ContainerProfileCache
  participant CELRules
  participant KubernetesCluster
  NodeAgent->>Informers: start Service, EndpointSlice, and Node watchers
  Informers->>KubernetesCluster: read cluster resources
  KubernetesCluster-->>Informers: cached services, endpoints, and node IPs
  NodeAgent->>ContainerProfileCache: install InformerLister
  ContainerProfileCache->>Informers: resolve service and host neighbors
  Informers-->>ContainerProfileCache: resolved addresses, DNS names, and generation
  ContainerProfileCache->>CELRules: provide projected profile
  CELRules-->>NodeAgent: evaluate address, port, protocol, and selector match
Loading

Suggested reviewers: matthyx

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.66% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 145 functions across 39 files. (5 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: symmetric R0011/R0012 rules, internal allowlisting, and port-aware alerting.
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 49.66% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 145 functions across 39 files. (5 skipped: 5 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 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.

The selector clause was deferred while the engine lived on a separate branch,
then forgotten when that branch merged: selectors resolved and matched but no
rule consulted them. Both rules now also allowlist via
was_selector_in_egress/ingress, matching the form already deployed downstream.

Signed-off-by: tanzee <einentlein@gmail.com>
Comment thread charts/kubescape-rules/templates/binding.yaml Outdated
ruleExpression:
- eventType: "network"
expression: "event.pktType == 'OUTGOING' && !net.is_private_ip(event.dstAddr) && !cp.was_address_in_egress(event.containerId, event.dstAddr)"
expression: "event.pktType == 'OUTGOING' && !event.dstAddr.startsWith('127.') && event.dstAddr != '::1' && !cp.was_address_port_protocol_in_egress(event.containerId, event.dstAddr, event.dstPort, event.proto) && !cp.was_selector_in_egress(event.containerId, event.dstNamespace, event.dstPodLabels)"

@entlein entlein Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will make the rules broad and OFF by default. I.e. the private ip and localhost filters will be removed

Comment thread tests/resources/serviceref-k6.yaml Outdated

@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.

Reviewed as requested. This lines up with the author's own "NOT READY FOR REVIEW" flag — the networkpeer resolution/fail-closed design (empty selectors → nil, MatchExpressions rejected, ClusterIPNone excluded, gateway IP verified via ipNet.Contains) is sound, but there are concrete blockers before this can merge:

Critical

  • go.mod/go.sum: replace github.com/kubescape/storage => github.com/k8sstormcenter/storage v0.0.240-... pins the module to a fork, with the pre-replace requirement left at the placeholder v0.0.0-00010101000000-000000000000. This is unbuildable for any consumer until kubescape/storage#364 merges and a real pseudo-version replaces it.
  • Unrelated dependency downgrades pulled in alongside the fork pin: kubescape/backend v0.0.39 → v0.0.31 and gotest.tools/v3 v3.5.2 → v3.5.0. These look like tidying against the stale fork rather than an intentional change — please restore both unless there's a reason to downgrade.

High

  • pkg/objectcache/containerprofilecache/reconciler.go (~L499-510): the cache-invalidation fix reads listerGen() after Apply(...) runs, in two separate calls. If a Bump() happens during resolution, the new generation gets stamped onto IPs resolved against the old view, so refreshOneEntry's staleness check never fires and the stale result is served from the LRU indefinitely — the exact bug this fix was meant to close. containerprofilecache.go (~L618-620) does this correctly by reading the generation once, before resolution — please match that pattern here.
  • The widened R0011 (drops net.is_private_ip) and new R0012 ship enabled: true in default-rules.yaml/binding, but EnableNetworkServiceResolution defaults to false in pkg/config/config.go — only the test chart's values.yaml turns it on. On upgrade, existing profiles start alerting on all internal peers with no serviceRef/serviceSelector allowlisting available to quiet them (resolution is off by default). Either default resolution on alongside the widened rules, or ship R0012/the widened R0011 disabled until resolution is on by default.
  • pkg/networkpeer/expand.go (~L127): nil namespaceSelector on a serviceSelector resolves cluster-wide ("cluster-wide by design" per the comment), which contradicts both the PR description ("nil namespaceSelector = same namespace, as in NetworkPolicy") and the same-namespace default used elsewhere (network.go's namespaceSelectorMatches). resolveServices doesn't have the profile's namespace in scope to fix this today — serviceSelector: {app: foo} currently allowlists that label across every namespace in the cluster, which is a much broader allowlist than the feature intends.
  • RBAC (discovery.k8s.io/endpointslices) only lands in tests/chart's ClusterRole. Per the PR description, the shipped chart lives in kubescape/helm-charts and "needs to be moved" — without a companion PR there, real deployments enabling this feature get informer 403s and silently degrade to ClusterIP-only resolution (the exact failure mode already found and fixed once during this PR's own validation).

Medium

  • expand.go's hasServiceFields accepts a ServiceRefNamespace-only neighbor, but specFromNeighbor rejects it — causes permanent re-projection churn for that shape.
  • pkg/utils/cel.go's dstPodLabels returns a raw map[string]string while sibling CEL accessors return celtypes-wrapped values — worth double-checking this doesn't break type coercion in CEL expressions that consume it.

No fork images found in shipped config (only a ghcr.io/fluxcd reference in a test fixture, which is fine). Given the go.mod fork dependency alone, this isn't mergeable yet — requesting changes rather than approving.

@entlein

entlein commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

thanks for catching the regression during the merge/rebase

**AI, **
please
pkg/objectcache/containerprofilecache/reconciler.go (~L499-510): the cache-invalidation fix reads listerGen() after Apply(...) runs, in two separate calls. If a Bump() happens during resolution, the new generation gets stamped onto IPs resolved against the old view, so refreshOneEntry's staleness check never fires and the stale result is served from the LRU indefinitely — the exact bug this fix was meant to close. containerprofilecache.go (~L618-620) does this correctly by reading the generation once, before resolution — please match that pattern here.
-> recheck and fix

Clusterrole RBAC: Thanks for bringing it up -> AI, check how much surface this introduces to the deaomnset overall and align with how e.g. cilium minimizes that role

EnableNetworkServiceResolution defaults to false in pkg/config/config.go : we assume that noone is currently using those profiles, the default loud is desired, else a TDD approach to find/scan for all the connections to add to a profile is difficult.
That being said: you are correct that the mechanism is inconsistent and silly ATM: better to turn it on in ...config.go and ship the rules default OFF
AI, please lets flip config to ON (or even ditch the feature-flag) and we turn the rule OFF (not remove internal IPs, that defeats the purpose, just ship the rules R0011/R0012 as OFF)

fork dependencies: for the CTs to run, AI, please patch storage-tag.sh with our fork ref, else it builds against upstream which doesnt have the change. That defeats the test

Human readers,
Bringing us to point of how to split the rules: the whole slew of changes here break a lot of default kubescape behavior IF people use profiles at all (again: probably noone is): things will be default loud, not default learn.

I m also adding an alert on new Container , such that any workload not known to kubescape will alert instead of learn - that completely flips the basic node-agent UX ->
Possibly there should be just one global flag in the helm values.yaml to toogle all user-defined-profiles to on. Not sure yet.

Reason this is in Draft:

  • turning on these rules with bad profiles causes massive floods of alerts that may even DoS a cluster, if RuleCoolDown is disabled (my default setting), as every internal connection from e.g. DB to prometeus is going to alert ... yes.

Namespaces: That is correct: namespaces are not read - meaning, if there is a collision in names across namespaces they act as allowlists -> I think, that should be how it works.

Exec summary : 🙏 thanks for catching the technical bugs again (this is embarassing) while I sort out my brain

- reconciler: read lister generation once BEFORE service resolution and
  stamp that same gen as ResolvedGen/ListerGen, matching addContainer's
  ordering; a Bump() during resolution now invalidates the projection
  instead of masking stale IPs behind the fast-skip.
- config: networkServiceResolutionEnabled now defaults true (rules ship
  enabled, resolution must match); test chart configmap falls back to
  true via hasKey so an explicit false still renders false.
- deps: restore accidental downgrades kubescape/backend v0.0.31->v0.0.39
  and gotest.tools/v3 v3.5.0->v3.5.2; k8sstormcenter/storage replace pin
  unchanged (tidy normalized the require placeholder to v0.0.258, the
  replace still governs).
- networkpeer: hasServiceFields no longer counts a ServiceRefNamespace-only
  neighbor that specFromNeighbor rejects, ending permanent re-projection
  churn on such profiles; test pins the agreement.
- rbac: drop unnecessary get verb on endpointslices (cache-backed
  informer needs only list+watch).
- ct: storage-tag.sh emits the fork storage image tag (net-v2-rc1) while
  go.mod replaces storage with k8sstormcenter/storage, and the test chart
  pulls ghcr.io/k8sstormcenter/storage, so CTs run a server that has the
  serviceRef/dnsNames schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
Signed-off-by: tanzee <einentlein@gmail.com>
Comment thread pkg/rulemanager/cel/libraries/containerprofilenetwork/selector_test.go Outdated
Comment thread pkg/rulemanager/cel/libraries/containerprofilenetwork/selector_test.go Outdated
@matthyx matthyx moved this to WIP in KS PRs tracking Aug 25, 2026
@entlein

entlein commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Here s the benchmakr from this run BTW @matthyx : could have sworn that benchmark-thingy posted to the PR for the last round, now the widget is getting 403. Possibly intentional, possibly not.
BTW: I think, the benchmark is wrong, but here we are:

EDIT: entlein does not believe that her nonsense improved the perf :)

eBPF Dedup Benchmark Results

Node-Agent Resource Usage

Metric BEFORE AFTER Delta

Avg CPU (cores) 0.218 0.213 -2.0%
Peak CPU (cores) 0.231 0.230 -0.6%
Avg Memory (MiB) 374.079 297.109 -20.6%
Peak Memory (MiB) 377.723 302.402 -19.9%

Dedup Effectiveness: no data available

Peer matching is on pod labels; a nil namespaceSelector no longer requires the
same namespace — namespace is consulted only when the selector is explicitly
set (collision disambiguation). This aligns peer selectors with the
serviceSelector nil-namespace semantics. An empty podSelector now matches
NOTHING (fail closed → the peer alerts), the opposite of NetworkPolicy's
match-all: an allowlist entry must name what it permits. An unresolved peer
still never matches.

Full truth tables added: wasSelectorInPeers and namespaceSelectorMatches
(pod/namespace edge cases), and serviceRef (via the always-present
default/kubernetes API server) and entity:host resolution + matching in
networkpeer.

Signed-off-by: tanzee <einentlein@gmail.com>
@matthyx

matthyx commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

@entlein I think the benchmark is accurate on main, I have fixed my IG fork to use the latest deepcopy implementation with a pool... we had a more efficient implementation that was never used for months, my bad.

@entlein

entlein commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

NICE!

entlein and others added 3 commits August 25, 2026 16:52
excludeNamespaces (Config.SkipNamespace) filters which workloads node-agent
profiles; the selector resolver queries a cluster-wide Service/Node view that
takes no namespace-exclusion input. The resulting source/peer asymmetry — a
workload in an excluded namespace is never profiled, yet any monitored profile
may still allowlist a Service in that excluded namespace via serviceRef or an
unscoped serviceSelector — is easy to overlook.

These pin it down: serviceRef into an excluded ns resolves; an unscoped
serviceSelector fans across the exclusion boundary; authored NamespaceLabels is
the only mechanism that scopes fanout (and can deliberately target an excluded
ns); the host entity is orthogonal. Asserted under both the exclude-denylist and
include-allowlist forms of SkipNamespace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
…id, fail-closed edges

The was_selector_in_{egress,ingress} CEL path had zero eval-level tests: only
the pure helpers were covered, and the mock projection never populated
EgressPeers/IngressPeers, making the path untestable end-to-end. The mock now
mirrors production extractPeers (plus Namespace), enabling:

- selector_eval_test.go (new): 12-row truth table over wasSelectorIn direct
  calls (direction isolation, ns-scoped peers, empty-namespace/nil-labels/
  empty-labels/profile-unavailable fail-closed), no-peers fail-closed,
  nil-objectCache and wrong-arg-type CEL errors, refValToStringMap edges,
  and a compiled-CEL end-to-end run incl. profile-unavailable -> false at
  the binding.
- selector_test.go: invalid (unparseable) podSelector/namespaceSelector fail
  closed and never poison later valid peers.
- port_protocol_test.go: matchAddrPort 12-row ip/port/proto/want/why grid -
  case-insensitive protocol both directions, absent-ports-stanza any-port,
  empty address, nil/empty groups fail closed.
- legacy_test.go: nn.* parity now also covers both selector functions
  (hit + miss), closing the 6-of-8 gap.
- integration_test.go: 7 selector expressions through the real env incl.
  direction isolation and combined address+selector checks.
- cost_test.go: every declared funcSpec must have a cost estimate; legacy
  nn.-> cp. estimator translation costs identically.
- networkpeer/expand_test.go: portless serviceRef synthesizes an any-port
  entry; serviceSelector fanout implies both guestbook FQDNs.
…est_53)

Characterizes upstream's default network posture — learned ContainerProfile
only, no user-defined profile. Two assertions: the learn window itself is
alert-free (R0011/R0012 are profileDependency:0 == Required, suppressed as
profile_incomplete until the profile completes), and replaying the exact
learn-window traffic after completion must yield zero R0011/R0012.

The post-completion assertion is EXPECTED TO FAIL on iptables/kube-proxy CI
runners (TDD-against-red): a label-less Service is learned by selector but
detected by metadata labels, so its ClusterIP is a permanent R0011 FP tuple.
The red run documents the service selector-vs-labels asymmetry that a default
merge would expose. Fixture: nginx + stock nginx-service + unlabeled client.

Numbered 53 to avoid colliding with Test_52 (R1017 UnknownContainerInBundle)
on the signed branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
@entlein

entlein commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Note: Test20 really needs a fluke-fix, AI, be nice and check it AGAIN , please.

53: we need a concise side-by-side of the current UX (which must be preserved in this PRs default settings) vs the new functionality
Anticipate Test 52 must be aligned with the behavior (sign on AND off)

Hard rule: The current default learning->enforce UX must be preserved if default settings are used.

…positive R0011/R0012

A learned network profile stored a Service peer only by svc.Spec.Selector (the
pod selector), but at detection IG stamps the resolved object's METADATA labels
(endpoint.k8s.labels) — for a ClusterIP that resolves to a Service, the
Service's own labels, a different set. So cp.was_selector_in_egress matched the
pod selector against the service's metadata labels and missed, and a service
with no selector was dropped entirely (return nil) — every subsequent connection
became an R0011 (and, symmetrically, R0012) false positive. Worst case is a
label-less Service (e.g. a bare ClusterIP): a permanent FP on every iptables/
kube-proxy cluster, where capture sees the pre-DNAT ClusterIP on the wire.

Fix: record the (stable) ClusterIP as the neighbor's IPAddress for every Service
peer, and stop dropping selectorless services. The address matcher then clears
the ClusterIP the event carries on iptables-class CNIs, while the pod selector
(kept when present) still covers CNIs that rewrite to the backing pod IP before
capture. ClusterIPs don't churn, so recording them is safe and needs no selector
— this generalizes what the default/kubernetes service already did.

Validated locally on Kind (kindnet/iptables): Test_53 (default-learned FP) now
passes; Test_21 (learn->alert network) and Test_34 (CIDR collapse) unchanged.
New unit test reproduces the dropped-selectorless-service and missing-ClusterIP
regressions at the learn site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
Comment thread pkg/containerprofilemanager/v1/container_data.go Outdated
… R0011/R0012

Two changes to the default posture of the widened R0011/R0012 network rules,
each grounded in a measured false-positive class.

1. Host-peer allowlist (default on). A node/host IP has no pod identity, so the
   selector matcher fails closed (dstNamespace=="") and node IPs are volatile
   and intermittent, so the address matcher misses too — every kubelet probe,
   hostNetwork peer or masqueraded connection recurs as an R0011/R0012 FP. A new
   alertOnHostPeers flag (default false) injects a synthetic entity:host neighbor
   into every projected profile, which resolves live (via the Node informer) to
   the node's InternalIP(s) + CNI gateway and lands in the address surface, so
   node-peer traffic never alerts. Set alertOnHostPeers=true to surface it.
   Reuses the existing entity resolution + address matcher; self-heals across
   node churn (the reason a learned address can't). Marks host-injected profiles
   as resolution-dependent so they re-project when the node view moves.

2. Loopback exposed. Removed the !startsWith('127.') && != '::1' guards from
   R0011/R0012 and the 127.0.0.1 learn-time drop, so localhost traffic is now
   subject to the rules and is learnable — loopback is a real attack surface
   (localhost admin panels, sidecar pivots) and is less trusted than node infra.

Validated locally on Kind: Test_53 gains node_ip_ingress_silenced_by_default
(hostNetwork client -> nginx, R0012 stays 0 under the default allowlist) and
loopback_learn_enforce_measurement (learned loopback replays with 0 FP; the run
independently shows prometheus/alertmanager self-scrape loopback firing R0012
where their profiles completed partial — the measured loopback tax). Test_21,
Test_34, Test_50, Test_51 unchanged. Unit: TestWithHostPeer_* pins that the
node IPs resolve into the address surface (the (a) leg: uncovered without it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
@entlein

entlein commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit : review this PR now, please

We have two open caveats

  • we are only testing the network capture on iptables CNI
  • for partially learnt profiles, the loopback from host seems to FP occasionally

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

@entlein I will review PR #923. I will assess the current implementation and preserve the stated default learning->enforce UX requirement. I will treat the CNI coverage limitation and the occasional host-loopback false positive as known caveats.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 7

🧹 Nitpick comments (4)
pkg/networkpeer/perf_bench_test.go (1)

232-245: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Gate the memory-estimate test behind -short.

TestInformerCacheMemoryEstimate is a diagnostic harness. It makes no assertions and only logs. It runs on every go test ./... and builds 15,000 EndpointSlice objects plus three forced GCs, so it adds CI time for no signal. The doc comment already describes it as a manual -run target. Add a short-mode skip so the intent is enforced.

♻️ Proposed change
 func TestInformerCacheMemoryEstimate(t *testing.T) {
+	if testing.Short() {
+		t.Skip("diagnostic measurement only; run explicitly with -run InformerCacheMemoryEstimate -v")
+	}
 	measure := func(build func() []interface{}) uint64 {
🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. 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/networkpeer/perf_bench_test.go` around lines 232 - 245, Update
TestInformerCacheMemoryEstimate to skip when testing.Short() is true, before
running the memory-building and garbage-collection measurement logic; preserve
the existing diagnostic behavior for explicit non-short runs.
pkg/networkpeer/lister.go (1)

76-98: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use nodes.Get(nodeName) when nodeName is set.

HostIPs lists every Node and then discards all but one. In a large cluster this allocates and scans the whole Node cache on each call, and the resolver calls it per projected profile. If nodeName is set, a direct Get is exact and cheaper. Keep the list path only for the empty-nodeName test case.

♻️ Proposed refactor
 func (l *InformerLister) HostIPs() []string {
-	nodes, err := l.nodes.List(labels.Everything())
-	if err != nil {
-		return nil
+	var nodes []*corev1.Node
+	if l.nodeName != "" {
+		n, err := l.nodes.Get(l.nodeName)
+		if err != nil {
+			return nil
+		}
+		nodes = []*corev1.Node{n}
+	} else {
+		var err error
+		nodes, err = l.nodes.List(labels.Everything())
+		if err != nil {
+			return nil
+		}
 	}
 	var ips []string
 	for _, n := range nodes {
-		if l.nodeName != "" && n.Name != l.nodeName {
-			continue
-		}
 		for _, addr := range n.Status.Addresses {
🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. 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/networkpeer/lister.go` around lines 76 - 98, Update
InformerLister.HostIPs to use nodes.Get(nodeName) and process only that node
when nodeName is set, preserving the existing address and gateway aggregation
and nil-on-error behavior. Retain nodes.List(labels.Everything()) only when
nodeName is empty, so the all-nodes path remains unchanged.
pkg/objectcache/addr_ports_test.go (1)

34-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the all-nil-ports case.

The tests pin two ends of the wildcard boundary: an absent Ports stanza is a wildcard, and a nil Port pointer beside a valid entry contributes nothing. The remaining boundary is a neighbor whose Ports stanza is present but where every entry has a nil Port. That input must not collapse to the wildcard form, for the same reason as TestExtractAddrPorts_ZeroPortIsALiteralNotAWildcard. Pin the expected result.

💚 Proposed test addition
func TestExtractAddrPorts_AllNilPortsIsNotAWildcard(t *testing.T) {
	groups := ExtractAddrPorts([]v1beta1.NetworkNeighbor{
		{IPAddresses: []string{"10.1.2.3"}, Ports: []v1beta1.NetworkPort{{Protocol: "TCP", Port: nil}}},
	})
	assert.Len(t, groups, 1)
	assert.NotNil(t, groups[0].Ports, "a present Ports stanza must not become a wildcard")
	assert.Empty(t, groups[0].Ports)
}
🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. 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/objectcache/addr_ports_test.go` around lines 34 - 43, Add a test
alongside TestExtractAddrPorts_NilPortEntryContributesNothing for a neighbor
whose present Ports stanza contains only nil Port pointers, asserting one group
with a non-nil, empty Ports map; ensure this does not become the wildcard form,
consistent with TestExtractAddrPorts_ZeroPortIsALiteralNotAWildcard.
pkg/rulemanager/cel/libraries/containerprofilenetwork/network.go (1)

248-268: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the contradictory doc comment on namespaceSelectorMatches.

Two comment blocks state opposite semantics. Lines 248-254 say a nil selector "matches only the profiled workload's own namespace". Lines 255-258 say a nil selector does not consult the namespace at all. The code implements the second form: sel == nil returns true for any namespace. Keep only the second block so the documented fail-open scope of this security-relevant matcher matches the code. Consider also marking profileNs as intentionally unused, since it is now dead.

🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. 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/rulemanager/cel/libraries/containerprofilenetwork/network.go` around
lines 248 - 268, Remove the first contradictory documentation block above
namespaceSelectorMatches, retaining only the comment that describes nil
selectors returning true without consulting the namespace. In
namespaceSelectorMatches, explicitly mark the unused profileNs parameter as
intentional without changing matching behavior.
🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. 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 `@charts/kubescape-rules/templates/rules.yaml`:
- Around line 307-356: Set enabled to false for the R0011 “Unexpected Egress
Network Traffic” and R0012 “Unexpected Ingress Network Traffic” rules,
preserving all other rule configuration so operators must explicitly opt in.

In `@pkg/containerprofilemanager/v1/container_data_service_test.go`:
- Around line 16-20: Update the fakeServiceClient struct by removing its unused
namespace and name fields, while preserving the selector and labels fields and
all existing behavior.

In `@pkg/objectcache/projection_types.go`:
- Around line 18-21: Extend PeerSelector to retain NetworkNeighbor.Ports, then
update the selector-matching predicate to enforce the stored port and protocol
constraints alongside pod and namespace selectors. Preserve existing selector
behavior when no port constraints are declared, and add coverage for a matching
pod using an undeclared port.

In `@tests/component_test.go`:
- Around line 4164-4182: Add a require.Eventually storage-readiness wait after
creating the ContainerProfile and before applying the suite, matching the
existing waits used by Test 21 and Test 50. Update the waitDeploy rollout
predicate to require status.observedGeneration >= metadata.generation in
addition to the existing ReadyReplicas and UpdatedReplicas checks, ensuring the
patched deployment generation has been observed before proceeding.

In `@tests/resources/containerprofile-user-defined-network.yaml`:
- Around line 63-93: Remove the redundant second cluster-dns egress entry from
the user-defined network fixture, preserving the existing cluster-dns CIDR entry
and its UDP/TCP port 53 restrictions.

In `@tests/resources/network_fixture_lint_test.go`:
- Around line 214-216: Update the port validation comment and diagnostic in the
fixture lint test so port 0 is described as a literal accepted value, not an
any-port wildcard; preserve the existing range validation and acceptance of 0.

In `@tests/testutils/k8s.go`:
- Around line 108-135: The multi-document parsing loop must use
k8s.io/apimachinery/pkg/util/yaml.NewYAMLReader instead of strings.Split, so
only valid YAML document boundaries are separated and sequences such as “\n----”
remain intact. Update the raw-input reader and loop while preserving the
existing YAML-to-JSON conversion, object mapping, and resource creation
behavior.

---

Nitpick comments:
In `@pkg/networkpeer/lister.go`:
- Around line 76-98: Update InformerLister.HostIPs to use nodes.Get(nodeName)
and process only that node when nodeName is set, preserving the existing address
and gateway aggregation and nil-on-error behavior. Retain
nodes.List(labels.Everything()) only when nodeName is empty, so the all-nodes
path remains unchanged.

In `@pkg/networkpeer/perf_bench_test.go`:
- Around line 232-245: Update TestInformerCacheMemoryEstimate to skip when
testing.Short() is true, before running the memory-building and
garbage-collection measurement logic; preserve the existing diagnostic behavior
for explicit non-short runs.

In `@pkg/objectcache/addr_ports_test.go`:
- Around line 34-43: Add a test alongside
TestExtractAddrPorts_NilPortEntryContributesNothing for a neighbor whose present
Ports stanza contains only nil Port pointers, asserting one group with a
non-nil, empty Ports map; ensure this does not become the wildcard form,
consistent with TestExtractAddrPorts_ZeroPortIsALiteralNotAWildcard.

In `@pkg/rulemanager/cel/libraries/containerprofilenetwork/network.go`:
- Around line 248-268: Remove the first contradictory documentation block above
namespaceSelectorMatches, retaining only the comment that describes nil
selectors returning true without consulting the namespace. In
namespaceSelectorMatches, explicitly mark the unused profileNs parameter as
intentional without changing matching behavior.
🪄 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: 4f01ac73-775f-4fc5-8da1-7d3324dacbd5

📥 Commits

Reviewing files that changed from the base of the PR and between 6fb4444 and 96cb0b9.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (61)
  • .github/workflows/component-tests.yaml
  • charts/kubescape-rules/Chart.yaml
  • charts/kubescape-rules/templates/binding.yaml
  • charts/kubescape-rules/templates/rules.yaml
  • charts/kubescape-rules/values.yaml
  • cmd/main.go
  • go.mod
  • pkg/config/config.go
  • pkg/config/config_test.go
  • pkg/containerprofilemanager/v1/container_data.go
  • pkg/containerprofilemanager/v1/container_data_service_test.go
  • pkg/networkpeer/exclude_namespaces_test.go
  • pkg/networkpeer/expand.go
  • pkg/networkpeer/expand_test.go
  • pkg/networkpeer/lister.go
  • pkg/networkpeer/lister_test.go
  • pkg/networkpeer/perf_bench_test.go
  • pkg/networkpeer/resolve.go
  • pkg/networkpeer/resolve_test.go
  • pkg/objectcache/addr_ports_test.go
  • pkg/objectcache/containerprofilecache/containerprofilecache.go
  • pkg/objectcache/containerprofilecache/projection_apply.go
  • pkg/objectcache/containerprofilecache/projection_golden_test.go
  • pkg/objectcache/containerprofilecache/reconciler.go
  • pkg/objectcache/containerprofilecache/resolvedgen_test.go
  • pkg/objectcache/containerprofilecache/testdata/golden/network_all.json
  • pkg/objectcache/containerprofilecache/testdata/golden/rich_filtered.json
  • pkg/objectcache/containerprofilecache/testdata/golden/rich_passthrough.json
  • pkg/objectcache/projection_types.go
  • pkg/objectcache/v1/mock.go
  • pkg/rulemanager/cel/libraries/cache/function_cache.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/containerprofilenetwork.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/cost_test.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/integration_test.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/legacy_test.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/network.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/network_test.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/port_protocol_test.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/selector_eval_test.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/selector_test.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/wildcard_test.go
  • pkg/rulemanager/cel/selector_compile_test.go
  • pkg/utils/cel.go
  • tests/chart/templates/node-agent/clusterrole.yaml
  • tests/chart/templates/node-agent/configmap.yaml
  • tests/chart/templates/node-agent/default-rule-binding.yaml
  • tests/chart/templates/node-agent/default-rules.yaml
  • tests/chart/values.yaml
  • tests/component_test.go
  • tests/resources/containerprofile-user-defined-network.yaml
  • tests/resources/network-default-client.yaml
  • tests/resources/network-hostnet-client.yaml
  • tests/resources/network-loopback-pod.yaml
  • tests/resources/network_fixture_lint_test.go
  • tests/resources/rules_chart_drift_test.go
  • tests/resources/serviceref-k6.yaml
  • tests/resources/serviceref-suite/00-flux-source-crds.yaml
  • tests/resources/serviceref-suite/10-helm-repo.yaml
  • tests/resources/serviceref-suite/20-source-controller.yaml
  • tests/scripts/storage-tag.sh
  • tests/testutils/k8s.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread charts/kubescape-rules/templates/rules.yaml Outdated
Comment thread pkg/containerprofilemanager/v1/container_data_service_test.go
Comment thread pkg/objectcache/projection_types.go
Comment thread tests/component_test.go
Comment thread tests/resources/containerprofile-user-defined-network.yaml
Comment thread tests/resources/network_fixture_lint_test.go Outdated
Comment thread tests/testutils/k8s.go
@entlein
entlein force-pushed the feat/network-v2 branch 2 times, most recently from b8e9154 to 96cb0b9 Compare August 26, 2026 17:40
entlein and others added 2 commits August 26, 2026 19:50
…cape#925)

Switch the replace directive from the personal matthyx/inspektor-gadget fork to
the org-owned kubescape/inspektor-gadget at the same commit main pins post-kubescape#925
(v0.0.0-20260826074832-06b0d12baca0). The fork module content is identical
(same go.mod hash), only the org path changed; go.sum h1 matches kubescape#925. Node-agent
builds clean against it. No rebase onto main for kubescape#924 — the syscall poll-interval
change is orthogonal to the network feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
A third copy of the ruleset — a verbatim duplicate of the test chart's
default-rules.yaml, guarded by a drift test that existed only to protect the
copy, and published by no workflow. The rules belong in kubescape/rulelibrary,
which already ships R0011/R0012 with per-rule tests and is what this repo syncs
FROM. Neither path exists on main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread tests/chart/templates/node-agent/configmap.yaml
Comment thread tests/chart/values.yaml
entlein and others added 2 commits August 26, 2026 21:08
Port-aware selector matching (CodeRabbit Major): PeerSelector dropped Ports, so
was_selector_in_{egress,ingress} allowed a matching pod on ANY port/protocol —
asymmetric with the port-aware address matcher. Thread Ports through the selector
projection (extractPeers + mock) and the CEL matcher, mirroring AddrPortGroup's
nil=any / empty=nothing convention, and add event.dstPort/event.proto to the
was_selector_in calls in the rules. New unit test covers nil/declared/undeclared/
wrong-proto/empty-map port cases.

Also from review:
- loopback aliases: unit test pinning that 127.0.0.1/127.0.0.53/::1/0.0.0.0 are
  now learned (subject to R0011/R0012) after the learn-drop removal.
- serviceref-k6: 30m -> 5m load duration (the test tears down its ns in ~3m).
- containerprofile-user-defined-network: de-dup the cluster-dns identifier.
- network_fixture_lint: port 0 is a literal, not the any-port wildcard (absent
  ports stanza is); widen the range check to 0..65535.
- drop unused fakeServiceClient fields (golangci-lint).
- regenerate projection golden for the new PeerSelector.Ports field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
…Rabbit)

ApplyMultiDocYAML split on the literal "\n---", which mis-splits any document
that contains "---" (a "----" log line, a multi-line string, or "---" not at
column 0). Use k8s.io/apimachinery/pkg/util/yaml.NewYAMLReader, which honours the
YAML document-separator semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
@entlein
entlein marked this pull request as ready for review August 26, 2026 19:34

@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
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. 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 `@tests/chart/templates/node-agent/default-rules.yaml`:
- Line 341: Update the R0012 rule expression to exclude loopback traffic by
adding the destination-address predicate requiring event.dstAddr not to start
with “127.”, while preserving the existing HOST, ingress-port/protocol, and
selector checks.
🪄 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: 25f84087-cd67-41f8-8080-93e6c015c7d4

📥 Commits

Reviewing files that changed from the base of the PR and between 96cb0b9 and 1001913.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (18)
  • go.mod
  • pkg/containerprofilemanager/v1/container_data_service_test.go
  • pkg/objectcache/containerprofilecache/projection_apply.go
  • pkg/objectcache/containerprofilecache/testdata/golden/network_all.json
  • pkg/objectcache/projection_types.go
  • pkg/objectcache/v1/mock.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/containerprofilenetwork.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/integration_test.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/legacy_test.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/network.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/selector_eval_test.go
  • pkg/rulemanager/cel/libraries/containerprofilenetwork/selector_test.go
  • pkg/rulemanager/cel/selector_compile_test.go
  • tests/chart/templates/node-agent/default-rules.yaml
  • tests/resources/containerprofile-user-defined-network.yaml
  • tests/resources/network_fixture_lint_test.go
  • tests/resources/serviceref-k6.yaml
  • tests/testutils/k8s.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

uniqueId: "event.dstAddr + '_' + string(event.dstPort) + '_' + event.proto"
ruleExpression:
- eventType: "network"
expression: "event.pktType == 'HOST' && !cp.was_address_port_protocol_in_ingress(event.containerId, event.dstAddr, event.dstPort, event.proto) && !cp.was_selector_in_ingress(event.containerId, event.dstNamespace, event.dstPodLabels, event.dstPort, event.proto)"

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

Exclude loopback traffic in R0012.

Line 341 evaluates loopback HOST traffic. The R0012 description says loopback is excluded. The compilation test also includes !event.dstAddr.startsWith('127.').

Restore the loopback predicate in the shipped rule.

Proposed fix
-              expression: "event.pktType == 'HOST' && !cp.was_address_port_protocol_in_ingress(event.containerId, event.dstAddr, event.dstPort, event.proto) && !cp.was_selector_in_ingress(event.containerId, event.dstNamespace, event.dstPodLabels, event.dstPort, event.proto)"
+              expression: "event.pktType == 'HOST' && !event.dstAddr.startsWith('127.') && !cp.was_address_port_protocol_in_ingress(event.containerId, event.dstAddr, event.dstPort, event.proto) && !cp.was_selector_in_ingress(event.containerId, event.dstNamespace, event.dstPodLabels, event.dstPort, event.proto)"
🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

In `@tests/chart/templates/node-agent/default-rules.yaml` at line 341, Update the
R0012 rule expression to exclude loopback traffic by adding the
destination-address predicate requiring event.dstAddr not to start with “127.”,
while preserving the existing HOST, ingress-port/protocol, and selector checks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: WIP

Development

Successfully merging this pull request may close these issues.

2 participants