Skip to content

Commit

Permalink
add patching for applicationprofiles
Browse files Browse the repository at this point in the history
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
  • Loading branch information
matthyx committed Jan 19, 2024
1 parent 68b2d63 commit 4a57361
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 45 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/kubescape/backend v0.0.16
github.com/kubescape/go-logger v0.0.22
github.com/kubescape/k8s-interface v0.0.158-0.20240117162237-b087cd69bcf1
github.com/kubescape/storage v0.0.59
github.com/kubescape/storage v0.0.61
github.com/panjf2000/ants/v2 v2.9.0
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.8.4
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -494,8 +494,8 @@ github.com/kubescape/go-logger v0.0.22 h1:gle7wH6emOiGv9ljdpVi82pWLQ3jGucrUucvil
github.com/kubescape/go-logger v0.0.22/go.mod h1:x3HBpZo3cMT/WIdy18BxvVVd5D0e/PWFVk/HiwBNu3g=
github.com/kubescape/k8s-interface v0.0.158-0.20240117162237-b087cd69bcf1 h1:RPrJ95wiCaywdjgFzalOhTH3jyTOAZ6n19cNWjWL5KU=
github.com/kubescape/k8s-interface v0.0.158-0.20240117162237-b087cd69bcf1/go.mod h1:5sz+5Cjvo98lTbTVDiDA4MmlXxeHSVMW/wR0V3hV4K8=
github.com/kubescape/storage v0.0.59 h1:Su7Rn0JxMCzo2DkMD/yRniivvYP1p3fbNhhUtuUKZdE=
github.com/kubescape/storage v0.0.59/go.mod h1:I7LUpFfRwVeVphrB83cw4Hz+48sy3Qe6l5H2RDpD43A=
github.com/kubescape/storage v0.0.61 h1:T6NIZP+80ILKLVOV9KFU/0OuH4unoLGXo4mO0zwv6/Q=
github.com/kubescape/storage v0.0.61/go.mod h1:uMwudLhZCPgjf4JEbRSUZ20JmQJitLVrexZb7S1N4b0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
Expand Down
69 changes: 31 additions & 38 deletions pkg/applicationprofilemanager/v1/applicationprofile_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand All @@ -37,6 +38,8 @@ type ApplicationProfileManager struct {
execMaps maps.SafeMap[string, *maps.SafeMap[string, mapset.Set[string]]] // key is k8sContainerID
openMaps maps.SafeMap[string, *maps.SafeMap[string, mapset.Set[string]]] // key is k8sContainerID
watchedContainerChannels maps.SafeMap[string, chan error] // key is ContainerID
savedCapabilities maps.SafeMap[string, int]
savedSyscalls maps.SafeMap[string, int]
k8sClient k8sclient.K8sClientInterface
storageClient storage.StorageClient
syscallPeekFunc func(nsMountId uint64) ([]string, error)
Expand Down Expand Up @@ -153,22 +156,13 @@ func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedCon
return
}

