Skip to content
Merged
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ require (
google.golang.org/grpc v1.82.1
google.golang.org/protobuf v1.36.11
gopkg.in/mcuadros/go-syslog.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.1
istio.io/pkg v0.0.0-20231221211216-7635388a563e
k8s.io/api v0.35.0
k8s.io/apimachinery v0.35.0
Expand Down Expand Up @@ -452,6 +451,7 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.35.0 // indirect
k8s.io/apiserver v0.35.0 // indirect
k8s.io/cli-runtime v0.35.0 // indirect
Expand Down
9 changes: 6 additions & 3 deletions pkg/objectcache/containerprofilecache/projection_compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,12 @@ func CompileSpec(rules []typesv1.Rule) objectcache.RuleProjectionSpec {
return spec
}

// mergeField unions one rule's FieldRequirement into the accumulator FieldSpec.
func mergeField(dst *objectcache.FieldSpec, src typesv1.FieldRequirement) {
if !src.Declared {
// mergeField unions one rule's profile-data field into the accumulator
// FieldSpec. src is nil when the rule does not declare this surface (the role
// the old FieldRequirement.Declared bool played before the schema moved to
// armoapi-go).
func mergeField(dst *objectcache.FieldSpec, src *typesv1.FieldRequirement) {
if src == nil {
return
}
dst.InUse = true
Expand Down
11 changes: 6 additions & 5 deletions pkg/objectcache/containerprofilecache/projection_compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ func makeRule(pdr *typesv1.ProfileDataRequired) typesv1.Rule {
}
}

// fieldReqAll returns a FieldRequirement that requests all entries.
func fieldReqAll() typesv1.FieldRequirement {
return typesv1.FieldRequirement{Declared: true, All: true}
// fieldReqAll returns a FieldRequirement that requests all entries. A non-nil
// pointer marks the surface as declared (the old Declared bool's role).
func fieldReqAll() *typesv1.FieldRequirement {
return &typesv1.FieldRequirement{All: true}
}

// fieldReqPatterns returns a FieldRequirement with the supplied patterns.
func fieldReqPatterns(patterns ...typesv1.PatternObject) typesv1.FieldRequirement {
return typesv1.FieldRequirement{Declared: true, Patterns: patterns}
func fieldReqPatterns(patterns ...typesv1.PatternObject) *typesv1.FieldRequirement {
return &typesv1.FieldRequirement{Patterns: patterns}
}

func exactPattern(path string) typesv1.PatternObject {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,14 @@ func toGolden(pcp *objectcache.ProjectedContainerProfile, tree *callstackcache.C
// --- fixture construction helpers ---

// declaredAll returns a FieldRequirement declaring the whole surface.
func declaredAll() typesv1.FieldRequirement {
return typesv1.FieldRequirement{Declared: true, All: true}
func declaredAll() *typesv1.FieldRequirement {
return &typesv1.FieldRequirement{All: true}
}

// declaredPatterns returns a FieldRequirement declaring a set of pattern
// selectors (exact / prefix / suffix / contains).
func declaredPatterns(pats ...typesv1.PatternObject) typesv1.FieldRequirement {
return typesv1.FieldRequirement{Declared: true, Patterns: pats}
func declaredPatterns(pats ...typesv1.PatternObject) *typesv1.FieldRequirement {
return &typesv1.FieldRequirement{Patterns: pats}
}

// linearCallStack builds a single-path identified call stack from an ordered
Expand Down
43 changes: 43 additions & 0 deletions pkg/rulemanager/ruleswatcher/watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ruleswatcher

import (
"context"
"fmt"
"os"

"github.com/Masterminds/semver/v3"
Expand Down Expand Up @@ -118,14 +119,56 @@ func (w *RulesWatcherImpl) InitialSync(ctx context.Context) error {
}

func unstructuredToRules(obj *unstructured.Unstructured) (*typesv1.Rules, error) {
if err := validateRawProfileDataInUnstructured(obj); err != nil {
return nil, err
}

rule := &typesv1.Rules{}
if err := k8sruntime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &rule); err != nil {
return nil, err
}

for _, r := range rule.Spec.Rules {
if r.ProfileDataRequired != nil {
if err := r.ProfileDataRequired.Validate(); err != nil {
return nil, fmt.Errorf("rule %q invalid profileDataRequired: %w", r.ID, err)
}
}
}

return rule, nil
}

func validateRawProfileDataInUnstructured(obj *unstructured.Unstructured) error {
if obj == nil || obj.Object == nil {
return nil
}
spec, ok := obj.Object["spec"].(map[string]any)
if !ok {
return nil
}
rulesRaw, ok := spec["rules"].([]any)
if !ok {
return nil
}
for i, r := range rulesRaw {
ruleMap, ok := r.(map[string]any)
if !ok {
continue
}
if pdr, exists := ruleMap["profileDataRequired"]; exists && pdr != nil {
if err := typesv1.ValidateRawProfileDataRequired(pdr); err != nil {
ruleID, _ := ruleMap["id"].(string)
if ruleID != "" {
return fmt.Errorf("rule %q: %w", ruleID, err)
}
return fmt.Errorf("rule[%d]: %w", i, err)
}
}
}
return nil
}

// isAgentVersionCompatible checks if the current agent version satisfies the given requirement
// using semantic versioning constraints. Returns true if compatible, false otherwise.
func isAgentVersionCompatible(requirement string) bool {
Expand Down
137 changes: 137 additions & 0 deletions pkg/rulemanager/ruleswatcher/watcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package ruleswatcher

import (
"os"
"testing"

typesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/yaml"
)

func TestUnstructuredToRules_ProfileDataRequired(t *testing.T) {
t.Run("valid profileDataRequired", func(t *testing.T) {
obj := &unstructured.Unstructured{
Object: map[string]any{
"spec": map[string]any{
"rules": []any{
map[string]any{
"id": "R0001",
"profileDataRequired": map[string]any{
"opens": "all",
"execs": []any{
map[string]any{"exact": "/bin/sh"},
},
},
},
},
},
},
}

rules, err := unstructuredToRules(obj)
require.NoError(t, err)
require.NotNil(t, rules.Spec.Rules[0].ProfileDataRequired)
require.NotNil(t, rules.Spec.Rules[0].ProfileDataRequired.Opens)
assert.True(t, rules.Spec.Rules[0].ProfileDataRequired.Opens.All)
require.NotNil(t, rules.Spec.Rules[0].ProfileDataRequired.Execs)
require.Len(t, rules.Spec.Rules[0].ProfileDataRequired.Execs.Patterns, 1)
assert.Equal(t, "/bin/sh", rules.Spec.Rules[0].ProfileDataRequired.Execs.Patterns[0].Exact)
})

t.Run("rejects unknown surface key", func(t *testing.T) {
obj := &unstructured.Unstructured{
Object: map[string]any{
"spec": map[string]any{
"rules": []any{
map[string]any{
"id": "R0001",
"profileDataRequired": map[string]any{
"unknownSurface": "all",
},
},
},
},
},
}

_, err := unstructuredToRules(obj)
require.Error(t, err)
assert.Contains(t, err.Error(), `unknown field "unknownSurface"`)
})

t.Run("rejects unknown pattern key", func(t *testing.T) {
obj := &unstructured.Unstructured{
Object: map[string]any{
"spec": map[string]any{
"rules": []any{
map[string]any{
"id": "R0001",
"profileDataRequired": map[string]any{
"opens": []any{
map[string]any{"exct": "/bin/sh"},
},
},
},
},
},
},
}

_, err := unstructuredToRules(obj)
require.Error(t, err)
assert.Contains(t, err.Error(), `unknown field "exct"`)
})

t.Run("rejects invalid pattern object with multiple fields", func(t *testing.T) {
obj := &unstructured.Unstructured{
Object: map[string]any{
"spec": map[string]any{
"rules": []any{
map[string]any{
"id": "R0001",
"profileDataRequired": map[string]any{
"opens": []any{
map[string]any{"exact": "/bin/sh", "prefix": "/usr/"},
},
},
},
},
},
},
}

_, err := unstructuredToRules(obj)
require.Error(t, err)
assert.Contains(t, err.Error(), "exactly one of {exact, prefix, suffix, contains} must be set")
})
}

func TestUnstructuredToRules_DefaultRulesYAML(t *testing.T) {
data, err := os.ReadFile("../../../tests/chart/templates/node-agent/default-rules.yaml")
require.NoError(t, err)

var objMap map[string]any
err = yaml.Unmarshal(data, &objMap)
require.NoError(t, err)

obj := &unstructured.Unstructured{Object: objMap}
rules, err := unstructuredToRules(obj)
require.NoError(t, err)
require.NotEmpty(t, rules.Spec.Rules)

var r0001 *typesv1.Rule
for i := range rules.Spec.Rules {
if rules.Spec.Rules[i].ID == "R0001" {
r0001 = &rules.Spec.Rules[i]
break
}
}
require.NotNil(t, r0001)
require.NotNil(t, r0001.ProfileDataRequired)
require.NotNil(t, r0001.ProfileDataRequired.Execs)
assert.True(t, r0001.ProfileDataRequired.Execs.All)
}

Loading
Loading