Skip to content

Add CIDR-based collapsing for NetworkNeighbors#348

Merged
matthyx merged 8 commits into
mainfrom
feat/networkneighbors-cidr-collapsing
Jul 25, 2026
Merged

Add CIDR-based collapsing for NetworkNeighbors#348
matthyx merged 8 commits into
mainfrom
feat/networkneighbors-cidr-collapsing

Conversation

@matthyx

@matthyx matthyx commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds CIDR-based collapsing for NetworkNeighbor entries, extending the existing path-collapsing pattern (CollapseConfiguration CRD, TTL-cached provider, PreSave hook) to the network side. Real-world case: node-agent bakes the literal destination IP into the NetworkNeighbor identifier hash, so every distinct external IP produces a separate entry — a real profile hit 8,687 such entries and was flagged too-large. See docs/features/networkneighbors-collapsing.md for the full mechanism, config knobs, and explicit accepted trade-offs.
  • Fixes a pre-existing bug found while validating this feature: compareNP (test helper) compared p1's Egress/Ingress against itself instead of p2, silently making every compareNP-based assertion in TestGenerateNetworkPolicy a no-op for years. Fixing it surfaced a real (harmless) latent bug — mergeIngressRulesByPorts/mergeEgressRulesByPorts could duplicate the same IPBlock peer 2-3x when the same IP appeared across multiple distinct NetworkNeighbor entries — now deduped.

Test plan

  • go build ./... passes
  • go vet ./... passes (no new warnings; one pre-existing unrelated warning in callstack_test.go)
  • go test ./... passes, including new unit tests for the collapse algorithm (idempotency, floor-bucketing, multi-bucket field replication), policy-generation tests for the new IPAddresses-consuming path, and the corrected TestGenerateNetworkPolicy fixtures
  • hack/verify-codegen.sh passes (CRD field additions regenerated)
  • Independently verified against the approved implementation plan (all 12 acceptance criteria checked with file/line evidence, idempotency traced rather than just test-passed)

Known follow-up (not fixed in this PR, flagged during investigation): TestGenerateNetworkPolicy/real_duplicate_bug_test is flaky due to hash() using encoding/gob over a struct containing a map — Go's randomized map iteration order occasionally produces different hashes for the same logical rule. Unrelated to this PR's changes.

🤖 Generated with Claude Code

AI-skills: oh-my-claudecode:plan,oh-my-claudecode:team | cmds: /oh-my-claudecode:deep-interview

Summary by CodeRabbit

  • New Features
    • Added CIDR-based collapsing for network neighbors with configurable thresholds for group sizing and minimum CIDR prefix.
    • Network policy generation now supports the plural IPAddresses field for ingress/egress peers (including CIDRs and wildcard).
  • Bug Fixes
    • Avoids duplicate IPBlock peers during rule merging and improves determinism of generated policies.
  • Documentation
    • Added a dedicated guide for CIDR-based collapsing and updated configuration samples/checklists.
  • Tests & Performance
    • Expanded rule/collapse coverage with new unit tests and added benchmarks for the collapsing steps.

@matthyx matthyx added the ai-assisted Created through Armosec AI tooling (armosec-shared-rules plugin) label Jul 22, 2026
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds configurable CIDR collapsing for high-cardinality NetworkNeighbor entries, propagates settings through CRD and processor paths, and updates NetworkPolicy generation to consume plural IP addresses, CIDRs, and wildcard peers.

Changes

NetworkNeighbor CIDR collapsing