// check if we have new activities to save
var addedActivities int
syscalls := mapset.NewSet[string]()
// existing activity
existingActivity, _ := am.storageClient.GetApplicationActivity(namespace, slug)
if existingActivity != nil && existingActivity.Spec.Syscalls != nil {
syscalls.Append(existingActivity.Spec.Syscalls...)
}
// get syscalls from IG
observedSyscalls, err := am.syscallPeekFunc(watchedContainer.NsMntId)
if err != nil {
logger.L().Ctx(ctx).Error("ApplicationProfileManager - failed to get syscalls", helpers.Error(err))
}
addedActivities += syscalls.Append(observedSyscalls...)
// new activity
if addedActivities > 0 {
// check if we have new activities to save
if len(observedSyscalls) > am.savedSyscalls.Get(watchedContainer.K8sContainerID) {
newActivity := &v1beta1.ApplicationActivity{
ObjectMeta: metav1.ObjectMeta{
Name: slug,
Expand All @@ -180,40 +174,24 @@ func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedCon
},
}
// add syscalls
newActivity.Spec.Syscalls = syscalls.ToSlice()
newActivity.Spec.Syscalls = observedSyscalls
// save application activity
if err := am.storageClient.CreateApplicationActivity(newActivity, namespace); err != nil {
logger.L().Ctx(ctx).Error("ApplicationProfileManager - failed to save application activity", helpers.Error(err))
} else {
am.savedSyscalls.Set(watchedContainer.K8sContainerID, len(observedSyscalls))
logger.L().Debug("ApplicationProfileManager - saved application activity", helpers.String("slug", slug), helpers.String("container ID", watchedContainer.ContainerID), helpers.String("k8s workload", watchedContainer.K8sContainerID))
}
logger.L().Debug("ApplicationProfileManager - saved application activity", helpers.String("slug", slug), helpers.String("container ID", watchedContainer.ContainerID), helpers.String("k8s workload", watchedContainer.K8sContainerID))
}

// profile sets
var addedProfiles int
capabilities := mapset.NewSet[string]()
execs := make(map[string]mapset.Set[string])
opens := make(map[string]mapset.Set[string])
// existing profile
existingProfile, _ := am.storageClient.GetApplicationProfile(namespace, slug)
existingProfileContainer := utils.GetApplicationProfileContainer(existingProfile, watchedContainer.ContainerType, watchedContainer.ContainerIndex)
if existingProfileContainer != nil {
capabilities.Append(existingProfileContainer.Capabilities...)
for _, exec := range existingProfileContainer.Execs {
if _, exist := execs[exec.Path]; !exist {
execs[exec.Path] = mapset.NewSet[string]()
}
execs[exec.Path].Append(exec.Args...)
}
for _, open := range existingProfileContainer.Opens {
if _, exist := opens[open.Path]; !exist {
opens[open.Path] = mapset.NewSet[string]()
}
opens[open.Path].Append(open.Flags...)
}
}
// get capabilities, execs and opens from IG
var observedCapabilities []string
if capabilitiesSet, ok := am.capabilitiesSets.Load(watchedContainer.K8sContainerID); ok {
addedProfiles += capabilities.Append(capabilitiesSet.ToSlice()...)
observedCapabilities = capabilitiesSet.ToSlice()
}
if execMap, ok := am.execMaps.Load(watchedContainer.K8sContainerID); ok {
execMap.Range(func(path string, exec mapset.Set[string]) bool {
Expand All @@ -234,7 +212,7 @@ func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedCon
})
}
// new profile
if addedProfiles > 0 {
if addedProfiles > 0 || len(observedCapabilities) > am.savedCapabilities.Get(watchedContainer.K8sContainerID) {
newProfile := &v1beta1.ApplicationProfile{
ObjectMeta: metav1.ObjectMeta{
Name: slug,
Expand All @@ -249,7 +227,7 @@ func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedCon
Name: watchedContainer.InstanceID.GetContainerName(),
}
// add capabilities
newProfileContainer.Capabilities = capabilities.ToSlice()
newProfileContainer.Capabilities = observedCapabilities
sort.Strings(newProfileContainer.Capabilities)
// add execs
newProfileContainer.Execs = make([]v1beta1.ExecCalls, 0)
Expand All @@ -274,10 +252,25 @@ func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedCon
// insert application profile container
utils.InsertApplicationProfileContainer(newProfile, watchedContainer.ContainerType, watchedContainer.ContainerIndex, newProfileContainer)
// save application profile
if err := am.storageClient.CreateApplicationProfile(newProfile, namespace); err != nil {
logger.L().Ctx(ctx).Error("ApplicationProfileManager - failed to save application profile", helpers.Error(err))
var gotErr error
if err := am.storageClient.PatchApplicationProfile(slug, namespace, newProfile); err != nil {
if apierrors.IsNotFound(err) {
if err := am.storageClient.CreateApplicationProfile(newProfile, namespace); err != nil {
gotErr = err
logger.L().Ctx(ctx).Error("ApplicationProfileManager - failed to create application profile", helpers.Error(err))
}
} else {
gotErr = err
logger.L().Ctx(ctx).Error("ApplicationProfileManager - failed to patch application profile", helpers.Error(err))
}
}
if gotErr == nil {
am.savedCapabilities.Set(watchedContainer.K8sContainerID, len(observedCapabilities))
// clear maps now that we saved the profile
am.execMaps.Get(watchedContainer.K8sContainerID).Clear()
am.openMaps.Get(watchedContainer.K8sContainerID).Clear()
logger.L().Debug("ApplicationProfileManager - saved application profile", helpers.String("slug", slug), helpers.String("container ID", watchedContainer.ContainerID), helpers.String("k8s workload", watchedContainer.K8sContainerID))
}
logger.L().Debug("ApplicationProfileManager - saved application profile", helpers.String("slug", slug), helpers.String("container ID", watchedContainer.ContainerID), helpers.String("k8s workload", watchedContainer.K8sContainerID))
// profile summary
summary := &v1beta1.ApplicationProfileSummary{
ObjectMeta: newProfile.ObjectMeta,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ func TestApplicationProfileManager(t *testing.T) {
})
// let it run for a while
time.Sleep(12 * time.Second) // need to sleep longer because of AddRandomDuration in startApplicationProfiling
// report another file open
go am.ReportFileOpen("ns/pod/cont", "/etc/hosts", []string{"O_RDONLY"})
// sleep more
time.Sleep(1 * time.Second)
// report container stopped
am.ContainerCallback(containercollection.PubSubEvent{
Type: containercollection.EventTypeRemoveContainer,
Expand All @@ -67,13 +71,19 @@ func TestApplicationProfileManager(t *testing.T) {
// let it stop
time.Sleep(1 * time.Second)
// verify generated CRDs
assert.Equal(t, 2, len(storageClient.ApplicationActivities))
assert.Equal(t, 1, len(storageClient.ApplicationActivities))
sort.Strings(storageClient.ApplicationActivities[0].Spec.Syscalls)
assert.Equal(t, []string{"dup", "listen", "open"}, storageClient.ApplicationActivities[0].Spec.Syscalls)
assert.Equal(t, []string{"dup", "listen"}, storageClient.ApplicationActivities[0].Spec.Syscalls)
assert.Equal(t, 2, len(storageClient.ApplicationProfiles))
assert.Equal(t, 2, len(storageClient.ApplicationProfileSummaries))
// check the first profile
sort.Strings(storageClient.ApplicationProfiles[0].Spec.Containers[0].Capabilities)
assert.Equal(t, []string{"NET_BIND_SERVICE", "NET_BROADCAST"}, storageClient.ApplicationProfiles[0].Spec.Containers[1].Capabilities)
assert.Equal(t, []string{"NET_BIND_SERVICE"}, storageClient.ApplicationProfiles[0].Spec.Containers[1].Capabilities)
assert.Equal(t, []v1beta1.ExecCalls{{Path: "/bin/bash", Args: []string{"-c", "ls"}, Envs: []string(nil)}}, storageClient.ApplicationProfiles[0].Spec.Containers[1].Execs)
assert.Equal(t, []v1beta1.OpenCalls{{Path: "/etc/passwd", Flags: []string{"O_RDONLY"}}}, storageClient.ApplicationProfiles[0].Spec.Containers[1].Opens)
assert.Equal(t, 2, len(storageClient.ApplicationProfileSummaries))
// check the second profile - this is a patch for execs and opens
sort.Strings(storageClient.ApplicationProfiles[1].Spec.Containers[0].Capabilities)
assert.Equal(t, []string{"NET_BIND_SERVICE"}, storageClient.ApplicationProfiles[1].Spec.Containers[1].Capabilities)
assert.Equal(t, []v1beta1.ExecCalls{}, storageClient.ApplicationProfiles[1].Spec.Containers[1].Execs)
assert.Equal(t, []v1beta1.OpenCalls{{Path: "/etc/hosts", Flags: []string{"O_RDONLY"}}}, storageClient.ApplicationProfiles[1].Spec.Containers[1].Opens)
}
1 change: 1 addition & 0 deletions pkg/storage/storage_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type StorageClient interface {
CreateApplicationActivity(activity *v1beta1.ApplicationActivity, namespace string) error
GetApplicationActivity(namespace, name string) (*v1beta1.ApplicationActivity, error)
CreateApplicationProfile(profile *v1beta1.ApplicationProfile, namespace string) error
PatchApplicationProfile(name, namespace string, profile *v1beta1.ApplicationProfile) error
GetApplicationProfile(namespace, name string) (*v1beta1.ApplicationProfile, error)
CreateApplicationProfileSummary(profile *v1beta1.ApplicationProfileSummary, namespace string) error
CreateFilteredSBOM(SBOM *v1beta1.SBOMSyftFiltered) error
Expand Down
9 changes: 9 additions & 0 deletions pkg/storage/storage_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package storage
import (
"encoding/json"
"errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"node-agent/pkg/utils"
"os"
"path"
Expand Down Expand Up @@ -84,6 +85,14 @@ func (sc *StorageHttpClientMock) CreateApplicationProfile(profile *spdxv1beta1.A
return nil
}

func (sc *StorageHttpClientMock) PatchApplicationProfile(name, namespace string, profile *spdxv1beta1.ApplicationProfile) error {
if len(sc.ApplicationProfiles) == 0 {
return apierrors.NewNotFound(v1beta1.Resource("applicationprofile"), name)
}
sc.ApplicationProfiles = append(sc.ApplicationProfiles, profile)
return nil
}

func (sc *StorageHttpClientMock) CreateApplicationProfileSummary(summary *spdxv1beta1.ApplicationProfileSummary, namespace string) error {
sc.ApplicationProfileSummaries = append(sc.ApplicationProfileSummaries, summary)
return nil
Expand Down
12 changes: 12 additions & 0 deletions pkg/storage/v1/storage_nocache.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@ func (sc StorageNoCache) CreateApplicationProfile(profile *v1beta1.ApplicationPr
return nil
}

func (sc StorageNoCache) PatchApplicationProfile(name, namespace string, profile *v1beta1.ApplicationProfile) error {
bytes, err := json.Marshal(profile)
if err != nil {
return fmt.Errorf("marshal neighbors: %w", err)
}
_, err = sc.StorageClient.ApplicationProfiles(namespace).Patch(context.Background(), name, types.StrategicMergePatchType, bytes, metav1.PatchOptions{})
if err != nil {
return fmt.Errorf("patch neighbors: %w", err)
}
return nil
}

func (sc StorageNoCache) GetApplicationProfile(namespace, name string) (*v1beta1.ApplicationProfile, error) {
return sc.StorageClient.ApplicationProfiles(namespace).Get(context.Background(), name, metav1.GetOptions{})
}
Expand Down
135 changes: 135 additions & 0 deletions pkg/storage/v1/storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,138 @@ func TestStorageNoCache_PatchFilteredSBOM(t *testing.T) {
})
}
}

func TestStorageNoCache_PatchNetworkNeighbors(t *testing.T) {
type args struct {
name string
neighbors *v1beta1.NetworkNeighbors
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "test",
args: args{
name: storage.NginxKey,
neighbors: &v1beta1.NetworkNeighbors{
Spec: v1beta1.NetworkNeighborsSpec{
Ingress: []v1beta1.NetworkNeighbor{
{
Ports: []v1beta1.NetworkPort{
{Name: "test2"},
{Name: "test3"},
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sc, _ := CreateFakeStorageNoCache("kubescape")
existingProfile := &v1beta1.NetworkNeighbors{
ObjectMeta: v1.ObjectMeta{
Name: tt.args.name,
},
Spec: v1beta1.NetworkNeighborsSpec{
Ingress: []v1beta1.NetworkNeighbor{
{
Ports: []v1beta1.NetworkPort{
{Name: "test"},
{Name: "test1"},
},
},
},
},
}
_, _ = sc.StorageClient.NetworkNeighborses("default").Create(context.Background(), existingProfile, v1.CreateOptions{})
if err := sc.PatchNetworkNeighborsIngressAndEgress(tt.args.name, "default", tt.args.neighbors); (err != nil) != tt.wantErr {
t.Errorf("PatchFilteredSBOM() error = %v, wantErr %v", err, tt.wantErr)
}
got, err := sc.StorageClient.NetworkNeighborses("default").Get(context.Background(), tt.args.name, v1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, 4, len(got.Spec.Ingress[0].Ports))
})
}
}

func TestStorageNoCache_PatchApplicationProfile(t *testing.T) {
type args struct {
name string
profile *v1beta1.ApplicationProfile
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "test",
args: args{
name: storage.NginxKey,
profile: &v1beta1.ApplicationProfile{
Spec: v1beta1.ApplicationProfileSpec{
Containers: []v1beta1.ApplicationProfileContainer{
{
Name: "test",
Capabilities: []string{"SYS_ADMIN"},
Execs: []v1beta1.ExecCalls{
{Path: "/usr/bin/test2"},
{Path: "/usr/bin/test3"},
},
Opens: []v1beta1.OpenCalls{
{Path: "/usr/bin/test2"},
{Path: "/usr/bin/test3"},
},
Syscalls: []string{"open"},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sc, _ := CreateFakeStorageNoCache("kubescape")
existingProfile := &v1beta1.ApplicationProfile{
ObjectMeta: v1.ObjectMeta{
Name: tt.args.name,
},
Spec: v1beta1.ApplicationProfileSpec{
Containers: []v1beta1.ApplicationProfileContainer{
{
Name: "test",
Capabilities: []string{"NET_ADMIN"},
Execs: []v1beta1.ExecCalls{
{Path: "/usr/bin/test"},
{Path: "/usr/bin/test1"},
},
Opens: []v1beta1.OpenCalls{
{Path: "/usr/bin/test"},
{Path: "/usr/bin/test1"},
},
Syscalls: []string{"execve"},
},
},
},
}
_, _ = sc.StorageClient.ApplicationProfiles("default").Create(context.Background(), existingProfile, v1.CreateOptions{})
if err := sc.PatchApplicationProfile(tt.args.name, "default", tt.args.profile); (err != nil) != tt.wantErr {
t.Errorf("PatchFilteredSBOM() error = %v, wantErr %v", err, tt.wantErr)
}
got, err := sc.StorageClient.ApplicationProfiles("default").Get(context.Background(), tt.args.name, v1.GetOptions{})
assert.NoError(t, err)
// []string cannot be merged, so the default replace strategy is used
assert.Equal(t, []string{"SYS_ADMIN"}, got.Spec.Containers[0].Capabilities)
assert.Equal(t, []string{"open"}, got.Spec.Containers[0].Syscalls)
// here the slices can merge
assert.Equal(t, 4, len(got.Spec.Containers[0].Execs))
assert.Equal(t, 4, len(got.Spec.Containers[0].Opens))
})
}
}

0 comments on commit 4a57361

Please sign in to comment.