From 549e1a8fc4eb18fc300908f7d781bec0f734463a Mon Sep 17 00:00:00 2001 From: Afek Berger Date: Mon, 18 Nov 2024 09:43:48 +0200 Subject: [PATCH 1/3] Feature/policy rules (#399) * Added rule policies Signed-off-by: Afek Berger * Added rule policy to symlink Signed-off-by: Afek Berger * Added rule condition interface & modified rules Signed-off-by: Afek Berger * Added rule condition logic Signed-off-by: Afek Berger * Added rule policy reporter Signed-off-by: Afek Berger * Modified rule policy reporting process Signed-off-by: Afek Berger * Modified tests & fixed enrichment Signed-off-by: Afek Berger * Added tests Signed-off-by: Afek Berger * Fixed create patch function Signed-off-by: Afek Berger * Added support to hardlink & exec Signed-off-by: Afek Berger * Fixed symlink rule Signed-off-by: Afek Berger * Fixed rules test Signed-off-by: Afek Berger * Refactor merge policy Signed-off-by: Afek Berger * Fixed rule policy adding operations * Added init operations Signed-off-by: Afek Berger * Added Cache to savedRulePolicies Signed-off-by: Afek Berger * Added component test & cache for policies Signed-off-by: Afek Berger * Fixed init ops Signed-off-by: Afek Berger * Fixed component test Signed-off-by: Afek Berger * Update default rule binding Signed-off-by: Afek Berger --------- Signed-off-by: Afek Berger --- .github/workflows/component-tests.yaml | 7 +- go.mod | 2 +- go.sum | 4 +- main.go | 2 +- .../applicationprofile_manager_interface.go | 1 + .../applicationprofile_manager_mock.go | 4 + .../v1/applicationprofile_manager.go | 110 ++++++- .../v1/applicationprofile_manager_test.go | 163 ++++++++++ .../v1/{endpoint_utils.go => helpers.go} | 49 ++- pkg/containerwatcher/v1/container_watcher.go | 21 +- .../dnsmanager/dns_manager.go | 4 +- .../dnsmanager/dns_manager_interface.go | 2 +- .../dnsmanager/dns_manager_mock.go | 2 +- .../dnsmanager/dns_manager_test.go | 10 +- pkg/eventreporters/rulepolicy/rule_policy.go | 27 ++ pkg/networkmanager/v2/network_manager.go | 2 +- pkg/networkmanager/v2/network_manager_test.go | 2 +- .../applicationprofilecache.go | 7 + pkg/objectcache/dnscache/dnscache.go | 2 +- .../rulebindingmanager_interface.go | 1 + .../rulebindingmanager_mock.go | 4 + pkg/ruleengine/ruleengine_interface.go | 6 + pkg/ruleengine/ruleengine_mock.go | 4 + pkg/ruleengine/v1/factory.go | 31 +- pkg/ruleengine/v1/helpers.go | 25 ++ ...010_symlink_created_over_sensitive_file.go | 118 ++++---- ...ymlink_created_over_sensitive_file_test.go | 11 +- pkg/ruleengine/v1/r1011_ld_preload_hook.go | 286 ++++++++++-------- .../v1/r1011_ld_preload_hook_test.go | 66 +++- ...12_hardlink_created_over_sensitive_file.go | 117 ++++--- ...rdlink_created_over_sensitive_file_test.go | 12 +- pkg/rulemanager/rule_manager_interface.go | 1 + pkg/rulemanager/rule_manager_mock.go | 4 + pkg/rulemanager/v1/rule_manager.go | 20 ++ pkg/storage/v1/applicationprofile.go | 1 + pkg/utils/applicationprofile.go | 75 ++++- pkg/utils/applicationprofile_test.go | 102 ++++++- .../chart/crds/runtime-rule-binding.crd.yaml | 125 ++------ .../node-agent/default-rule-binding.yaml | 11 +- tests/component_test.go | 92 +++++- 40 files changed, 1110 insertions(+), 423 deletions(-) rename pkg/applicationprofilemanager/v1/{endpoint_utils.go => helpers.go} (62%) rename pkg/{ => eventreporters}/dnsmanager/dns_manager.go (95%) rename pkg/{ => eventreporters}/dnsmanager/dns_manager_interface.go (82%) rename pkg/{ => eventreporters}/dnsmanager/dns_manager_mock.go (86%) rename pkg/{ => eventreporters}/dnsmanager/dns_manager_test.go (97%) create mode 100644 pkg/eventreporters/rulepolicy/rule_policy.go diff --git a/.github/workflows/component-tests.yaml b/.github/workflows/component-tests.yaml index 2c741b92..4f8a160e 100644 --- a/.github/workflows/component-tests.yaml +++ b/.github/workflows/component-tests.yaml @@ -52,6 +52,7 @@ jobs: Test_11_EndpointTest, Test_12_MergingProfilesTest, Test_13_MergingNetworkNeighborhoodTest, + Test_14_RulePoliciesTest, ] steps: - name: Checkout code @@ -97,9 +98,13 @@ jobs: - name: Run test run: | cd tests && go test -v ./... -run ${{ matrix.test }} --timeout=20m --tags=component - - name: Print storage logs + - name: Print node agent & storage logs if: always() run: | + echo "Node agent logs" + kubectl logs $(kubectl get pods -n kubescape -o name | grep node-agent) -n kubescape -c node-agent + echo "-----------------------------------------" + echo "Storage logs" kubectl logs $(kubectl get pods -n kubescape -o name | grep storage) -n kubescape # - name: Upload plot images diff --git a/go.mod b/go.mod index 053666f2..9d7b9510 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/kubescape/backend v0.0.20 github.com/kubescape/go-logger v0.0.23 github.com/kubescape/k8s-interface v0.0.170 - github.com/kubescape/storage v0.0.119 + github.com/kubescape/storage v0.0.132 github.com/panjf2000/ants/v2 v2.9.1 github.com/prometheus/alertmanager v0.27.0 github.com/prometheus/client_golang v1.20.4 diff --git a/go.sum b/go.sum index e53a03ca..138ab6d2 100644 --- a/go.sum +++ b/go.sum @@ -553,8 +553,8 @@ github.com/kubescape/go-logger v0.0.23 h1:5xh+Nm8eGImhFbtippRKLaFgsvlKE1ufvQhNM2 github.com/kubescape/go-logger v0.0.23/go.mod h1:Ayg7g769c7sXVB+P3fkJmbsJpoEmMmaUf9jeo+XuC3U= github.com/kubescape/k8s-interface v0.0.170 h1:EtzomWoeeIWDz7QrAEsqUDpLHQwoh2m3tZITfrE/tiE= github.com/kubescape/k8s-interface v0.0.170/go.mod h1:VoEoHI4Va08NiGAkYzbITF50aFMT5y4fPHRb4x2LtME= -github.com/kubescape/storage v0.0.119 h1:7qCSxMRfuCG35H3o832q69hBA06KKHyyLVW76nFy5YA= -github.com/kubescape/storage v0.0.119/go.mod h1:DAR1CmSDhRRBK26nNU4MrVpRAst5nN7IuPuvcnw9XeI= +github.com/kubescape/storage v0.0.132 h1:OmZ/thFrh0n29yvYYTce6aoVfpgSDi5k7rwtFHHGAoA= +github.com/kubescape/storage v0.0.132/go.mod h1:0MIrMh9DVEPmT1+d7siysH6TX+8fTjXIIedoot/6klI= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= diff --git a/main.go b/main.go index 34e0318b..3e667efa 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( cloudmetadata "github.com/kubescape/node-agent/pkg/cloudmetadata" "github.com/kubescape/node-agent/pkg/config" "github.com/kubescape/node-agent/pkg/containerwatcher/v1" - "github.com/kubescape/node-agent/pkg/dnsmanager" + "github.com/kubescape/node-agent/pkg/eventreporters/dnsmanager" "github.com/kubescape/node-agent/pkg/exporters" "github.com/kubescape/node-agent/pkg/filehandler/v1" "github.com/kubescape/node-agent/pkg/healthmanager" diff --git a/pkg/applicationprofilemanager/applicationprofile_manager_interface.go b/pkg/applicationprofilemanager/applicationprofile_manager_interface.go index f929ea33..8e0e4732 100644 --- a/pkg/applicationprofilemanager/applicationprofile_manager_interface.go +++ b/pkg/applicationprofilemanager/applicationprofile_manager_interface.go @@ -12,6 +12,7 @@ type ApplicationProfileManagerClient interface { ReportFileExec(k8sContainerID, path string, args []string) ReportFileOpen(k8sContainerID, path string, flags []string) ReportHTTPEvent(k8sContainerID string, event *tracerhttptype.Event) + ReportRulePolicy(k8sContainerID, ruleId, allowedProcess string, allowedContainer bool) ReportDroppedEvent(k8sContainerID string) ContainerReachedMaxTime(containerID string) } diff --git a/pkg/applicationprofilemanager/applicationprofile_manager_mock.go b/pkg/applicationprofilemanager/applicationprofile_manager_mock.go index 56b57834..b1bd0528 100644 --- a/pkg/applicationprofilemanager/applicationprofile_manager_mock.go +++ b/pkg/applicationprofilemanager/applicationprofile_manager_mock.go @@ -42,6 +42,10 @@ func (a ApplicationProfileManagerMock) ReportHTTPEvent(_ string, _ *tracerhttpty // noop } +func (a ApplicationProfileManagerMock) ReportRulePolicy(_, _, _ string, _ bool) { + // noop +} + func (a ApplicationProfileManagerMock) ContainerReachedMaxTime(_ string) { // noop } diff --git a/pkg/applicationprofilemanager/v1/applicationprofile_manager.go b/pkg/applicationprofilemanager/v1/applicationprofile_manager.go index 33eeadb5..e62cb5d6 100644 --- a/pkg/applicationprofilemanager/v1/applicationprofile_manager.go +++ b/pkg/applicationprofilemanager/v1/applicationprofile_manager.go @@ -6,6 +6,7 @@ import ( "fmt" "regexp" "runtime" + "slices" "strings" "time" @@ -53,10 +54,12 @@ type ApplicationProfileManager struct { savedExecs maps.SafeMap[string, cache.ExpiringCache] // key is k8sContainerID savedOpens maps.SafeMap[string, cache.ExpiringCache] // key is k8sContainerID savedSyscalls maps.SafeMap[string, mapset.Set[string]] // key is k8sContainerID + savedRulePolicies maps.SafeMap[string, cache.ExpiringCache] // key is k8sContainerID toSaveCapabilities maps.SafeMap[string, mapset.Set[string]] // key is k8sContainerID toSaveEndpoints maps.SafeMap[string, *maps.SafeMap[string, *v1beta1.HTTPEndpoint]] // key is k8sContainerID toSaveExecs maps.SafeMap[string, *maps.SafeMap[string, []string]] // key is k8sContainerID toSaveOpens maps.SafeMap[string, *maps.SafeMap[string, mapset.Set[string]]] // key is k8sContainerID + toSaveRulePolicies maps.SafeMap[string, *maps.SafeMap[string, *v1beta1.RulePolicy]] // key is k8sContainerID watchedContainerChannels maps.SafeMap[string, chan error] // key is ContainerID k8sClient k8sclient.K8sClientInterface k8sObjectCache objectcache.K8sObjectCache @@ -146,10 +149,12 @@ func (am *ApplicationProfileManager) deleteResources(watchedContainer *utils.Wat am.savedExecs.Delete(watchedContainer.K8sContainerID) am.savedOpens.Delete(watchedContainer.K8sContainerID) am.savedSyscalls.Delete(watchedContainer.K8sContainerID) + am.savedRulePolicies.Delete(watchedContainer.K8sContainerID) am.toSaveCapabilities.Delete(watchedContainer.K8sContainerID) am.toSaveEndpoints.Delete(watchedContainer.K8sContainerID) am.toSaveExecs.Delete(watchedContainer.K8sContainerID) am.toSaveOpens.Delete(watchedContainer.K8sContainerID) + am.toSaveRulePolicies.Delete(watchedContainer.K8sContainerID) am.watchedContainerChannels.Delete(watchedContainer.ContainerID) } @@ -173,7 +178,8 @@ func (am *ApplicationProfileManager) monitorContainer(ctx context.Context, conta watchedContainer.SetCompletionStatus(utils.WatchedContainerCompletionStatusFull) } watchedContainer.SetStatus(utils.WatchedContainerStatusInitializing) - am.saveProfile(ctx, watchedContainer, container.K8s.Namespace) + + initOps := GetInitOperations(watchedContainer.ContainerType.String(), watchedContainer.ContainerIndex) for { select { @@ -184,7 +190,14 @@ func (am *ApplicationProfileManager) monitorContainer(ctx context.Context, conta watchedContainer.UpdateDataTicker.Reset(utils.AddJitter(am.cfg.UpdateDataPeriod, am.cfg.MaxJitterPercentage)) } watchedContainer.SetStatus(utils.WatchedContainerStatusReady) - am.saveProfile(ctx, watchedContainer, container.K8s.Namespace) + am.saveProfile(ctx, watchedContainer, container.K8s.Namespace, nil) + + // save profile after initialaztion + if initOps != nil { + am.saveProfile(ctx, watchedContainer, container.K8s.Namespace, initOps) + initOps = nil + } + case err := <-watchedContainer.SyncChannel: switch { case errors.Is(err, utils.ContainerHasTerminatedError): @@ -192,12 +205,11 @@ func (am *ApplicationProfileManager) monitorContainer(ctx context.Context, conta if objectcache.GetTerminationExitCode(am.k8sObjectCache, container.K8s.Namespace, container.K8s.PodName, container.K8s.ContainerName, container.Runtime.ContainerID) == 0 { watchedContainer.SetStatus(utils.WatchedContainerStatusCompleted) } - - am.saveProfile(ctx, watchedContainer, container.K8s.Namespace) + am.saveProfile(ctx, watchedContainer, container.K8s.Namespace, nil) return err case errors.Is(err, utils.ContainerReachedMaxTime): watchedContainer.SetStatus(utils.WatchedContainerStatusCompleted) - am.saveProfile(ctx, watchedContainer, container.K8s.Namespace) + am.saveProfile(ctx, watchedContainer, container.K8s.Namespace, nil) return err case errors.Is(err, utils.ObjectCompleted): watchedContainer.SetStatus(utils.WatchedContainerStatusCompleted) @@ -211,7 +223,7 @@ func (am *ApplicationProfileManager) monitorContainer(ctx context.Context, conta } } -func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedContainer *utils.WatchedContainerData, namespace string) { +func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedContainer *utils.WatchedContainerData, namespace string, initalizeOperations []utils.PatchOperation) { ctx, span := otel.Tracer("").Start(ctx, "ApplicationProfileManager.saveProfile") defer span.End() @@ -314,6 +326,18 @@ func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedCon opens[path].Append(open.ToSlice()...) return true }) + + // get rule policies + rulePolicies := make(map[string]v1beta1.RulePolicy) + toSaveRulePolicies := am.toSaveRulePolicies.Get(watchedContainer.K8sContainerID) + // point IG to a new rule policies map + am.toSaveRulePolicies.Set(watchedContainer.K8sContainerID, new(maps.SafeMap[string, *v1beta1.RulePolicy])) + // prepare rule policies map + toSaveRulePolicies.Range(func(ruleIdentifier string, rulePolicy *v1beta1.RulePolicy) bool { + rulePolicies[ruleIdentifier] = *rulePolicy + return true + }) + // new activity // the process tries to use JSON patching to avoid conflicts between updates on the same object from different containers // 0. create both a patch and a new object @@ -323,9 +347,13 @@ func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedCon // 3a. the object is missing its container slice - ADD one with the container profile at the right index // 3b. the object is missing the container profile - ADD the container profile at the right index // 3c. default - patch the container ourselves and REPLACE it at the right index - if len(capabilities) > 0 || len(endpoints) > 0 || len(execs) > 0 || len(opens) > 0 || len(toSaveSyscalls) > 0 || watchedContainer.StatusUpdated() { + if len(capabilities) > 0 || len(endpoints) > 0 || len(execs) > 0 || len(opens) > 0 || len(toSaveSyscalls) > 0 || len(initalizeOperations) > 0 || watchedContainer.StatusUpdated() { // 0. calculate patch - operations := utils.CreateCapabilitiesPatchOperations(capabilities, observedSyscalls, execs, opens, endpoints, watchedContainer.ContainerType.String(), watchedContainer.ContainerIndex) + operations := utils.CreateCapabilitiesPatchOperations(capabilities, observedSyscalls, execs, opens, endpoints, rulePolicies, watchedContainer.ContainerType.String(), watchedContainer.ContainerIndex) + if len(initalizeOperations) > 0 { + operations = append(operations, initalizeOperations...) + } + operations = utils.AppendStatusAnnotationPatchOperations(operations, watchedContainer) operations = append(operations, utils.PatchOperation{ Op: "add", @@ -366,6 +394,7 @@ func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedCon Opens: make([]v1beta1.OpenCalls, 0), Capabilities: make([]string, 0), Syscalls: make([]string, 0), + PolicyByRuleId: make(map[string]v1beta1.RulePolicy), SeccompProfile: seccompProfile, }) } @@ -377,7 +406,7 @@ func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedCon newObject.Spec.EphemeralContainers = addContainers(newObject.Spec.EphemeralContainers, watchedContainer.ContainerNames[utils.EphemeralContainer]) // enrich container newContainer := utils.GetApplicationProfileContainer(newObject, watchedContainer.ContainerType, watchedContainer.ContainerIndex) - utils.EnrichApplicationProfileContainer(newContainer, capabilities, observedSyscalls, execs, opens, endpoints) + utils.EnrichApplicationProfileContainer(newContainer, capabilities, observedSyscalls, execs, opens, endpoints, rulePolicies) // try to create object if err := am.storageClient.CreateApplicationProfile(newObject, namespace); err != nil { gotErr = err @@ -425,11 +454,12 @@ func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedCon Opens: make([]v1beta1.OpenCalls, 0), Capabilities: make([]string, 0), Syscalls: make([]string, 0), + PolicyByRuleId: make(map[string]v1beta1.RulePolicy), SeccompProfile: seccompProfile, } } // update it - utils.EnrichApplicationProfileContainer(existingContainer, capabilities, observedSyscalls, execs, opens, endpoints) + utils.EnrichApplicationProfileContainer(existingContainer, capabilities, observedSyscalls, execs, opens, endpoints, rulePolicies) // get existing containers var existingContainers []v1beta1.ApplicationProfileContainer if watchedContainer.ContainerType == utils.Container { @@ -469,6 +499,7 @@ func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedCon Opens: make([]v1beta1.OpenCalls, 0), Capabilities: make([]string, 0), Syscalls: make([]string, 0), + PolicyByRuleId: make(map[string]v1beta1.RulePolicy), SeccompProfile: seccompProfile, }, }) @@ -558,11 +589,22 @@ func (am *ApplicationProfileManager) saveProfile(ctx context.Context, watchedCon } return true }) + + // record saved rule policies + toSaveRulePolicies.Range(func(ruleIdentifier string, rulePolicy *v1beta1.RulePolicy) bool { + if !am.toSaveRulePolicies.Get(watchedContainer.K8sContainerID).Has(ruleIdentifier) { + am.savedRulePolicies.Get(watchedContainer.K8sContainerID).Set(ruleIdentifier, rulePolicy) + } + return true + }) + logger.L().Debug("ApplicationProfileManager - saved application profile", helpers.Int("capabilities", len(capabilities)), helpers.Int("endpoints", toSaveEndpoints.Len()), helpers.Int("execs", toSaveExecs.Len()), helpers.Int("opens", toSaveOpens.Len()), + helpers.Int("rule policies", toSaveRulePolicies.Len()), + helpers.Int("init operations", len(initalizeOperations)), helpers.String("slug", slug), helpers.Int("container index", watchedContainer.ContainerIndex), helpers.String("container ID", watchedContainer.ContainerID), @@ -638,10 +680,12 @@ func (am *ApplicationProfileManager) ContainerCallback(notif containercollection am.savedExecs.Set(k8sContainerID, cache.NewTTL(5*am.cfg.UpdateDataPeriod, am.cfg.UpdateDataPeriod)) am.savedOpens.Set(k8sContainerID, cache.NewTTL(5*am.cfg.UpdateDataPeriod, am.cfg.UpdateDataPeriod)) am.savedSyscalls.Set(k8sContainerID, mapset.NewSet[string]()) + am.savedRulePolicies.Set(k8sContainerID, cache.NewTTL(5*am.cfg.UpdateDataPeriod, am.cfg.UpdateDataPeriod)) am.toSaveCapabilities.Set(k8sContainerID, mapset.NewSet[string]()) am.toSaveEndpoints.Set(k8sContainerID, new(maps.SafeMap[string, *v1beta1.HTTPEndpoint])) am.toSaveExecs.Set(k8sContainerID, new(maps.SafeMap[string, []string])) am.toSaveOpens.Set(k8sContainerID, new(maps.SafeMap[string, mapset.Set[string]])) + am.toSaveRulePolicies.Set(k8sContainerID, new(maps.SafeMap[string, *v1beta1.RulePolicy])) am.removedContainers.Remove(k8sContainerID) // make sure container is not in the removed list am.trackedContainers.Add(k8sContainerID) go am.startApplicationProfiling(ctx, notif.Container, k8sContainerID) @@ -718,8 +762,8 @@ func (am *ApplicationProfileManager) ReportHTTPEvent(k8sContainerID string, even if err := am.waitForContainer(k8sContainerID); err != nil { return } - // get endpoint from event - endpointIdentifier, err := am.GetEndpointIdentifier(event) + + endpointIdentifier, err := GetEndpointIdentifier(event) if err != nil { logger.L().Ctx(am.ctx).Warning("ApplicationProfileManager - failed to get endpoint identifier", helpers.Error(err)) return @@ -737,3 +781,45 @@ func (am *ApplicationProfileManager) ReportHTTPEvent(k8sContainerID string, even // add to endpoint map am.toSaveEndpoints.Get(k8sContainerID).Set(endpointHash, endpoint) } + +func (am *ApplicationProfileManager) ReportRulePolicy(k8sContainerID, ruleId, allowedProcess string, allowedContainer bool) { + if err := am.waitForContainer(k8sContainerID); err != nil { + return + } + + newPolicy := &v1beta1.RulePolicy{ + AllowedContainer: allowedContainer, + AllowedProcesses: []string{allowedProcess}, + } + + savedPolicies := am.savedRulePolicies.Get(k8sContainerID) + savedPolicy, ok := savedPolicies.Get(ruleId) + if ok { + savedPolicy := savedPolicy.(*v1beta1.RulePolicy) + if IsPolicyIncluded(savedPolicy, newPolicy) { + return + } + } + + toBeSavedPolicies := am.toSaveRulePolicies.Get(k8sContainerID) + toBeSavedPolicy := toBeSavedPolicies.Get(ruleId) + + if IsPolicyIncluded(toBeSavedPolicy, newPolicy) { + return + } + + var finalPolicy *v1beta1.RulePolicy + if toBeSavedPolicy != nil { + finalPolicy = toBeSavedPolicy + if allowedContainer { + finalPolicy.AllowedContainer = true + } + if allowedProcess != "" && !slices.Contains(finalPolicy.AllowedProcesses, allowedProcess) { + finalPolicy.AllowedProcesses = append(finalPolicy.AllowedProcesses, allowedProcess) + } + } else { + finalPolicy = newPolicy + } + + toBeSavedPolicies.Set(ruleId, finalPolicy) +} diff --git a/pkg/applicationprofilemanager/v1/applicationprofile_manager_test.go b/pkg/applicationprofilemanager/v1/applicationprofile_manager_test.go index aac26329..49cc02d5 100644 --- a/pkg/applicationprofilemanager/v1/applicationprofile_manager_test.go +++ b/pkg/applicationprofilemanager/v1/applicationprofile_manager_test.go @@ -23,6 +23,7 @@ import ( "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" "github.com/kubescape/storage/pkg/registry/file/dynamicpathdetector" "github.com/stretchr/testify/assert" + istiocache "istio.io/pkg/cache" ) func TestApplicationProfileManager(t *testing.T) { @@ -295,3 +296,165 @@ func BenchmarkReportFileOpen(b *testing.B) { } b.ReportAllocs() } + +func TestReportRulePolicy(t *testing.T) { + // Setup common test environment + cfg := config.Config{ + InitialDelay: 1 * time.Second, + MaxSniffingTime: 5 * time.Minute, + UpdateDataPeriod: 1 * time.Second, + } + ctx := context.TODO() + k8sClient := &k8sclient.K8sClientMock{} + storageClient := &storage.StorageHttpClientMock{} + k8sObjectCacheMock := &objectcache.K8sObjectCacheMock{} + seccompManagerMock := &seccompmanager.SeccompManagerMock{} + + tests := []struct { + name string + k8sContainerID string + ruleID string + allowedProcess string + allowedContainer bool + existingSaved *v1beta1.RulePolicy + existingToSave *v1beta1.RulePolicy + expectedPolicy *v1beta1.RulePolicy + shouldSet bool + }{ + { + name: "New policy with process", + k8sContainerID: "ns/pod/cont", + ruleID: "rule1", + allowedProcess: "process1", + allowedContainer: false, + existingSaved: nil, + existingToSave: nil, + expectedPolicy: &v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{"process1"}, + }, + shouldSet: true, + }, + { + name: "New policy with container allowed", + k8sContainerID: "ns/pod/cont", + ruleID: "rule2", + allowedProcess: "", + allowedContainer: true, + existingSaved: nil, + existingToSave: nil, + expectedPolicy: &v1beta1.RulePolicy{ + AllowedContainer: true, + AllowedProcesses: []string{""}, + }, + shouldSet: true, + }, + { + name: "Merge with existing toBeSaved policy - new process", + k8sContainerID: "ns/pod/cont", + ruleID: "rule3", + allowedProcess: "process2", + allowedContainer: false, + existingToSave: &v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{"process1"}, + }, + expectedPolicy: &v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{"process1", "process2"}, + }, + shouldSet: true, + }, + { + name: "Merge with existing toBeSaved policy - enable container", + k8sContainerID: "ns/pod/cont", + ruleID: "rule4", + allowedProcess: "", + allowedContainer: true, + existingToSave: &v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{"process1"}, + }, + expectedPolicy: &v1beta1.RulePolicy{ + AllowedContainer: true, + AllowedProcesses: []string{"process1"}, + }, + shouldSet: true, + }, + { + name: "Skip if policy already in saved", + k8sContainerID: "ns/pod/cont", + ruleID: "rule5", + allowedProcess: "process1", + allowedContainer: false, + existingSaved: &v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{"process1"}, + }, + shouldSet: false, + }, + { + name: "Skip if policy already in toBeSaved", + k8sContainerID: "ns/pod/cont", + ruleID: "rule6", + allowedProcess: "process1", + allowedContainer: false, + existingToSave: &v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{"process1"}, + }, + shouldSet: false, + }, + { + name: "Deduplicate processes", + k8sContainerID: "ns/pod/cont", + ruleID: "rule7", + allowedProcess: "process1", + allowedContainer: false, + existingToSave: &v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{"process1", "process2"}, + }, + expectedPolicy: &v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{"process1", "process2"}, + }, + shouldSet: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + am, err := CreateApplicationProfileManager(ctx, cfg, "cluster", k8sClient, storageClient, mapset.NewSet[string](), k8sObjectCacheMock, seccompManagerMock) + assert.NoError(t, err) + + am.savedRulePolicies.Set(tt.k8sContainerID, istiocache.NewTTL(5*am.cfg.UpdateDataPeriod, am.cfg.UpdateDataPeriod)) + am.toSaveRulePolicies.Set(tt.k8sContainerID, new(maps.SafeMap[string, *v1beta1.RulePolicy])) + am.trackedContainers.Add(tt.k8sContainerID) + + if tt.existingSaved != nil { + am.savedRulePolicies.Get(tt.k8sContainerID).Set(tt.ruleID, tt.existingSaved) + } + if tt.existingToSave != nil { + am.toSaveRulePolicies.Get(tt.k8sContainerID).Set(tt.ruleID, tt.existingToSave) + } + + am.ReportRulePolicy(tt.k8sContainerID, tt.ruleID, tt.allowedProcess, tt.allowedContainer) + + if tt.shouldSet { + resultPolicy := am.toSaveRulePolicies.Get(tt.k8sContainerID).Get(tt.ruleID) + assert.NotNil(t, resultPolicy) + assert.Equal(t, tt.expectedPolicy.AllowedContainer, resultPolicy.AllowedContainer) + assert.ElementsMatch(t, tt.expectedPolicy.AllowedProcesses, resultPolicy.AllowedProcesses) + } else { + resultPolicy := am.toSaveRulePolicies.Get(tt.k8sContainerID).Get(tt.ruleID) + if tt.existingToSave != nil { + assert.Equal(t, tt.existingToSave.AllowedContainer, resultPolicy.AllowedContainer) + assert.ElementsMatch(t, tt.existingToSave.AllowedProcesses, resultPolicy.AllowedProcesses) + } else { + assert.Nil(t, resultPolicy) + } + } + }) + } +} diff --git a/pkg/applicationprofilemanager/v1/endpoint_utils.go b/pkg/applicationprofilemanager/v1/helpers.go similarity index 62% rename from pkg/applicationprofilemanager/v1/endpoint_utils.go rename to pkg/applicationprofilemanager/v1/helpers.go index 6acf18e8..da5235ca 100644 --- a/pkg/applicationprofilemanager/v1/endpoint_utils.go +++ b/pkg/applicationprofilemanager/v1/helpers.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/url" + "slices" "sort" "strings" @@ -14,6 +15,8 @@ import ( "github.com/kubescape/go-logger/helpers" tracerhttphelper "github.com/kubescape/node-agent/pkg/ebpf/gadgets/http/tracer" tracerhttptype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/http/types" + "github.com/kubescape/node-agent/pkg/ruleengine/v1" + "github.com/kubescape/node-agent/pkg/utils" "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" ) @@ -34,7 +37,7 @@ func GetNewEndpoint(event *tracerhttptype.Event, identifier string) (*v1beta1.HT Headers: rawJSON}, nil } -func (am *ApplicationProfileManager) GetEndpointIdentifier(request *tracerhttptype.Event) (string, error) { +func GetEndpointIdentifier(request *tracerhttptype.Event) (string, error) { identifier := request.Request.URL.String() if host := request.Request.Host; host != "" { @@ -70,20 +73,58 @@ func CalculateHTTPEndpointHash(endpoint *v1beta1.HTTPEndpoint) string { } func isValidHost(host string) bool { - // Check if the host is empty if host == "" { return false } - // Check if host contains spaces or invalid characters if strings.ContainsAny(host, " \t\r\n") { return false } - // Parse the host using http's standard URL parser if _, err := url.ParseRequestURI("http://" + host); err != nil { return false } return true } + +func IsPolicyIncluded(existingPolicy, newPolicy *v1beta1.RulePolicy) bool { + if existingPolicy == nil { + return false + } + + if newPolicy.AllowedContainer && !existingPolicy.AllowedContainer { + return false + } + + for _, newProcess := range newPolicy.AllowedProcesses { + if !slices.Contains(existingPolicy.AllowedProcesses, newProcess) { + return false + } + } + + return true +} + +func GetInitOperations(containerType string, containerIndex int) []utils.PatchOperation { + var operations []utils.PatchOperation + + ids := ruleengine.NewRuleCreator().GetAllRuleIDs() + rulePoliciesMap := make(map[string]v1beta1.RulePolicy) + for _, id := range ids { + rulePoliciesMap[id] = v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{}, + } + } + + createMap := utils.PatchOperation{ + Op: "replace", + Path: fmt.Sprintf("/spec/%s/%d/rulePolicies", containerType, containerIndex), + Value: rulePoliciesMap, + } + + operations = append(operations, createMap) + + return operations +} diff --git a/pkg/containerwatcher/v1/container_watcher.go b/pkg/containerwatcher/v1/container_watcher.go index 4e1af499..96d03c1d 100644 --- a/pkg/containerwatcher/v1/container_watcher.go +++ b/pkg/containerwatcher/v1/container_watcher.go @@ -29,7 +29,6 @@ import ( "github.com/kubescape/node-agent/pkg/applicationprofilemanager" "github.com/kubescape/node-agent/pkg/config" "github.com/kubescape/node-agent/pkg/containerwatcher" - "github.com/kubescape/node-agent/pkg/dnsmanager" tracerhardlink "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/tracer" tracerhardlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/types" tracerhttp "github.com/kubescape/node-agent/pkg/ebpf/gadgets/http/tracer" @@ -42,11 +41,12 @@ import ( tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" tracersymlink "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/tracer" tracersymlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/types" - "github.com/kubescape/node-agent/pkg/processmanager" - + "github.com/kubescape/node-agent/pkg/eventreporters/dnsmanager" + "github.com/kubescape/node-agent/pkg/eventreporters/rulepolicy" "github.com/kubescape/node-agent/pkg/malwaremanager" "github.com/kubescape/node-agent/pkg/metricsmanager" "github.com/kubescape/node-agent/pkg/networkmanager" + "github.com/kubescape/node-agent/pkg/processmanager" "github.com/kubescape/node-agent/pkg/relevancymanager" rulebinding "github.com/kubescape/node-agent/pkg/rulebindingmanager" "github.com/kubescape/node-agent/pkg/rulemanager" @@ -168,6 +168,9 @@ func CreateIGContainerWatcher(cfg config.Config, applicationProfileManager appli if err != nil { return nil, fmt.Errorf("creating tracer collection: %w", err) } + + rulePolicyReporter := rulepolicy.NewRulePolicyReporter(ruleManager, applicationProfileManager) + // Create a capabilities worker pool capabilitiesWorkerPool, err := ants.NewPoolWithFunc(capabilitiesWorkerPoolSize, func(i interface{}) { event := i.(tracercapabilitiestype.Event) @@ -175,6 +178,7 @@ func CreateIGContainerWatcher(cfg config.Config, applicationProfileManager appli if event.K8s.ContainerName == "" { return } + metrics.ReportEvent(utils.CapabilitiesEventType) k8sContainerID := utils.CreateK8sContainerID(event.K8s.Namespace, event.K8s.PodName, event.K8s.ContainerName) applicationProfileManager.ReportCapability(k8sContainerID, event.CapName) @@ -211,6 +215,7 @@ func CreateIGContainerWatcher(cfg config.Config, applicationProfileManager appli relevancyManager.ReportFileExec(event.Runtime.ContainerID, k8sContainerID, path) ruleManager.ReportEvent(utils.ExecveEventType, &event) malwareManager.ReportEvent(utils.ExecveEventType, &event) + rulePolicyReporter.ReportEvent(utils.ExecveEventType, &event, k8sContainerID, event.Comm) // Report exec events to event receivers reportEventToThirdPartyTracers(utils.ExecveEventType, &event, thirdPartyEventReceivers) @@ -291,7 +296,7 @@ func CreateIGContainerWatcher(cfg config.Config, applicationProfileManager appli } metrics.ReportEvent(utils.DnsEventType) - dnsManagerClient.ReportDNSEvent(event) + dnsManagerClient.ReportEvent(event) ruleManager.ReportEvent(utils.DnsEventType, &event) // Report DNS events to event receivers @@ -321,8 +326,11 @@ func CreateIGContainerWatcher(cfg config.Config, applicationProfileManager appli if event.K8s.ContainerName == "" { return } + k8sContainerID := utils.CreateK8sContainerID(event.K8s.Namespace, event.K8s.PodName, event.K8s.ContainerName) + metrics.ReportEvent(utils.SymlinkEventType) ruleManager.ReportEvent(utils.SymlinkEventType, &event) + rulePolicyReporter.ReportEvent(utils.SymlinkEventType, &event, k8sContainerID, event.Comm) // Report symlink events to event receivers reportEventToThirdPartyTracers(utils.SymlinkEventType, &event, thirdPartyEventReceivers) @@ -336,9 +344,12 @@ func CreateIGContainerWatcher(cfg config.Config, applicationProfileManager appli if event.K8s.ContainerName == "" { return } + + k8sContainerID := utils.CreateK8sContainerID(event.K8s.Namespace, event.K8s.PodName, event.K8s.ContainerName) + metrics.ReportEvent(utils.HardlinkEventType) ruleManager.ReportEvent(utils.HardlinkEventType, &event) - + rulePolicyReporter.ReportEvent(utils.HardlinkEventType, &event, k8sContainerID, event.Comm) // Report hardlink events to event receivers reportEventToThirdPartyTracers(utils.HardlinkEventType, &event, thirdPartyEventReceivers) }) diff --git a/pkg/dnsmanager/dns_manager.go b/pkg/eventreporters/dnsmanager/dns_manager.go similarity index 95% rename from pkg/dnsmanager/dns_manager.go rename to pkg/eventreporters/dnsmanager/dns_manager.go index 9234137c..de6efb3b 100644 --- a/pkg/dnsmanager/dns_manager.go +++ b/pkg/eventreporters/dnsmanager/dns_manager.go @@ -36,8 +36,8 @@ func CreateDNSManager() *DNSManager { } } -func (dm *DNSManager) ReportDNSEvent(dnsEvent tracerdnstype.Event) { - // If we have addresses in the event, use them directly +func (dm *DNSManager) ReportEvent(dnsEvent tracerdnstype.Event) { + if len(dnsEvent.Addresses) > 0 { for _, address := range dnsEvent.Addresses { dm.addressToDomainMap.Set(address, dnsEvent.DNSName) diff --git a/pkg/dnsmanager/dns_manager_interface.go b/pkg/eventreporters/dnsmanager/dns_manager_interface.go similarity index 82% rename from pkg/dnsmanager/dns_manager_interface.go rename to pkg/eventreporters/dnsmanager/dns_manager_interface.go index 1f834f9f..a7fc4a6f 100644 --- a/pkg/dnsmanager/dns_manager_interface.go +++ b/pkg/eventreporters/dnsmanager/dns_manager_interface.go @@ -5,7 +5,7 @@ import ( ) type DNSManagerClient interface { - ReportDNSEvent(networkEvent tracerdnstype.Event) + ReportEvent(networkEvent tracerdnstype.Event) } type DNSResolver interface { diff --git a/pkg/dnsmanager/dns_manager_mock.go b/pkg/eventreporters/dnsmanager/dns_manager_mock.go similarity index 86% rename from pkg/dnsmanager/dns_manager_mock.go rename to pkg/eventreporters/dnsmanager/dns_manager_mock.go index 812067ab..45eed926 100644 --- a/pkg/dnsmanager/dns_manager_mock.go +++ b/pkg/eventreporters/dnsmanager/dns_manager_mock.go @@ -14,7 +14,7 @@ func CreateDNSManagerMock() *DNSManagerMock { return &DNSManagerMock{} } -func (n *DNSManagerMock) ReportDNSEvent(_ tracerdnstype.Event) { +func (n *DNSManagerMock) ReportEvent(_ tracerdnstype.Event) { } func (n *DNSManagerMock) ResolveIPAddress(_ string) (string, bool) { diff --git a/pkg/dnsmanager/dns_manager_test.go b/pkg/eventreporters/dnsmanager/dns_manager_test.go similarity index 97% rename from pkg/dnsmanager/dns_manager_test.go rename to pkg/eventreporters/dnsmanager/dns_manager_test.go index 8fd6e173..6b6ee213 100644 --- a/pkg/dnsmanager/dns_manager_test.go +++ b/pkg/eventreporters/dnsmanager/dns_manager_test.go @@ -60,7 +60,7 @@ func TestResolveIPAddress(t *testing.T) { t.Run(tt.name, func(t *testing.T) { dm := CreateDNSManager() - dm.ReportDNSEvent(tt.dnsEvent) + dm.ReportEvent(tt.dnsEvent) got, ok := dm.ResolveIPAddress(tt.ipAddr) if got != tt.want || ok != tt.wantOk { t.Errorf("ResolveIPAddress() got = %v, ok = %v, want = %v, wantOk = %v", got, ok, tt.want, tt.wantOk) @@ -107,7 +107,7 @@ func TestResolveIPAddressFallback(t *testing.T) { return } - dm.ReportDNSEvent(tt.dnsEvent) + dm.ReportEvent(tt.dnsEvent) got, ok := dm.ResolveIPAddress(addresses[0].String()) if got != tt.want || ok != tt.wantOk { t.Errorf("ResolveIPAddress() got = %v, ok = %v, want = %v, wantOk = %v", got, ok, tt.want, tt.wantOk) @@ -126,7 +126,7 @@ func TestCacheFallbackBehavior(t *testing.T) { "1.2.3.4", }, } - dm.ReportDNSEvent(event) + dm.ReportEvent(event) // Check if the lookup is cached cached, found := dm.lookupCache.Get(event.DNSName) @@ -146,7 +146,7 @@ func TestCacheFallbackBehavior(t *testing.T) { failEvent := tracerdnstype.Event{ DNSName: "nonexistent.local", } - dm.ReportDNSEvent(failEvent) + dm.ReportEvent(failEvent) // Check if the failure is cached _, found = dm.failureCache.Get(failEvent.DNSName) @@ -204,7 +204,7 @@ func TestConcurrentAccess(t *testing.T) { if rand.Float32() < 0.5 { // Write operation event := testEvents[rand.IntN(len(testEvents))] - dm.ReportDNSEvent(event) + dm.ReportEvent(event) } else { // Read operation if cached, found := dm.lookupCache.Get("test1.com"); found { diff --git a/pkg/eventreporters/rulepolicy/rule_policy.go b/pkg/eventreporters/rulepolicy/rule_policy.go new file mode 100644 index 00000000..8dc3ddae --- /dev/null +++ b/pkg/eventreporters/rulepolicy/rule_policy.go @@ -0,0 +1,27 @@ +package rulepolicy + +import ( + "github.com/kubescape/node-agent/pkg/applicationprofilemanager" + "github.com/kubescape/node-agent/pkg/rulemanager" + "github.com/kubescape/node-agent/pkg/utils" +) + +type RulePolicyReporter struct { + ruleManager rulemanager.RuleManagerClient + applicationProfileManager applicationprofilemanager.ApplicationProfileManagerClient +} + +func NewRulePolicyReporter(ruleManager rulemanager.RuleManagerClient, applicationProfileManager applicationprofilemanager.ApplicationProfileManagerClient) *RulePolicyReporter { + return &RulePolicyReporter{ + ruleManager: ruleManager, + applicationProfileManager: applicationProfileManager, + } +} + +func (rpm *RulePolicyReporter) ReportEvent(eventType utils.EventType, event utils.K8sEvent, k8sContainerID string, allowedProcess string) { + rulesIds := rpm.ruleManager.EvaluateRulesForEvent(eventType, event) + for _, rule := range rulesIds { + // TODO: Add a check to see if the rule is using rule policy + rpm.applicationProfileManager.ReportRulePolicy(k8sContainerID, rule, allowedProcess, false) + } +} diff --git a/pkg/networkmanager/v2/network_manager.go b/pkg/networkmanager/v2/network_manager.go index 13cddc70..1572fc1c 100644 --- a/pkg/networkmanager/v2/network_manager.go +++ b/pkg/networkmanager/v2/network_manager.go @@ -19,7 +19,7 @@ import ( helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" "github.com/kubescape/k8s-interface/workloadinterface" "github.com/kubescape/node-agent/pkg/config" - "github.com/kubescape/node-agent/pkg/dnsmanager" + "github.com/kubescape/node-agent/pkg/eventreporters/dnsmanager" "github.com/kubescape/node-agent/pkg/k8sclient" "github.com/kubescape/node-agent/pkg/networkmanager" "github.com/kubescape/node-agent/pkg/objectcache" diff --git a/pkg/networkmanager/v2/network_manager_test.go b/pkg/networkmanager/v2/network_manager_test.go index fa4c04c2..cd384b03 100644 --- a/pkg/networkmanager/v2/network_manager_test.go +++ b/pkg/networkmanager/v2/network_manager_test.go @@ -7,7 +7,7 @@ import ( "time" "github.com/kubescape/node-agent/pkg/config" - "github.com/kubescape/node-agent/pkg/dnsmanager" + "github.com/kubescape/node-agent/pkg/eventreporters/dnsmanager" "github.com/kubescape/node-agent/pkg/k8sclient" "github.com/kubescape/node-agent/pkg/networkmanager" "github.com/kubescape/node-agent/pkg/objectcache" diff --git a/pkg/objectcache/applicationprofilecache/applicationprofilecache.go b/pkg/objectcache/applicationprofilecache/applicationprofilecache.go index eae254ef..84faf8da 100644 --- a/pkg/objectcache/applicationprofilecache/applicationprofilecache.go +++ b/pkg/objectcache/applicationprofilecache/applicationprofilecache.go @@ -350,6 +350,13 @@ func (ap *ApplicationProfileCacheImpl) mergeContainer(normalContainer, userConta normalContainer.Opens = append(normalContainer.Opens, userContainer.Opens...) normalContainer.Syscalls = append(normalContainer.Syscalls, userContainer.Syscalls...) normalContainer.Endpoints = append(normalContainer.Endpoints, userContainer.Endpoints...) + for k, v := range userContainer.PolicyByRuleId { + if existingPolicy, exists := normalContainer.PolicyByRuleId[k]; exists { + normalContainer.PolicyByRuleId[k] = utils.MergePolicies(existingPolicy, v) + } else { + normalContainer.PolicyByRuleId[k] = v + } + } } func (ap *ApplicationProfileCacheImpl) deleteApplicationProfile(obj runtime.Object) { diff --git a/pkg/objectcache/dnscache/dnscache.go b/pkg/objectcache/dnscache/dnscache.go index 6d1dff09..f96cf784 100644 --- a/pkg/objectcache/dnscache/dnscache.go +++ b/pkg/objectcache/dnscache/dnscache.go @@ -2,7 +2,7 @@ package dnscache import ( "github.com/kubescape/go-logger" - "github.com/kubescape/node-agent/pkg/dnsmanager" + "github.com/kubescape/node-agent/pkg/eventreporters/dnsmanager" "github.com/kubescape/node-agent/pkg/objectcache" ) diff --git a/pkg/rulebindingmanager/rulebindingmanager_interface.go b/pkg/rulebindingmanager/rulebindingmanager_interface.go index 1ebd729f..993dad24 100644 --- a/pkg/rulebindingmanager/rulebindingmanager_interface.go +++ b/pkg/rulebindingmanager/rulebindingmanager_interface.go @@ -7,4 +7,5 @@ import ( type RuleBindingCache interface { ListRulesForPod(namespace, name string) []ruleengine.RuleEvaluator AddNotifier(*chan RuleBindingNotify) + GetRuleCreator() ruleengine.RuleCreator } diff --git a/pkg/rulebindingmanager/rulebindingmanager_mock.go b/pkg/rulebindingmanager/rulebindingmanager_mock.go index 937fe58c..fa657986 100644 --- a/pkg/rulebindingmanager/rulebindingmanager_mock.go +++ b/pkg/rulebindingmanager/rulebindingmanager_mock.go @@ -12,3 +12,7 @@ func (r *RuleBindingCacheMock) ListRulesForPod(_, _ string) []ruleengine.RuleEva } func (r *RuleBindingCacheMock) AddNotifier(_ *chan RuleBindingNotify) { } + +func (r *RuleBindingCacheMock) GetRuleCreator() ruleengine.RuleCreator { + return nil +} diff --git a/pkg/ruleengine/ruleengine_interface.go b/pkg/ruleengine/ruleengine_interface.go index d512670f..d6c7ab39 100644 --- a/pkg/ruleengine/ruleengine_interface.go +++ b/pkg/ruleengine/ruleengine_interface.go @@ -51,6 +51,7 @@ type RuleCreator interface { CreateRuleByID(id string) RuleEvaluator CreateRuleByName(name string) RuleEvaluator RegisterRule(rule RuleDescriptor) + CreateRulesByEventType(eventType utils.EventType) []RuleEvaluator } type RuleEvaluator interface { @@ -73,6 +74,11 @@ type RuleEvaluator interface { GetParameters() map[string]interface{} } +type RuleCondition interface { + EvaluateRule(eventType utils.EventType, event utils.K8sEvent, k8sObjCache objectcache.K8sObjectCache) bool + ID() string +} + // RuleSpec is an interface for rule requirements type RuleSpec interface { // Event types required for the rule diff --git a/pkg/ruleengine/ruleengine_mock.go b/pkg/ruleengine/ruleengine_mock.go index 40e46b89..150b749c 100644 --- a/pkg/ruleengine/ruleengine_mock.go +++ b/pkg/ruleengine/ruleengine_mock.go @@ -28,6 +28,10 @@ func (r *RuleCreatorMock) CreateRuleByName(name string) RuleEvaluator { func (r *RuleCreatorMock) RegisterRule(rule RuleDescriptor) { } +func (r *RuleCreatorMock) CreateRulesByEventType(eventType utils.EventType) []RuleEvaluator { + return []RuleEvaluator{} +} + var _ RuleEvaluator = (*RuleMock)(nil) type RuleMock struct { diff --git a/pkg/ruleengine/v1/factory.go b/pkg/ruleengine/v1/factory.go index ceaa1357..ddc00407 100644 --- a/pkg/ruleengine/v1/factory.go +++ b/pkg/ruleengine/v1/factory.go @@ -1,6 +1,9 @@ package ruleengine -import "github.com/kubescape/node-agent/pkg/ruleengine" +import ( + "github.com/kubescape/node-agent/pkg/ruleengine" + "github.com/kubescape/node-agent/pkg/utils" +) var _ ruleengine.RuleCreator = (*RuleCreatorImpl)(nil) @@ -75,3 +78,29 @@ func (r *RuleCreatorImpl) GetAllRuleDescriptors() []ruleengine.RuleDescriptor { func (r *RuleCreatorImpl) RegisterRule(rule ruleengine.RuleDescriptor) { r.ruleDescriptions = append(r.ruleDescriptions, rule) } + +func (r *RuleCreatorImpl) CreateRulesByEventType(eventType utils.EventType) []ruleengine.RuleEvaluator { + var rules []ruleengine.RuleEvaluator + for _, rule := range r.ruleDescriptions { + if containsEventType(rule.Requirements.RequiredEventTypes(), eventType) { + rules = append(rules, rule.RuleCreationFunc()) + } + } + return rules +} +func (r *RuleCreatorImpl) GetAllRuleIDs() []string { + var ruleIDs []string + for _, rule := range r.ruleDescriptions { + ruleIDs = append(ruleIDs, rule.ID) + } + return ruleIDs +} + +func containsEventType(eventTypes []utils.EventType, eventType utils.EventType) bool { + for _, et := range eventTypes { + if et == eventType { + return true + } + } + return false +} diff --git a/pkg/ruleengine/v1/helpers.go b/pkg/ruleengine/v1/helpers.go index 3a8b0024..9cd5c84f 100644 --- a/pkg/ruleengine/v1/helpers.go +++ b/pkg/ruleengine/v1/helpers.go @@ -7,9 +7,12 @@ import ( "slices" "strings" + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" "github.com/kubescape/node-agent/pkg/objectcache" tracerexectype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/exec/types" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" ) @@ -153,3 +156,25 @@ func interfaceToStringSlice(val interface{}) ([]string, bool) { } return nil, false } + +func isAllowed(event *eventtypes.Event, objCache objectcache.ObjectCache, process string, ruleId string) (bool, error) { + ap := objCache.ApplicationProfileCache().GetApplicationProfile(event.Runtime.ContainerID) + if ap == nil { + return false, errors.New("application profile not found") + } + + appProfile, err := getContainerFromApplicationProfile(ap, event.GetContainer()) + if err != nil { + return false, err + } + + if policy, ok := appProfile.PolicyByRuleId[ruleId]; ok { + if policy.AllowedContainer || slices.Contains(policy.AllowedProcesses, process) { + logger.L().Debug("process is allowed by policy", helpers.String("ruleID", ruleId), helpers.String("process", process)) + return true, nil + } + } + + logger.L().Debug("process is not allowed by policy", helpers.String("ruleID", ruleId), helpers.String("process", process)) + return false, nil +} diff --git a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file.go b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file.go index 8a4b97d3..0e46234d 100644 --- a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file.go +++ b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file.go @@ -1,7 +1,6 @@ package ruleengine import ( - "errors" "fmt" "strings" @@ -36,6 +35,7 @@ var R1010SymlinkCreatedOverSensitiveFileRuleDescriptor = ruleengine.RuleDescript return CreateRuleR1010SymlinkCreatedOverSensitiveFile() }, } + var _ ruleengine.RuleEvaluator = (*R1010SymlinkCreatedOverSensitiveFile)(nil) type R1010SymlinkCreatedOverSensitiveFile struct { @@ -79,61 +79,75 @@ func (rule *R1010SymlinkCreatedOverSensitiveFile) DeleteRule() { } func (rule *R1010SymlinkCreatedOverSensitiveFile) ProcessEvent(eventType utils.EventType, event utils.K8sEvent, objCache objectcache.ObjectCache) ruleengine.RuleFailure { - if eventType != utils.SymlinkEventType { - return nil - } + logger.L().Debug("Processing event", helpers.String("ruleID", rule.ID()), helpers.String("eventType", string(eventType))) - symlinkEvent, ok := event.(*tracersymlinktype.Event) - if !ok { + if !rule.EvaluateRule(eventType, event, objCache.K8sObjectCache()) { + logger.L().Debug("Event does not match rule", helpers.String("ruleID", rule.ID()), helpers.String("eventType", string(eventType))) return nil } - if allowed, err := isSymLinkAllowed(symlinkEvent, objCache); err != nil { + symlinkEvent, _ := event.(*tracersymlinktype.Event) + + if allowed, err := isAllowed(&symlinkEvent.Event, objCache, symlinkEvent.Comm, R1010ID); err != nil { + logger.L().Error("failed to check if symlink is allowed", helpers.String("ruleID", rule.ID()), helpers.String("error", err.Error())) return nil } else if allowed { return nil } + return &GenericRuleFailure{ + BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ + AlertName: rule.Name(), + Arguments: map[string]interface{}{ + "oldPath": symlinkEvent.OldPath, + "newPath": symlinkEvent.NewPath, + }, + InfectedPID: symlinkEvent.Pid, + FixSuggestions: "If this is a legitimate action, please consider removing this workload from the binding of this rule.", + Severity: R1010SymlinkCreatedOverSensitiveFileRuleDescriptor.Priority, + }, + RuntimeProcessDetails: apitypes.ProcessTree{ + ProcessTree: apitypes.Process{ + Comm: symlinkEvent.Comm, + PPID: symlinkEvent.PPid, + PID: symlinkEvent.Pid, + UpperLayer: &symlinkEvent.UpperLayer, + Uid: &symlinkEvent.Uid, + Gid: &symlinkEvent.Gid, + Hardlink: symlinkEvent.ExePath, + Path: symlinkEvent.ExePath, + }, + ContainerID: symlinkEvent.Runtime.ContainerID, + }, + TriggerEvent: symlinkEvent.Event, + RuleAlert: apitypes.RuleAlert{ + RuleDescription: fmt.Sprintf("Symlink created over sensitive file: %s - %s in: %s", symlinkEvent.OldPath, symlinkEvent.NewPath, symlinkEvent.GetContainer()), + }, + RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ + PodName: symlinkEvent.GetPod(), + PodLabels: symlinkEvent.K8s.PodLabels, + }, + RuleID: rule.ID(), + } +} + +func (rule *R1010SymlinkCreatedOverSensitiveFile) EvaluateRule(eventType utils.EventType, event utils.K8sEvent, _ objectcache.K8sObjectCache) bool { + if eventType != utils.SymlinkEventType { + return false + } + + symlinkEvent, ok := event.(*tracersymlinktype.Event) + if !ok { + return false + } + for _, path := range rule.additionalPaths { if strings.HasPrefix(symlinkEvent.OldPath, path) { - return &GenericRuleFailure{ - BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ - AlertName: rule.Name(), - Arguments: map[string]interface{}{ - "oldPath": symlinkEvent.OldPath, - "newPath": symlinkEvent.NewPath, - }, - InfectedPID: symlinkEvent.Pid, - FixSuggestions: "If this is a legitimate action, please consider removing this workload from the binding of this rule.", - Severity: R1010SymlinkCreatedOverSensitiveFileRuleDescriptor.Priority, - }, - RuntimeProcessDetails: apitypes.ProcessTree{ - ProcessTree: apitypes.Process{ - Comm: symlinkEvent.Comm, - PPID: symlinkEvent.PPid, - PID: symlinkEvent.Pid, - UpperLayer: &symlinkEvent.UpperLayer, - Uid: &symlinkEvent.Uid, - Gid: &symlinkEvent.Gid, - Hardlink: symlinkEvent.ExePath, - Path: symlinkEvent.ExePath, - }, - ContainerID: symlinkEvent.Runtime.ContainerID, - }, - TriggerEvent: symlinkEvent.Event, - RuleAlert: apitypes.RuleAlert{ - RuleDescription: fmt.Sprintf("Symlink created over sensitive file: %s - %s in: %s", symlinkEvent.OldPath, symlinkEvent.NewPath, symlinkEvent.GetContainer()), - }, - RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ - PodName: symlinkEvent.GetPod(), - PodLabels: symlinkEvent.K8s.PodLabels, - }, - RuleID: rule.ID(), - } + return true } } - return nil + return false } func (rule *R1010SymlinkCreatedOverSensitiveFile) Requirements() ruleengine.RuleSpec { @@ -141,23 +155,3 @@ func (rule *R1010SymlinkCreatedOverSensitiveFile) Requirements() ruleengine.Rule EventTypes: R1010SymlinkCreatedOverSensitiveFileRuleDescriptor.Requirements.RequiredEventTypes(), } } - -func isSymLinkAllowed(symlinkEvent *tracersymlinktype.Event, objCache objectcache.ObjectCache) (bool, error) { - ap := objCache.ApplicationProfileCache().GetApplicationProfile(symlinkEvent.Runtime.ContainerID) - if ap == nil { - return true, errors.New("application profile not found") - } - - appProfileExecList, err := getContainerFromApplicationProfile(ap, symlinkEvent.GetContainer()) - if err != nil { - return true, err - } - - for _, exec := range appProfileExecList.Execs { - if exec.Path == symlinkEvent.Comm { - return true, nil - } - } - - return false, nil -} diff --git a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go index 61689b8c..cc08428f 100644 --- a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go +++ b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go @@ -26,18 +26,17 @@ func TestR1010SymlinkCreatedOverSensitiveFile(t *testing.T) { Containers: []v1beta1.ApplicationProfileContainer{ { Name: "test", + PolicyByRuleId: map[string]v1beta1.RulePolicy{ + R1010ID: v1beta1.RulePolicy{ + AllowedProcesses: []string{"/usr/sbin/groupadd"}, + }, + }, Opens: []v1beta1.OpenCalls{ { Path: "/test", Flags: []string{"O_RDONLY"}, }, }, - Execs: []v1beta1.ExecCalls{ - { - Path: "/usr/sbin/groupadd", - Args: []string{"test"}, - }, - }, }, }, }, diff --git a/pkg/ruleengine/v1/r1011_ld_preload_hook.go b/pkg/ruleengine/v1/r1011_ld_preload_hook.go index 30041aac..aba0ff86 100644 --- a/pkg/ruleengine/v1/r1011_ld_preload_hook.go +++ b/pkg/ruleengine/v1/r1011_ld_preload_hook.go @@ -62,158 +62,202 @@ func (rule *R1011LdPreloadHook) ID() string { func (rule *R1011LdPreloadHook) DeleteRule() { } -func (rule *R1011LdPreloadHook) handleExecEvent(execEvent *tracerexectype.Event, k8sObjCache objectcache.K8sObjectCache) ruleengine.RuleFailure { - // Java is a special case, we don't want to alert on it because it uses LD_LIBRARY_PATH. - if execEvent.Comm == JAVA_COMM { +func (rule *R1011LdPreloadHook) ProcessEvent(eventType utils.EventType, event utils.K8sEvent, objectCache objectcache.ObjectCache) ruleengine.RuleFailure { + + if !rule.EvaluateRule(eventType, event, objectCache.K8sObjectCache()) { return nil } - // Check if the process is a MATLAB process and ignore it. - if execEvent.GetContainer() == "matlab" { - return nil + if eventType == utils.ExecveEventType { + execEvent, ok := event.(*tracerexectype.Event) + if !ok { + return nil + } + + if allowed, err := isAllowed(&execEvent.Event, objectCache, execEvent.Comm, R1011ID); err != nil { + logger.L().Error("failed to check if ld_preload is allowed", helpers.String("ruleID", rule.ID()), helpers.String("error", err.Error())) + return nil + } else if allowed { + return nil + } + + return rule.ruleFailureExecEvent(execEvent) + } else if eventType == utils.OpenEventType { + openEvent, ok := event.(*traceropentype.Event) + if !ok { + return nil + } + + if allowed, err := isAllowed(&openEvent.Event, objectCache, openEvent.Comm, R1011ID); err != nil { + logger.L().Error("failed to check if ld_preload is allowed", helpers.String("ruleID", rule.ID()), helpers.String("error", err.Error())) + return nil + } else if allowed { + return nil + } + + return rule.ruleFailureOpenEvent(openEvent) } + return nil +} + +func (rule *R1011LdPreloadHook) EvaluateRule(eventType utils.EventType, event utils.K8sEvent, k8sObjCache objectcache.K8sObjectCache) bool { + switch eventType { + case utils.ExecveEventType: + execEvent, ok := event.(*tracerexectype.Event) + if !ok { + return false + } + return rule.shouldAlertExec(execEvent, k8sObjCache) + + case utils.OpenEventType: + openEvent, ok := event.(*traceropentype.Event) + if !ok { + return false + } + return rule.shouldAlertOpen(openEvent) + + default: + return false + } +} + +func (rule *R1011LdPreloadHook) Requirements() ruleengine.RuleSpec { + return &RuleRequirements{ + EventTypes: R1011LdPreloadHookRuleDescriptor.Requirements.RequiredEventTypes(), + } +} + +func (rule *R1011LdPreloadHook) ruleFailureExecEvent(execEvent *tracerexectype.Event) ruleengine.RuleFailure { envVars, err := utils.GetProcessEnv(int(execEvent.Pid)) if err != nil { logger.L().Debug("Failed to get process environment variables", helpers.Error(err)) return nil } - shouldCheck := false - ldHookVar := "" - for _, envVar := range LD_PRELOAD_ENV_VARS { - if _, ok := envVars[envVar]; ok { - shouldCheck = true - ldHookVar = envVar - break - } - } + ldHookVar, _ := GetLdHookVar(envVars) - // Check if the environment variable is in the list of LD_PRELOAD_ENV_VARS - if shouldCheck { - // Check the pod spec for env vars that match the LD_PRELOAD_ENV_VARS - podSpec := k8sObjCache.GetPodSpec(execEvent.GetNamespace(), execEvent.GetPod()) - if podSpec != nil { - for _, container := range podSpec.Containers { - if container.Name == execEvent.GetContainer() { - for _, envVar := range container.Env { - if envVar.Name == ldHookVar { - // The environment variable is set in the pod spec - return nil - } - } - } - } - } + upperLayer := execEvent.UpperLayer || execEvent.PupperLayer - upperLayer := execEvent.UpperLayer || execEvent.PupperLayer - - ruleFailure := GenericRuleFailure{ - BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ - AlertName: rule.Name(), - Arguments: map[string]interface{}{"envVar": ldHookVar}, - InfectedPID: execEvent.Pid, - FixSuggestions: fmt.Sprintf("Check the environment variable %s", ldHookVar), - Severity: R1011LdPreloadHookRuleDescriptor.Priority, - }, - RuntimeProcessDetails: apitypes.ProcessTree{ - ProcessTree: apitypes.Process{ - Comm: execEvent.Comm, - Gid: &execEvent.Gid, - PID: execEvent.Pid, - Uid: &execEvent.Uid, - UpperLayer: &upperLayer, - PPID: execEvent.Ppid, - Pcomm: execEvent.Pcomm, - Cwd: execEvent.Cwd, - Hardlink: execEvent.ExePath, - Path: getExecFullPathFromEvent(execEvent), - Cmdline: fmt.Sprintf("%s %s", getExecPathFromEvent(execEvent), strings.Join(utils.GetExecArgsFromEvent(execEvent), " ")), - }, - ContainerID: execEvent.Runtime.ContainerID, - }, - TriggerEvent: execEvent.Event, - RuleAlert: apitypes.RuleAlert{ - RuleDescription: fmt.Sprintf("Process (%s) was executed in: %s and is using the environment variable %s", execEvent.Comm, execEvent.GetContainer(), fmt.Sprintf("%s=%s", ldHookVar, envVars[ldHookVar])), - }, - RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ - PodName: execEvent.GetPod(), - PodLabels: execEvent.K8s.PodLabels, + ruleFailure := GenericRuleFailure{ + BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ + AlertName: rule.Name(), + Arguments: map[string]interface{}{"envVar": ldHookVar}, + InfectedPID: execEvent.Pid, + FixSuggestions: fmt.Sprintf("Check the environment variable %s", ldHookVar), + Severity: R1011LdPreloadHookRuleDescriptor.Priority, + }, + RuntimeProcessDetails: apitypes.ProcessTree{ + ProcessTree: apitypes.Process{ + Comm: execEvent.Comm, + Gid: &execEvent.Gid, + PID: execEvent.Pid, + Uid: &execEvent.Uid, + UpperLayer: &upperLayer, + PPID: execEvent.Ppid, + Pcomm: execEvent.Pcomm, + Cwd: execEvent.Cwd, + Hardlink: execEvent.ExePath, + Path: getExecFullPathFromEvent(execEvent), + Cmdline: fmt.Sprintf("%s %s", getExecPathFromEvent(execEvent), strings.Join(utils.GetExecArgsFromEvent(execEvent), " ")), }, - RuleID: rule.ID(), - } - - return &ruleFailure + ContainerID: execEvent.Runtime.ContainerID, + }, + TriggerEvent: execEvent.Event, + RuleAlert: apitypes.RuleAlert{ + RuleDescription: fmt.Sprintf("Process (%s) was executed in: %s and is using the environment variable %s", execEvent.Comm, execEvent.GetContainer(), fmt.Sprintf("%s=%s", ldHookVar, envVars[ldHookVar])), + }, + RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ + PodName: execEvent.GetPod(), + PodLabels: execEvent.K8s.PodLabels, + }, + RuleID: rule.ID(), } - return nil + return &ruleFailure } -func (rule *R1011LdPreloadHook) handleOpenEvent(openEvent *traceropentype.Event) ruleengine.RuleFailure { - if openEvent.FullPath == LD_PRELOAD_FILE && (openEvent.FlagsRaw&(int32(os.O_WRONLY)|int32(os.O_RDWR))) != 0 { - ruleFailure := GenericRuleFailure{ - BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ - AlertName: rule.Name(), - Arguments: map[string]interface{}{ - "path": openEvent.FullPath, - "flags": openEvent.Flags, - }, - InfectedPID: openEvent.Pid, - FixSuggestions: "Check the file /etc/ld.so.preload", - Severity: R1011LdPreloadHookRuleDescriptor.Priority, - }, - RuntimeProcessDetails: apitypes.ProcessTree{ - ProcessTree: apitypes.Process{ - Comm: openEvent.Comm, - Gid: &openEvent.Gid, - PID: openEvent.Pid, - Uid: &openEvent.Uid, - }, - ContainerID: openEvent.Runtime.ContainerID, +func (rule *R1011LdPreloadHook) ruleFailureOpenEvent(openEvent *traceropentype.Event) ruleengine.RuleFailure { + ruleFailure := GenericRuleFailure{ + BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ + AlertName: rule.Name(), + Arguments: map[string]interface{}{ + "path": openEvent.FullPath, + "flags": openEvent.Flags, }, - TriggerEvent: openEvent.Event, - RuleAlert: apitypes.RuleAlert{ - RuleDescription: fmt.Sprintf("Process (%s) was executed in: %s and is opening the file %s", openEvent.Comm, openEvent.GetContainer(), openEvent.Path), - }, - RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ - PodName: openEvent.GetPod(), - PodLabels: openEvent.K8s.PodLabels, + InfectedPID: openEvent.Pid, + FixSuggestions: "Check the file /etc/ld.so.preload", + Severity: R1011LdPreloadHookRuleDescriptor.Priority, + }, + RuntimeProcessDetails: apitypes.ProcessTree{ + ProcessTree: apitypes.Process{ + Comm: openEvent.Comm, + Gid: &openEvent.Gid, + PID: openEvent.Pid, + Uid: &openEvent.Uid, }, - RuleID: rule.ID(), - } - - return &ruleFailure + ContainerID: openEvent.Runtime.ContainerID, + }, + TriggerEvent: openEvent.Event, + RuleAlert: apitypes.RuleAlert{ + RuleDescription: fmt.Sprintf("Process (%s) was executed in: %s and is opening the file %s", openEvent.Comm, openEvent.GetContainer(), openEvent.Path), + }, + RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ + PodName: openEvent.GetPod(), + PodLabels: openEvent.K8s.PodLabels, + }, + RuleID: rule.ID(), } - return nil + return &ruleFailure } -func (rule *R1011LdPreloadHook) ProcessEvent(eventType utils.EventType, event utils.K8sEvent, objectCache objectcache.ObjectCache) ruleengine.RuleFailure { - if eventType != utils.ExecveEventType && eventType != utils.OpenEventType { - return nil +func (rule *R1011LdPreloadHook) shouldAlertExec(execEvent *tracerexectype.Event, k8sObjCache objectcache.K8sObjectCache) bool { + // Java is a special case, we don't want to alert on it because it uses LD_LIBRARY_PATH. + if execEvent.Comm == JAVA_COMM { + return false } - if eventType == utils.ExecveEventType { - execEvent, ok := event.(*tracerexectype.Event) - if !ok { - return nil - } + // Check if the process is a MATLAB process and ignore it. + if execEvent.GetContainer() == "matlab" { + return false + } - return rule.handleExecEvent(execEvent, objectCache.K8sObjectCache()) - } else if eventType == utils.OpenEventType { - openEvent, ok := event.(*traceropentype.Event) - if !ok { - return nil - } + envVars, err := utils.GetProcessEnv(int(execEvent.Pid)) + if err != nil { + logger.L().Debug("Failed to get process environment variables", helpers.Error(err)) + return false + } - return rule.handleOpenEvent(openEvent) + ldHookVar, shouldCheck := GetLdHookVar(envVars) + if shouldCheck { + podSpec := k8sObjCache.GetPodSpec(execEvent.GetNamespace(), execEvent.GetPod()) + if podSpec != nil { + for _, container := range podSpec.Containers { + if container.Name == execEvent.GetContainer() { + for _, envVar := range container.Env { + if envVar.Name == ldHookVar { + return false + } + } + } + } + } + return true } - return nil + return false } -func (rule *R1011LdPreloadHook) Requirements() ruleengine.RuleSpec { - return &RuleRequirements{ - EventTypes: R1011LdPreloadHookRuleDescriptor.Requirements.RequiredEventTypes(), +func (rule *R1011LdPreloadHook) shouldAlertOpen(openEvent *traceropentype.Event) bool { + return openEvent.FullPath == LD_PRELOAD_FILE && (openEvent.FlagsRaw&(int32(os.O_WRONLY)|int32(os.O_RDWR))) != 0 +} + +func GetLdHookVar(envVars map[string]string) (string, bool) { + for _, envVar := range LD_PRELOAD_ENV_VARS { + if _, ok := envVars[envVar]; ok { + return envVar, true + } } + return "", false } diff --git a/pkg/ruleengine/v1/r1011_ld_preload_hook_test.go b/pkg/ruleengine/v1/r1011_ld_preload_hook_test.go index 38d77904..4c7028a8 100644 --- a/pkg/ruleengine/v1/r1011_ld_preload_hook_test.go +++ b/pkg/ruleengine/v1/r1011_ld_preload_hook_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/kubescape/node-agent/pkg/utils" + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" tracerexectype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/exec/types" traceropentype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types" @@ -13,12 +14,26 @@ import ( func TestR1011LdPreloadHook(t *testing.T) { // Create a new rule - r := CreateRuleR1011LdPreloadHook() - // Assert r is not nil + r := CreateRuleR1011LdPreloadHook() // Assert r is not nil if r == nil { t.Errorf("Expected r to not be nil") } + objCache := RuleObjectCacheMock{} + profile := objCache.ApplicationProfileCache().GetApplicationProfile("test") + if profile == nil { + profile = &v1beta1.ApplicationProfile{ + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "test", + }, + }, + }, + } + objCache.SetApplicationProfile(profile) + } + // Create open event e := &traceropentype.Event{ Event: eventtypes.Event{ @@ -36,20 +51,19 @@ func TestR1011LdPreloadHook(t *testing.T) { } // Test with existing ld_preload file - ruleResult := r.ProcessEvent(utils.OpenEventType, e, &RuleObjectCacheMock{}) + ruleResult := r.ProcessEvent(utils.OpenEventType, e, &objCache) if ruleResult == nil { t.Errorf("Expected ruleResult to not be nil since ld_preload file is opened with write flag") } // Test with ld.so.preload file opened with read flag e.FlagsRaw = 0 - ruleResult = r.ProcessEvent(utils.OpenEventType, e, &RuleObjectCacheMock{}) + ruleResult = r.ProcessEvent(utils.OpenEventType, e, &objCache) if ruleResult != nil { t.Errorf("Expected ruleResult to be nil since ld_preload file is opened with read flag") } // Test with pod spec - objCache := RuleObjectCacheMock{} objCache.SetPodSpec(&corev1.PodSpec{ Containers: []corev1.Container{ { @@ -99,7 +113,47 @@ func TestR1011LdPreloadHook(t *testing.T) { Comm: "java", } // Test with exec event - ruleResult = r.ProcessEvent(utils.ExecveEventType, e2, &RuleObjectCacheMock{}) + ruleResult = r.ProcessEvent(utils.ExecveEventType, e2, &objCache) + if ruleResult != nil { + t.Errorf("Expected ruleResult to be nil since exec event is on java") + } + + e3 := &traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test", + }, + }, + }, + }, + Comm: "test", + FullPath: "/etc/ld.so.preload", + FlagsRaw: 1, + } + + objCache = RuleObjectCacheMock{} + profile = objCache.ApplicationProfileCache().GetApplicationProfile("test") + if profile == nil { + profile = &v1beta1.ApplicationProfile{ + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "test", + PolicyByRuleId: map[string]v1beta1.RulePolicy{ + R1011ID: v1beta1.RulePolicy{ + AllowedProcesses: []string{"test"}, + }, + }, + }, + }, + }, + } + objCache.SetApplicationProfile(profile) + } + // Test with exec event + ruleResult = r.ProcessEvent(utils.OpenEventType, e3, &objCache) if ruleResult != nil { t.Errorf("Expected ruleResult to be nil since exec event is on java") } diff --git a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file.go b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file.go index 2458929a..25533fc1 100644 --- a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file.go +++ b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file.go @@ -1,7 +1,6 @@ package ruleengine import ( - "errors" "fmt" "strings" @@ -79,85 +78,77 @@ func (rule *R1012HardlinkCreatedOverSensitiveFile) DeleteRule() { } func (rule *R1012HardlinkCreatedOverSensitiveFile) ProcessEvent(eventType utils.EventType, event utils.K8sEvent, objCache objectcache.ObjectCache) ruleengine.RuleFailure { - if eventType != utils.HardlinkEventType { + logger.L().Debug("Processing event", helpers.String("ruleID", rule.ID()), helpers.String("eventType", string(eventType))) + if !rule.EvaluateRule(eventType, event, objCache.K8sObjectCache()) { + logger.L().Debug("Event does not match rule", helpers.String("ruleID", rule.ID()), helpers.String("eventType", string(eventType))) return nil } - hardlinkEvent, ok := event.(*tracerhardlinktype.Event) - if !ok { - return nil - } + hardlinkEvent, _ := event.(*tracerhardlinktype.Event) - if allowed, err := isHardLinkAllowed(hardlinkEvent, objCache); err != nil { + if allowed, err := isAllowed(&hardlinkEvent.Event, objCache, hardlinkEvent.Comm, R1012ID); err != nil { + logger.L().Error("failed to check if hardlink is allowed", helpers.String("ruleID", rule.ID()), helpers.String("error", err.Error())) return nil } else if allowed { return nil } - for _, path := range rule.additionalPaths { - if strings.HasPrefix(hardlinkEvent.OldPath, path) { - return &GenericRuleFailure{ - BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ - AlertName: rule.Name(), - Arguments: map[string]interface{}{ - "oldPath": hardlinkEvent.OldPath, - "newPath": hardlinkEvent.NewPath, - }, - InfectedPID: hardlinkEvent.Pid, - FixSuggestions: "If this is a legitimate action, please consider removing this workload from the binding of this rule.", - Severity: R1012HardlinkCreatedOverSensitiveFileRuleDescriptor.Priority, - }, - RuntimeProcessDetails: apitypes.ProcessTree{ - ProcessTree: apitypes.Process{ - Comm: hardlinkEvent.Comm, - PPID: hardlinkEvent.PPid, - PID: hardlinkEvent.Pid, - UpperLayer: &hardlinkEvent.UpperLayer, - Uid: &hardlinkEvent.Uid, - Gid: &hardlinkEvent.Gid, - Path: hardlinkEvent.ExePath, - Hardlink: hardlinkEvent.ExePath, - }, - ContainerID: hardlinkEvent.Runtime.ContainerID, - }, - TriggerEvent: hardlinkEvent.Event, - RuleAlert: apitypes.RuleAlert{ - RuleDescription: fmt.Sprintf("Hardlink created over sensitive file: %s - %s in: %s", hardlinkEvent.OldPath, hardlinkEvent.NewPath, hardlinkEvent.GetContainer()), - }, - RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ - PodName: hardlinkEvent.GetPod(), - PodLabels: hardlinkEvent.K8s.PodLabels, - }, - RuleID: rule.ID(), - } - } - } - - return nil -} - -func (rule *R1012HardlinkCreatedOverSensitiveFile) Requirements() ruleengine.RuleSpec { - return &RuleRequirements{ - EventTypes: R1012HardlinkCreatedOverSensitiveFileRuleDescriptor.Requirements.RequiredEventTypes(), + return &GenericRuleFailure{ + BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ + AlertName: rule.Name(), + Arguments: map[string]interface{}{ + "oldPath": hardlinkEvent.OldPath, + "newPath": hardlinkEvent.NewPath, + }, + InfectedPID: hardlinkEvent.Pid, + FixSuggestions: "If this is a legitimate action, please consider removing this workload from the binding of this rule.", + Severity: R1012HardlinkCreatedOverSensitiveFileRuleDescriptor.Priority, + }, + RuntimeProcessDetails: apitypes.ProcessTree{ + ProcessTree: apitypes.Process{ + Comm: hardlinkEvent.Comm, + PPID: hardlinkEvent.PPid, + PID: hardlinkEvent.Pid, + UpperLayer: &hardlinkEvent.UpperLayer, + Uid: &hardlinkEvent.Uid, + Gid: &hardlinkEvent.Gid, + Path: hardlinkEvent.ExePath, + Hardlink: hardlinkEvent.ExePath, + }, + ContainerID: hardlinkEvent.Runtime.ContainerID, + }, + TriggerEvent: hardlinkEvent.Event, + RuleAlert: apitypes.RuleAlert{ + RuleDescription: fmt.Sprintf("Hardlink created over sensitive file: %s - %s in: %s", hardlinkEvent.OldPath, hardlinkEvent.NewPath, hardlinkEvent.GetContainer()), + }, + RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ + PodName: hardlinkEvent.GetPod(), + PodLabels: hardlinkEvent.K8s.PodLabels, + }, + RuleID: rule.ID(), } } -func isHardLinkAllowed(hardlinkEvent *tracerhardlinktype.Event, objCache objectcache.ObjectCache) (bool, error) { - ap := objCache.ApplicationProfileCache().GetApplicationProfile(hardlinkEvent.Runtime.ContainerID) - if ap == nil { - return true, errors.New("application profile not found") +func (rule *R1012HardlinkCreatedOverSensitiveFile) EvaluateRule(eventType utils.EventType, event utils.K8sEvent, _ objectcache.K8sObjectCache) bool { + if eventType != utils.HardlinkEventType { + return false } - appProfileExecList, err := getContainerFromApplicationProfile(ap, hardlinkEvent.GetContainer()) - if err != nil { - return true, err + hardlinkEvent, ok := event.(*tracerhardlinktype.Event) + if !ok { + return false } - for _, exec := range appProfileExecList.Execs { - if exec.Path == hardlinkEvent.Comm { - return true, nil + for _, path := range rule.additionalPaths { + if strings.HasPrefix(hardlinkEvent.OldPath, path) { + return true } } + return false +} - return false, nil +func (rule *R1012HardlinkCreatedOverSensitiveFile) Requirements() ruleengine.RuleSpec { + return &RuleRequirements{ + EventTypes: R1012HardlinkCreatedOverSensitiveFileRuleDescriptor.Requirements.RequiredEventTypes(), + } } diff --git a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go index b106f1c1..20197646 100644 --- a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go +++ b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go @@ -26,18 +26,17 @@ func TestR1012HardlinkCreatedOverSensitiveFile(t *testing.T) { Containers: []v1beta1.ApplicationProfileContainer{ { Name: "test", + PolicyByRuleId: map[string]v1beta1.RulePolicy{ + R1012ID: v1beta1.RulePolicy{ + AllowedProcesses: []string{"/usr/sbin/groupadd"}, + }, + }, Opens: []v1beta1.OpenCalls{ { Path: "/test", Flags: []string{"O_RDONLY"}, }, }, - Execs: []v1beta1.ExecCalls{ - { - Path: "/usr/sbin/groupadd", - Args: []string{"test"}, - }, - }, }, }, }, @@ -90,7 +89,6 @@ func TestR1012HardlinkCreatedOverSensitiveFile(t *testing.T) { e.Comm = "/usr/sbin/groupadd" e.OldPath = "/etc/passwd" e.NewPath = "/etc/abc" - ruleResult = r.ProcessEvent(utils.HardlinkEventType, e, &objCache) if ruleResult != nil { fmt.Printf("ruleResult: %v\n", ruleResult) diff --git a/pkg/rulemanager/rule_manager_interface.go b/pkg/rulemanager/rule_manager_interface.go index 76f71de8..f39720f5 100644 --- a/pkg/rulemanager/rule_manager_interface.go +++ b/pkg/rulemanager/rule_manager_interface.go @@ -16,4 +16,5 @@ type RuleManagerClient interface { HasFinalApplicationProfile(pod *v1.Pod) bool IsContainerMonitored(k8sContainerID string) bool IsPodMonitored(namespace, pod string) bool + EvaluateRulesForEvent(eventType utils.EventType, event utils.K8sEvent) []string } diff --git a/pkg/rulemanager/rule_manager_mock.go b/pkg/rulemanager/rule_manager_mock.go index 3cf78d59..e88e92cd 100644 --- a/pkg/rulemanager/rule_manager_mock.go +++ b/pkg/rulemanager/rule_manager_mock.go @@ -43,3 +43,7 @@ func (r *RuleManagerMock) IsContainerMonitored(_ string) bool { func (r *RuleManagerMock) IsPodMonitored(_, _ string) bool { return false } + +func (r *RuleManagerMock) EvaluateRulesForEvent(_ utils.EventType, _ utils.K8sEvent) []string { + return []string{} +} diff --git a/pkg/rulemanager/v1/rule_manager.go b/pkg/rulemanager/v1/rule_manager.go index 88e0800a..e25159fd 100644 --- a/pkg/rulemanager/v1/rule_manager.go +++ b/pkg/rulemanager/v1/rule_manager.go @@ -504,3 +504,23 @@ func (rm *RuleManager) IsContainerMonitored(k8sContainerID string) bool { func (rm *RuleManager) IsPodMonitored(namespace, pod string) bool { return rm.podToWlid.Has(utils.CreateK8sPodID(namespace, pod)) } + +func (rm *RuleManager) EvaluateRulesForEvent(eventType utils.EventType, event utils.K8sEvent) []string { + results := []string{} + + creator := rm.ruleBindingCache.GetRuleCreator() + rules := creator.CreateRulesByEventType(eventType) + + for _, rule := range rules { + rule, ok := rule.(ruleengine.RuleCondition) + if !ok { + continue + } + + if rule.EvaluateRule(eventType, event, rm.objectCache.K8sObjectCache()) { + results = append(results, rule.ID()) + } + } + + return results +} diff --git a/pkg/storage/v1/applicationprofile.go b/pkg/storage/v1/applicationprofile.go index 2b804f50..ec161f3d 100644 --- a/pkg/storage/v1/applicationprofile.go +++ b/pkg/storage/v1/applicationprofile.go @@ -48,6 +48,7 @@ func (sc Storage) patchApplicationProfile(name, namespace string, operations []u if err != nil { return fmt.Errorf("marshal patch: %w", err) } + profile, err := sc.StorageClient.ApplicationProfiles(namespace).Patch(context.Background(), sc.modifyName(name), types.JSONPatchType, patch, v1.PatchOptions{}) if err != nil { return fmt.Errorf("patch application profile: %w", err) diff --git a/pkg/utils/applicationprofile.go b/pkg/utils/applicationprofile.go index b83248e3..1c968ab4 100644 --- a/pkg/utils/applicationprofile.go +++ b/pkg/utils/applicationprofile.go @@ -8,7 +8,12 @@ import ( "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" ) -func CreateCapabilitiesPatchOperations(capabilities, syscalls []string, execs map[string][]string, opens map[string]mapset.Set[string], endpoints map[string]*v1beta1.HTTPEndpoint, containerType string, containerIndex int) []PatchOperation { +const ( + ProcessAllowed = "processAllowed" + ContainerAllowed = "containerAllowed" +) + +func CreateCapabilitiesPatchOperations(capabilities, syscalls []string, execs map[string][]string, opens map[string]mapset.Set[string], endpoints map[string]*v1beta1.HTTPEndpoint, rulePolicies map[string]v1beta1.RulePolicy, containerType string, containerIndex int) []PatchOperation { var profileOperations []PatchOperation // add capabilities sort.Strings(capabilities) @@ -73,10 +78,13 @@ func CreateCapabilitiesPatchOperations(capabilities, syscalls []string, execs ma }) } + // add rule policies + profileOperations = append(profileOperations, createRulePolicyOperations(rulePolicies, containerType, containerIndex)...) + return profileOperations } -func EnrichApplicationProfileContainer(container *v1beta1.ApplicationProfileContainer, observedCapabilities, observedSyscalls []string, execs map[string][]string, opens map[string]mapset.Set[string], endpoints map[string]*v1beta1.HTTPEndpoint) { +func EnrichApplicationProfileContainer(container *v1beta1.ApplicationProfileContainer, observedCapabilities, observedSyscalls []string, execs map[string][]string, opens map[string]mapset.Set[string], endpoints map[string]*v1beta1.HTTPEndpoint, rulePolicies map[string]v1beta1.RulePolicy) { // add capabilities caps := mapset.NewSet(observedCapabilities...) caps.Append(container.Capabilities...) @@ -115,6 +123,20 @@ func EnrichApplicationProfileContainer(container *v1beta1.ApplicationProfileCont for _, endpoint := range endpoints { container.Endpoints = append(container.Endpoints, *endpoint) } + + // add rule policies + for ruleID, policy := range rulePolicies { + if container.PolicyByRuleId == nil { + container.PolicyByRuleId = make(map[string]v1beta1.RulePolicy) + } + if existingPolicy, ok := container.PolicyByRuleId[ruleID]; ok { + policy = MergePolicies(existingPolicy, policy) + container.PolicyByRuleId[ruleID] = policy + } else { + container.PolicyByRuleId[ruleID] = policy + } + } + } // TODO make generic? @@ -138,3 +160,52 @@ func GetApplicationProfileContainer(object *v1beta1.ApplicationProfile, containe } return nil } + +func MergePolicies(primary, secondary v1beta1.RulePolicy) v1beta1.RulePolicy { + mergedPolicy := v1beta1.RulePolicy{ + AllowedContainer: primary.AllowedContainer || secondary.AllowedContainer, + } + + processes := mapset.NewSet[string]() + + for _, process := range primary.AllowedProcesses { + processes.Add(process) + } + for _, process := range secondary.AllowedProcesses { + processes.Add(process) + } + + for process := range processes.Iter() { + mergedPolicy.AllowedProcesses = append(mergedPolicy.AllowedProcesses, process) + } + + return mergedPolicy +} + +func createRulePolicyOperations(rulePolicies map[string]v1beta1.RulePolicy, containerType string, containerIndex int) []PatchOperation { + var profileOperations []PatchOperation + + if len(rulePolicies) == 0 { + return profileOperations + } + + for ruleID, policy := range rulePolicies { + if len(policy.AllowedProcesses) != 0 { + for _, process := range policy.AllowedProcesses { + profileOperations = append(profileOperations, PatchOperation{ + Op: "add", + Path: fmt.Sprintf("/spec/%s/%d/rulePolicies/%s/%s/-", containerType, containerIndex, ruleID, ProcessAllowed), + Value: process, + }) + } + } else if policy.AllowedContainer { + profileOperations = append(profileOperations, PatchOperation{ + Op: "add", + Path: fmt.Sprintf("/spec/%s/%d/rulePolicies/%s/%s", containerType, containerIndex, ruleID, ContainerAllowed), + Value: policy.AllowedContainer, + }) + } + } + + return profileOperations +} diff --git a/pkg/utils/applicationprofile_test.go b/pkg/utils/applicationprofile_test.go index c65407c1..7b6c2e8a 100644 --- a/pkg/utils/applicationprofile_test.go +++ b/pkg/utils/applicationprofile_test.go @@ -43,21 +43,21 @@ func Test_EnrichApplicationProfileContainer(t *testing.T) { var test map[string]*v1beta1.HTTPEndpoint // empty enrich - EnrichApplicationProfileContainer(existingContainer, []string{}, []string{}, map[string][]string{}, map[string]mapset.Set[string]{}, test) + EnrichApplicationProfileContainer(existingContainer, []string{}, []string{}, map[string][]string{}, map[string]mapset.Set[string]{}, test, map[string]v1beta1.RulePolicy{}) assert.Equal(t, 5, len(existingContainer.Capabilities)) assert.Equal(t, 2, len(existingContainer.Execs)) assert.Equal(t, 5, len(existingContainer.Syscalls)) assert.Equal(t, 0, len(existingContainer.Opens)) // enrich with existing capabilities, syscalls - no change - EnrichApplicationProfileContainer(existingContainer, []string{"SETGID"}, []string{"listen"}, map[string][]string{}, map[string]mapset.Set[string]{}, test) + EnrichApplicationProfileContainer(existingContainer, []string{"SETGID"}, []string{"listen"}, map[string][]string{}, map[string]mapset.Set[string]{}, test, map[string]v1beta1.RulePolicy{}) assert.Equal(t, 5, len(existingContainer.Capabilities)) assert.Equal(t, 2, len(existingContainer.Execs)) assert.Equal(t, 5, len(existingContainer.Syscalls)) assert.Equal(t, 0, len(existingContainer.Opens)) // enrich with new capabilities, syscalls - add - EnrichApplicationProfileContainer(existingContainer, []string{"NEW"}, []string{"xxx", "yyy"}, map[string][]string{}, map[string]mapset.Set[string]{}, test) + EnrichApplicationProfileContainer(existingContainer, []string{"NEW"}, []string{"xxx", "yyy"}, map[string][]string{}, map[string]mapset.Set[string]{}, test, map[string]v1beta1.RulePolicy{}) assert.Equal(t, 6, len(existingContainer.Capabilities)) assert.Equal(t, 2, len(existingContainer.Execs)) assert.Equal(t, 7, len(existingContainer.Syscalls)) @@ -67,9 +67,103 @@ func Test_EnrichApplicationProfileContainer(t *testing.T) { opens := map[string]mapset.Set[string]{ "/checkoutservice": mapset.NewSet("O_RDONLY", "O_WRONLY"), } - EnrichApplicationProfileContainer(existingContainer, []string{"NEW"}, []string{"xxx", "yyy"}, map[string][]string{}, opens, test) + EnrichApplicationProfileContainer(existingContainer, []string{"NEW"}, []string{"xxx", "yyy"}, map[string][]string{}, opens, test, map[string]v1beta1.RulePolicy{}) assert.Equal(t, 6, len(existingContainer.Capabilities)) assert.Equal(t, 2, len(existingContainer.Execs)) assert.Equal(t, 7, len(existingContainer.Syscalls)) assert.Equal(t, 1, len(existingContainer.Opens)) } + +func TestMergePolicies(t *testing.T) { + tests := []struct { + name string + primary v1beta1.RulePolicy + secondary v1beta1.RulePolicy + expectedPolicy v1beta1.RulePolicy + }{ + { + name: "Both policies allow containers", + primary: v1beta1.RulePolicy{ + AllowedContainer: true, + AllowedProcesses: []string{"process1", "process2"}, + }, + secondary: v1beta1.RulePolicy{ + AllowedContainer: true, + AllowedProcesses: []string{"process2", "process3"}, + }, + expectedPolicy: v1beta1.RulePolicy{ + AllowedContainer: true, + AllowedProcesses: []string{"process1", "process2", "process3"}, + }, + }, + { + name: "Only primary allows containers", + primary: v1beta1.RulePolicy{ + AllowedContainer: true, + AllowedProcesses: []string{"process1"}, + }, + secondary: v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{"process2"}, + }, + expectedPolicy: v1beta1.RulePolicy{ + AllowedContainer: true, + AllowedProcesses: []string{"process1", "process2"}, + }, + }, + { + name: "Only secondary allows containers", + primary: v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{"process1"}, + }, + secondary: v1beta1.RulePolicy{ + AllowedContainer: true, + AllowedProcesses: []string{"process3"}, + }, + expectedPolicy: v1beta1.RulePolicy{ + AllowedContainer: true, + AllowedProcesses: []string{"process1", "process3"}, + }, + }, + { + name: "No duplicate processes in merged policy", + primary: v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{"process1", "process2"}, + }, + secondary: v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{"process1", "process2"}, + }, + expectedPolicy: v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{"process1", "process2"}, + }, + }, + { + name: "Both policies empty", + primary: v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{}, + }, + secondary: v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{}, + }, + expectedPolicy: v1beta1.RulePolicy{ + AllowedContainer: false, + AllowedProcesses: []string{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mergedPolicy := MergePolicies(tt.primary, tt.secondary) + + assert.Equal(t, tt.expectedPolicy.AllowedContainer, mergedPolicy.AllowedContainer) + assert.ElementsMatch(t, tt.expectedPolicy.AllowedProcesses, mergedPolicy.AllowedProcesses) + }) + } +} diff --git a/tests/chart/crds/runtime-rule-binding.crd.yaml b/tests/chart/crds/runtime-rule-binding.crd.yaml index 67de8f5e..d3728006 100644 --- a/tests/chart/crds/runtime-rule-binding.crd.yaml +++ b/tests/chart/crds/runtime-rule-binding.crd.yaml @@ -13,155 +13,86 @@ spec: scope: Cluster versions: - name: v1 + served: true + storage: true schema: openAPIV3Schema: + type: object properties: spec: + type: object properties: namespaceSelector: + type: object properties: matchExpressions: + type: array items: + type: object properties: key: type: string operator: type: string values: + type: array items: type: string - type: array - type: object - type: array matchLabels: + type: object additionalProperties: type: string - type: object - type: object podSelector: + type: object properties: matchExpressions: + type: array items: + type: object properties: key: type: string operator: type: string values: + type: array items: type: string - type: array - type: object - type: array matchLabels: + type: object additionalProperties: type: string - type: object - type: object rules: + type: array items: + type: object oneOf: - not: anyOf: - - required: - - ruleID - - required: - - ruleName - required: - - ruleTags + - required: ["ruleID"] + - required: ["ruleName"] + required: ["ruleTags"] - not: anyOf: - - required: - - ruleTags - - required: - - ruleName - required: - - ruleID + - required: ["ruleTags"] + - required: ["ruleName"] + required: ["ruleID"] - not: anyOf: - - required: - - ruleTags - - required: - - ruleID - required: - - ruleName + - required: ["ruleTags"] + - required: ["ruleID"] + required: ["ruleName"] properties: parameters: - additionalProperties: true type: object + additionalProperties: true ruleID: - enum: - - R0001 - - R0002 - - R0003 - - R0004 - - R0005 - - R0006 - - R0007 - - R1000 - - R1001 - - R1002 - - R1003 - - R1004 - - R1005 - - R1006 - - R1007 - - R1008 - - R1009 type: string ruleName: - enum: - - Unexpected process launched - - Unexpected file access - - Unexpected system call - - Unexpected capability used - - Unexpected domain request - - Unexpected Service Account Token Access - - Kubernetes Client Executed - - Exec from malicious source - - Exec Binary Not In Base Image - - Kernel Module Load - - Malicious SSH Connection - - Exec from mount - - Fileless Execution - - Unshare System Call usage - - XMR Crypto Mining Detection - - Crypto Mining Domain Communication - - Crypto Mining Related Port Communication type: string ruleTags: + type: array items: - enum: - - base image - - binary - - capabilities - - connection - - crypto - - dns - - escape - - exec - - kernel - - load - - malicious - - miners - - module - - mount - - network - - open - - port - - signature - - ssh - - syscall - - token - - unshare - - whitelisted type: string - type: array severity: - type: string - type: object - type: array - type: object - type: object - served: true - storage: true + type: string \ No newline at end of file diff --git a/tests/chart/templates/node-agent/default-rule-binding.yaml b/tests/chart/templates/node-agent/default-rule-binding.yaml index 772fbb35..2a089870 100644 --- a/tests/chart/templates/node-agent/default-rule-binding.yaml +++ b/tests/chart/templates/node-agent/default-rule-binding.yaml @@ -32,4 +32,13 @@ spec: - ruleName: "XMR Crypto Mining Detection" - ruleName: "Exec from mount" - ruleName: "Crypto Mining Related Port Communication" - - ruleName: "Crypto Mining Domain Communication" \ No newline at end of file + - ruleName: "Crypto Mining Domain Communication" + - ruleName: "Read Environment Variables from procfs" + - ruleName: "eBPF Program Load" + - ruleName: "Symlink Created Over Sensitive File" + - ruleName: "Unexpected Sensitive File Access" + - ruleName: "Hardlink Created Over Sensitive File" + - ruleName: "Exec to pod" + - ruleName: "Port forward" + - ruleName: "Unexpected Egress Network Traffic" + - ruleName: "Malicious Ptrace Usage" diff --git a/tests/component_test.go b/tests/component_test.go index 46ee65f1..deb52b93 100644 --- a/tests/component_test.go +++ b/tests/component_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/kubescape/k8s-interface/k8sinterface" + "github.com/kubescape/node-agent/pkg/ruleengine/v1" "github.com/kubescape/node-agent/pkg/utils" "github.com/kubescape/node-agent/tests/testutils" "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" @@ -398,21 +399,8 @@ func Test_07_RuleBindingApplyTest(t *testing.T) { assert.Equal(t, 0, exitCode, "Error applying valid rule binding") _ = testutils.RunCommand("kubectl", "delete", "-f", ruleBindingPath("all-valid.yaml")) - // invalid fields - file := ruleBindingPath("invalid-name.yaml") - exitCode = testutils.RunCommand("kubectl", "apply", "-f", file) - assert.NotEqualf(t, 0, exitCode, "Expected error when applying rule binding '%s'", file) - - file = ruleBindingPath("invalid-id.yaml") - exitCode = testutils.RunCommand("kubectl", "apply", "-f", file) - assert.NotEqualf(t, 0, exitCode, "Expected error when applying rule binding '%s'", file) - - file = ruleBindingPath("invalid-tag.yaml") - exitCode = testutils.RunCommand("kubectl", "apply", "-f", file) - assert.NotEqualf(t, 0, exitCode, "Expected error when applying rule binding '%s'", file) - // duplicate fields - file = ruleBindingPath("dup-fields-name-tag.yaml") + file := ruleBindingPath("dup-fields-name-tag.yaml") exitCode = testutils.RunCommand("kubectl", "apply", "-f", file) assert.NotEqualf(t, 0, exitCode, "Expected error when applying rule binding '%s'", file) @@ -638,7 +626,10 @@ func Test_11_EndpointTest(t *testing.T) { _, _, err = endpointTraffic.ExecIntoPod([]string{"wget", "http://127.0.0.1:80/users/99", "--header", "Connection:1234r"}, "") _, _, err = endpointTraffic.ExecIntoPod([]string{"wget", "http://127.0.0.1:80/users/12", "--header", "Connection:ziz"}, "") - err = endpointTraffic.WaitForApplicationProfileCompletion(10) + err = endpointTraffic.WaitForApplicationProfileCompletion(80) + if err != nil { + t.Errorf("Error waiting for application profile to be completed: %v", err) + } applicationProfile, err := endpointTraffic.GetApplicationProfile() if err != nil { @@ -1120,6 +1111,77 @@ func Test_13_MergingNetworkNeighborhoodTest(t *testing.T) { } } +func Test_14_RulePoliciesTest(t *testing.T) { + ns := testutils.NewRandomNamespace() + + endpointTraffic, err := testutils.NewTestWorkload(ns.Name, path.Join(utils.CurrentDir(), "resources/endpoint-traffic.yaml")) + if err != nil { + t.Errorf("Error creating workload: %v", err) + } + err = endpointTraffic.WaitForReady(80) + if err != nil { + t.Errorf("Error waiting for workload to be ready: %v", err) + } + + assert.NoError(t, endpointTraffic.WaitForApplicationProfile(80, "ready")) + + // Add to rule policy symlink + _, _, err = endpointTraffic.ExecIntoPod([]string{"ln", "-s", "/etc/shadow", "/tmp/a"}, "") + assert.NoError(t, err) + + _, _, err = endpointTraffic.ExecIntoPod([]string{"rm", "/tmp/a"}, "") + assert.NoError(t, err) + + // Not add to rule policy + _, _, err = endpointTraffic.ExecIntoPod([]string{"ln", "/bin/sh", "/tmp/a"}, "") + assert.NoError(t, err) + + _, _, err = endpointTraffic.ExecIntoPod([]string{"rm", "/tmp/a"}, "") + assert.NoError(t, err) + + err = endpointTraffic.WaitForApplicationProfileCompletion(80) + if err != nil { + t.Errorf("Error waiting for application profile to be completed: %v", err) + } + + applicationProfile, err := endpointTraffic.GetApplicationProfile() + if err != nil { + t.Errorf("Error getting application profile: %v", err) + } + + symlinkPolicy := applicationProfile.Spec.Containers[0].PolicyByRuleId[ruleengine.R1010ID] + assert.Equal(t, []string{"ln"}, symlinkPolicy.AllowedProcesses) + + hardlinkPolicy := applicationProfile.Spec.Containers[0].PolicyByRuleId[ruleengine.R1012ID] + assert.Len(t, hardlinkPolicy.AllowedProcesses, 0) + + fmt.Println("After completed") + + // wait for cache + time.Sleep(120 * time.Second) + + // generate hardlink alert + _, _, err = endpointTraffic.ExecIntoPod([]string{"ln", "/etc/shadow", "/tmp/a"}, "") + _, _, err = endpointTraffic.ExecIntoPod([]string{"rm", "/tmp/a"}, "") + assert.NoError(t, err) + + // not generate alert + _, _, err = endpointTraffic.ExecIntoPod([]string{"ln", "-s", "/etc/shadow", "/tmp/a"}, "") + _, _, err = endpointTraffic.ExecIntoPod([]string{"rm", "/tmp/a"}, "") + assert.NoError(t, err) + + // Wait for the alert to be signaled + time.Sleep(60 * time.Second) + + alerts, err := testutils.GetAlerts(endpointTraffic.Namespace) + if err != nil { + t.Errorf("Error getting alerts: %v", err) + } + + testutils.AssertContains(t, alerts, "Hardlink Created Over Sensitive File", "ln", "endpoint-traffic") + testutils.AssertNotContains(t, alerts, "Symlink Created Over Sensitive File", "ln", "endpoint-traffic") +} + func ptr(i int32) *int32 { return &i } From d26c8ffc2ebb451e4ace9d9ceab5279a279dee4a Mon Sep 17 00:00:00 2001 From: Amit Schendel Date: Wed, 20 Nov 2024 07:45:47 +0000 Subject: [PATCH 2/3] Adding more tests Signed-off-by: Amit Schendel --- pkg/ruleengine/v1/r1003_malicious_ssh_connection_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/ruleengine/v1/r1003_malicious_ssh_connection_test.go b/pkg/ruleengine/v1/r1003_malicious_ssh_connection_test.go index f61c0b45..852cbae4 100644 --- a/pkg/ruleengine/v1/r1003_malicious_ssh_connection_test.go +++ b/pkg/ruleengine/v1/r1003_malicious_ssh_connection_test.go @@ -80,4 +80,12 @@ func TestR1003DisallowedSSHConnectionPort_ProcessEvent(t *testing.T) { if failure == nil { t.Errorf("Expected failure since the SSH connection is to a disallowed port, got nil") } + + // Test allowed port + sshEvent.DstPort = 2022 + sshEvent.DstIP = "3.3.3.3" + failure = rule.ProcessEvent(utils.SSHEventType, sshEvent, &objCache) + if failure != nil { + t.Errorf("Expected nil since the SSH connection is to an allowed port, got %v", failure) + } } From a7cb3c828059f77306e365d1cb80cd747174416b Mon Sep 17 00:00:00 2001 From: Afek Berger Date: Wed, 20 Nov 2024 10:46:14 +0200 Subject: [PATCH 3/3] Modified comments Signed-off-by: Afek Berger --- pkg/eventreporters/rulepolicy/rule_policy.go | 1 - .../v1/r1010_symlink_created_over_sensitive_file_test.go | 2 +- pkg/ruleengine/v1/r1011_ld_preload_hook_test.go | 2 +- .../v1/r1012_hardlink_created_over_sensitive_file_test.go | 2 +- tests/component_test.go | 1 + 5 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/eventreporters/rulepolicy/rule_policy.go b/pkg/eventreporters/rulepolicy/rule_policy.go index 8dc3ddae..83e0c4bd 100644 --- a/pkg/eventreporters/rulepolicy/rule_policy.go +++ b/pkg/eventreporters/rulepolicy/rule_policy.go @@ -21,7 +21,6 @@ func NewRulePolicyReporter(ruleManager rulemanager.RuleManagerClient, applicatio func (rpm *RulePolicyReporter) ReportEvent(eventType utils.EventType, event utils.K8sEvent, k8sContainerID string, allowedProcess string) { rulesIds := rpm.ruleManager.EvaluateRulesForEvent(eventType, event) for _, rule := range rulesIds { - // TODO: Add a check to see if the rule is using rule policy rpm.applicationProfileManager.ReportRulePolicy(k8sContainerID, rule, allowedProcess, false) } } diff --git a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go index cc08428f..6c5031ce 100644 --- a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go +++ b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go @@ -27,7 +27,7 @@ func TestR1010SymlinkCreatedOverSensitiveFile(t *testing.T) { { Name: "test", PolicyByRuleId: map[string]v1beta1.RulePolicy{ - R1010ID: v1beta1.RulePolicy{ + R1010ID: { AllowedProcesses: []string{"/usr/sbin/groupadd"}, }, }, diff --git a/pkg/ruleengine/v1/r1011_ld_preload_hook_test.go b/pkg/ruleengine/v1/r1011_ld_preload_hook_test.go index 4c7028a8..1807b379 100644 --- a/pkg/ruleengine/v1/r1011_ld_preload_hook_test.go +++ b/pkg/ruleengine/v1/r1011_ld_preload_hook_test.go @@ -142,7 +142,7 @@ func TestR1011LdPreloadHook(t *testing.T) { { Name: "test", PolicyByRuleId: map[string]v1beta1.RulePolicy{ - R1011ID: v1beta1.RulePolicy{ + R1011ID: { AllowedProcesses: []string{"test"}, }, }, diff --git a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go index 20197646..be19ab96 100644 --- a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go +++ b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go @@ -27,7 +27,7 @@ func TestR1012HardlinkCreatedOverSensitiveFile(t *testing.T) { { Name: "test", PolicyByRuleId: map[string]v1beta1.RulePolicy{ - R1012ID: v1beta1.RulePolicy{ + R1012ID: { AllowedProcesses: []string{"/usr/sbin/groupadd"}, }, }, diff --git a/tests/component_test.go b/tests/component_test.go index deb52b93..3f0daf56 100644 --- a/tests/component_test.go +++ b/tests/component_test.go @@ -1123,6 +1123,7 @@ func Test_14_RulePoliciesTest(t *testing.T) { t.Errorf("Error waiting for workload to be ready: %v", err) } + // Wait for application profile to be ready assert.NoError(t, endpointTraffic.WaitForApplicationProfile(80, "ready")) // Add to rule policy symlink