Layer / File(s) Summary
Collapse configuration and validation
pkg/apis/softwarecomposition/..., pkg/registry/file/dynamicpathdetector/..., pkg/registry/softwarecomposition/collapseconfiguration/..., artifacts/collapseconfiguration-default-sample.yaml
Adds network collapse thresholds to API types, runtime settings, conversions, defaults, CRD round-tripping, validation, and the sample configuration.
CIDR aggregation algorithm
pkg/registry/file/networkneighborhood_ipcollapse.go, pkg/registry/file/networkneighborhood_ipcollapse_test.go, pkg/registry/file/networkneighborhood_ipcollapse_bench_test.go, docs/features/networkneighbors-collapsing.md
Groups neighbors by communication metadata and selectors, aggregates IPv4 hosts into floor-constrained CIDRs, preserves non-aggregatable values, emits deterministic identifiers, and tests fixpoint behavior, metadata retention, and performance.
Processor and storage integration
pkg/registry/file/networkneighborhood_processor.go, pkg/registry/file/containerprofile_processor.go, pkg/apiserver/apiserver.go, pkg/registry/file/*_test.go
Threads collapse settings through deflation, applies collapsing before persistence, and wires the CRD-backed settings provider into storage.
Plural IP policy generation
pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy.go, pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy_test.go
Builds policy peers from CIDRs, wildcard values, and bare IPv4 addresses, preserves bare-IP enrichment, suppresses empty rules, deduplicates IPBlock peers, and uses deterministic hashing.

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

Sequence Diagram(s)

sequenceDiagram
  participant NetworkNeighborhoodProcessor
  participant collapseIPGroups
  participant generateEgressRule
  participant buildIPAddressesPeers
  NetworkNeighborhoodProcessor->>collapseIPGroups: Collapse deflated neighbor hosts
  collapseIPGroups-->>NetworkNeighborhoodProcessor: Return CIDR-bearing IPAddresses
  NetworkNeighborhoodProcessor->>generateEgressRule: Generate policy rule from neighbor
  generateEgressRule->>buildIPAddressesPeers: Parse CIDRs, wildcard, or bare IPv4
  buildIPAddressesPeers-->>generateEgressRule: Return peers and PolicyRefs
Loading

Possibly related PRs

  • kubescape/storage#324: Introduces the plural NetworkNeighbor.IPAddresses field consumed by this change.
  • kubescape/storage#325: Provides related CollapseConfiguration and NetworkNeighbor.IPAddresses plumbing extended here with network-specific collapsing.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.50% which is insufficient. The required threshold is 80.00%. 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 change: adding CIDR-based collapsing for NetworkNeighbors.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/networkneighbors-cidr-collapsing

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.

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

🧹 Nitpick comments (3)
pkg/registry/file/networkneighborhood_ipcollapse.go (1)

177-202: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Simplify commonPrefixLen with math/bits.

The manual byte/bit loop is correct (verified against the threshold/bucketing tests), but math/bits.LeadingZeros8 removes the hand-rolled bit-counting and the associated risk of a subtle off-by-one regression on future edits.

♻️ Proposed simplification
+import "math/bits"
+
 func commonPrefixLen(addrs []netip.Addr) int {
 	base := addrs[0].As4()
 	common := 32
 	for _, addr := range addrs[1:] {
 		b := addr.As4()
 		n := 0
 		for i := 0; i < 4 && n < common; i++ {
 			x := base[i] ^ b[i]
-			if x == 0 {
-				n += 8
-				continue
-			}
-			for bit := 7; bit >= 0; bit-- {
-				if x&(1<<uint(bit)) != 0 {
-					break
-				}
-				n++
-			}
-			break
+			n += bits.LeadingZeros8(x)
+			if x != 0 {
+				break
+			}
 		}
 		if n < common {
 			common = n
 		}
 	}
 	return common
 }
🤖 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/registry/file/networkneighborhood_ipcollapse.go` around lines 177 - 202,
Update commonPrefixLen to use math/bits.LeadingZeros8 when calculating the first
differing byte’s shared-bit count, replacing the manual inner bit loop while
preserving the existing prefix-length and threshold behavior.
pkg/registry/file/networkneighborhood_ipcollapse_test.go (1)

136-157: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test comment doesn't match generated data.

The comment says the second batch spans "16.15.183.x plus neighboring /24s", but with i capped at 249, i/256 is always 0, so every address lands in 16.15.180.x only — no distinct /24s are actually exercised. The assertions still pass, but the test doesn't exercise the multi-subnet scenario it claims to.

♻️ Suggested fix to actually span multiple /24s
-	for i := 0; i < 250; i++ {
-		in = append(in, hostNeighbor(fmt.Sprintf("16.15.%d.%d", 180+i/256, i%256)))
-	}
+	for i := 0; i < 500; i++ {
+		in = append(in, hostNeighbor(fmt.Sprintf("16.15.%d.%d", 180+i/256, i%256)))
+	}
🤖 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/registry/file/networkneighborhood_ipcollapse_test.go` around lines 136 -
157, Correct the second data-generation loop in
TestCollapseIPGroups_RealWorldShapeOrdersOfMagnitude so its generated addresses
actually span 16.15.183.x and neighboring /24 subnets as described, while
retaining roughly 250 inputs and the existing assertions.
pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy.go (1)

360-368: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated skipPorts block between egress/ingress.

The same 6-line buildIPAddressesPeers + skip-check block is repeated verbatim in generateEgressRule and generateIngressRule. Given generateEgressRule/generateIngressRule already largely mirror each other, extracting this into a small shared helper (e.g. resolvePeersAndSkip(neighbor, knownServers) (peers, refs, skip)) would remove the duplication introduced by this change and reduce the risk of the two copies drifting apart on a future edit.

Also applies to: 451-459

🤖 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/apis/softwarecomposition/networkpolicy/v2/networkpolicy.go` around lines
360 - 368, The duplicated IP-address peer resolution and skip-check logic in
generateEgressRule and generateIngressRule should be extracted into one shared
helper, such as resolvePeersAndSkip. Have both rule generators use the helper to
obtain peers, policy references, and the skipPorts result, preserving the
existing behavior and removing both inline copies.
🤖 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.

Nitpick comments:
In `@pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy.go`:
- Around line 360-368: The duplicated IP-address peer resolution and skip-check
logic in generateEgressRule and generateIngressRule should be extracted into one
shared helper, such as resolvePeersAndSkip. Have both rule generators use the
helper to obtain peers, policy references, and the skipPorts result, preserving
the existing behavior and removing both inline copies.

In `@pkg/registry/file/networkneighborhood_ipcollapse_test.go`:
- Around line 136-157: Correct the second data-generation loop in
TestCollapseIPGroups_RealWorldShapeOrdersOfMagnitude so its generated addresses
actually span 16.15.183.x and neighboring /24 subnets as described, while
retaining roughly 250 inputs and the existing assertions.

In `@pkg/registry/file/networkneighborhood_ipcollapse.go`:
- Around line 177-202: Update commonPrefixLen to use math/bits.LeadingZeros8
when calculating the first differing byte’s shared-bit count, replacing the
manual inner bit loop while preserving the existing prefix-length and threshold
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c1e20614-f36d-44c5-86db-aa361d0b7dfb

📥 Commits

Reviewing files that changed from the base of the PR and between b35788b and d0390e6.

⛔ Files ignored due to path filters (3)
  • pkg/apis/softwarecomposition/v1beta1/generated.pb.go is excluded by !**/*.pb.go
  • pkg/generated/applyconfiguration/softwarecomposition/v1beta1/collapseconfigurationspec.go is excluded by !**/generated/**
  • pkg/generated/openapi/zz_generated.openapi.go is excluded by !**/generated/**
📒 Files selected for processing (19)
  • docs/features/networkneighbors-collapsing.md
  • pkg/apis/softwarecomposition/collapse_types.go
  • pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy.go
  • pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy_test.go
  • pkg/apis/softwarecomposition/v1beta1/collapse_types.go
  • pkg/apis/softwarecomposition/v1beta1/generated.proto
  • pkg/apis/softwarecomposition/v1beta1/zz_generated.conversion.go
  • pkg/apiserver/apiserver.go
  • pkg/registry/file/containerprofile_processor.go
  • pkg/registry/file/containerprofile_processor_test.go
  • pkg/registry/file/dynamicpathdetector/collapse_config_from_crd.go
  • pkg/registry/file/dynamicpathdetector/tests/collapse_config_crd_test.go
  • pkg/registry/file/dynamicpathdetector/types.go
  • pkg/registry/file/networkneighborhood_ipcollapse.go
  • pkg/registry/file/networkneighborhood_ipcollapse_test.go
  • pkg/registry/file/networkneighborhood_processor.go
  • pkg/registry/file/networkneighborhood_processor_test.go
  • pkg/registry/softwarecomposition/collapseconfiguration/strategy.go
  • pkg/registry/softwarecomposition/collapseconfiguration/strategy_test.go

@matthyx

matthyx commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Good catch — confirmed. Added a third commit (2fee917) fixing the actual root cause: `hash()` gob-encoded rules/policyRefs for dedup, but gob's map encoding follows Go's randomized map iteration order, so a rule containing a `LabelSelector.MatchLabels` map could hash differently across otherwise-identical calls. That's real product nondeterminism (generated NetworkPolicies weren't byte-stable run-to-run), not just a test artifact, as you noted.

Fix: switched `hash()` from `encoding/gob` to `encoding/json`, which sorts map keys deterministically.

Verification:

  • Before: `go test -run TestGenerateNetworkPolicy -count=40` failed 3/40 on `real_duplicate_bug_test`.
  • After: `go test -run TestGenerateNetworkPolicy -count=100` — 0/100 failures.
  • Full `go build ./...` and relevant `go test ./...` still pass.

matthyx and others added 3 commits July 22, 2026 13:27
Node-agent bakes the literal destination IP into the NetworkNeighbor
identifier hash for external traffic, so every distinct observed IP
produces a separate entry - a real profile hit 8,687 such entries and
was flagged too-large. Extend the existing path-collapsing pattern
(CollapseConfiguration CRD, TTL-cached provider, PreSave hook) to
NetworkNeighbor: group entries by (Type, DNS, NamespaceSelector,
PodSelector), aggregate IPv4 hosts into CIDR blocks once a configurable
count threshold is exceeded, bounded by a configurable breadth floor.
Held-stable CIDRs and a deterministic collapsed Identifier keep the
pass idempotent across repeated saves. Policy generation now consumes
the plural IPAddresses field so generated NetworkPolicies still reflect
collapsed ranges, with known-server enrichment preserved for bare IPs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
…ison

compareNP compared p1's Egress/Ingress against itself instead of p2,
making every compareNP-based assertion in TestGenerateNetworkPolicy a
no-op (pre-existing, unrelated to any recent feature work). Fixing the
comparison surfaced a real latent bug: mergeIngressRulesByPorts and
mergeEgressRulesByPorts appended every IPBlock peer sharing a port key
without deduping, so the same IP declared across multiple distinct
NetworkNeighbor entries (e.g. seen from different containers) ended up
duplicated 2-3x in the merged rule. Harmless semantically but clearly
unintended. Add containsIPBlockPeer and use it at both merge sites, and
correct the 3 stale test fixtures that had assumed a merged-multi-port
rule shape the merge functions never actually produce (confirmed by the
existing, always-passing TestMergeIngressRulesByPorts/
TestMergeEgressRulesByPorts, which document the real one-rule-per-port
design).

A separate, unrelated flaky test (TestGenerateNetworkPolicy/
real_duplicate_bug_test) was found during investigation: hash() uses
encoding/gob on a struct containing a map, so Go's randomized map
iteration order occasionally produces two different hashes for the
same logical rule. Not fixed here - left as a follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
hash() gob-encoded rules/policyRefs to dedupe them, but gob's map
encoding follows Go's randomized map iteration order. A rule containing
a LabelSelector's MatchLabels map could therefore hash differently
across otherwise-identical calls, so GenerateNetworkPolicy's dedup-by-
hash sometimes kept a duplicate identical rule and sometimes didn't -
reproduced reliably under `go test -run TestGenerateNetworkPolicy
-count=40` (3/40 failures on TestGenerateNetworkPolicy/
real_duplicate_bug_test before this fix, 0/100 after).

This was real product nondeterminism (generated NetworkPolicies weren't
byte-stable run-to-run), not just test flakiness. Switch hash() to
json.Marshal, which sorts map keys, giving a stable encoding regardless
of map iteration order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
@matthyx
matthyx force-pushed the feat/networkneighbors-cidr-collapsing branch from 2fee917 to a5b922e Compare July 22, 2026 11:27
@github-actions

Copy link
Copy Markdown

Summary:

  • License scan: failure
  • Credentials scan: failure
  • Vulnerabilities scan: failure
  • Unit test: success
  • Go linting: failure

@matthyx matthyx moved this to Needs Reviewer in KS PRs tracking Jul 22, 2026
Comment thread docs/features/networkneighbors-collapsing.md Outdated
/16 delivers the most aggressive entry-count reduction (the real
8,687-entry motivating case collapses to ~10 entries) but at the cost
of much broader, less precise CIDR blocks. Measured against that same
real-world IP set, a /24 floor still collapses it to ~341 entries (down
from 8,687) while keeping blocks 256x tighter. 341 is an acceptable
trade-off, so make /24 the default; /16 remains available for
operators who want maximum compaction.

Also fill in the two new fields in the sample CollapseConfiguration
YAML, which was never updated when they were introduced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>

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

🤖 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 `@docs/features/networkneighbors-collapsing.md`:
- Line 83: Update the CIDR breadth statement near the NetworkNeighbor validation
guidance to clarify that emitted CIDR blocks must not be broader than the
configured floor, or equivalently that their prefix length must be greater than
or equal to floorBits; preserve the default /24 reference.
- Line 75: Update the configuration refresh statement in
networkneighbors-collapsing.md to clarify that changed CRD singleton values take
effect after the provider’s cache TTL expires, while preserving that no operator
restart is required.
- Line 19: Update the CIDR collapsing documentation to state that non-empty
buckets may over-approximate the observed hosts by allowing the entire CIDR
range, rather than claiming the same destination ranges are preserved. Clearly
describe this allowlist broadening for generated NetworkPolicies and retain the
existing efficiency explanation.
- Around line 30-33: The documentation uses inconsistent threshold semantics for
network-neighbor collapsing. Update the aggregation description and the “50+
hosts” example to consistently reflect the implementation’s boundary behavior,
explicitly stating whether exactly NetworkIPGroupThreshold aggregatable hosts
trigger collapsing.

In `@pkg/registry/file/networkneighborhood_processor_test.go`:
- Around line 166-168: Update the test assertion sequence around the collapsed
result `got` so the length check is fatal before accessing `got[0]`; replace the
non-fatal length assertion with the appropriate fatal assertion while preserving
the existing empty-IP and CIDR validations.
🪄 Autofix (Beta)

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: dd85722f-9ea0-45d1-915f-0f57939c094f

📥 Commits

Reviewing files that changed from the base of the PR and between d0390e6 and 7142969.

⛔ Files ignored due to path filters (3)
  • pkg/apis/softwarecomposition/v1beta1/generated.pb.go is excluded by !**/*.pb.go
  • pkg/generated/applyconfiguration/softwarecomposition/v1beta1/collapseconfigurationspec.go is excluded by !**/generated/**
  • pkg/generated/openapi/zz_generated.openapi.go is excluded by !**/generated/**
📒 Files selected for processing (20)
  • artifacts/collapseconfiguration-default-sample.yaml
  • docs/features/networkneighbors-collapsing.md
  • pkg/apis/softwarecomposition/collapse_types.go
  • pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy.go
  • pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy_test.go
  • pkg/apis/softwarecomposition/v1beta1/collapse_types.go
  • pkg/apis/softwarecomposition/v1beta1/generated.proto
  • pkg/apis/softwarecomposition/v1beta1/zz_generated.conversion.go
  • pkg/apiserver/apiserver.go
  • pkg/registry/file/containerprofile_processor.go
  • pkg/registry/file/containerprofile_processor_test.go
  • pkg/registry/file/dynamicpathdetector/collapse_config_from_crd.go
  • pkg/registry/file/dynamicpathdetector/tests/collapse_config_crd_test.go
  • pkg/registry/file/dynamicpathdetector/types.go
  • pkg/registry/file/networkneighborhood_ipcollapse.go
  • pkg/registry/file/networkneighborhood_ipcollapse_test.go
  • pkg/registry/file/networkneighborhood_processor.go
  • pkg/registry/file/networkneighborhood_processor_test.go
  • pkg/registry/softwarecomposition/collapseconfiguration/strategy.go
  • pkg/registry/softwarecomposition/collapseconfiguration/strategy_test.go
🚧 Files skipped from review as they are similar to previous changes (14)
  • pkg/registry/file/dynamicpathdetector/types.go
  • pkg/apis/softwarecomposition/v1beta1/collapse_types.go
  • pkg/registry/file/dynamicpathdetector/tests/collapse_config_crd_test.go
  • pkg/registry/softwarecomposition/collapseconfiguration/strategy_test.go
  • pkg/registry/file/containerprofile_processor.go
  • pkg/registry/file/containerprofile_processor_test.go
  • pkg/registry/file/networkneighborhood_ipcollapse.go
  • pkg/apis/softwarecomposition/v1beta1/zz_generated.conversion.go
  • pkg/registry/file/networkneighborhood_processor.go
  • pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy.go
  • pkg/registry/file/networkneighborhood_ipcollapse_test.go
  • pkg/registry/file/dynamicpathdetector/collapse_config_from_crd.go
  • pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy_test.go
  • pkg/apis/softwarecomposition/v1beta1/generated.proto


Before collapsing, external-traffic profiles with 8,687 individual IP entries consumed excessive storage, slowed queries, and produced NetworkPolicies with thousands of rules — most of which could be expressed as a handful of CIDR blocks.

Collapsing reduces entry count by orders of magnitude for traffic aimed at cloud IP blocks (e.g. `100.68.24.0/22`, `16.15.183.0/24`) while preserving policy correctness: generated NetworkPolicies still include the same destination ranges, just expressed more efficiently as CIDR blocks instead of `/32` host routes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Document CIDR policy over-approximation.

A non-empty /24 bucket can contain only a few observed hosts, yet the generated IPBlock permits the entire /24. Therefore, the claim that policy correctness is preserved with the “same destination ranges” is not strictly true. Explicitly document this allowlist broadening, or only emit CIDRs whose address space is fully covered by observed hosts.

Also applies to: 30-33

🤖 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 `@docs/features/networkneighbors-collapsing.md` at line 19, Update the CIDR
collapsing documentation to state that non-empty buckets may over-approximate
the observed hosts by allowing the entire CIDR range, rather than claiming the
same destination ranges are preserved. Clearly describe this allowlist
broadening for generated NetworkPolicies and retain the existing efficiency
explanation.

Comment on lines +30 to +33
**Aggregation**: If a group's count of aggregatable IPv4 host addresses exceeds `NetworkIPGroupThreshold`:
1. Compute the smallest covering prefix (common leading bits across all hosts).
2. If that prefix's length is at least `NetworkCIDRFloorBits` (e.g., `/24` or longer), emit it as-is.
3. If it would be broader than `NetworkCIDRFloorBits`, split the group into floor-length buckets (e.g., `/24` buckets) and emit one CIDR entry per non-empty bucket.

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

Make threshold semantics consistent.

The text says collapsing occurs when the count “exceeds” the threshold, which implies > 50, while the example says “50+ hosts,” which implies >= 50. Clarify whether exactly 50 aggregatable hosts trigger collapsing and align both descriptions with the implementation.

Also applies to: 71-72

🤖 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 `@docs/features/networkneighbors-collapsing.md` around lines 30 - 33, The
documentation uses inconsistent threshold semantics for network-neighbor
collapsing. Update the aggregation description and the “50+ hosts” example to
consistently reflect the implementation’s boundary behavior, explicitly stating
whether exactly NetworkIPGroupThreshold aggregatable hosts trigger collapsing.

networkCIDRFloorBits: 24 # No block narrower than /24
```

Zero or omitted values use the compiled-in defaults (50 and 24 respectively). No operator restart is required; the provider reads the singleton at each request.

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

Correct the configuration refresh semantics.

The provider is TTL-cached, so it does not read the CRD singleton on every request. State that changes take effect after the cache TTL while still not requiring an operator restart.

🤖 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 `@docs/features/networkneighbors-collapsing.md` at line 75, Update the
configuration refresh statement in networkneighbors-collapsing.md to clarify
that changed CRD singleton values take effect after the provider’s cache TTL
expires, while preserving that no operator restart is required.


**Entry count**: Compare entry counts before and after enabling the feature on a real profile with external traffic. A profile with 8,687 external-traffic entries should drop to a small number (typically hundreds or fewer CIDR entries, depending on traffic distribution).

**CIDR breadth**: Inspect emitted `NetworkNeighbor` entries; no single `IPAddresses` CIDR block should exceed the configured floor (default `/24`).

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

Clarify the floor verification wording.

“Should not exceed the configured floor” is ambiguous: /28 numerically exceeds /24 but is valid. Use “must not be broader than the configured floor” or “prefix length must be greater than or equal to floorBits.”

🤖 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 `@docs/features/networkneighbors-collapsing.md` at line 83, Update the CIDR
breadth statement near the NetworkNeighbor validation guidance to clarify that
emitted CIDR blocks must not be broader than the configured floor, or
equivalently that their prefix length must be greater than or equal to
floorBits; preserve the default /24 reference.

Comment on lines +166 to +168
assert.Len(t, got, 1, "expected all same-group host IPs to collapse into a single CIDR entry")
assert.Empty(t, got[0].IPAddress)
assert.Equal(t, []string{"10.0.0.0/26"}, got[0].IPAddresses)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid indexing after a non-fatal length assertion.

If collapsing returns zero entries, assert.Len records a failure but execution continues and got[0] panics. Use a fatal length check before indexing.

Proposed fix
-	assert.Len(t, got, 1, "expected all same-group host IPs to collapse into a single CIDR entry")
+	if len(got) != 1 {
+		t.Fatalf("expected one collapsed entry, got %d", len(got))
+	}
📝 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
assert.Len(t, got, 1, "expected all same-group host IPs to collapse into a single CIDR entry")
assert.Empty(t, got[0].IPAddress)
assert.Equal(t, []string{"10.0.0.0/26"}, got[0].IPAddresses)
if len(got) != 1 {
t.Fatalf("expected one collapsed entry, got %d", len(got))
}
assert.Empty(t, got[0].IPAddress)
assert.Equal(t, []string{"10.0.0.0/26"}, got[0].IPAddresses)
🤖 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/registry/file/networkneighborhood_processor_test.go` around lines 166 -
168, Update the test assertion sequence around the collapsed result `got` so the
length check is fatal before accessing `got[0]`; replace the non-fatal length
assertion with the appropriate fatal assertion while preserving the existing
empty-IP and CIDR validations.

@github-actions

Copy link
Copy Markdown

Summary:

  • License scan: failure
  • Credentials scan: failure
  • Vulnerabilities scan: failure
  • Unit test: success
  • Go linting: failure

entlein added 2 commits July 23, 2026 19:54
Signed-off-by: entlein <einentlein@gmail.com>
Signed-off-by: entlein <einentlein@gmail.com>
entlein and others added 2 commits July 24, 2026 12:32
Signed-off-by: entlein <einentlein@gmail.com>
…apse-dedup

collapse dedup (also known as "greedy bucket")
@matthyx
matthyx merged commit 72c61d4 into main Jul 25, 2026
4 of 5 checks passed
@matthyx
matthyx deleted the feat/networkneighbors-cidr-collapsing branch July 25, 2026 08:08

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/registry/file/containerprofile_processor_test.go (1)

28-63: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid indexing after a non-fatal length assertion.

If collapsing unexpectedly returns zero entries, assert.Len at line 54 records a failure but execution continues, and result.Ingress[0] panics on the next line. Use a fatal check (require.Len) before indexing, mirroring the same fix needed in the sibling test in pkg/registry/file/networkneighborhood_processor_test.go.

Proposed fix
-	assert.Len(t, result.Ingress, 1, "expected all same-group host IPs to collapse into a single CIDR entry")
+	require.Len(t, result.Ingress, 1, "expected all same-group host IPs to collapse into a single CIDR entry")
🤖 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/registry/file/containerprofile_processor_test.go` around lines 28 - 63,
Replace the non-fatal assert.Len check in
TestDeflateContainerProfileSpec_NetworkNeighborsCollapse with require.Len before
accessing result.Ingress[0], and add or reuse the test package’s require
assertion import as needed. Preserve the existing expected length and subsequent
assertions, matching the sibling network-neighborhood test.
🧹 Nitpick comments (2)
pkg/registry/file/networkneighborhood_ipcollapse.go (2)

204-208: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Silent data loss on IPSet() build error.

If b.IPSet() errors (or returns a nil set), coverPrefixes returns nil. Back in collapseIPGroups, that discards the whole group's aggregated values — sentinels are still appended, but if there are none, the group's ports/DNS/selectors data for every host/CIDR in it vanishes from the output entirely, with no error surfaced. Given this feeds NetworkPolicy generation, a silently dropped peer set means broken/incomplete policies rather than a visible failure.

In practice this path is unlikely to trigger since all inputs are pre-validated netip.Prefix/Addr values, but a defensive fallback (log + fall back to the original g.entries, or to the un-covered host/CIDR strings) would avoid a full-group silent drop.

🤖 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/registry/file/networkneighborhood_ipcollapse.go` around lines 204 - 208,
Update the IPSet handling in coverPrefixes so an error or nil set is logged and
falls back to the original group entries (or their un-covered host/CIDR strings)
instead of returning nil. Ensure collapseIPGroups preserves the group’s ports,
DNS, and selectors when coverage construction fails, while retaining the
existing covered-prefix path for successful builds.

113-125: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider combining a group's values into a single NetworkNeighbor instead of one per value.

Each collapsed value (v) currently produces its own NetworkNeighbor, duplicating dnsNames/ports/selectors across N entries. collapsedIdentifier already accepts a []string, which suggests batching the whole values slice into one entry (IPAddresses: values) was a viable design.

This matters because NetworkNeighborhoodProcessor.PreSave measures profile size as len(Ingress)/len(Egress) (entry count), not address count. For scattered-host groups (e.g. TestCollapseIPGroups_ScatteredHostsBucketedToFloor, 60 buckets from 60 hosts), this yields zero net reduction in the size metric that gates ObjectTooLargeError, partially defeating the collapsing goal for high-cardinality scattered traffic.

Note: several existing tests (e.g. ScatteredHostsBucketedToFloor, RealWorldShapeOrdersOfMagnitude) assert one-entry-per-value output today, so this would require updating those expectations alongside the code change.

♻️ Sketch of combined-entry approach
-		for _, v := range values {
-			out = append(out, softwarecomposition.NetworkNeighbor{
-				Identifier:        collapsedIdentifier(g.repType, g.repDNS, g.repNsSel, g.repPodSel, []string{v}),
-				Type:              g.repType,
-				DNS:               g.repDNS,
-				DNSNames:          append([]string(nil), dnsNames...),
-				Ports:             append([]softwarecomposition.NetworkPort(nil), ports...),
-				PodSelector:       g.repPodSel,
-				NamespaceSelector: g.repNsSel,
-				IPAddress:         "",
-				IPAddresses:       []string{v},
-			})
-		}
+		out = append(out, softwarecomposition.NetworkNeighbor{
+			Identifier:        collapsedIdentifier(g.repType, g.repDNS, g.repNsSel, g.repPodSel, values),
+			Type:              g.repType,
+			DNS:               g.repDNS,
+			DNSNames:          dnsNames,
+			Ports:             ports,
+			PodSelector:       g.repPodSel,
+			NamespaceSelector: g.repNsSel,
+			IPAddress:         "",
+			IPAddresses:       values,
+		})
🤖 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/registry/file/networkneighborhood_ipcollapse.go` around lines 113 - 125,
Update the value-collapsing loop in the relevant network-neighborhood collapse
function to emit one NetworkNeighbor for the entire values slice, using
collapsedIdentifier with values and assigning IPAddresses to that slice while
preserving the shared DNS, ports, and selector fields. Update affected tests,
including scattered-host and real-world shape cases, to expect a single combined
entry rather than one entry per value.
🤖 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.

Outside diff comments:
In `@pkg/registry/file/containerprofile_processor_test.go`:
- Around line 28-63: Replace the non-fatal assert.Len check in
TestDeflateContainerProfileSpec_NetworkNeighborsCollapse with require.Len before
accessing result.Ingress[0], and add or reuse the test package’s require
assertion import as needed. Preserve the existing expected length and subsequent
assertions, matching the sibling network-neighborhood test.

---

Nitpick comments:
In `@pkg/registry/file/networkneighborhood_ipcollapse.go`:
- Around line 204-208: Update the IPSet handling in coverPrefixes so an error or
nil set is logged and falls back to the original group entries (or their
un-covered host/CIDR strings) instead of returning nil. Ensure collapseIPGroups
preserves the group’s ports, DNS, and selectors when coverage construction
fails, while retaining the existing covered-prefix path for successful builds.
- Around line 113-125: Update the value-collapsing loop in the relevant
network-neighborhood collapse function to emit one NetworkNeighbor for the
entire values slice, using collapsedIdentifier with values and assigning
IPAddresses to that slice while preserving the shared DNS, ports, and selector
fields. Update affected tests, including scattered-host and real-world shape
cases, to expect a single combined entry rather than one entry per value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 690873b4-055c-4fcc-a619-ce4c9c14e27d

📥 Commits

Reviewing files that changed from the base of the PR and between 7142969 and 67e466c.

📒 Files selected for processing (7)
  • go.mod
  • pkg/registry/file/containerprofile_processor_test.go
  • pkg/registry/file/dynamicpathdetector/types.go
  • pkg/registry/file/networkneighborhood_ipcollapse.go
  • pkg/registry/file/networkneighborhood_ipcollapse_bench_test.go
  • pkg/registry/file/networkneighborhood_ipcollapse_test.go
  • pkg/registry/file/networkneighborhood_processor_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/registry/file/dynamicpathdetector/types.go

@github-actions

Copy link
Copy Markdown

Summary:

  • License scan: failure
  • Credentials scan: failure
  • Vulnerabilities scan: failure
  • Unit test: success
  • Go linting: failure

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

Projects

Status: Needs Reviewer

Development

Successfully merging this pull request may close these issues.

2 participants