Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ resources/ebpf/falco/*
node-agent
__pycache__
tracers.tar
vendor
vendor
.claude/
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -507,4 +507,4 @@ replace github.com/inspektor-gadget/inspektor-gadget => github.com/matthyx/inspe

replace github.com/cilium/ebpf => github.com/matthyx/ebpf v0.0.0-20260421101317-8a32d06def6c

replace github.com/kubescape/storage => github.com/k8sstormcenter/storage v0.0.240-0.20260509184329-a7e6234349ab
replace github.com/kubescape/storage => github.com/k8sstormcenter/storage v0.0.240-0.20260510175248-02c4438f072f
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -981,8 +981,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/k8sstormcenter/storage v0.0.240-0.20260509184329-a7e6234349ab h1:DNjKAs888GzW7P9gJUKtldL6E7zYzjLiO6pVUTvnzqc=
github.com/k8sstormcenter/storage v0.0.240-0.20260509184329-a7e6234349ab/go.mod h1:amdg/Qok9bqPzs1vZH5FW9/3MbCawc5wVsz9u3uIfu4=
github.com/k8sstormcenter/storage v0.0.240-0.20260510175248-02c4438f072f h1:TaffnMdzqwUKfWgjIcjorDjJRhJD99ISzK3NzxZIq1c=
github.com/k8sstormcenter/storage v0.0.240-0.20260510175248-02c4438f072f/go.mod h1:amdg/Qok9bqPzs1vZH5FW9/3MbCawc5wVsz9u3uIfu4=
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 h1:WdAeg/imY2JFPc/9CST4bZ80nNJbiBFCAdSZCSgrS5Y=
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953/go.mod h1:6o+UrvuZWc4UTyBhQf0LGjW9Ld7qJxLz/OqvSOWWlEc=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
Expand Down
239 changes: 239 additions & 0 deletions pkg/rulemanager/cel/libraries/networkneighborhood/fixtures_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
package networkneighborhood

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/google/cel-go/common/types"
"github.com/kubescape/node-agent/pkg/rulemanager/cel/libraries/cache"
"github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1"
"github.com/stretchr/testify/require"
"sigs.k8s.io/yaml"
)

// TestFixturesParse validates that every YAML fixture under
// tests/resources/network-wildcards/ parses against the v1beta1
// NetworkNeighborhood schema. This is the user-facing-examples gate:
// the fixtures double as authoritative syntax documentation, so a
// fixture that fails to parse is a documentation bug.
//
// Fixture 14 (recursive-star-rejected) parses but its dnsNames entry
// '**' is rejected at admission time — see the storage REST strategy
// validation test (TestValidate_NetworkProfileEntries).
func TestFixturesParse(t *testing.T) {
fixturesDir := findFixturesDir(t)
entries, err := os.ReadDir(fixturesDir)
require.NoError(t, err)

if len(entries) == 0 {
t.Fatalf("no fixtures found under %s", fixturesDir)
}

parsed := 0
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".yaml") {
continue
}
name := e.Name()
t.Run(name, func(t *testing.T) {
data, err := os.ReadFile(filepath.Join(fixturesDir, name))
require.NoError(t, err)

// Strip the literal "{{NAMESPACE}}" placeholder; the fixtures
// are templates, runtime substitutes a real namespace.
data = []byte(strings.ReplaceAll(string(data), "{{NAMESPACE}}", "test-ns"))

var nn v1beta1.NetworkNeighborhood
// Strict mode: any unknown field in a fixture is a typo
// against the v1beta1 schema. Documentation must not drift
// from the runtime types.
err = yaml.UnmarshalStrict(data, &nn)
require.NoError(t, err, "fixture %s must parse against v1beta1 schema (strict)", name)
require.Equal(t, "NetworkNeighborhood", nn.Kind, "fixture %s wrong kind", name)
require.NotEmpty(t, nn.Spec.Containers, "fixture %s should declare at least one container", name)
})
parsed++
}
if parsed < 20 {
t.Errorf("expected ≥ 20 fixtures, parsed %d", parsed)
}
}

// TestFixturesMatchExpectedBehaviour walks a curated subset of fixtures
// through the actual CEL library matchers, asserting the documented
// observed→match behaviour from each fixture's header comment.
//
// This is the contract pin between the user-facing examples and the
// runtime: if a fixture says "10.1.2.3 → match" and the matcher
// disagrees, ONE of them is wrong. Today both are pinned by this test.
//
// Coverage: representative cases for each major edge case. Not every
// (fixture × observation) is exercised — that would be brittle as
// the fixtures evolve.
func TestFixturesMatchExpectedBehaviour(t *testing.T) {
cases := []struct {
name string
neighbors []v1beta1.NetworkNeighbor
ingress []v1beta1.NetworkNeighbor
// ipChecks verifies wasAddressInEgress only (back-compat for cases
// with no ingress declared; runs only the egress matcher).
ipChecks []ipCheck
// ipBothChecks verifies BOTH wasAddressInEgress and wasAddressInIngress
// — used for direction-isolation cases so the assertion goes both ways.
ipBothChecks []ipBothCheck
dnsChecks []dnsCheck
}{
{
name: "fixture-01-literal-ipv4",
neighbors: []v1beta1.NetworkNeighbor{
{IPAddresses: []string{"10.1.2.3"}},
},
ipChecks: []ipCheck{
{"10.1.2.3", true},
{"10.1.2.4", false},
},
},
{
name: "fixture-03-cidr-ipv4",
neighbors: []v1beta1.NetworkNeighbor{
{IPAddresses: []string{"10.0.0.0/8"}},
},
ipChecks: []ipCheck{
{"10.0.0.0", true},
{"10.255.255.255", true},
{"11.0.0.1", false},
},
},
{
name: "fixture-05-any-ip-sentinel",
neighbors: []v1beta1.NetworkNeighbor{
{IPAddresses: []string{"*"}},
},
ipChecks: []ipCheck{
{"1.2.3.4", true},
{"::1", true},
},
},
{
name: "fixture-08-deprecated-ipaddress",
neighbors: []v1beta1.NetworkNeighbor{
{IPAddress: "10.1.2.3"}, // singular, deprecated form
},
ipChecks: []ipCheck{
{"10.1.2.3", true},
{"10.1.2.4", false},
},
},
{
name: "fixture-10-dns-leading-wildcard",
neighbors: []v1beta1.NetworkNeighbor{
{DNSNames: []string{"*.example.com."}},
},
dnsChecks: []dnsCheck{
{"api.example.com.", true},
{"v1.api.example.com.", false}, // RFC 4592: exactly one label
{"example.com.", false}, // zero labels
},
},
{
name: "fixture-18-cluster-dns-mid-ellipsis",
neighbors: []v1beta1.NetworkNeighbor{
{DNSNames: []string{"kubernetes.⋯.svc.cluster.local."}},
},
dnsChecks: []dnsCheck{
{"kubernetes.default.svc.cluster.local.", true},
{"kubernetes.kube-system.svc.cluster.local.", true},
{"redis.default.svc.cluster.local.", false},
},
},
{
name: "fixture-15-egress-and-ingress-direction-isolation",
neighbors: []v1beta1.NetworkNeighbor{
{IPAddresses: []string{"8.8.8.8"}},
},
ingress: []v1beta1.NetworkNeighbor{
{IPAddresses: []string{"10.244.0.0/16"}},
},
// Direction isolation: each address MUST hit only the direction
// it was declared on. CR (node-agent#41) flagged that the prior
// version only checked egress; this asserts ingress too.
ipBothChecks: []ipBothCheck{
{observed: "8.8.8.8", wantEgress: true, wantIngress: false}, // egress-only
{observed: "10.244.5.5", wantEgress: false, wantIngress: true}, // ingress-only
},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
lib := buildLibWithContainer(t, tc.neighbors, tc.ingress)
for _, c := range tc.ipChecks {
res := lib.wasAddressInEgress(types.String("cid"), types.String(c.observed))
res = cache.ConvertProfileNotAvailableErrToBool(res, false)
if res != types.Bool(c.want) {
t.Errorf("egress ip %q: got %v, want %v", c.observed, res, c.want)
}
}
for _, c := range tc.ipBothChecks {
eg := lib.wasAddressInEgress(types.String("cid"), types.String(c.observed))
eg = cache.ConvertProfileNotAvailableErrToBool(eg, false)
if eg != types.Bool(c.wantEgress) {
t.Errorf("egress ip %q: got %v, want %v", c.observed, eg, c.wantEgress)
}
in := lib.wasAddressInIngress(types.String("cid"), types.String(c.observed))
in = cache.ConvertProfileNotAvailableErrToBool(in, false)
if in != types.Bool(c.wantIngress) {
t.Errorf("ingress ip %q: got %v, want %v", c.observed, in, c.wantIngress)
}
}
for _, c := range tc.dnsChecks {
res := lib.isDomainInEgress(types.String("cid"), types.String(c.observed))
res = cache.ConvertProfileNotAvailableErrToBool(res, false)
if res != types.Bool(c.want) {
t.Errorf("dns %q: got %v, want %v", c.observed, res, c.want)
}
}
})
}
}

type ipCheck struct {
observed string
want bool
}

type ipBothCheck struct {
observed string
wantEgress bool
wantIngress bool
}

type dnsCheck struct {
observed string
want bool
}

// findFixturesDir walks up from the test's working directory to locate
// tests/resources/network-wildcards/. The package's own working dir
// when `go test` runs is its source dir, so we walk up to find the
// repo root.
func findFixturesDir(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
require.NoError(t, err)
for i := 0; i < 10; i++ {
candidate := filepath.Join(dir, "tests", "resources", "network-wildcards")
if _, err := os.Stat(candidate); err == nil {
return candidate
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
t.Fatalf("could not find tests/resources/network-wildcards/ from %s", dir)
return ""
}
Loading
Loading