diff --git a/cmd/migration/main.go b/cmd/migration/main.go index 1b9584840..435f7f6c3 100644 --- a/cmd/migration/main.go +++ b/cmd/migration/main.go @@ -1,8 +1,8 @@ package main // migration is a standalone utility to decode legacy Gob data into JSON. -// It is used to handle breaking changes in the Gob format (e.g., transitioning -// fields from uint64 to int64) that cannot be handled by the main service +// It is used to handle breaking changes in the Gob format (e.g., transitioning +// fields from uint64 to int64) that cannot be handled by the main service // due to Go's global Gob type registration constraints. import ( @@ -42,38 +42,6 @@ type LegacyOpenCalls struct { Flags []string `json:"Flags"` } -type LegacyApplicationProfileContainer struct { - Name string `json:"Name"` - Capabilities []string `json:"Capabilities"` - Execs []LegacyExecCalls `json:"Execs"` - Opens []LegacyOpenCalls `json:"Opens"` - Syscalls []string `json:"Syscalls"` - SeccompProfile LegacySingleSeccompProfile `json:"SeccompProfile"` - Endpoints []softwarecomposition.HTTPEndpoint `json:"Endpoints"` - ImageID string `json:"ImageID"` - ImageTag string `json:"ImageTag"` - PolicyByRuleId map[string]softwarecomposition.RulePolicy `json:"PolicyByRuleId"` - IdentifiedCallStacks []softwarecomposition.IdentifiedCallStack `json:"IdentifiedCallStacks"` -} - -type LegacyApplicationProfileSpec struct { - Architectures []string `json:"Architectures,omitempty"` - Containers []LegacyApplicationProfileContainer `json:"Containers,omitempty"` - InitContainers []LegacyApplicationProfileContainer `json:"InitContainers,omitempty"` - EphemeralContainers []LegacyApplicationProfileContainer `json:"EphemeralContainers,omitempty"` -} - -type LegacyApplicationProfile struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:",inline"` - - // +k8s:conversion-gen=false - Parts map[string]string `json:"Parts,omitempty"` - // +k8s:conversion-gen=false - SchemaVersion int64 `json:"SchemaVersion,omitempty"` - Spec LegacyApplicationProfileSpec `json:"Spec,omitempty"` -} - type LegacySingleSeccompProfile struct { Name string `json:"Name"` Path string `json:"Path"` @@ -100,34 +68,34 @@ type LegacyContainerProfileSpec struct { ImageID string `json:"ImageID"` ImageTag string `json:"ImageTag"` PolicyByRuleId map[string]softwarecomposition.RulePolicy `json:"PolicyByRuleId"` - IdentifiedCallStacks []softwarecomposition.IdentifiedCallStack `json:"IdentifiedCallStacks"` + IdentifiedCallStacks []softwarecomposition.IdentifiedCallStack `json:"IdentifiedCallStacks"` metav1.LabelSelector `json:"LabelSelector"` - Ingress []LegacyNetworkNeighbor `json:"Ingress"` - Egress []LegacyNetworkNeighbor `json:"Egress"` + Ingress []LegacyNetworkNeighbor `json:"Ingress"` + Egress []LegacyNetworkNeighbor `json:"Egress"` } type LegacyContainerProfile struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:",inline"` - Spec LegacyContainerProfileSpec `json:"Spec,omitempty"` + Spec LegacyContainerProfileSpec `json:"Spec,omitempty"` Status softwarecomposition.ContainerProfileStatus `json:"Status,omitempty"` } type LegacyNetworkPort struct { Name string `json:"Name"` Protocol string `json:"Protocol"` - Port *int32 `json:"Port"` + Port *int32 `json:"Port"` } type LegacyNetworkNeighbor struct { - Identifier string `json:"Identifier"` - Type string `json:"Type"` - DNS string `json:"DNS"` - DNSNames []string `json:"DNSNames"` - Ports []LegacyNetworkPort `json:"Ports"` - PodSelector *metav1.LabelSelector `json:"PodSelector"` - NamespaceSelector *metav1.LabelSelector `json:"NamespaceSelector"` - IPAddress string `json:"IPAddress"` + Identifier string `json:"Identifier"` + Type string `json:"Type"` + DNS string `json:"DNS"` + DNSNames []string `json:"DNSNames"` + Ports []LegacyNetworkPort `json:"Ports"` + PodSelector *metav1.LabelSelector `json:"PodSelector"` + NamespaceSelector *metav1.LabelSelector `json:"NamespaceSelector"` + IPAddress string `json:"IPAddress"` } type LegacySeccompProfileSpec struct { @@ -139,17 +107,17 @@ type LegacySeccompProfileSpec struct { type LegacySeccompProfile struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:",inline"` - Spec LegacySeccompProfileSpec `json:"Spec,omitempty"` + Spec LegacySeccompProfileSpec `json:"Spec,omitempty"` Status softwarecomposition.SeccompProfileStatus `json:"Status,omitempty"` } func main() { filePath := flag.String("file", "", "Path to the gob file to decode") - typeName := flag.String("type", "ApplicationProfile", "Type to decode (ApplicationProfile, ContainerProfile, or SeccompProfile)") + typeName := flag.String("type", "ContainerProfile", "Type to decode (ContainerProfile or SeccompProfile)") flag.Parse() if *filePath == "" { - fmt.Fprintf(os.Stderr, "Usage: migration -file [-type ]\n") + fmt.Fprintf(os.Stderr, "Usage: migration -file [-type ]\n") os.Exit(1) } @@ -162,8 +130,6 @@ func main() { var result interface{} switch *typeName { - case "ApplicationProfile": - result = &LegacyApplicationProfile{} case "ContainerProfile": result = &LegacyContainerProfile{} case "SeccompProfile": @@ -176,7 +142,7 @@ func main() { // Important: We need to register types that might be in the gob stream // but are defined locally in this 'main' package to avoid name mismatches // although gob name matching is usually package-scoped. - // Since this is a separate binary, its 'main.LegacyApplicationProfile' + // Since this is a separate binary, its 'main.LegacyContainerProfile' // registration is isolated from the storage binary's registration. // Register common types that might be inside interface{} fields or nested structs diff --git a/cmd/migration/main_test.go b/cmd/migration/main_test.go new file mode 100644 index 000000000..03e9d8fe3 --- /dev/null +++ b/cmd/migration/main_test.go @@ -0,0 +1,286 @@ +package main + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kubescape/storage/pkg/apis/softwarecomposition" +) + +// The migration binary is a one-shot gob->JSON decoder. Its whole reason to +// exist is to read legacy gob streams whose numeric fields were encoded as +// uint64 (LegacyArg.Index/Value/ValueTwo, LegacySyscall.ErrnoRet) and re-emit +// them as JSON so they can be re-imported under the current int64 layout. These +// tests pin that contract end-to-end by driving the real built binary against +// fixtures, exercising both the happy path (legacy uint64 fields survive the +// round-trip, ContainerProfile spec shape is preserved) and the error branches +// (unsupported type, corrupt stream, missing file). + +var ( + migrationBinOnce sync.Once + migrationBinPath string + migrationBinErr error +) + +// buildMigrationBinary compiles cmd/migration once per test binary and returns +// the path to the built executable. It does not restructure or modify main.go. +func buildMigrationBinary(t *testing.T) string { + t.Helper() + migrationBinOnce.Do(func() { + dir, err := os.MkdirTemp("", "migration-bin") + if err != nil { + migrationBinErr = err + return + } + bin := filepath.Join(dir, "migration") + // Build the current package (cmd/migration). The test's working + // directory is the package directory. + cmd := exec.CommandContext(t.Context(), "go", "build", "-mod=mod", "-o", bin, ".") + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + migrationBinErr = err + t.Logf("go build stderr: %s", stderr.String()) + return + } + migrationBinPath = bin + }) + require.NoError(t, migrationBinErr, "failed to build migration binary") + return migrationBinPath +} + +// writeGobFixture gob-encodes the given value to a temp file, mirroring how the +// legacy storage binary persisted these structures. Because the fixture is +// encoded with the very LegacyContainerProfile/LegacySeccompProfile types the +// migration binary decodes into, gob's structural, name-scoped matching lets the +// stream decode cleanly - exactly as a real legacy stream would. +func writeGobFixture(t *testing.T, v interface{}) string { + t.Helper() + // The migration binary registers these types before decoding; register the + // same set here so the encoder and decoder agree on the wire format. + gob.Register(map[string]interface{}{}) + gob.Register([]interface{}{}) + gob.Register(metav1.Time{}) + + var buf bytes.Buffer + require.NoError(t, gob.NewEncoder(&buf).Encode(v)) + + path := filepath.Join(t.TempDir(), "fixture.gob") + require.NoError(t, os.WriteFile(path, buf.Bytes(), 0o600)) + return path +} + +// runMigration runs the built binary with the given args and returns +// stdout, stderr, and the process exit code. +func runMigration(t *testing.T, bin string, args ...string) (string, string, int) { + t.Helper() + cmd := exec.CommandContext(t.Context(), bin, args...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + code := 0 + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + code = exitErr.ExitCode() + } else { + t.Fatalf("failed to run migration binary: %v", err) + } + } + return stdout.String(), stderr.String(), code +} + +// legacyContainerProfileFixture builds a known legacy ContainerProfile that +// populates every legacy uint64-bearing field the migration must preserve. +func legacyContainerProfileFixture() *LegacyContainerProfile { + cp := &LegacyContainerProfile{ + TypeMeta: metav1.TypeMeta{ + Kind: "ContainerProfile", + APIVersion: "spdx.softwarecomposition.kubescape.io/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "replicaset-nginx-abc123-nginx-1a2b-3c4d", + Namespace: "kubescape", + Labels: map[string]string{ + "kubescape.io/workload-kind": "Deployment", + "kubescape.io/workload-name": "nginx", + }, + }, + } + cp.Spec.Architectures = []string{"amd64", "arm64"} + cp.Spec.Capabilities = []string{"NET_ADMIN", "SYS_PTRACE"} + cp.Spec.Execs = []LegacyExecCalls{{Path: "/bin/sh", Args: []string{"-c", "echo hi"}, Envs: []string{"PATH=/bin"}}} + cp.Spec.Opens = []LegacyOpenCalls{{Path: "/etc/passwd", Flags: []string{"O_RDONLY"}}} + cp.Spec.Syscalls = []string{"read", "write", "openat"} + cp.Spec.ImageID = "sha256:deadbeef" + cp.Spec.ImageTag = "nginx:1.25" + + // Legacy uint64 fields live under the seccomp profile's syscalls/args. + cp.Spec.SeccompProfile = LegacySingleSeccompProfile{ + Name: "nginx", + Path: "/nginx", + } + cp.Spec.SeccompProfile.Spec.DefaultAction = "SCMP_ACT_ERRNO" + cp.Spec.SeccompProfile.Spec.Architectures = []string{"SCMP_ARCH_X86_64"} + cp.Spec.SeccompProfile.Spec.Syscalls = []*LegacySyscall{ + { + Names: []string{"ptrace"}, + Action: "SCMP_ACT_ERRNO", + ErrnoRet: 1, // legacy uint64 + Args: []*LegacyArg{ + {Index: 0, Value: 18446744073709551615, ValueTwo: 42, Op: "SCMP_CMP_EQ"}, // max uint64 + }, + }, + } + + // A representative ingress neighbor to prove the network shape survives. + port := int32(443) + cp.Spec.Ingress = []LegacyNetworkNeighbor{ + { + Identifier: "in-1", + Type: "internal", + IPAddress: "10.0.0.5", + Ports: []LegacyNetworkPort{{Name: "TCP-443", Protocol: "TCP", Port: &port}}, + }, + } + return cp +} + +// TestMigration_GoldenContainerProfile is the golden migration test: a known +// legacy ContainerProfile gob stream decoded by the real binary must round-trip +// every legacy uint64 field and preserve the ContainerProfile spec shape as JSON. +func TestMigration_GoldenContainerProfile(t *testing.T) { + bin := buildMigrationBinary(t) + fixture := legacyContainerProfileFixture() + path := writeGobFixture(t, fixture) + + stdout, stderr, code := runMigration(t, bin, "-file", path, "-type", "ContainerProfile") + require.Equal(t, 0, code, "decode must succeed; stderr=%s", stderr) + require.NotEmpty(t, stdout) + + // The JSON must decode back into the same legacy layout: this proves each + // legacy field round-trips through gob->JSON without loss or type change. + var got LegacyContainerProfile + require.NoError(t, json.Unmarshal([]byte(stdout), &got), "output must be valid JSON: %s", stdout) + + // Object identity / spec shape. + assert.Equal(t, fixture.Name, got.Name) + assert.Equal(t, fixture.Namespace, got.Namespace) + assert.Equal(t, fixture.Labels, got.Labels) + assert.Equal(t, fixture.Spec.Architectures, got.Spec.Architectures) + assert.Equal(t, fixture.Spec.Capabilities, got.Spec.Capabilities) + assert.Equal(t, fixture.Spec.Syscalls, got.Spec.Syscalls) + assert.Equal(t, fixture.Spec.ImageID, got.Spec.ImageID) + assert.Equal(t, fixture.Spec.ImageTag, got.Spec.ImageTag) + require.Len(t, got.Spec.Execs, 1) + assert.Equal(t, "/bin/sh", got.Spec.Execs[0].Path) + require.Len(t, got.Spec.Opens, 1) + assert.Equal(t, "/etc/passwd", got.Spec.Opens[0].Path) + + // Legacy uint64 fields: the whole reason the tool exists. + require.Len(t, got.Spec.SeccompProfile.Spec.Syscalls, 1) + sc := got.Spec.SeccompProfile.Spec.Syscalls[0] + assert.Equal(t, uint64(1), sc.ErrnoRet, "LegacySyscall.ErrnoRet must round-trip") + require.Len(t, sc.Args, 1) + arg := sc.Args[0] + assert.Equal(t, uint64(0), arg.Index, "LegacyArg.Index must round-trip") + assert.Equal(t, uint64(18446744073709551615), arg.Value, "LegacyArg.Value must round-trip full uint64 range") + assert.Equal(t, uint64(42), arg.ValueTwo, "LegacyArg.ValueTwo must round-trip") + assert.Equal(t, "SCMP_CMP_EQ", arg.Op) + + // Network shape. + require.Len(t, got.Spec.Ingress, 1) + assert.Equal(t, "10.0.0.5", got.Spec.Ingress[0].IPAddress) + require.Len(t, got.Spec.Ingress[0].Ports, 1) + require.NotNil(t, got.Spec.Ingress[0].Ports[0].Port) + assert.Equal(t, int32(443), *got.Spec.Ingress[0].Ports[0].Port) + + // Also assert the raw JSON carries the legacy uint64 as a plain number under + // the documented json tag, so a consumer re-importing the JSON sees the full + // value verbatim (a float64 round-trip would lose the low bits of maxuint64). + assert.Contains(t, stdout, `"value":18446744073709551615`, "value json tag must carry full uint64") +} + +// TestMigration_GoldenSeccompProfile pins the standalone SeccompProfile decode +// path (the other supported -type), which also carries legacy uint64 ErrnoRet/Args. +func TestMigration_GoldenSeccompProfile(t *testing.T) { + bin := buildMigrationBinary(t) + + sp := &LegacySeccompProfile{ + TypeMeta: metav1.TypeMeta{Kind: "SeccompProfile"}, + ObjectMeta: metav1.ObjectMeta{Name: "nginx", Namespace: "kubescape"}, + } + single := LegacySingleSeccompProfile{Name: "nginx"} + single.Spec.DefaultAction = "SCMP_ACT_ERRNO" + single.Spec.Syscalls = []*LegacySyscall{ + { + Names: []string{"ptrace"}, + Action: "SCMP_ACT_ERRNO", + ErrnoRet: 13, + Args: []*LegacyArg{{Index: 1, Value: 100, ValueTwo: 200, Op: "SCMP_CMP_GE"}}, + }, + } + sp.Spec.Containers = []LegacySingleSeccompProfile{single} + + path := writeGobFixture(t, sp) + stdout, stderr, code := runMigration(t, bin, "-file", path, "-type", "SeccompProfile") + require.Equal(t, 0, code, "decode must succeed; stderr=%s", stderr) + + var got LegacySeccompProfile + require.NoError(t, json.Unmarshal([]byte(stdout), &got)) + require.Len(t, got.Spec.Containers, 1) + require.Len(t, got.Spec.Containers[0].Spec.Syscalls, 1) + sc := got.Spec.Containers[0].Spec.Syscalls[0] + assert.Equal(t, uint64(13), sc.ErrnoRet) + require.Len(t, sc.Args, 1) + assert.Equal(t, uint64(100), sc.Args[0].Value) + assert.Equal(t, uint64(200), sc.Args[0].ValueTwo) +} + +// TestMigration_UnsupportedType exercises the unsupported -type branch: the +// binary must reject it with a non-zero exit and a diagnostic on stderr. +func TestMigration_UnsupportedType(t *testing.T) { + bin := buildMigrationBinary(t) + // Any existing file works; the type is rejected before decode. + path := writeGobFixture(t, legacyContainerProfileFixture()) + stdout, stderr, code := runMigration(t, bin, "-file", path, "-type", "NotAType") + assert.NotEqual(t, 0, code, "unsupported type must be a non-zero exit") + assert.Empty(t, stdout) + assert.Contains(t, stderr, "unsupported type") +} + +// TestMigration_CorruptStream exercises the decode-failure branch: a stream that +// is not a valid gob for the requested type must fail with a non-zero exit. +func TestMigration_CorruptStream(t *testing.T) { + bin := buildMigrationBinary(t) + path := filepath.Join(t.TempDir(), "corrupt.gob") + require.NoError(t, os.WriteFile(path, []byte("this is not a gob stream at all"), 0o600)) + + stdout, stderr, code := runMigration(t, bin, "-file", path, "-type", "ContainerProfile") + assert.NotEqual(t, 0, code, "corrupt stream must be a non-zero exit") + assert.Empty(t, stdout) + assert.Contains(t, stderr, "decode failed") +} + +// TestMigration_MissingFile exercises the open-failure branch. +func TestMigration_MissingFile(t *testing.T) { + bin := buildMigrationBinary(t) + stdout, stderr, code := runMigration(t, bin, "-file", filepath.Join(t.TempDir(), "does-not-exist.gob"), "-type", "ContainerProfile") + assert.NotEqual(t, 0, code) + assert.Empty(t, stdout) + assert.Contains(t, stderr, "failed to open file") +} + +// Ensure the softwarecomposition import is used (the legacy types reference it), +// keeping the fixture honest about the shared spec types. +var _ = softwarecomposition.HTTPEndpoint{} diff --git a/pkg/apis/softwarecomposition/network_types.go b/pkg/apis/softwarecomposition/network_types.go index 1b4778652..76a1be969 100644 --- a/pkg/apis/softwarecomposition/network_types.go +++ b/pkg/apis/softwarecomposition/network_types.go @@ -18,44 +18,6 @@ const ( CommunicationTypeEgress CommunicationType = "external" ) -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// NetworkNeighborhoodList is a list of NetworkNeighborhoods. -type NetworkNeighborhoodList struct { - metav1.TypeMeta - metav1.ListMeta - - Items []NetworkNeighborhood -} - -// +genclient -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// NetworkNeighborhood represents a list of network communications for a specific workload. -type NetworkNeighborhood struct { - metav1.TypeMeta - metav1.ObjectMeta - - // +k8s:conversion-gen=false - Parts map[string]string - // +k8s:conversion-gen=false - SchemaVersion int64 - Spec NetworkNeighborhoodSpec -} - -type NetworkNeighborhoodSpec struct { - metav1.LabelSelector // The labels which are inside spec.selector in the parent workload. - Containers []NetworkNeighborhoodContainer - InitContainers []NetworkNeighborhoodContainer - EphemeralContainers []NetworkNeighborhoodContainer -} - -type NetworkNeighborhoodContainer struct { - Name string - Ingress []NetworkNeighbor - Egress []NetworkNeighbor -} - // NetworkNeighbor represents a single network communication made by this resource. type NetworkNeighbor struct { Identifier string diff --git a/pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy.go b/pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy.go index 9febd5121..8d5b8cead 100644 --- a/pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy.go +++ b/pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy.go @@ -21,33 +21,33 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func GenerateNetworkPolicy(nn *softwarecomposition.NetworkNeighborhood, knownServers softwarecomposition.IKnownServersFinder, timeProvider metav1.Time) (softwarecomposition.GeneratedNetworkPolicy, error) { - if !IsAvailable(nn) { - return softwarecomposition.GeneratedNetworkPolicy{}, fmt.Errorf("nn %s/%s status annotation is not ready nor completed", nn.Namespace, nn.Name) +func GenerateNetworkPolicy(cp *softwarecomposition.ContainerProfile, knownServers softwarecomposition.IKnownServersFinder, timeProvider metav1.Time) (softwarecomposition.GeneratedNetworkPolicy, error) { + if !IsAvailable(cp) { + return softwarecomposition.GeneratedNetworkPolicy{}, fmt.Errorf("container profile %s/%s status annotation is not ready nor completed", cp.Namespace, cp.Name) } // get name from labels and clean labels - kind, ok := nn.Labels[helpersv1.RelatedKindMetadataKey] + kind, ok := cp.Labels[helpersv1.RelatedKindMetadataKey] if !ok { - return softwarecomposition.GeneratedNetworkPolicy{}, fmt.Errorf("nn %s/%s does not have a kind label", nn.Namespace, nn.Name) + return softwarecomposition.GeneratedNetworkPolicy{}, fmt.Errorf("container profile %s/%s does not have a kind label", cp.Namespace, cp.Name) } - name, ok := nn.Labels[helpersv1.RelatedNameMetadataKey] + name, ok := cp.Labels[helpersv1.RelatedNameMetadataKey] if !ok { - logger.L().Debug("nn does not have a workload-name label, falling back to nn.Name", helpers.String("name", nn.Name), helpers.String("namespace", nn.Namespace)) - name = nn.Name + logger.L().Debug("container profile does not have a workload-name label, falling back to cp.Name", helpers.String("name", cp.Name), helpers.String("namespace", cp.Namespace)) + name = cp.Name } - delete(nn.Labels, helpersv1.TemplateHashKey) + delete(cp.Labels, helpersv1.TemplateHashKey) networkPolicy := softwarecomposition.NetworkPolicy{ Kind: "NetworkPolicy", APIVersion: "networking.k8s.io/v1", ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-%s", strings.ToLower(kind), name), - Namespace: nn.Namespace, + Namespace: cp.Namespace, Annotations: map[string]string{ "generated-by": "kubescape", }, - Labels: nn.Labels, + Labels: cp.Labels, }, Spec: softwarecomposition.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{}, @@ -58,12 +58,12 @@ func GenerateNetworkPolicy(nn *softwarecomposition.NetworkNeighborhood, knownSer }, } - if nn.Spec.MatchLabels != nil { - networkPolicy.Spec.PodSelector.MatchLabels = nn.Spec.MatchLabels + if cp.Spec.MatchLabels != nil { + networkPolicy.Spec.PodSelector.MatchLabels = cp.Spec.MatchLabels } - if nn.Spec.MatchExpressions != nil { - networkPolicy.Spec.PodSelector.MatchExpressions = nn.Spec.MatchExpressions + if cp.Spec.MatchExpressions != nil { + networkPolicy.Spec.PodSelector.MatchExpressions = cp.Spec.MatchExpressions } generatedNetworkPolicy := softwarecomposition.GeneratedNetworkPolicy{ @@ -72,9 +72,9 @@ func GenerateNetworkPolicy(nn *softwarecomposition.NetworkNeighborhood, knownSer APIVersion: "spdx.softwarecomposition.kubescape.io/v1beta1", }, ObjectMeta: metav1.ObjectMeta{ - Name: nn.Name, - Namespace: nn.Namespace, - Labels: nn.Labels, + Name: cp.Name, + Namespace: cp.Namespace, + Labels: cp.Labels, CreationTimestamp: timeProvider, }, PoliciesRef: []softwarecomposition.PolicyRef{}, @@ -82,7 +82,7 @@ func GenerateNetworkPolicy(nn *softwarecomposition.NetworkNeighborhood, knownSer ingressHash := make(map[string]bool) ingressPolicyRefsHash := make(map[string]bool) - for _, neighbor := range listIngressNetworkNeighbors(nn) { + for _, neighbor := range listIngressNetworkNeighbors(cp) { rule, policyRefs := generateIngressRule(neighbor, knownServers) @@ -104,7 +104,7 @@ func GenerateNetworkPolicy(nn *softwarecomposition.NetworkNeighborhood, knownSer egressHash := make(map[string]bool) egressPolicyRefsHash := make(map[string]bool) - for _, neighbor := range listEgressNetworkNeighbors(nn) { + for _, neighbor := range listEgressNetworkNeighbors(cp) { rule, policyRefs := generateEgressRule(neighbor, knownServers) @@ -132,34 +132,17 @@ func GenerateNetworkPolicy(nn *softwarecomposition.NetworkNeighborhood, knownSer return generatedNetworkPolicy, nil } -func listIngressNetworkNeighbors(nn *softwarecomposition.NetworkNeighborhood) []softwarecomposition.NetworkNeighbor { - var neighbors []softwarecomposition.NetworkNeighbor - for i := range nn.Spec.Containers { - neighbors = append(neighbors, nn.Spec.Containers[i].Ingress...) - } - for i := range nn.Spec.InitContainers { - neighbors = append(neighbors, nn.Spec.InitContainers[i].Ingress...) - } - for i := range nn.Spec.EphemeralContainers { - neighbors = append(neighbors, nn.Spec.EphemeralContainers[i].Ingress...) - } - return neighbors - +// listIngressNetworkNeighbors returns the ingress neighbors for the container +// profile. A ContainerProfile describes a single container, so its Spec.Ingress +// is the exact equivalent of the previously-flattened per-container ingress list. +func listIngressNetworkNeighbors(cp *softwarecomposition.ContainerProfile) []softwarecomposition.NetworkNeighbor { + return cp.Spec.Ingress } -func listEgressNetworkNeighbors(nn *softwarecomposition.NetworkNeighborhood) []softwarecomposition.NetworkNeighbor { - var neighbors []softwarecomposition.NetworkNeighbor - for i := range nn.Spec.Containers { - neighbors = append(neighbors, nn.Spec.Containers[i].Egress...) - } - for i := range nn.Spec.InitContainers { - neighbors = append(neighbors, nn.Spec.InitContainers[i].Egress...) - } - for i := range nn.Spec.EphemeralContainers { - neighbors = append(neighbors, nn.Spec.EphemeralContainers[i].Egress...) - } - return neighbors - +// listEgressNetworkNeighbors returns the egress neighbors for the container +// profile. See listIngressNetworkNeighbors for the single-container rationale. +func listEgressNetworkNeighbors(cp *softwarecomposition.ContainerProfile) []softwarecomposition.NetworkNeighbor { + return cp.Spec.Egress } // containsIPBlockPeer reports whether peers already contains an entry with the given CIDR. @@ -609,11 +592,11 @@ func removeLabels(labels map[string]string) { } } -func IsAvailable(nn *softwarecomposition.NetworkNeighborhood) bool { - if nn.GetAnnotations()[helpersv1.ManagedByMetadataKey] == helpersv1.ManagedByUserValue { +func IsAvailable(cp *softwarecomposition.ContainerProfile) bool { + if cp.GetAnnotations()[helpersv1.ManagedByMetadataKey] == helpersv1.ManagedByUserValue { return true } - switch nn.GetAnnotations()[helpersv1.StatusMetadataKey] { + switch cp.GetAnnotations()[helpersv1.StatusMetadataKey] { case helpersv1.Learning, helpersv1.Completed: return true default: diff --git a/pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy_file_test.go b/pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy_file_test.go new file mode 100644 index 000000000..dc5bca260 --- /dev/null +++ b/pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy_file_test.go @@ -0,0 +1,81 @@ +package networkpolicy + +import ( + _ "embed" + "encoding/json" + "testing" + + "github.com/kubescape/storage/pkg/apis/softwarecomposition" + softwarecompositionv1beta1 "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// cp-operator.json is the ContainerProfile-shaped migration of the pre-migration +// nn-operator.json NetworkNeighborhood fixture: the single container's ingress / +// egress moved into spec.ingress / spec.egress, the neighborhood selector into the +// spec's embedded label selector, and the container-name label / type annotation set. +// +//go:embed testdata/cp-operator.json +var containerProfileFile string + +// np-operator.json and np.new.json are the pre-migration golden GeneratedNetworkPolicy +// outputs, kept UNCHANGED. They are the diff-oracle: generating directly from the +// ContainerProfile must reproduce the exact network content that the removed +// NetworkNeighborhood intermediate produced. +// +//go:embed testdata/np-operator.json +var networkPolicyFile string + +//go:embed testdata/np.new.json +var networkPolicyNewFile string + +//go:embed testdata/known-servers.json +var knownServersFile string + +// TestGenerateNetworkPolicyFromFile is the migrated diff-oracle for the removed +// NetworkNeighborhood generation path. It loads the migrated ContainerProfile +// fixture, generates a policy directly from it, and asserts the generated +// ingress/egress rules match the pre-migration golden outputs (compared after the +// generator's own deterministic sort, exactly as the pre-migration file test did). +func TestGenerateNetworkPolicyFromFile(t *testing.T) { + timeProvider := metav1.Now() + + // The fixture is the served v1beta1 shape (matching the goldens and a real + // ContainerProfile CRD). Unmarshal it as v1beta1, then convert to the + // internal type the generator consumes — exactly as storage does on the read + // path. (Unmarshalling directly into the internal type would drop metadata: + // its embedded ObjectMeta carries no `json:"metadata"` tag.) + v1beta1Profile := &softwarecompositionv1beta1.ContainerProfile{} + if err := json.Unmarshal([]byte(containerProfileFile), v1beta1Profile); err != nil { + t.Fatalf("failed to unmarshal container profile fixture: %v", err) + } + containerProfile := &softwarecomposition.ContainerProfile{} + if err := softwarecompositionv1beta1.Convert_v1beta1_ContainerProfile_To_softwarecomposition_ContainerProfile(v1beta1Profile, containerProfile, nil); err != nil { + t.Fatalf("failed to convert container profile fixture to internal type: %v", err) + } + knownServers := []softwarecomposition.KnownServer{} + + if err := json.Unmarshal([]byte(knownServersFile), &knownServers); err != nil { + t.Fatalf("failed to unmarshal known servers fixture: %v", err) + } + + generatedNetworkPolicy, err := GenerateNetworkPolicy(containerProfile, softwarecomposition.NewKnownServersFinderImpl(knownServers), timeProvider) + if err != nil { + t.Fatalf("failed to generate network policy: %v", err) + } + + // The generated policy must reproduce the pre-migration golden output. + for name, golden := range map[string]string{ + "np-operator.json": networkPolicyFile, + "np.new.json": networkPolicyNewFile, + } { + t.Run(name, func(t *testing.T) { + expected := &softwarecomposition.GeneratedNetworkPolicy{} + if err := json.Unmarshal([]byte(golden), expected); err != nil { + t.Fatalf("failed to unmarshal golden %s: %v", name, err) + } + assert.Nil(t, compareNP(&generatedNetworkPolicy, expected), "generated policy diverged from golden %s", name) + }) + } +} diff --git a/pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy_test.go b/pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy_test.go index c489d9be7..84dafb748 100644 --- a/pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy_test.go +++ b/pkg/apis/softwarecomposition/networkpolicy/v2/networkpolicy_test.go @@ -22,14 +22,14 @@ func TestGenerateNetworkPolicy(t *testing.T) { tests := []struct { name string - networkNeighborhood softwarecomposition.NetworkNeighborhood + containerProfile softwarecomposition.ContainerProfile knownServers []softwarecomposition.KnownServer expectedNetworkPolicy softwarecomposition.GeneratedNetworkPolicy expectError bool }{ { name: "basic ingress rule", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-nginx", Namespace: "kubescape", @@ -41,28 +41,25 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "nginx", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "nginx", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + + Ingress: []softwarecomposition.NetworkNeighbor{ { - Ingress: []softwarecomposition.NetworkNeighbor{ + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "nginx", + }, + }, + Ports: []softwarecomposition.NetworkPort{ { - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": "nginx", - }, - }, - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(80), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-80", - }, - }, + Port: ptrToInt32(80), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-80", }, }, }, @@ -128,7 +125,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "network neighborhood not ready", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-nginx", Namespace: "kubescape", @@ -140,14 +137,14 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "nginx", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{}, + Spec: softwarecomposition.ContainerProfileSpec{}, }, expectedNetworkPolicy: softwarecomposition.GeneratedNetworkPolicy{}, expectError: true, }, { name: "network_policy_with_multiple_ports_and_labels", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-multi", Namespace: "kubescape", @@ -159,7 +156,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "multi", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "multi-app", @@ -173,35 +170,32 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + + Ingress: []softwarecomposition.NetworkNeighbor{ { - Ingress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "10.0.0.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(80)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-80", - }, - { - Port: ptr.To(int32(443)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-443", - }, - }, + Port: ptr.To(int32(80)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-80", + }, + { + Port: ptr.To(int32(443)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-443", }, }, - Egress: []softwarecomposition.NetworkNeighbor{ + }, + }, + Egress: []softwarecomposition.NetworkNeighbor{ + { + IPAddress: "192.168.1.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "192.168.1.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(8080)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8080", - }, - }, + Port: ptr.To(int32(8080)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8080", }, }, }, @@ -302,7 +296,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "policy_with_known_servers", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-known-servers", Namespace: "kubescape", @@ -314,38 +308,33 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "known-servers", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "known-app", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + + Ingress: []softwarecomposition.NetworkNeighbor{ { - Ingress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "10.0.0.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(80), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-80", - }, - }, + Port: ptrToInt32(80), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-80", }, }, }, + }, + Egress: []softwarecomposition.NetworkNeighbor{ { - Egress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "192.168.1.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "192.168.1.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(8080), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8080", - }, - }, + Port: ptrToInt32(8080), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8080", }, }, }, @@ -453,7 +442,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "policy_with_known_servers", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-known-servers", Namespace: "kubescape", @@ -465,38 +454,33 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "known-servers", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "known-app", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + + Ingress: []softwarecomposition.NetworkNeighbor{ { - Ingress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "10.0.0.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(80), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-80", - }, - }, + Port: ptrToInt32(80), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-80", }, }, }, + }, + Egress: []softwarecomposition.NetworkNeighbor{ { - Egress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "192.168.1.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "192.168.1.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(8080), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8080", - }, - }, + Port: ptrToInt32(8080), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8080", }, }, }, @@ -604,7 +588,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "policy_with_dns_neighbors", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-dns", Namespace: "kubescape", @@ -616,25 +600,21 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "dns", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "dns-app", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + Egress: []softwarecomposition.NetworkNeighbor{ { - Egress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "192.168.1.1", + DNS: "example.com", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "192.168.1.1", - DNS: "example.com", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(80)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-80", - }, - }, + Port: ptr.To(int32(80)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-80", }, }, }, @@ -704,7 +684,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "network_policy_with_multiple_containers", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-multi-container", Namespace: "kubescape", @@ -716,46 +696,40 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "multi-container", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "multi-container", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + + Ingress: []softwarecomposition.NetworkNeighbor{ { - Ingress: []softwarecomposition.NetworkNeighbor{ + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "nginx", + }, + }, + Ports: []softwarecomposition.NetworkPort{ { - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": "nginx", - }, - }, - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(80), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-80", - }, - }, + Port: ptrToInt32(80), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-80", }, }, }, + { - Ingress: []softwarecomposition.NetworkNeighbor{ + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "nginx", + }, + }, + Ports: []softwarecomposition.NetworkPort{ { - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": "nginx", - }, - }, - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(443), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-443", - }, - }, + Port: ptrToInt32(443), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-443", }, }, }, @@ -838,7 +812,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "network_policy_with_multiple_containers_with_same_ip", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-multi-containers", Namespace: "kubescape", @@ -850,69 +824,60 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "multi-containers", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "multi-container-app", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + + Ingress: []softwarecomposition.NetworkNeighbor{ { - Name: "container-1", - Ingress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "10.0.0.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(80)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-80", - }, - { - Port: ptr.To(int32(443)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-443", - }, - }, + Port: ptr.To(int32(80)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-80", + }, + { + Port: ptr.To(int32(443)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-443", }, }, - Egress: []softwarecomposition.NetworkNeighbor{ + }, + + { + IPAddress: "10.0.0.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "192.168.1.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(8080)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8080", - }, - }, + Port: ptr.To(int32(80)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-80", }, }, }, + }, + Egress: []softwarecomposition.NetworkNeighbor{ { - Name: "container-2", - Ingress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "192.168.1.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(80)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-80", - }, - }, + Port: ptr.To(int32(8080)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8080", }, }, - Egress: []softwarecomposition.NetworkNeighbor{ + }, + + { + IPAddress: "192.168.1.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "192.168.1.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(8080)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8080", - }, - }, + Port: ptr.To(int32(8080)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8080", }, }, }, @@ -1006,7 +971,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "network_policy_with_multiple_different_containers", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-multi-containers", Namespace: "kubescape", @@ -1018,203 +983,177 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "multi-containers", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "multi-container-app", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + + Ingress: []softwarecomposition.NetworkNeighbor{ { - Ingress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "10.0.0.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(80)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-80", - }, - { - Port: ptr.To(int32(443)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-443", - }, - }, + Port: ptr.To(int32(80)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-80", }, - }, - Egress: []softwarecomposition.NetworkNeighbor{ { - IPAddress: "192.168.1.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(8080)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8080", - }, - }, + Port: ptr.To(int32(443)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-443", }, }, }, + { - Ingress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "10.0.0.2", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.2", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(8081)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8081", - }, - }, + Port: ptr.To(int32(8081)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8081", }, }, - Egress: []softwarecomposition.NetworkNeighbor{ + }, + + { + IPAddress: "10.0.0.3", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "192.168.1.2", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(8082)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8082", - }, - }, + Port: ptr.To(int32(80)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-80", + }, + { + Port: ptr.To(int32(443)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-443", }, }, }, - }, - InitContainers: []softwarecomposition.NetworkNeighborhoodContainer{ { - Ingress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "10.0.0.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.3", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(80)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-80", - }, - { - Port: ptr.To(int32(443)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-443", - }, - }, + Port: ptr.To(int32(90)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-90", }, { - IPAddress: "10.0.0.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(90)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-90", - }, - { - Port: ptr.To(int32(443)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-443", - }, - }, + Port: ptr.To(int32(443)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-443", }, }, - Egress: []softwarecomposition.NetworkNeighbor{ + }, + + { + IPAddress: "10.0.0.2", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "192.168.1.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(8080)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8080", - }, - }, + Port: ptr.To(int32(8081)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8081", }, }, }, + { - Ingress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "10.0.0.4", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.2", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(8081)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8081", - }, - }, + Port: ptr.To(int32(80)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-80", + }, + { + Port: ptr.To(int32(443)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-443", }, }, - Egress: []softwarecomposition.NetworkNeighbor{ + }, + { + IPAddress: "10.0.0.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "192.168.1.2", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(8082)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8082", - }, - }, + Port: ptr.To(int32(100)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-100", + }, + { + Port: ptr.To(int32(443)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-443", + }, + }, + }, + + { + IPAddress: "10.0.0.2", + Ports: []softwarecomposition.NetworkPort{ + { + Port: ptr.To(int32(8081)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8081", }, }, }, }, - EphemeralContainers: []softwarecomposition.NetworkNeighborhoodContainer{ + Egress: []softwarecomposition.NetworkNeighbor{ { - Ingress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "192.168.1.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.4", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(80)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-80", - }, - { - Port: ptr.To(int32(443)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-443", - }, - }, + Port: ptr.To(int32(8080)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8080", }, + }, + }, + + { + IPAddress: "192.168.1.2", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(100)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-100", - }, - { - Port: ptr.To(int32(443)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-443", - }, - }, + Port: ptr.To(int32(8082)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8082", }, }, - Egress: []softwarecomposition.NetworkNeighbor{ + }, + + { + IPAddress: "192.168.1.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "192.168.1.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(8080)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8080", - }, - }, + Port: ptr.To(int32(8080)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8080", }, }, }, + { - Ingress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "192.168.1.2", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.2", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptr.To(int32(8081)), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8081", - }, - }, + Port: ptr.To(int32(8082)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8082", + }, + }, + }, + + { + IPAddress: "192.168.1.1", + Ports: []softwarecomposition.NetworkPort{ + { + Port: ptr.To(int32(8080)), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8080", }, }, }, @@ -1388,7 +1327,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "real_duplicate_bug_test", - networkNeighborhood: func() softwarecomposition.NetworkNeighborhood { + containerProfile: func() softwarecomposition.ContainerProfile { sharedPodSelector := &metav1.LabelSelector{ MatchLabels: map[string]string{ "app.kubernetes.io/component": "master", @@ -1401,7 +1340,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { Name: "TCP-6379", } - return softwarecomposition.NetworkNeighborhood{ + return softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-real-bug", Namespace: "kubescape", @@ -1413,24 +1352,20 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "real-bug", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "real-bug-app", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + Egress: []softwarecomposition.NetworkNeighbor{ { - Egress: []softwarecomposition.NetworkNeighbor{ - { - PodSelector: sharedPodSelector, - Ports: []softwarecomposition.NetworkPort{sharedPort}, - }, - { - PodSelector: sharedPodSelector, - Ports: []softwarecomposition.NetworkPort{sharedPort}, - }, - }, + PodSelector: sharedPodSelector, + Ports: []softwarecomposition.NetworkPort{sharedPort}, + }, + { + PodSelector: sharedPodSelector, + Ports: []softwarecomposition.NetworkPort{sharedPort}, }, }, }, @@ -1495,7 +1430,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "duplicate_ports_within_single_neighbor", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-duplicate-ports", Namespace: "kubescape", @@ -1507,29 +1442,25 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "duplicate-ports", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "duplicate-ports-app", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + Egress: []softwarecomposition.NetworkNeighbor{ { - Egress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "192.168.1.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "192.168.1.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(8080), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8080", - }, - { - Port: ptrToInt32(8080), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8080", - }, - }, + Port: ptrToInt32(8080), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8080", + }, + { + Port: ptrToInt32(8080), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8080", }, }, }, @@ -1592,7 +1523,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "duplicate_ports_with_pod_selector", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-duplicate-ports-pod", Namespace: "kubescape", @@ -1604,33 +1535,29 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "duplicate-ports-pod", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "duplicate-ports-pod-app", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + Egress: []softwarecomposition.NetworkNeighbor{ { - Egress: []softwarecomposition.NetworkNeighbor{ + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "redis", + }, + }, + Ports: []softwarecomposition.NetworkPort{ { - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": "redis", - }, - }, - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(6379), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-6379", - }, - { - Port: ptrToInt32(6379), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-6379", - }, - }, + Port: ptrToInt32(6379), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-6379", + }, + { + Port: ptrToInt32(6379), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-6379", }, }, }, @@ -1695,7 +1622,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "selector_based_rules_not_merged", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-selector-rules", Namespace: "kubescape", @@ -1707,42 +1634,38 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "selector-rules", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "selector-rules-app", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + Egress: []softwarecomposition.NetworkNeighbor{ { - Egress: []softwarecomposition.NetworkNeighbor{ + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "redis", + }, + }, + Ports: []softwarecomposition.NetworkPort{ { - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": "redis", - }, - }, - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(6379), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-6379", - }, - }, + Port: ptrToInt32(6379), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-6379", + }, + }, + }, + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "postgres", }, + }, + Ports: []softwarecomposition.NetworkPort{ { - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": "postgres", - }, - }, - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(6379), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-6379", - }, - }, + Port: ptrToInt32(6379), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-6379", }, }, }, @@ -1824,7 +1747,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "network neighborhood with managed by user annotation should generate policy", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-managed-by-user", Namespace: "kubescape", @@ -1836,24 +1759,21 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "managed-by-user", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "managed-app", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + + Ingress: []softwarecomposition.NetworkNeighbor{ { - Ingress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "10.0.0.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(80), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-80", - }, - }, + Port: ptrToInt32(80), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-80", }, }, }, @@ -1916,7 +1836,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "network neighborhood with completed status should generate policy", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-completed", Namespace: "kubescape", @@ -1928,24 +1848,20 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "completed", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "completed-app", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + Egress: []softwarecomposition.NetworkNeighbor{ { - Egress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "192.168.1.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "192.168.1.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(8080), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-8080", - }, - }, + Port: ptrToInt32(8080), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-8080", }, }, }, @@ -2008,7 +1924,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { }, { name: "network neighborhood with not ready status should return error", - networkNeighborhood: softwarecomposition.NetworkNeighborhood{ + containerProfile: softwarecomposition.ContainerProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-not-ready", Namespace: "kubescape", @@ -2020,24 +1936,21 @@ func TestGenerateNetworkPolicy(t *testing.T) { helpersv1.RelatedNameMetadataKey: "not-ready", }, }, - Spec: softwarecomposition.NetworkNeighborhoodSpec{ + Spec: softwarecomposition.ContainerProfileSpec{ LabelSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "not-ready-app", }, }, - Containers: []softwarecomposition.NetworkNeighborhoodContainer{ + + Ingress: []softwarecomposition.NetworkNeighbor{ { - Ingress: []softwarecomposition.NetworkNeighbor{ + IPAddress: "10.0.0.1", + Ports: []softwarecomposition.NetworkPort{ { - IPAddress: "10.0.0.1", - Ports: []softwarecomposition.NetworkPort{ - { - Port: ptrToInt32(80), - Protocol: softwarecomposition.ProtocolTCP, - Name: "TCP-80", - }, - }, + Port: ptrToInt32(80), + Protocol: softwarecomposition.ProtocolTCP, + Name: "TCP-80", }, }, }, @@ -2051,7 +1964,7 @@ func TestGenerateNetworkPolicy(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := GenerateNetworkPolicy(&tt.networkNeighborhood, softwarecomposition.NewKnownServersFinderImpl(tt.knownServers), timeProvider) + got, err := GenerateNetworkPolicy(&tt.containerProfile, softwarecomposition.NewKnownServersFinderImpl(tt.knownServers), timeProvider) if tt.expectError { assert.Error(t, err) @@ -2063,6 +1976,129 @@ func TestGenerateNetworkPolicy(t *testing.T) { } } +// egressCIDRs collects the egress IPBlock CIDRs of a generated policy. +func egressCIDRs(gnp softwarecomposition.GeneratedNetworkPolicy) []string { + var out []string + for _, rule := range gnp.Spec.Spec.Egress { + for _, peer := range rule.To { + if peer.IPBlock != nil { + out = append(out, peer.IPBlock.CIDR) + } + } + } + return out +} + +// ingressPorts collects the ingress port numbers of a generated policy. +func ingressPorts(gnp softwarecomposition.GeneratedNetworkPolicy) []int32 { + var out []int32 + for _, rule := range gnp.Spec.Spec.Ingress { + for _, port := range rule.Ports { + if port.Port != nil { + out = append(out, *port.Port) + } + } + } + return out +} + +// TestGenerateNetworkPolicy_PerContainerProfiles pins the per-container naming +// contract: a workload with multiple containers is described by one +// ContainerProfile per container, named "