diff --git a/pkg/hostrules/hostrules_interface.go b/pkg/hostrules/hostrules_interface.go deleted file mode 100644 index f4157fbd16..0000000000 --- a/pkg/hostrules/hostrules_interface.go +++ /dev/null @@ -1,27 +0,0 @@ -package hostrules - -// import ( -// "github.com/kubescape/node-agent/pkg/objectcache" -// "github.com/kubescape/node-agent/pkg/ruleengine" -// "github.com/kubescape/node-agent/pkg/utils" -// ) - -// type HostRule interface { -// // Rule ID - this is the rules unique identifier -// ID() string - -// // Rule Name -// Name() string - -// // Rule processing -// ProcessEvent(eventType utils.EventType, event interface{}, objCache objectcache.ObjectCache) ruleengine.RuleFailure - -// // Rule requirements -// Requirements() ruleengine.RuleSpec - -// // Set rule parameters -// SetParameters(parameters map[string]interface{}) - -// // Get rule parameters -// GetParameters() map[string]interface{} -// } diff --git a/pkg/hostrules/v1/consts.go b/pkg/hostrules/v1/consts.go new file mode 100644 index 0000000000..954d89d6de --- /dev/null +++ b/pkg/hostrules/v1/consts.go @@ -0,0 +1,43 @@ +package hostrules + +import ( + _ "embed" + "encoding/json" +) + +var ( + writeFlags = []string{ + "O_WRONLY", + "O_RDWR", + "O_TRUNC", + "O_CREAT", + "O_APPEND"} + + allowedNamespaces = map[string]bool{ + "ci": true, // CI/CD pipelines + "jenkins": true, // Jenkins workloads + "gitlab": true, // GitLab runners + "build": true, // Build environments + "docker-mgmt": true, // Docker management tools + } +) + +//go:embed data/suspicious_tools.json +var suspiciousToolsJSON []byte + +var suspiciousTools = func() map[string]toolInfo { + var result struct { + Tools []toolInfo `json:"tools"` + } + + if err := json.Unmarshal(suspiciousToolsJSON, &result); err != nil { + panic(err) + } + + toolMap := make(map[string]toolInfo) + for _, tool := range result.Tools { + toolMap[tool.Name] = tool + } + + return toolMap +}() diff --git a/pkg/hostrules/v1/data/suspicious_tools.json b/pkg/hostrules/v1/data/suspicious_tools.json new file mode 100644 index 0000000000..6f39c05f82 --- /dev/null +++ b/pkg/hostrules/v1/data/suspicious_tools.json @@ -0,0 +1,189 @@ +{ + "tools": [ + { + "name": "nmap", + "severity": 1, + "category": "network scanner" + }, + { + "name": "masscan", + "severity": 1, + "category": "network scanner" + }, + { + "name": "nikto", + "severity": 3, + "category": "web vulnerability scanner" + }, + { + "name": "metasploit-framework", + "severity": 3, + "category": "exploitation framework" + }, + { + "name": "msfconsole", + "severity": 3, + "category": "exploitation framework" + }, + { + "name": "burpsuite", + "severity": 1, + "category": "web security tool" + }, + { + "name": "sqlmap", + "severity": 3, + "category": "SQL injection tool" + }, + { + "name": "wireshark", + "severity": 1, + "category": "network analyzer" + }, + { + "name": "tcpdump", + "severity": 1, + "category": "packet analyzer" + }, + { + "name": "tshark", + "severity": 1, + "category": "network analyzer" + }, + { + "name": "netcat", + "severity": 2, + "category": "network utility" + }, + { + "name": "nc", + "severity": 2, + "category": "network utility" + }, + { + "name": "socat", + "severity": 2, + "category": "network utility" + }, + { + "name": "john", + "severity": 3, + "category": "password cracker" + }, + { + "name": "hashcat", + "severity": 3, + "category": "password cracker" + }, + { + "name": "hydra", + "severity": 3, + "category": "brute-force tool" + }, + { + "name": "medusa", + "severity": 3, + "category": "brute-force tool" + }, + { + "name": "aircrack-ng", + "severity": 3, + "category": "wireless network cracker" + }, + { + "name": "reaver", + "severity": 3, + "category": "wireless network cracker" + }, + { + "name": "ettercap", + "severity": 3, + "category": "network sniffer & MITM tool" + }, + { + "name": "bettercap", + "severity": 3, + "category": "network sniffer & MITM tool" + }, + { + "name": "dnsrecon", + "severity": 3, + "category": "DNS reconnaissance tool" + }, + { + "name": "dnscan", + "severity": 3, + "category": "DNS reconnaissance tool" + }, + { + "name": "theHarvester", + "severity": 3, + "category": "information gathering tool" + }, + { + "name": "scapy", + "severity": 2, + "category": "packet manipulation tool" + }, + { + "name": "proxychains", + "severity": 2, + "category": "anonymous proxy tool" + }, + { + "name": "tor", + "severity": 2, + "category": "anonymity network tool" + }, + { + "name": "chisel", + "severity": 3, + "category": "port forwarding & tunneling tool" + }, + { + "name": "ngrok", + "severity": 3, + "category": "port forwarding & tunneling tool" + }, + { + "name": "responder", + "severity": 3, + "category": "LLMNR/NBT-NS/mDNS poisoning tool" + }, + { + "name": "crackmapexec", + "severity": 3, + "category": "post-exploitation tool" + }, + { + "name": "pupy", + "severity": 3, + "category": "post-exploitation RAT" + }, + { + "name": "empire", + "severity": 3, + "category": "post-exploitation framework" + }, + { + "name": "bloodhound", + "severity": 3, + "category": "Active Directory enumeration tool" + }, + { + "name": "neo4j", + "severity": 1, + "category": "database used by BloodHound" + }, + { + "name": "linpeas", + "severity": 3, + "category": "Linux privilege escalation script" + }, + { + "name": "linux-exploit-suggester", + "severity": 3, + "category": "Linux privilege escalation script" + } + ] +} diff --git a/pkg/hostrules/v1/factory.go b/pkg/hostrules/v1/factory.go index 9939800e6c..2b8b384561 100644 --- a/pkg/hostrules/v1/factory.go +++ b/pkg/hostrules/v1/factory.go @@ -19,6 +19,11 @@ func NewRuleCreator() *ruleenginev1.RuleCreatorImpl { ruleenginev1.R1007XMRCryptoMiningRuleDescriptor, ruleenginev1.R1008CryptoMiningDomainCommunicationRuleDescriptor, ruleenginev1.R1015MaliciousPtraceUsageRuleDescriptor, + R3001NewlyModifedBinaryLaunchedRuleDescriptor, + R3002CGroupsReleaseAgentModifiedRuleDescriptor, + R3003SuspiciousToolRuleDescriptor, + R3004DockerSocketAccessRuleDescriptor, + R3005MaliciousFsMemoryInjectionRuleDescriptor, ruleenginev1.R1030UnexpectedIouringOperationRuleDescriptor, }, } diff --git a/pkg/hostrules/v1/helpers.go b/pkg/hostrules/v1/helpers.go new file mode 100644 index 0000000000..2f50197020 --- /dev/null +++ b/pkg/hostrules/v1/helpers.go @@ -0,0 +1,65 @@ +package hostrules + +import ( + _ "embed" + "os" + "path/filepath" + "slices" + "strings" + "time" + + "github.com/kubescape/node-agent/pkg/ruleengine" +) + +func isSuspiciousTool(execPath string) (bool, int, string) { + parts := strings.Split(execPath, "/") + baseName := parts[len(parts)-1] + + if toolInfo, exists := suspiciousTools[baseName]; exists { + return true, toolInfo.Severity, toolInfo.Category + } + + return false, ruleengine.RulePriorityLow, "" +} + +func isPathNewlyModified(path string, modificationTime time.Time) bool { + fileInfo, err := os.Stat(path) + if err != nil { + return false + } + + return fileInfo.ModTime().After(modificationTime) +} + +func isAllowedNamespace(namespace string) bool { + if namespace == "" { + return false + } + + if allowedNamespaces[namespace] { + return true + } + + for pattern := range allowedNamespaces { + if strings.HasPrefix(namespace, pattern+"-") { + return true + } + } + + return false +} + +func ComparePaths(sourcePath, targetPath string) bool { + cleanSource := filepath.Clean(sourcePath) + cleanTarget := filepath.Clean(targetPath) + return cleanSource == cleanTarget +} + +func hasTargetFlags(flags []string, targetFlags []string) bool { + for _, flag := range flags { + if slices.Contains(targetFlags, flag) { + return true + } + } + return false +} diff --git a/pkg/hostrules/v1/r3001_executed_newly_modified_binary_from_container.go b/pkg/hostrules/v1/r3001_executed_newly_modified_binary_from_container.go new file mode 100644 index 0000000000..5cb5b1127c --- /dev/null +++ b/pkg/hostrules/v1/r3001_executed_newly_modified_binary_from_container.go @@ -0,0 +1,125 @@ +package hostrules + +import ( + "fmt" + "strings" + "time" + + events "github.com/kubescape/node-agent/pkg/ebpf/events" + "github.com/kubescape/node-agent/pkg/objectcache" + "github.com/kubescape/node-agent/pkg/ruleengine" + ruleenginev1 "github.com/kubescape/node-agent/pkg/ruleengine/v1" + "github.com/kubescape/node-agent/pkg/utils" + + apitypes "github.com/armosec/armoapi-go/armotypes" +) + +const ( + R3001ID = "R3001" + R3001Name = "Execution of newly modified binary from container" + NewlyModifiedThreshold = 5 * time.Minute +) + +var R3001NewlyModifedBinaryLaunchedRuleDescriptor = ruleengine.RuleDescriptor{ + ID: R3001ID, + Name: R3001Name, + Description: "Detects execution of newly modified binaries within containers", + Tags: []string{"exec", "whitelisted"}, + Priority: ruleengine.RulePriorityMed, + Requirements: &ruleenginev1.RuleRequirements{ + EventTypes: []utils.EventType{utils.ExecveEventType}, + }, + RuleCreationFunc: func() ruleengine.RuleEvaluator { + return CreateRuleR3001UnexpectedProcessLaunched() + }, +} +var _ ruleengine.RuleEvaluator = (*R3001NewlyModifedBinaryLaunched)(nil) + +type R3001NewlyModifedBinaryLaunched struct { + ruleenginev1.BaseRule + enforceArgs bool +} + +func (rule *R3001NewlyModifedBinaryLaunched) SetParameters(params map[string]interface{}) { + +} + +func (rule *R3001NewlyModifedBinaryLaunched) Name() string { + return R3001Name +} +func (rule *R3001NewlyModifedBinaryLaunched) ID() string { + return R3001ID +} + +func CreateRuleR3001UnexpectedProcessLaunched() *R3001NewlyModifedBinaryLaunched { + return &R3001NewlyModifedBinaryLaunched{enforceArgs: false} +} + +func (rule *R3001NewlyModifedBinaryLaunched) ProcessEvent(eventType utils.EventType, event utils.K8sEvent, _ objectcache.ObjectCache) ruleengine.RuleFailure { + if eventType != utils.ExecveEventType { + return nil + } + + execEvent, ok := event.(*events.ExecEvent) + if !ok { + return nil + } + + if execEvent.Runtime.ContainerID == "" { + return nil + } + + execPath := ruleenginev1.GetExecPathFromEvent(execEvent) // Is real path + + thresholdTime := time.Now().Add(-NewlyModifiedThreshold) + + if !isPathNewlyModified(execPath, thresholdTime) { + return nil + } + + ruleFailure := ruleenginev1.GenericRuleFailure{ + BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ + AlertName: rule.Name(), + InfectedPID: execEvent.Pid, + Arguments: map[string]interface{}{ + "retval": execEvent.Retval, + "exec": execEvent.ExePath, + "args": execEvent.Args, + }, + Severity: R3001NewlyModifedBinaryLaunchedRuleDescriptor.Priority, + }, + RuntimeProcessDetails: apitypes.ProcessTree{ + ProcessTree: apitypes.Process{ + Comm: execEvent.Comm, + Gid: &execEvent.Gid, + PID: execEvent.Pid, + Uid: &execEvent.Uid, + PPID: execEvent.Ppid, + Pcomm: execEvent.Pcomm, + Cwd: execEvent.Cwd, + Hardlink: execEvent.ExePath, + Path: execPath, + Cmdline: fmt.Sprintf("%s %s", execPath, strings.Join(utils.GetExecArgsFromEvent(&execEvent.Event), " ")), + }, + ContainerID: execEvent.Runtime.ContainerID, + }, + TriggerEvent: execEvent.Event.Event, + RuleAlert: apitypes.RuleAlert{ + RuleDescription: fmt.Sprintf("A newly modified binary was executed: %s in container: %s", execPath, execEvent.GetContainer()), + }, + RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ + PodName: execEvent.GetPod(), + PodLabels: execEvent.K8s.PodLabels, + }, + RuleID: rule.ID(), + Extra: execEvent.GetExtra(), + } + + return &ruleFailure +} + +func (rule *R3001NewlyModifedBinaryLaunched) Requirements() ruleengine.RuleSpec { + return &ruleenginev1.RuleRequirements{ + EventTypes: R3001NewlyModifedBinaryLaunchedRuleDescriptor.Requirements.RequiredEventTypes(), + } +} diff --git a/pkg/hostrules/v1/r3001_executed_newly_modified_binary_from_container_test.go b/pkg/hostrules/v1/r3001_executed_newly_modified_binary_from_container_test.go new file mode 100644 index 0000000000..faf824ab52 --- /dev/null +++ b/pkg/hostrules/v1/r3001_executed_newly_modified_binary_from_container_test.go @@ -0,0 +1,138 @@ +package hostrules + +import ( + "os" + "testing" + "time" + + events "github.com/kubescape/node-agent/pkg/ebpf/events" + ruleenginev1 "github.com/kubescape/node-agent/pkg/ruleengine/v1" + "github.com/kubescape/node-agent/pkg/utils" + + tracerexectype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/exec/types" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" +) + +type RuleObjectCacheMock struct{} + +func createTempFile(t *testing.T, timeOffset time.Duration) string { + tmpfile, err := os.CreateTemp("", "test-binary-*") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + + if _, err := tmpfile.Write([]byte("test binary")); err != nil { + t.Fatalf("Failed to write to temp file: %v", err) + } + + if err := tmpfile.Close(); err != nil { + t.Fatalf("Failed to close temp file: %v", err) + } + + if timeOffset != 0 { + modTime := time.Now().Add(timeOffset) + err = os.Chtimes(tmpfile.Name(), modTime, modTime) + if err != nil { + t.Fatalf("Failed to set file times: %v", err) + } + } + + return tmpfile.Name() +} + +func TestR3001UnexpectedProcessLaunched(t *testing.T) { + // Create a new rule + r := CreateRuleR3001UnexpectedProcessLaunched() + if r == nil { + t.Errorf("Expected r to not be nil") + } + + // Create test files + newFile := createTempFile(t, 0) // Just created + oldFile := createTempFile(t, -10*time.Minute) // Created 10 minutes ago + + // Cleanup + defer os.Remove(newFile) + defer os.Remove(oldFile) + + // Test cases + tests := []struct { + name string + event *events.ExecEvent + eventType utils.EventType + expectFailure bool + }{ + { + name: "Old binary execution", + event: &events.ExecEvent{ + Event: tracerexectype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + }, + }, + }, + }, + Comm: "test-process", + Args: []string{oldFile, "--test"}, + Retval: 0, + Pid: 1234, + ExePath: oldFile, + }, + }, + eventType: utils.ExecveEventType, + expectFailure: false, + }, + { + name: "Wrong event type", + event: &events.ExecEvent{ + Event: tracerexectype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test", + }, + }, + }, + }, + Comm: "test", + Args: []string{"test"}, + }, + }, + eventType: utils.PtraceEventType, + expectFailure: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Process the event + ruleResult := r.ProcessEvent(tc.eventType, tc.event, nil) + + // Verify results + if tc.expectFailure { + if ruleResult == nil { + t.Errorf("Expected rule failure but got nil") + return + } + + failure, ok := ruleResult.(*ruleenginev1.GenericRuleFailure) + if !ok { + t.Errorf("Expected GenericRuleFailure type") + return + } + + // Verify failure details + if failure.BaseRuntimeAlert.AlertName != R3001Name { + t.Errorf("Expected alert name %s, got %s", R3001Name, failure.BaseRuntimeAlert.AlertName) + } + + } else if ruleResult != nil { + t.Errorf("Expected no failure but got: %v", ruleResult) + } + }) + } +} diff --git a/pkg/hostrules/v1/r3002_cgroups_release_agent_modified.go b/pkg/hostrules/v1/r3002_cgroups_release_agent_modified.go new file mode 100644 index 0000000000..9c133af66c --- /dev/null +++ b/pkg/hostrules/v1/r3002_cgroups_release_agent_modified.go @@ -0,0 +1,124 @@ +package hostrules + +import ( + "fmt" + "strings" + + apitypes "github.com/armosec/armoapi-go/armotypes" + "github.com/kubescape/node-agent/pkg/ebpf/events" + "github.com/kubescape/node-agent/pkg/objectcache" + "github.com/kubescape/node-agent/pkg/ruleengine" + ruleenginev1 "github.com/kubescape/node-agent/pkg/ruleengine/v1" + "github.com/kubescape/node-agent/pkg/utils" +) + +const ( + R3002ID = "R3002" + R3002Name = "CGroups Release Agent Modified" +) + +var R3002CGroupsReleaseAgentModifiedRuleDescriptor = ruleengine.RuleDescriptor{ + ID: R3002ID, + Name: R3002Name, + Description: "Detecting attempts to modify control group (cgroup) release agent files which could indicate a container escape attempt", + Tags: []string{"container-escape", "privilege-escalation", "cgroups"}, + Priority: ruleenginev1.RulePriorityHigh, + Requirements: &ruleenginev1.RuleRequirements{ + EventTypes: []utils.EventType{utils.OpenEventType}, + }, + RuleCreationFunc: func() ruleengine.RuleEvaluator { + return CreateRuleR3002CGroupsReleaseAgent() + }, +} + +var _ ruleengine.RuleEvaluator = (*R3002CGroupsReleaseAgent)(nil) + +type R3002CGroupsReleaseAgent struct { + ruleenginev1.BaseRule +} + +func CreateRuleR3002CGroupsReleaseAgent() *R3002CGroupsReleaseAgent { + return &R3002CGroupsReleaseAgent{} +} + +func (rule *R3002CGroupsReleaseAgent) Name() string { + return R3002Name +} + +func (rule *R3002CGroupsReleaseAgent) ID() string { + return R3002ID +} + +func (rule *R3002CGroupsReleaseAgent) SetParameters(parameters map[string]interface{}) { + rule.BaseRule.SetParameters(parameters) +} + +func (rule *R3002CGroupsReleaseAgent) DeleteRule() { +} + +func (rule *R3002CGroupsReleaseAgent) ProcessEvent(eventType utils.EventType, event utils.K8sEvent, objCache objectcache.ObjectCache) ruleengine.RuleFailure { + if eventType != utils.OpenEventType { + return nil + } + + fullEvent, ok := event.(*events.OpenEvent) + if !ok { + return nil + } + + openEvent := fullEvent.Event + + if openEvent.Runtime.ContainerID == "" { + return nil + } + + if !ComparePaths(openEvent.FullPath, "/sys/fs/cgroup/memory/release_agent") && + !ComparePaths(openEvent.FullPath, "/sys/fs/cgroup/release_agent") { + return nil + } + + if !hasTargetFlags(openEvent.Flags, writeFlags) { + return nil + } + + ruleFailure := ruleenginev1.GenericRuleFailure{ + BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ + AlertName: rule.Name(), + InfectedPID: openEvent.Pid, + Arguments: map[string]interface{}{ + "flags": openEvent.Flags, + "path": openEvent.FullPath, + }, + Severity: R3002CGroupsReleaseAgentModifiedRuleDescriptor.Priority, + }, + RuntimeProcessDetails: apitypes.ProcessTree{ + ProcessTree: apitypes.Process{ + Comm: openEvent.Comm, + Gid: &openEvent.Gid, + PID: openEvent.Pid, + Uid: &openEvent.Uid, + }, + ContainerID: openEvent.Runtime.ContainerID, + }, + TriggerEvent: openEvent.Event, + RuleAlert: apitypes.RuleAlert{ + RuleDescription: fmt.Sprintf("Container escape attempt detected: Modification of cgroups release_agent file '%s' with flags %s in container: %s", + openEvent.FullPath, + strings.Join(openEvent.Flags, ","), + openEvent.GetContainer()), + }, + RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ + PodName: openEvent.GetPod(), + }, + RuleID: rule.ID(), + Extra: fullEvent.GetExtra(), + } + + return &ruleFailure +} + +func (rule *R3002CGroupsReleaseAgent) Requirements() ruleengine.RuleSpec { + return &ruleenginev1.RuleRequirements{ + EventTypes: R3002CGroupsReleaseAgentModifiedRuleDescriptor.Requirements.RequiredEventTypes(), + } +} diff --git a/pkg/hostrules/v1/r3002_cgroups_release_agent_modified_test.go b/pkg/hostrules/v1/r3002_cgroups_release_agent_modified_test.go new file mode 100644 index 0000000000..2815747401 --- /dev/null +++ b/pkg/hostrules/v1/r3002_cgroups_release_agent_modified_test.go @@ -0,0 +1,168 @@ +package hostrules + +import ( + "testing" + + traceropentype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" + "github.com/kubescape/node-agent/pkg/ebpf/events" + "github.com/kubescape/node-agent/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func TestR3002CGroupsReleaseAgent_ProcessEvent(t *testing.T) { + tests := []struct { + name string + eventType utils.EventType + event *events.OpenEvent + expectedAlert bool + description string + }{ + { + name: "should_alert_on_write_to_release_agent", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + PodName: "test-pod", + Namespace: "test-namespace", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ContainerID: "test"}, + }, + }, + Path: "/sys/../sys/fs/cgroup/release_agent", + FullPath: "/sys/../sys/fs/cgroup/release_agent", + Flags: []string{"O_WRONLY"}, + }, + }, + expectedAlert: true, + description: "Should alert when release_agent file is opened with write permissions", + }, + { + name: "should_alert_on_rdwr_to_release_agent", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + PodName: "test-pod", + Namespace: "test-namespace", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + }, + }, + }, + Path: "/sys/fs/cgroup/memory/release_agent", + FullPath: "/sys/fs/cgroup/memory/release_agent", + Flags: []string{"O_RDWR"}, + }, + }, + expectedAlert: true, + description: "Should alert when release_agent file is opened with read-write permissions", + }, + { + name: "should_not_alert_on_read_only_access", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + PodName: "test-pod", + Namespace: "test-namespace", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + }, + }, + }, + Path: "/sys/fs/cgroup/memory/release_agent", + FullPath: "/sys/fs/cgroup/memory/release_agent", + Flags: []string{"O_RDONLY"}, + }, + }, + expectedAlert: false, + description: "Should not alert when release_agent file is opened with read-only permissions", + }, + { + name: "should_not_alert_on_non_container_access", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "", + PodName: "", + Namespace: "", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "", + }, + }, + }, + Path: "/sys/fs/cgroup/memory/release_agent", + FullPath: "/sys/fs/cgroup/memory/release_agent", + Flags: []string{"O_WRONLY"}, + }, + }, + expectedAlert: false, + description: "Should not alert when access is from outside a container", + }, + { + name: "should_not_alert_on_different_path", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + PodName: "test-pod", + Namespace: "test-namespace", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + }, + }, + }, + Path: "/sys/fs/cgroup/memory/other_file", + FullPath: "/sys/fs/cgroup/memory/other_file", + Flags: []string{"O_WRONLY"}, + }, + }, + expectedAlert: false, + description: "Should not alert when accessing a different file in cgroups directory", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule := CreateRuleR3002CGroupsReleaseAgent() + failure := rule.ProcessEvent(tt.eventType, tt.event, nil) + + if tt.expectedAlert { + assert.NotNil(t, failure, tt.description) + } else { + assert.Nil(t, failure, tt.description) + } + }) + } +} diff --git a/pkg/hostrules/v1/r3003_suspicous_tool_executed.go b/pkg/hostrules/v1/r3003_suspicous_tool_executed.go new file mode 100644 index 0000000000..420609dd97 --- /dev/null +++ b/pkg/hostrules/v1/r3003_suspicous_tool_executed.go @@ -0,0 +1,117 @@ +package hostrules + +import ( + "fmt" + "strings" + + apitypes "github.com/armosec/armoapi-go/armotypes" + events "github.com/kubescape/node-agent/pkg/ebpf/events" + "github.com/kubescape/node-agent/pkg/objectcache" + "github.com/kubescape/node-agent/pkg/ruleengine" + ruleenginev1 "github.com/kubescape/node-agent/pkg/ruleengine/v1" + "github.com/kubescape/node-agent/pkg/utils" +) + +const ( + R3003ID = "R3003" + R3003Name = "Execution of suspicious security tools" +) + +var R3003SuspiciousToolRuleDescriptor = ruleengine.RuleDescriptor{ + ID: R3003ID, + Name: R3003Name, + Description: "Detects execution of known suspicious security tools within containers", + Tags: []string{"security", "pentest", "tools"}, + Priority: ruleengine.RulePriorityHigh, + Requirements: &ruleenginev1.RuleRequirements{ + EventTypes: []utils.EventType{utils.ExecveEventType}, + }, + RuleCreationFunc: func() ruleengine.RuleEvaluator { + return CreateRuleR3003SuspiciousTool() + }, +} + +type R3003SuspiciousTool struct { + ruleenginev1.BaseRule +} + +func CreateRuleR3003SuspiciousTool() *R3003SuspiciousTool { + return &R3003SuspiciousTool{} +} + +func (rule *R3003SuspiciousTool) Name() string { + return R3003Name +} + +func (rule *R3003SuspiciousTool) ID() string { + return R3003ID +} + +func (rule *R3003SuspiciousTool) SetParameters(params map[string]interface{}) { +} + +func (rule *R3003SuspiciousTool) ProcessEvent(eventType utils.EventType, event utils.K8sEvent, _ objectcache.ObjectCache) ruleengine.RuleFailure { + if eventType != utils.ExecveEventType { + return nil + } + + execEvent, ok := event.(*events.ExecEvent) + if !ok { + return nil + } + + execPath := ruleenginev1.GetExecPathFromEvent(execEvent) + isSuspicious, severity, category := isSuspiciousTool(execPath) + + if !isSuspicious { + return nil + } + + ruleFailure := ruleenginev1.GenericRuleFailure{ + BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ + AlertName: rule.Name(), + InfectedPID: execEvent.Pid, + Arguments: map[string]interface{}{ + "retval": execEvent.Retval, + "exec": execEvent.ExePath, + "args": execEvent.Args, + "category": category, + "severity": severity, + }, + Severity: severity, + }, + RuntimeProcessDetails: apitypes.ProcessTree{ + ProcessTree: apitypes.Process{ + Comm: execEvent.Comm, + Gid: &execEvent.Gid, + PID: execEvent.Pid, + Uid: &execEvent.Uid, + PPID: execEvent.Ppid, + Pcomm: execEvent.Pcomm, + Cwd: execEvent.Cwd, + Hardlink: execEvent.ExePath, + Path: execPath, + Cmdline: fmt.Sprintf("%s %s", execPath, strings.Join(utils.GetExecArgsFromEvent(&execEvent.Event), " ")), + }, + ContainerID: execEvent.Runtime.ContainerID, + }, + TriggerEvent: execEvent.Event.Event, + RuleAlert: apitypes.RuleAlert{ + RuleDescription: fmt.Sprintf("Suspicious security tool executed: %s", execPath), + }, + RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ + PodName: execEvent.GetPod(), + PodLabels: execEvent.K8s.PodLabels, + }, + RuleID: rule.ID(), + Extra: execEvent.GetExtra(), + } + + return &ruleFailure +} + +func (rule *R3003SuspiciousTool) Requirements() ruleengine.RuleSpec { + return &ruleenginev1.RuleRequirements{ + EventTypes: R3003SuspiciousToolRuleDescriptor.Requirements.RequiredEventTypes(), + } +} diff --git a/pkg/hostrules/v1/r3003_suspicous_tool_executed_test.go b/pkg/hostrules/v1/r3003_suspicous_tool_executed_test.go new file mode 100644 index 0000000000..7345e8c431 --- /dev/null +++ b/pkg/hostrules/v1/r3003_suspicous_tool_executed_test.go @@ -0,0 +1,171 @@ +package hostrules + +import ( + "testing" + + events "github.com/kubescape/node-agent/pkg/ebpf/events" + ruleenginev1 "github.com/kubescape/node-agent/pkg/ruleengine/v1" + "github.com/kubescape/node-agent/pkg/utils" + + tracerexectype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/exec/types" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" +) + +func TestR3003SuspiciousTool(t *testing.T) { + // Create a new rule + r := CreateRuleR3003SuspiciousTool() + if r == nil { + t.Errorf("Expected r to not be nil") + } + + // Test cases + tests := []struct { + name string + event *events.ExecEvent + eventType utils.EventType + expectFailure bool + expectedTool string + expectedSev int + }{ + { + name: "Nmap execution", + event: &events.ExecEvent{ + Event: tracerexectype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + PodName: "test-pod", + Namespace: "test-namespace", + }, + }, + }, + }, + Comm: "nmap", + Args: []string{"/usr/bin/nmap", "-sS", "192.168.1.1"}, + Retval: 0, + Uid: 1000, + Gid: 1000, + Pid: 1234, + Ppid: 5678, + ExePath: "/usr/bin/nmap", + }, + }, + eventType: utils.ExecveEventType, + expectFailure: true, + expectedTool: "nmap", + expectedSev: 1, + }, + { + name: "Metasploit execution", + event: &events.ExecEvent{ + Event: tracerexectype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + }, + }, + }, + }, + Comm: "msfconsole", + Args: []string{"/usr/bin/msfconsole", "-q"}, + Retval: 0, + Pid: 1234, + ExePath: "/usr/bin/msfconsole", + }, + }, + eventType: utils.ExecveEventType, + expectFailure: true, + expectedTool: "msfconsole", + expectedSev: 3, + }, + { + name: "Normal binary execution", + event: &events.ExecEvent{ + Event: tracerexectype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + }, + }, + }, + }, + Comm: "ls", + Args: []string{"/bin/ls", "-la"}, + Retval: 0, + Pid: 1234, + ExePath: "/bin/ls", + }, + }, + eventType: utils.ExecveEventType, + expectFailure: false, + }, + { + name: "Wrong event type", + event: &events.ExecEvent{ + Event: tracerexectype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test", + }, + }, + }, + }, + Comm: "nmap", + Args: []string{"/usr/bin/nmap"}, + ExePath: "/usr/bin/nmap", + }, + }, + eventType: utils.PtraceEventType, + expectFailure: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Process the event + ruleResult := r.ProcessEvent(tc.eventType, tc.event, nil) + + // Verify results + if tc.expectFailure { + if ruleResult == nil { + t.Errorf("Expected rule failure but got nil") + return + } + + failure, ok := ruleResult.(*ruleenginev1.GenericRuleFailure) + if !ok { + t.Errorf("Expected GenericRuleFailure type") + return + } + + // Verify failure details + if failure.BaseRuntimeAlert.AlertName != R3003Name { + t.Errorf("Expected alert name %s, got %s", R3003Name, failure.BaseRuntimeAlert.AlertName) + } + + // Verify severity for known tools + if tc.expectedSev != 0 { + severity, ok := failure.BaseRuntimeAlert.Arguments["severity"].(int) + if !ok { + t.Errorf("Expected severity to be int") + return + } + if severity != tc.expectedSev { + t.Errorf("Expected severity %d, got %d", tc.expectedSev, severity) + } + } + + } else if ruleResult != nil { + t.Errorf("Expected no failure but got: %v", ruleResult) + } + }) + } +} diff --git a/pkg/hostrules/v1/r3004_docker_socket_access.go b/pkg/hostrules/v1/r3004_docker_socket_access.go new file mode 100644 index 0000000000..b0502445f0 --- /dev/null +++ b/pkg/hostrules/v1/r3004_docker_socket_access.go @@ -0,0 +1,124 @@ +package hostrules + +import ( + "fmt" + "strings" + + apitypes "github.com/armosec/armoapi-go/armotypes" + "github.com/kubescape/node-agent/pkg/ebpf/events" + "github.com/kubescape/node-agent/pkg/objectcache" + "github.com/kubescape/node-agent/pkg/ruleengine" + ruleenginev1 "github.com/kubescape/node-agent/pkg/ruleengine/v1" + "github.com/kubescape/node-agent/pkg/utils" +) + +const ( + R3004ID = "R3004" + R3004Name = "Docker Socket Accessed From Container" + dockerSocketPath = "/var/run/docker.sock" +) + +var R3004DockerSocketAccessRuleDescriptor = ruleengine.RuleDescriptor{ + ID: R3004ID, + Name: R3004Name, + Description: "Detecting attempts to access Docker socket which could indicate a container escape attempt", + Tags: []string{"container-escape", "privilege-escalation", "docker"}, + Priority: ruleenginev1.RulePriorityMed, + Requirements: &ruleenginev1.RuleRequirements{ + EventTypes: []utils.EventType{utils.OpenEventType}, + }, + RuleCreationFunc: func() ruleengine.RuleEvaluator { + return CreateRuleR3004DockerSocketAccess() + }, +} + +var _ ruleengine.RuleEvaluator = (*R3004DockerSocketAccess)(nil) + +type R3004DockerSocketAccess struct { + ruleenginev1.BaseRule +} + +func CreateRuleR3004DockerSocketAccess() *R3004DockerSocketAccess { + return &R3004DockerSocketAccess{} +} + +func (rule *R3004DockerSocketAccess) Name() string { + return R3004Name +} + +func (rule *R3004DockerSocketAccess) ID() string { + return R3004ID +} + +func (rule *R3004DockerSocketAccess) SetParameters(parameters map[string]interface{}) { + rule.BaseRule.SetParameters(parameters) +} + +func (rule *R3004DockerSocketAccess) DeleteRule() { +} + +func (rule *R3004DockerSocketAccess) ProcessEvent(eventType utils.EventType, event utils.K8sEvent, objCache objectcache.ObjectCache) ruleengine.RuleFailure { + if eventType != utils.OpenEventType { + return nil + } + + fullEvent, ok := event.(*events.OpenEvent) + if !ok { + return nil + } + + openEvent := fullEvent.Event + + if openEvent.Runtime.ContainerID == "" { + return nil + } + + if !ComparePaths(openEvent.FullPath, dockerSocketPath) { + return nil + } + + if isAllowedNamespace(openEvent.GetNamespace()) { + return nil + } + + ruleFailure := ruleenginev1.GenericRuleFailure{ + BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ + AlertName: rule.Name(), + InfectedPID: openEvent.Pid, + Arguments: map[string]interface{}{ + "flags": openEvent.Flags, + "path": openEvent.FullPath, + }, + Severity: R3004DockerSocketAccessRuleDescriptor.Priority, + }, + RuntimeProcessDetails: apitypes.ProcessTree{ + ProcessTree: apitypes.Process{ + Comm: openEvent.Comm, + Gid: &openEvent.Gid, + PID: openEvent.Pid, + Uid: &openEvent.Uid, + }, + ContainerID: openEvent.Runtime.ContainerID, + }, + TriggerEvent: openEvent.Event, + RuleAlert: apitypes.RuleAlert{ + RuleDescription: fmt.Sprintf("Container escape attempt detected: Access to Docker socket '%s' with flags %s in container: %s", + openEvent.FullPath, + strings.Join(openEvent.Flags, ","), + openEvent.GetContainer()), + }, + RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ + PodName: openEvent.GetPod(), + }, + RuleID: rule.ID(), + Extra: fullEvent.GetExtra(), + } + + return &ruleFailure +} + +func (rule *R3004DockerSocketAccess) Requirements() ruleengine.RuleSpec { + return &ruleenginev1.RuleRequirements{ + EventTypes: R3004DockerSocketAccessRuleDescriptor.Requirements.RequiredEventTypes(), + } +} diff --git a/pkg/hostrules/v1/r3004_docker_socket_access_test.go b/pkg/hostrules/v1/r3004_docker_socket_access_test.go new file mode 100644 index 0000000000..15b306400c --- /dev/null +++ b/pkg/hostrules/v1/r3004_docker_socket_access_test.go @@ -0,0 +1,176 @@ +package hostrules + +import ( + "testing" + + traceropentype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" + "github.com/kubescape/node-agent/pkg/ebpf/events" + ruleenginev1 "github.com/kubescape/node-agent/pkg/ruleengine/v1" + "github.com/kubescape/node-agent/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func TestR3004DockerSocketAccess_ProcessEvent(t *testing.T) { + tests := []struct { + name string + eventType utils.EventType + event *events.OpenEvent + expectedAlert bool + description string + }{ + { + name: "should_alert_on_docker_socket_access", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + PodName: "test-pod", + Namespace: "test-namespace", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + }, + }, + }, + Path: "/var/run/docker.sock", + FullPath: "/var/run/docker.sock", + Flags: []string{"O_RDWR"}, + }, + }, + expectedAlert: true, + description: "Should alert when Docker socket is accessed from non-allowed namespace", + }, + { + name: "should_not_alert_in_allowed_namespace", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "jenkins-builder", + PodName: "jenkins-build-123", + Namespace: "ci", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + }, + }, + }, + Path: "/var/run/docker.sock", + FullPath: "/var/run/docker.sock", + Flags: []string{"O_RDWR"}, + }, + }, + expectedAlert: false, + description: "Should not alert when access is from allowed namespace", + }, + { + name: "should_not_alert_on_non_container_access", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "", + PodName: "", + Namespace: "", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "", + }, + }, + }, + Path: "/var/run/docker.sock", + FullPath: "/var/run/docker.sock", + Flags: []string{"O_RDWR"}, + }, + }, + expectedAlert: false, + description: "Should not alert when access is from outside a container", + }, + { + name: "should_not_alert_on_different_path", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + PodName: "test-pod", + Namespace: "test-namespace", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + }, + }, + }, + Path: "/var/run/other.sock", + FullPath: "/var/run/other.sock", + Flags: []string{"O_RDWR"}, + }, + }, + expectedAlert: false, + description: "Should not alert when accessing a different socket file", + }, + { + name: "should_not_alert_on_non_open_event", + eventType: utils.ExecveEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + PodName: "test-pod", + Namespace: "test-namespace", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + }, + }, + }, + Path: "/var/run/docker.sock", + FullPath: "/var/run/docker.sock", + Flags: []string{"O_RDWR"}, + }, + }, + expectedAlert: false, + description: "Should not alert for non-open event types", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule := CreateRuleR3004DockerSocketAccess() + failure := rule.ProcessEvent(tt.eventType, tt.event, nil) + + if tt.expectedAlert { + assert.NotNil(t, failure, tt.description) + if failure != nil { + // Verify the alert contains expected docker socket information + assert.Contains(t, failure.(*ruleenginev1.GenericRuleFailure).RuleAlert.RuleDescription, + "docker.sock", "Alert description should mention docker socket") + } + } else { + assert.Nil(t, failure, tt.description) + } + }) + } +} diff --git a/pkg/hostrules/v1/r3005_fs_memory_injection.go b/pkg/hostrules/v1/r3005_fs_memory_injection.go new file mode 100644 index 0000000000..4663ab0c8d --- /dev/null +++ b/pkg/hostrules/v1/r3005_fs_memory_injection.go @@ -0,0 +1,147 @@ +package hostrules + +import ( + "fmt" + "path/filepath" + "strings" + + apitypes "github.com/armosec/armoapi-go/armotypes" + "github.com/kubescape/node-agent/pkg/ebpf/events" + "github.com/kubescape/node-agent/pkg/objectcache" + "github.com/kubescape/node-agent/pkg/ruleengine" + ruleenginev1 "github.com/kubescape/node-agent/pkg/ruleengine/v1" + "github.com/kubescape/node-agent/pkg/utils" +) + +const ( + R3005ID = "R3005" + R3005Name = "Malicious Process Memory Injection From Filesystem" +) + +var R3005MaliciousFsMemoryInjectionRuleDescriptor = ruleengine.RuleDescriptor{ + ID: R3005ID, + Name: R3005Name, + Description: "Detecting malicious attempts to inject code into process memory through /proc/PID/mem which indicates a potential process injection attack", + Tags: []string{"process-injection", "defense-evasion", "privilege-escalation"}, + Priority: ruleenginev1.RulePriorityHigh, + Requirements: &ruleenginev1.RuleRequirements{ + EventTypes: []utils.EventType{utils.OpenEventType}, + }, + RuleCreationFunc: func() ruleengine.RuleEvaluator { + return CreateRuleR3005MaliciousFsMemoryInjection() + }, +} + +var _ ruleengine.RuleEvaluator = (*R3005MaliciousFsMemoryInjection)(nil) + +type R3005MaliciousFsMemoryInjection struct { + ruleenginev1.BaseRule +} + +func CreateRuleR3005MaliciousFsMemoryInjection() *R3005MaliciousFsMemoryInjection { + return &R3005MaliciousFsMemoryInjection{} +} + +func (rule *R3005MaliciousFsMemoryInjection) Name() string { + return R3005Name +} + +func (rule *R3005MaliciousFsMemoryInjection) ID() string { + return R3005ID +} + +func (rule *R3005MaliciousFsMemoryInjection) SetParameters(parameters map[string]interface{}) { + rule.BaseRule.SetParameters(parameters) +} + +func (rule *R3005MaliciousFsMemoryInjection) DeleteRule() { +} + +func isProcMemPath(path string) bool { + dir, file := filepath.Split(filepath.Clean(path)) + if file != "mem" { + return false + } + + dir = filepath.Clean(dir) + parent, pid := filepath.Split(dir) + + parent = filepath.Clean(parent) + + if parent != "proc" && parent != "/proc" { + return false + } + + for _, char := range pid { + if char < '0' || char > '9' { + return false + } + } + + return true +} + +func (rule *R3005MaliciousFsMemoryInjection) ProcessEvent(eventType utils.EventType, event utils.K8sEvent, objCache objectcache.ObjectCache) ruleengine.RuleFailure { + if eventType != utils.OpenEventType { + return nil + } + + fullEvent, ok := event.(*events.OpenEvent) + if !ok { + return nil + } + + openEvent := fullEvent.Event + + if !isProcMemPath(openEvent.FullPath) { + return nil + } + + if !hasTargetFlags(openEvent.Flags, writeFlags) { + return nil + } + + ruleFailure := ruleenginev1.GenericRuleFailure{ + BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ + AlertName: rule.Name(), + InfectedPID: openEvent.Pid, + Arguments: map[string]interface{}{ + "flags": openEvent.Flags, + "path": openEvent.FullPath, + }, + Severity: R3005MaliciousFsMemoryInjectionRuleDescriptor.Priority, + }, + RuntimeProcessDetails: apitypes.ProcessTree{ + ProcessTree: apitypes.Process{ + Comm: openEvent.Comm, + Gid: &openEvent.Gid, + PID: openEvent.Pid, + Uid: &openEvent.Uid, + }, + ContainerID: openEvent.Runtime.ContainerID, + }, + TriggerEvent: openEvent.Event, + RuleAlert: apitypes.RuleAlert{ + RuleDescription: fmt.Sprintf("Process memory injection attempt detected: Write access to process memory via '%s' with flags %s from PID: %d", + openEvent.FullPath, + strings.Join(openEvent.Flags, ","), + openEvent.Pid), + }, + RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ + PodName: openEvent.GetPod(), + ContainerID: openEvent.Runtime.ContainerID, + ContainerName: openEvent.GetContainer(), + Namespace: openEvent.GetNamespace(), + }, + RuleID: rule.ID(), + Extra: fullEvent.GetExtra(), + } + + return &ruleFailure +} + +func (rule *R3005MaliciousFsMemoryInjection) Requirements() ruleengine.RuleSpec { + return &ruleenginev1.RuleRequirements{ + EventTypes: R3005MaliciousFsMemoryInjectionRuleDescriptor.Requirements.RequiredEventTypes(), + } +} diff --git a/pkg/hostrules/v1/r3005_fs_memory_injection_test.go b/pkg/hostrules/v1/r3005_fs_memory_injection_test.go new file mode 100644 index 0000000000..7b7bfd7238 --- /dev/null +++ b/pkg/hostrules/v1/r3005_fs_memory_injection_test.go @@ -0,0 +1,170 @@ +package hostrules + +import ( + "testing" + + traceropentype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" + "github.com/kubescape/node-agent/pkg/ebpf/events" + ruleenginev1 "github.com/kubescape/node-agent/pkg/ruleengine/v1" + "github.com/kubescape/node-agent/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func TestR3005MaliciousFsMemoryInjection_ProcessEvent(t *testing.T) { + tests := []struct { + name string + eventType utils.EventType + event *events.OpenEvent + expectedAlert bool + description string + }{ + { + name: "should_alert_on_proc_mem_write", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + PodName: "test-pod", + Namespace: "test-namespace", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + }, + }, + }, + Path: "/proc/1234/mem", + FullPath: "/proc/../../proc/1234/mem", + Flags: []string{"O_WRONLY"}, + }, + }, + expectedAlert: true, + description: "Should alert when /proc/PID/mem is opened with write permissions", + }, + { + name: "should_not_alert_on_read_only", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + PodName: "test-pod", + Namespace: "test-namespace", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + }, + }, + }, + Path: "/proc/1234/mem", + FullPath: "/proc/1234/mem", + Flags: []string{"O_RDONLY"}, + }, + }, + expectedAlert: false, + description: "Should not alert when /proc/PID/mem is opened read-only", + }, + { + name: "should_not_alert_on_invalid_path", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + PodName: "test-pod", + Namespace: "test-namespace", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + }, + }, + }, + Path: "/proc/abc/mem", + FullPath: "/proc/abc/mem", + Flags: []string{"O_WRONLY"}, + }, + }, + expectedAlert: false, + description: "Should not alert when PID is not numeric", + }, + { + name: "should_not_alert_on_different_proc_file", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + PodName: "test-pod", + Namespace: "test-namespace", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + }, + }, + }, + Path: "/proc/1234/maps", + FullPath: "/proc/1234/maps", + Flags: []string{"O_WRONLY"}, + }, + }, + expectedAlert: false, + description: "Should not alert for different proc files", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule := CreateRuleR3005MaliciousFsMemoryInjection() + failure := rule.ProcessEvent(tt.eventType, tt.event, nil) + + if tt.expectedAlert { + assert.NotNil(t, failure, tt.description) + if failure != nil { + assert.Contains(t, failure.(*ruleenginev1.GenericRuleFailure).RuleAlert.RuleDescription, + "Process memory injection attempt detected", "Alert description should mention process injection") + } + } else { + assert.Nil(t, failure, tt.description) + } + }) + } +} + +func TestIsProcMemPath(t *testing.T) { + tests := []struct { + name string + path string + expected bool + }{ + {"valid path", "/proc/1234/mem", true}, + {"invalid pid", "/proc/abc/mem", false}, + {"wrong file", "/proc/1234/maps", false}, + {"too short", "/proc/mem", false}, + {"too long", "/proc/1234/mem/extra", false}, + {"not proc", "/var/1234/mem", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isProcMemPath(tt.path) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/hostrules/v1/r3006_suspicious_clean_logs.go b/pkg/hostrules/v1/r3006_suspicious_clean_logs.go new file mode 100644 index 0000000000..63362bc318 --- /dev/null +++ b/pkg/hostrules/v1/r3006_suspicious_clean_logs.go @@ -0,0 +1,162 @@ +package hostrules + +import ( + "fmt" + "path/filepath" + "strings" + + apitypes "github.com/armosec/armoapi-go/armotypes" + "github.com/kubescape/node-agent/pkg/ebpf/events" + "github.com/kubescape/node-agent/pkg/objectcache" + "github.com/kubescape/node-agent/pkg/ruleengine" + ruleenginev1 "github.com/kubescape/node-agent/pkg/ruleengine/v1" + "github.com/kubescape/node-agent/pkg/utils" +) + +const ( + R3006ID = "R3006" + R3006Name = "Suspicious Log Cleaning" +) + +var accessLogFiles = []string{ + "/var/log/auth.log", + "/var/log/secure", + "/var/log/audit/audit.log", + "/var/log/syslog", + "/var/log/messages", + "/var/log/kern.log", +} + +var trustedLoggingImages = []string{ + "logrotate", + "journald", + "syslog-ng", +} + +var R3006SuspiciousLogCleaningRuleDescriptor = ruleengine.RuleDescriptor{ + ID: R3006ID, + Name: R3006Name, + Description: "Detect clearing of critical access log files, typically done to erase evidence of adversary actions", + Tags: []string{"defense-evasion", "logging", "mitre_defense_evasion", "T1070", "NIST_800-53_AU-10"}, + Priority: ruleenginev1.RulePriorityMed, + Requirements: &ruleenginev1.RuleRequirements{ + EventTypes: []utils.EventType{utils.OpenEventType}, + }, + RuleCreationFunc: func() ruleengine.RuleEvaluator { + return CreateRuleR3006SuspiciousLogCleaning() + }, +} + +var _ ruleengine.RuleEvaluator = (*R3006SuspiciousLogCleaning)(nil) + +type R3006SuspiciousLogCleaning struct { + ruleenginev1.BaseRule +} + +func CreateRuleR3006SuspiciousLogCleaning() *R3006SuspiciousLogCleaning { + return &R3006SuspiciousLogCleaning{} +} + +func (rule *R3006SuspiciousLogCleaning) Name() string { + return R3006Name +} + +func (rule *R3006SuspiciousLogCleaning) ID() string { + return R3006ID +} + +func (rule *R3006SuspiciousLogCleaning) SetParameters(parameters map[string]interface{}) { + rule.BaseRule.SetParameters(parameters) +} + +func (rule *R3006SuspiciousLogCleaning) DeleteRule() { +} + +func (rule *R3006SuspiciousLogCleaning) ProcessEvent(eventType utils.EventType, event utils.K8sEvent, objCache objectcache.ObjectCache) ruleengine.RuleFailure { + if eventType != utils.OpenEventType { + return nil + } + + fullEvent, ok := event.(*events.OpenEvent) + if !ok { + return nil + } + + openEvent := fullEvent.Event + + if !isAccessLogFile(openEvent.FullPath) { + return nil + } + + if !hasTargetFlags(openEvent.Flags, []string{"O_WRONLY", "O_RDWR", "O_TRUNC", "O_CREAT"}) { + return nil + } + + if isTrustedLoggingImage(openEvent.Runtime.ContainerImageName) { + return nil + } + + ruleFailure := ruleenginev1.GenericRuleFailure{ + BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ + AlertName: rule.Name(), + InfectedPID: openEvent.Pid, + Arguments: map[string]interface{}{ + "flags": openEvent.Flags, + "file": openEvent.FullPath, + "command": openEvent.Comm, + }, + Severity: R3006SuspiciousLogCleaningRuleDescriptor.Priority, + }, + RuntimeProcessDetails: apitypes.ProcessTree{ + ProcessTree: apitypes.Process{ + Comm: openEvent.Comm, + Gid: &openEvent.Gid, + PID: openEvent.Pid, + Uid: &openEvent.Uid, + }, + ContainerID: openEvent.Runtime.ContainerID, + }, + TriggerEvent: openEvent.Event, + RuleAlert: apitypes.RuleAlert{ + RuleDescription: fmt.Sprintf("Log file tampering detected: '%s' was truncated by process '%s' (PID: %d)", + openEvent.FullPath, + openEvent.Comm, + openEvent.Pid), + }, + RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ + PodName: openEvent.GetPod(), + ContainerID: openEvent.Runtime.ContainerID, + ContainerName: openEvent.GetContainer(), + Namespace: openEvent.GetNamespace(), + }, + RuleID: rule.ID(), + Extra: fullEvent.GetExtra(), + } + + return &ruleFailure +} + +func (rule *R3006SuspiciousLogCleaning) Requirements() ruleengine.RuleSpec { + return &ruleenginev1.RuleRequirements{ + EventTypes: R3006SuspiciousLogCleaningRuleDescriptor.Requirements.RequiredEventTypes(), + } +} + +func isAccessLogFile(path string) bool { + cleanPath := filepath.Clean(path) + for _, logFile := range accessLogFiles { + if cleanPath == logFile { + return true + } + } + return false +} + +func isTrustedLoggingImage(imageName string) bool { + for _, trustedImage := range trustedLoggingImages { + if strings.Contains(strings.ToLower(imageName), trustedImage) { + return true + } + } + return false +} diff --git a/pkg/hostrules/v1/r3006_suspicious_clean_logs_test.go b/pkg/hostrules/v1/r3006_suspicious_clean_logs_test.go new file mode 100644 index 0000000000..1c3063bde4 --- /dev/null +++ b/pkg/hostrules/v1/r3006_suspicious_clean_logs_test.go @@ -0,0 +1,235 @@ +package hostrules + +import ( + "testing" + + traceropentype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" + "github.com/kubescape/node-agent/pkg/ebpf/events" + "github.com/kubescape/node-agent/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func TestR3006SuspiciousLogCleaning_ProcessEvent(t *testing.T) { + tests := []struct { + name string + eventType utils.EventType + event *events.OpenEvent + expectedAlert bool + description string + }{ + { + name: "should_alert_on_auth_log_truncate", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test-container", + PodName: "test-pod", + Namespace: "test-namespace", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + ContainerImageName: "malicious-image", + }, + }, + }, + Path: "/var/log/auth.log", + FullPath: "/var/log/auth.log", + Flags: []string{"O_TRUNC", "O_WRONLY"}, + Comm: "suspicious-process", + Pid: 1234, + }, + }, + expectedAlert: true, + description: "Should alert when auth.log is truncated by non-trusted process", + }, + { + name: "should_not_alert_trusted_image", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "logrotate", + PodName: "logrotate-pod", + Namespace: "logging", + }, + }, + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "logrotate-container", + ContainerImageName: "logrotate:latest", + }, + }, + }, + Path: "/var/log/auth.log", + FullPath: "/var/log/auth.log", + Flags: []string{"O_TRUNC", "O_WRONLY"}, + }, + }, + expectedAlert: false, + description: "Should not alert when log is truncated by trusted logging process", + }, + { + name: "should_not_alert_non_log_file", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + ContainerImageName: "test-image", + }, + }, + }, + Path: "/var/log/random.txt", + FullPath: "/var/log/random.txt", + Flags: []string{"O_TRUNC"}, + }, + }, + expectedAlert: false, + description: "Should not alert when truncating non-monitored log file", + }, + { + name: "should_not_alert_no_trunc", + eventType: utils.OpenEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + ContainerImageName: "test-image", + }, + }, + }, + Path: "/var/log/auth.log", + FullPath: "/var/log/auth.log", + Flags: []string{"O_RDONLY"}, + }, + }, + expectedAlert: false, + description: "Should not alert when opening log file without O_TRUNC flag", + }, + { + name: "should_not_alert_non_open_event", + eventType: utils.ExecveEventType, + event: &events.OpenEvent{ + Event: traceropentype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + Runtime: eventtypes.BasicRuntimeMetadata{ + ContainerID: "test-container", + ContainerImageName: "test-image", + }, + }, + }, + Path: "/var/log/auth.log", + FullPath: "/var/log/auth.log", + Flags: []string{"O_TRUNC"}, + }, + }, + expectedAlert: false, + description: "Should not alert for non-open event types", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule := CreateRuleR3006SuspiciousLogCleaning() + failure := rule.ProcessEvent(tt.eventType, tt.event, nil) + + if tt.expectedAlert { + assert.NotNil(t, failure, tt.description) + } else { + assert.Nil(t, failure, tt.description) + } + }) + } +} + +func TestIsAccessLogFile(t *testing.T) { + tests := []struct { + name string + path string + expected bool + }{ + { + name: "exact_match", + path: "/var/log/auth.log", + expected: true, + }, + { + name: "non_monitored_log", + path: "/var/log/other.log", + expected: false, + }, + { + name: "relative_path", + path: "var/log/auth.log", + expected: false, + }, + { + name: "path_traversal_attempt", + path: "../../../var/log/auth.log", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isAccessLogFile(tt.path) + assert.Equal(t, tt.expected, result, "isAccessLogFile(%s) = %v; want %v", + tt.path, result, tt.expected) + }) + } +} + +func TestIsTrustedLoggingImage(t *testing.T) { + tests := []struct { + name string + imageName string + expected bool + }{ + { + name: "exact_match", + imageName: "logrotate", + expected: true, + }, + { + name: "with_version", + imageName: "logrotate:1.0", + expected: true, + }, + { + name: "with_path", + imageName: "logging/syslog-ng:latest", + expected: true, + }, + { + name: "untrusted_image", + imageName: "nginx:latest", + expected: false, + }, + { + name: "empty_string", + imageName: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isTrustedLoggingImage(tt.imageName) + assert.Equal(t, tt.expected, result, "isTrustedLoggingImage(%s) = %v; want %v", + tt.imageName, result, tt.expected) + }) + } +} diff --git a/pkg/hostrules/v1/types.go b/pkg/hostrules/v1/types.go new file mode 100644 index 0000000000..ab9cf8715c --- /dev/null +++ b/pkg/hostrules/v1/types.go @@ -0,0 +1,7 @@ +package hostrules + +type toolInfo struct { + Name string `json:"name"` + Severity int `json:"severity"` + Category string `json:"category"` +} diff --git a/pkg/ruleengine/v1/r1015_malicious_ptrace_usage.go b/pkg/ruleengine/v1/r1015_malicious_ptrace_usage.go index 7589368eab..ccd22346e2 100644 --- a/pkg/ruleengine/v1/r1015_malicious_ptrace_usage.go +++ b/pkg/ruleengine/v1/r1015_malicious_ptrace_usage.go @@ -8,8 +8,6 @@ import ( "github.com/kubescape/node-agent/pkg/utils" apitypes "github.com/armosec/armoapi-go/armotypes" - "github.com/kubescape/go-logger" - "github.com/kubescape/go-logger/helpers" tracerptracetype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ptrace/tracer/types" ) @@ -38,31 +36,14 @@ var _ ruleengine.RuleEvaluator = (*R1015MaliciousPtraceUsage)(nil) type R1015MaliciousPtraceUsage struct { BaseRule - allowedProcesses []string } func CreateRuleR1015MaliciousPtraceUsage() *R1015MaliciousPtraceUsage { - return &R1015MaliciousPtraceUsage{ - allowedProcesses: []string{}, - } + return &R1015MaliciousPtraceUsage{} } func (rule *R1015MaliciousPtraceUsage) SetParameters(parameters map[string]interface{}) { - rule.BaseRule.SetParameters(parameters) - - allowedProcessesInterface := rule.GetParameters()["allowedProcesses"] - if allowedProcessesInterface == nil { - return - } - allowedProcesses, ok := InterfaceToStringSlice(allowedProcessesInterface) - if ok { - for _, process := range allowedProcesses { - rule.allowedProcesses = append(rule.allowedProcesses, fmt.Sprintf("%v", process)) - } - } else { - logger.L().Warning("failed to convert allowedProcesses to []string", helpers.String("ruleID", rule.ID())) - } } func (rule *R1015MaliciousPtraceUsage) Name() string {