From ef8478603be192395d39796ba2300ceb761c5415 Mon Sep 17 00:00:00 2001 From: gui774ume Date: Fri, 28 Feb 2025 18:14:29 +0100 Subject: [PATCH 1/2] [CWS] sysctl_snapshot event --- docs/cloud-workload-security/backend_linux.md | 9 + .../backend_linux.schema.json | 4 + pkg/config/setup/system_probe_cws.go | 4 + pkg/security/config/config.go | 9 + pkg/security/events/custom.go | 6 + pkg/security/probe/probe_ebpf.go | 29 ++ pkg/security/probe/sysctl/snapshot.go | 166 ++++++++++++ pkg/security/probe/sysctl/snapshot_test.go | 256 ++++++++++++++++++ pkg/security/secl/schemas/sysctl.schema.json | 6 +- pkg/security/serializers/serializers_base.go | 2 + .../serializers_base_linux_easyjson.go | 180 +++++++----- pkg/security/serializers/serializers_linux.go | 7 + 12 files changed, 615 insertions(+), 63 deletions(-) create mode 100644 pkg/security/probe/sysctl/snapshot.go create mode 100644 pkg/security/probe/sysctl/snapshot_test.go diff --git a/docs/cloud-workload-security/backend_linux.md b/docs/cloud-workload-security/backend_linux.md index 7dc5ba87fa9da..41e3e63936028 100644 --- a/docs/cloud-workload-security/backend_linux.md +++ b/docs/cloud-workload-security/backend_linux.md @@ -1622,6 +1622,10 @@ CSM Threats event for Linux systems have the following JSON schema: }, "SysCtlEvent": { "properties": { + "proc": { + "type": "object", + "description": "Proc contains the /proc system control parameters and their values" + }, "action": { "type": "string", "description": "action performed on the system control parameter" @@ -4399,6 +4403,10 @@ CSM Threats event for Linux systems have the following JSON schema: {{< code-block lang="json" collapsible="true" >}} { "properties": { + "proc": { + "type": "object", + "description": "Proc contains the /proc system control parameters and their values" + }, "action": { "type": "string", "description": "action performed on the system control parameter" @@ -4441,6 +4449,7 @@ CSM Threats event for Linux systems have the following JSON schema: | Field | Description | | ----- | ----------- | +| `proc` | Proc contains the /proc system control parameters and their values | | `action` | action performed on the system control parameter | | `file_position` | file_position is the position in the sysctl control parameter file at which the action occurred | | `name` | name is the name of the system control parameter | diff --git a/docs/cloud-workload-security/backend_linux.schema.json b/docs/cloud-workload-security/backend_linux.schema.json index b8d4d80bac344..66c19a15cb6a7 100644 --- a/docs/cloud-workload-security/backend_linux.schema.json +++ b/docs/cloud-workload-security/backend_linux.schema.json @@ -1611,6 +1611,10 @@ }, "SysCtlEvent": { "properties": { + "proc": { + "type": "object", + "description": "Proc contains the /proc system control parameters and their values" + }, "action": { "type": "string", "description": "action performed on the system control parameter" diff --git a/pkg/config/setup/system_probe_cws.go b/pkg/config/setup/system_probe_cws.go index b831c58087600..80359bc1aedd3 100644 --- a/pkg/config/setup/system_probe_cws.go +++ b/pkg/config/setup/system_probe_cws.go @@ -118,6 +118,10 @@ func initCWSSystemProbeConfig(cfg pkgconfigmodel.Config) { cfg.BindEnvAndSetDefault("runtime_security_config.hash_resolver.cache_size", 500) cfg.BindEnvAndSetDefault("runtime_security_config.hash_resolver.replace", map[string]string{}) + // CWS - SysCtl snapshot + cfg.BindEnvAndSetDefault("runtime_security_config.sysctl_snapshot.enabled", false) + cfg.BindEnvAndSetDefault("runtime_security_config.sysctl_snapshot.period", "1h") + // CWS - UserSessions cfg.BindEnvAndSetDefault("runtime_security_config.user_sessions.cache_size", 1024) diff --git a/pkg/security/config/config.go b/pkg/security/config/config.go index a8b423705c1b3..765e89ce8f28e 100644 --- a/pkg/security/config/config.go +++ b/pkg/security/config/config.go @@ -226,6 +226,11 @@ type RuntimeSecurityConfig struct { // HashResolverReplace is used to apply specific hash to specific file path HashResolverReplace map[string]string + // SysCtlSnapshotEnabled defines if the sysctl snapshot feature should be enabled + SysCtlSnapshotEnabled bool + // SysCtlSnapshotPeriod defines at which time interval a new snapshot of sysctl parameters should be sent + SysCtlSnapshotPeriod time.Duration + // UserSessionsCacheSize defines the size of the User Sessions cache size UserSessionsCacheSize int @@ -413,6 +418,10 @@ func NewRuntimeSecurityConfig() (*RuntimeSecurityConfig, error) { HashResolverCacheSize: pkgconfigsetup.SystemProbe().GetInt("runtime_security_config.hash_resolver.cache_size"), HashResolverReplace: pkgconfigsetup.SystemProbe().GetStringMapString("runtime_security_config.hash_resolver.replace"), + // SysCtl config parameter + SysCtlSnapshotEnabled: pkgconfigsetup.SystemProbe().GetBool("runtime_security_config.sysctl_snapshot.enabled"), + SysCtlSnapshotPeriod: pkgconfigsetup.SystemProbe().GetDuration("runtime_security_config.sysctl_snapshot.period"), + // security profiles SecurityProfileEnabled: pkgconfigsetup.SystemProbe().GetBool("runtime_security_config.security_profile.enabled"), SecurityProfileMaxImageTags: pkgconfigsetup.SystemProbe().GetInt("runtime_security_config.security_profile.max_image_tags"), diff --git a/pkg/security/events/custom.go b/pkg/security/events/custom.go index 511ef4ef521ef..ca5f50dad1a04 100644 --- a/pkg/security/events/custom.go +++ b/pkg/security/events/custom.go @@ -63,6 +63,11 @@ const ( InternalCoreDumpRuleID = "internal_core_dump" // InternalCoreDumpRuleDesc internal core dump InternalCoreDumpRuleDesc = "Internal Core Dump" + + // SysCtlSnapshotRuleID is the rule ID used when sending a sysctl snapshot event + SysCtlSnapshotRuleID = "sysctl_snapshot" + // SysCtlSnapshotRuleDesc is the description of the sysctl snapshot rule + SysCtlSnapshotRuleDesc = "A new sysctl snapshot was generated" ) // AgentContainerContext is like model.ContainerContext, but without event based resolvers @@ -105,6 +110,7 @@ func AllCustomRuleIDs() []string { NoProcessContextErrorRuleID, BrokenProcessLineageErrorRuleID, InternalCoreDumpRuleID, + SysCtlSnapshotRuleID, } } diff --git a/pkg/security/probe/probe_ebpf.go b/pkg/security/probe/probe_ebpf.go index 52a142b30f078..470152ca122b3 100644 --- a/pkg/security/probe/probe_ebpf.go +++ b/pkg/security/probe/probe_ebpf.go @@ -52,6 +52,7 @@ import ( "github.com/DataDog/datadog-agent/pkg/security/probe/eventstream/ringbuffer" "github.com/DataDog/datadog-agent/pkg/security/probe/kfilters" "github.com/DataDog/datadog-agent/pkg/security/probe/managerhelper" + "github.com/DataDog/datadog-agent/pkg/security/probe/sysctl" "github.com/DataDog/datadog-agent/pkg/security/resolvers" "github.com/DataDog/datadog-agent/pkg/security/resolvers/mount" "github.com/DataDog/datadog-agent/pkg/security/resolvers/netns" @@ -597,6 +598,9 @@ func (p *EBPFProbe) Start() error { // start new tc classifier loop go p.startSetupNewTCClassifierLoop() + // start sysctl snapshot loop + go p.startSysCtlSnapshotLoop() + return p.eventStream.Start(&p.wg) } @@ -1749,6 +1753,31 @@ func (p *EBPFProbe) Close() error { return p.Resolvers.Close() } +func (p *EBPFProbe) startSysCtlSnapshotLoop() { + ticker := time.NewTicker(p.config.RuntimeSecurity.SysCtlSnapshotPeriod) + defer ticker.Stop() + for { + select { + case <-p.ctx.Done(): + return + case <-ticker.C: + // create the sysctl snapshot + event, err := sysctl.NewSnapshotEvent() + if err != nil { + seclog.Errorf("sysctl snapshot failed: %v", err) + continue + } + + // send a sysctl snapshot event + rule := events.NewCustomRule(events.SysCtlSnapshotRuleID, events.SysCtlSnapshotRuleDesc) + customEvent := events.NewCustomEvent(model.CustomEventType, event) + + p.probe.DispatchCustomEvent(rule, customEvent) + seclog.Tracef("sysctl snapshot sent !") + } + } +} + // QueuedNetworkDeviceError is used to indicate that the new network device was queued until its namespace handle is // resolved. type QueuedNetworkDeviceError struct { diff --git a/pkg/security/probe/sysctl/snapshot.go b/pkg/security/probe/sysctl/snapshot.go new file mode 100644 index 0000000000000..ce1b5913bdfa7 --- /dev/null +++ b/pkg/security/probe/sysctl/snapshot.go @@ -0,0 +1,166 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build linux + +// Package sysctl is used to process and analyze sysctl data +package sysctl + +import ( + "encoding/json" + "fmt" + "io/fs" + "os" + "path" + "path/filepath" + "slices" + "strings" +) + +var ( + ignoredBaseNames = []string{ + "netdev_rss_key", + "stable_secret", + } + redactedContent = "******" +) + +// readFileContent reads a file and processes its content based on the given rules. +func readFileContent(file string) (string, error) { + if slices.Contains(ignoredBaseNames, path.Base(file)) { + return redactedContent, nil + } + + data, err := os.ReadFile(file) + if err != nil { + return "", err + } + + return string(data), nil +} + +// SnapshotEvent is a wrapper used for serialization +type SnapshotEvent struct { + Sysctl Snapshot `json:"sysctl"` +} + +// NewSnapshotEvent returns a new sysctl snapshot event +func NewSnapshotEvent() (*SnapshotEvent, error) { + se := &SnapshotEvent{ + Sysctl: NewSnapshot(), + } + if err := se.Sysctl.Snapshot(); err != nil { + return nil, err + } + return se, nil +} + +// ToJSON serializes the current SnapshotEvent object to JSON +func (s *SnapshotEvent) ToJSON() ([]byte, error) { + return json.Marshal(s) +} + +// Snapshot defines an internal core dump +type Snapshot struct { + // Proc contains the /proc system control parameters and their values + Proc map[string]interface{} `json:"proc,omitempty"` + // Sys contains the /sys system control parameters and their values + Sys map[string]interface{} `json:"sys,omitempty"` +} + +// NewSnapshot returns a new sysctl snapshot +func NewSnapshot() Snapshot { + return Snapshot{ + Proc: make(map[string]interface{}), + Sys: make(map[string]interface{}), + } +} + +// Snapshot runs the snapshot by going through the filesystem +func (s *Snapshot) Snapshot() error { + if err := s.snapshotProcSys(); err != nil { + return fmt.Errorf("couldn't snapshot /proc/sys: %w", err) + } + + if err := s.snapshotSys(); err != nil { + return fmt.Errorf("coudln't snapshot /sys: %w", err) + } + return nil +} + +// snapshotProcSys recursively reads files in /proc/sys and builds a nested JSON structure. +func (s *Snapshot) snapshotProcSys() error { + return filepath.Walk("/proc/sys", func(file string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + + // Skip directories + if info.IsDir() { + return nil + } + + // Skip if mode doesn't allow reading + mode := info.Mode() + if mode&0400 == 0 && mode&0040 == 0 && mode&0004 == 0 { + return nil + } + + relPath, err := filepath.Rel("/proc", file) + if err != nil { + return err + } + + value, err := readFileContent(file) + if err != nil { + return nil // Skip files that can't be read + } + + s.InsertSnapshotEntry(s.Proc, relPath, value) + return nil + }) +} + +// snapshotSys reads security relevant files from the /sys filesystem +func (s *Snapshot) snapshotSys() error { + lockdownValue, err := readFileContent("/sys/kernel/security/lockdown") + if err != nil { + return err + } + s.InsertSnapshotEntry(s.Sys, "kernel/security/lockdown", lockdownValue) + + lsmValue, err := readFileContent("/sys/kernel/security/lsm") + if err != nil { + return err + } + s.InsertSnapshotEntry(s.Sys, "kernel/security/lsm", lsmValue) + return nil +} + +// InsertSnapshotEntry inserts the provided file and its value in the input data +func (s *Snapshot) InsertSnapshotEntry(data map[string]interface{}, file string, value string) { + if len(value) == 0 { + // ignore + return + } + + parts := strings.Split(strings.TrimPrefix(file, "/"), string(os.PathSeparator)) + current := data + for i, part := range parts { + if i == len(parts)-1 { + current[part] = strings.TrimSpace(value) + } else { + if _, exists := current[part]; !exists { + current[part] = make(map[string]interface{}) + } + current = current[part].(map[string]interface{}) + } + } +} + +// ToJSON serializes the current Snapshot object to JSON +func (s *Snapshot) ToJSON() ([]byte, error) { + return json.Marshal(s) +} diff --git a/pkg/security/probe/sysctl/snapshot_test.go b/pkg/security/probe/sysctl/snapshot_test.go new file mode 100644 index 0000000000000..d8852618d8649 --- /dev/null +++ b/pkg/security/probe/sysctl/snapshot_test.go @@ -0,0 +1,256 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build linux + +package sysctl + +import ( + "reflect" + "testing" +) + +func TestSnapshotEvent_ToJSON(t *testing.T) { + tests := []struct { + name string + input Snapshot + want string + wantErr bool + }{ + { + name: "empty_snapshot", + input: NewSnapshot(), + want: "{\"sysctl\":{}}", + }, + { + name: "full_snapshot", + input: Snapshot{ + Proc: map[string]interface{}{ + "sys": map[string]interface{}{ + "kernel": map[string]interface{}{ + "yama": map[string]interface{}{ + "ptrace_scope": "2", + }, + }, + }, + }, + Sys: map[string]interface{}{ + "kernel": map[string]interface{}{ + "security": map[string]interface{}{ + "lockdown": []string{"none", "[integrity]", "confidentiality"}, + }, + }, + }, + }, + want: "{\"sysctl\":{\"proc\":{\"sys\":{\"kernel\":{\"yama\":{\"ptrace_scope\":\"2\"}}}},\"sys\":{\"kernel\":{\"security\":{\"lockdown\":[\"none\",\"[integrity]\",\"confidentiality\"]}}}}}", + }, + { + name: "proc_only", + input: Snapshot{ + Proc: map[string]interface{}{ + "sys": map[string]interface{}{ + "kernel": map[string]interface{}{ + "yama": map[string]interface{}{ + "ptrace_scope": "2", + }, + }, + }, + }, + }, + want: "{\"sysctl\":{\"proc\":{\"sys\":{\"kernel\":{\"yama\":{\"ptrace_scope\":\"2\"}}}}}}", + }, + { + name: "sys_only", + input: Snapshot{ + Sys: map[string]interface{}{ + "kernel": map[string]interface{}{ + "security": map[string]interface{}{ + "lockdown": []string{"none", "[integrity]", "confidentiality"}, + }, + }, + }, + }, + want: "{\"sysctl\":{\"sys\":{\"kernel\":{\"security\":{\"lockdown\":[\"none\",\"[integrity]\",\"confidentiality\"]}}}}}", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &SnapshotEvent{ + Sysctl: tt.input, + } + got, err := s.ToJSON() + if (err != nil) != tt.wantErr { + t.Errorf("ToJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(string(got), tt.want) { + t.Errorf("ToJSON() got = %v, want %v", string(got), tt.want) + } + }) + } +} + +func TestSnapshot_InsertSnapshotEntry(t *testing.T) { + type args struct { + file string + value string + } + tests := []struct { + name string + snapshot Snapshot + args args + output string + }{ + { + name: "empty_snapshot_insert_nothing", + snapshot: NewSnapshot(), + args: args{}, + output: "{}", + }, + { + name: "empty_snapshot_insert_string_single_file", + snapshot: NewSnapshot(), + args: args{ + file: "sys", + value: "123", + }, + output: "{\"proc\":{\"sys\":\"123\"}}", + }, + { + name: "empty_snapshot_insert_string_root_file", + snapshot: NewSnapshot(), + args: args{ + file: "/", + value: "123", + }, + output: "{\"proc\":{\"\":\"123\"}}", + }, + { + name: "empty_snapshot_insert_string_empty_file", + snapshot: NewSnapshot(), + args: args{ + file: "", + value: "123", + }, + output: "{\"proc\":{\"\":\"123\"}}", + }, + { + name: "empty_snapshot_insert_string", + snapshot: NewSnapshot(), + args: args{ + file: "sys/kernel/random/uuid", + value: "123", + }, + output: "{\"proc\":{\"sys\":{\"kernel\":{\"random\":{\"uuid\":\"123\"}}}}}", + }, + { + name: "empty_snapshot_insert_string", + snapshot: NewSnapshot(), + args: args{ + file: "/sys/kernel/random/uuid", + value: "123", + }, + output: "{\"proc\":{\"sys\":{\"kernel\":{\"random\":{\"uuid\":\"123\"}}}}}", + }, + { + name: "empty_snapshot_insert_string_space", + snapshot: NewSnapshot(), + args: args{ + file: "/sys/kernel/random/uuid", + value: "123 ", + }, + output: "{\"proc\":{\"sys\":{\"kernel\":{\"random\":{\"uuid\":\"123\"}}}}}", + }, + { + name: "empty_snapshot_insert_string_tab", + snapshot: NewSnapshot(), + args: args{ + file: "/sys/kernel/random/uuid", + value: "123\t", + }, + output: "{\"proc\":{\"sys\":{\"kernel\":{\"random\":{\"uuid\":\"123\"}}}}}", + }, + { + name: "empty_snapshot_insert_string_newline", + snapshot: NewSnapshot(), + args: args{ + file: "/sys/kernel/random/uuid", + value: "123\n", + }, + output: "{\"proc\":{\"sys\":{\"kernel\":{\"random\":{\"uuid\":\"123\"}}}}}", + }, + { + name: "snapshot_with_ptrace_scope_insert_uuid_string", + snapshot: Snapshot{ + Proc: map[string]interface{}{ + "sys": map[string]interface{}{ + "kernel": map[string]interface{}{ + "yama": map[string]interface{}{ + "ptrace_scope": "2", + }, + }, + }, + }, + }, + args: args{ + file: "/sys/kernel/random/uuid", + value: "123", + }, + output: "{\"proc\":{\"sys\":{\"kernel\":{\"random\":{\"uuid\":\"123\"},\"yama\":{\"ptrace_scope\":\"2\"}}}}}", + }, + { + name: "snapshot_with_ptrace_scope_insert_hello_string_spaces", + snapshot: Snapshot{ + Proc: map[string]interface{}{ + "sys": map[string]interface{}{ + "kernel": map[string]interface{}{ + "yama": map[string]interface{}{ + "ptrace_scope": "2", + }, + }, + }, + }, + }, + args: args{ + file: "/hello/world", + value: " 123 ", + }, + output: "{\"proc\":{\"hello\":{\"world\":\"123\"},\"sys\":{\"kernel\":{\"yama\":{\"ptrace_scope\":\"2\"}}}}}", + }, + { + name: "snapshot_with_ptrace_scope_override", + snapshot: Snapshot{ + Proc: map[string]interface{}{ + "sys": map[string]interface{}{ + "kernel": map[string]interface{}{ + "yama": map[string]interface{}{ + "ptrace_scope": "2", + }, + }, + }, + }, + }, + args: args{ + file: "/sys/kernel/yama/ptrace_scope", + value: "0", + }, + output: "{\"proc\":{\"sys\":{\"kernel\":{\"yama\":{\"ptrace_scope\":\"0\"}}}}}", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.snapshot.InsertSnapshotEntry(tt.snapshot.Proc, tt.args.file, tt.args.value) + got, err := tt.snapshot.ToJSON() + if err != nil { + t.Errorf("InsertSnapshotEntry - ToJSON error: %v", err) + return + } + if !reflect.DeepEqual(string(got), tt.output) { + t.Errorf("InsertSnapshotEntry got = %v, want %v", string(got), tt.output) + } + + }) + } +} diff --git a/pkg/security/secl/schemas/sysctl.schema.json b/pkg/security/secl/schemas/sysctl.schema.json index 5889782188c8f..501b2f08de596 100644 --- a/pkg/security/secl/schemas/sysctl.schema.json +++ b/pkg/security/secl/schemas/sysctl.schema.json @@ -18,7 +18,8 @@ "required": [ "action", "value", - "name" + "name", + "proc" ], "properties": { "action": { @@ -32,6 +33,9 @@ }, "old_value": { "type": "string" + }, + "proc": { + "type": "object" } } } diff --git a/pkg/security/serializers/serializers_base.go b/pkg/security/serializers/serializers_base.go index 0a3d4e2235192..5061bae399de6 100644 --- a/pkg/security/serializers/serializers_base.go +++ b/pkg/security/serializers/serializers_base.go @@ -264,6 +264,8 @@ type NetworkFlowMonitorSerializer struct { // SysCtlEventSerializer defines a sysctl event serializer // easyjson:json type SysCtlEventSerializer struct { + // Proc contains the /proc system control parameters and their values + Proc map[string]interface{} `json:"proc,omitempty"` // action performed on the system control parameter Action string `json:"action,omitempty"` // file_position is the position in the sysctl control parameter file at which the action occurred diff --git a/pkg/security/serializers/serializers_base_linux_easyjson.go b/pkg/security/serializers/serializers_base_linux_easyjson.go index 92a4f0b995ce0..029be8877310c 100644 --- a/pkg/security/serializers/serializers_base_linux_easyjson.go +++ b/pkg/security/serializers/serializers_base_linux_easyjson.go @@ -155,6 +155,32 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(i continue } switch key { + case "proc": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.Proc = make(map[string]interface{}) + } else { + out.Proc = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v3 interface{} + if m, ok := v3.(easyjson.Unmarshaler); ok { + m.UnmarshalEasyJSON(in) + } else if m, ok := v3.(json.Unmarshaler); ok { + _ = m.UnmarshalJSON(in.Raw()) + } else { + v3 = in.Interface() + } + (out.Proc)[key] = v3 + in.WantComma() + } + in.Delim('}') + } case "action": out.Action = string(in.String()) case "file_position": @@ -185,10 +211,40 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(o out.RawByte('{') first := true _ = first - if in.Action != "" { - const prefix string = ",\"action\":" + if len(in.Proc) != 0 { + const prefix string = ",\"proc\":" first = false out.RawString(prefix[1:]) + { + out.RawByte('{') + v4First := true + for v4Name, v4Value := range in.Proc { + if v4First { + v4First = false + } else { + out.RawByte(',') + } + out.String(string(v4Name)) + out.RawByte(':') + if m, ok := v4Value.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := v4Value.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(v4Value)) + } + } + out.RawByte('}') + } + } + if in.Action != "" { + const prefix string = ",\"action\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } out.String(string(in.Action)) } if in.FilePosition != 0 { @@ -448,17 +504,17 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(i out.Ancestors = (out.Ancestors)[:0] } for !in.IsDelim(']') { - var v3 *ProcessSerializer + var v5 *ProcessSerializer if in.IsNull() { in.Skip() - v3 = nil + v5 = nil } else { - if v3 == nil { - v3 = new(ProcessSerializer) + if v5 == nil { + v5 = new(ProcessSerializer) } - (*v3).UnmarshalEasyJSON(in) + (*v5).UnmarshalEasyJSON(in) } - out.Ancestors = append(out.Ancestors, v3) + out.Ancestors = append(out.Ancestors, v5) in.WantComma() } in.Delim(']') @@ -599,9 +655,9 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(i out.Args = (out.Args)[:0] } for !in.IsDelim(']') { - var v4 string - v4 = string(in.String()) - out.Args = append(out.Args, v4) + var v6 string + v6 = string(in.String()) + out.Args = append(out.Args, v6) in.WantComma() } in.Delim(']') @@ -624,9 +680,9 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(i out.Envs = (out.Envs)[:0] } for !in.IsDelim(']') { - var v5 string - v5 = string(in.String()) - out.Envs = append(out.Envs, v5) + var v7 string + v7 = string(in.String()) + out.Envs = append(out.Envs, v7) in.WantComma() } in.Delim(']') @@ -664,9 +720,9 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(i *out.Syscalls = (*out.Syscalls)[:0] } for !in.IsDelim(']') { - var v6 SyscallSerializer - easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(in, &v6) - *out.Syscalls = append(*out.Syscalls, v6) + var v8 SyscallSerializer + easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(in, &v8) + *out.Syscalls = append(*out.Syscalls, v8) in.WantComma() } in.Delim(']') @@ -688,17 +744,17 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(i out.AWSSecurityCredentials = (out.AWSSecurityCredentials)[:0] } for !in.IsDelim(']') { - var v7 *AWSSecurityCredentialsSerializer + var v9 *AWSSecurityCredentialsSerializer if in.IsNull() { in.Skip() - v7 = nil + v9 = nil } else { - if v7 == nil { - v7 = new(AWSSecurityCredentialsSerializer) + if v9 == nil { + v9 = new(AWSSecurityCredentialsSerializer) } - (*v7).UnmarshalEasyJSON(in) + (*v9).UnmarshalEasyJSON(in) } - out.AWSSecurityCredentials = append(out.AWSSecurityCredentials, v7) + out.AWSSecurityCredentials = append(out.AWSSecurityCredentials, v9) in.WantComma() } in.Delim(']') @@ -733,14 +789,14 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(o } { out.RawByte('[') - for v8, v9 := range in.Ancestors { - if v8 > 0 { + for v10, v11 := range in.Ancestors { + if v10 > 0 { out.RawByte(',') } - if v9 == nil { + if v11 == nil { out.RawString("null") } else { - (*v9).MarshalEasyJSON(out) + (*v11).MarshalEasyJSON(out) } } out.RawByte(']') @@ -886,11 +942,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(o out.RawString(prefix) { out.RawByte('[') - for v10, v11 := range in.Args { - if v10 > 0 { + for v12, v13 := range in.Args { + if v12 > 0 { out.RawByte(',') } - out.String(string(v11)) + out.String(string(v13)) } out.RawByte(']') } @@ -905,11 +961,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(o out.RawString(prefix) { out.RawByte('[') - for v12, v13 := range in.Envs { - if v12 > 0 { + for v14, v15 := range in.Envs { + if v14 > 0 { out.RawByte(',') } - out.String(string(v13)) + out.String(string(v15)) } out.RawByte(']') } @@ -946,11 +1002,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(o out.RawString("null") } else { out.RawByte('[') - for v14, v15 := range *in.Syscalls { - if v14 > 0 { + for v16, v17 := range *in.Syscalls { + if v16 > 0 { out.RawByte(',') } - easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(out, v15) + easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers5(out, v17) } out.RawByte(']') } @@ -960,14 +1016,14 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(o out.RawString(prefix) { out.RawByte('[') - for v16, v17 := range in.AWSSecurityCredentials { - if v16 > 0 { + for v18, v19 := range in.AWSSecurityCredentials { + if v18 > 0 { out.RawByte(',') } - if v17 == nil { + if v19 == nil { out.RawString("null") } else { - (*v17).MarshalEasyJSON(out) + (*v19).MarshalEasyJSON(out) } } out.RawByte(']') @@ -1144,17 +1200,17 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(i out.Flows = (out.Flows)[:0] } for !in.IsDelim(']') { - var v18 *FlowSerializer + var v20 *FlowSerializer if in.IsNull() { in.Skip() - v18 = nil + v20 = nil } else { - if v18 == nil { - v18 = new(FlowSerializer) + if v20 == nil { + v20 = new(FlowSerializer) } - (*v18).UnmarshalEasyJSON(in) + (*v20).UnmarshalEasyJSON(in) } - out.Flows = append(out.Flows, v18) + out.Flows = append(out.Flows, v20) in.WantComma() } in.Delim(']') @@ -1189,14 +1245,14 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(o } { out.RawByte('[') - for v19, v20 := range in.Flows { - if v19 > 0 { + for v21, v22 := range in.Flows { + if v21 > 0 { out.RawByte(',') } - if v20 == nil { + if v22 == nil { out.RawString("null") } else { - (*v20).MarshalEasyJSON(out) + (*v22).MarshalEasyJSON(out) } } out.RawByte(']') @@ -1361,9 +1417,9 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(i out.Tags = (out.Tags)[:0] } for !in.IsDelim(']') { - var v21 string - v21 = string(in.String()) - out.Tags = append(out.Tags, v21) + var v23 string + v23 = string(in.String()) + out.Tags = append(out.Tags, v23) in.WantComma() } in.Delim(']') @@ -1412,11 +1468,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(o } { out.RawByte('[') - for v22, v23 := range in.Tags { - if v22 > 0 { + for v24, v25 := range in.Tags { + if v24 > 0 { out.RawByte(',') } - out.String(string(v23)) + out.String(string(v25)) } out.RawByte(']') } @@ -1885,9 +1941,9 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers15( out.MatchedRules = (out.MatchedRules)[:0] } for !in.IsDelim(']') { - var v24 MatchedRuleSerializer - (v24).UnmarshalEasyJSON(in) - out.MatchedRules = append(out.MatchedRules, v24) + var v26 MatchedRuleSerializer + (v26).UnmarshalEasyJSON(in) + out.MatchedRules = append(out.MatchedRules, v26) in.WantComma() } in.Delim(']') @@ -1954,11 +2010,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers15( } { out.RawByte('[') - for v25, v26 := range in.MatchedRules { - if v25 > 0 { + for v27, v28 := range in.MatchedRules { + if v27 > 0 { out.RawByte(',') } - (v26).MarshalEasyJSON(out) + (v28).MarshalEasyJSON(out) } out.RawByte(']') } diff --git a/pkg/security/serializers/serializers_linux.go b/pkg/security/serializers/serializers_linux.go index 567bb4e2921a8..bd641b664fa69 100644 --- a/pkg/security/serializers/serializers_linux.go +++ b/pkg/security/serializers/serializers_linux.go @@ -12,12 +12,14 @@ package serializers import ( "fmt" + "path" "syscall" "time" "golang.org/x/sys/unix" "github.com/DataDog/datadog-agent/pkg/security/events" + "github.com/DataDog/datadog-agent/pkg/security/probe/sysctl" sprocess "github.com/DataDog/datadog-agent/pkg/security/resolvers/process" "github.com/DataDog/datadog-agent/pkg/security/secl/compiler/eval" "github.com/DataDog/datadog-agent/pkg/security/secl/model" @@ -1097,7 +1099,12 @@ func newNetworkFlowMonitorSerializer(nm *model.NetworkFlowMonitorEvent, e *model } func newSysCtlEventSerializer(sce *model.SysCtlEvent, _ *model.Event) *SysCtlEventSerializer { + snapshot := sysctl.NewSnapshot() + relPath := path.Join("sys", sce.Name) + snapshot.InsertSnapshotEntry(snapshot.Proc, relPath, sce.Value) + return &SysCtlEventSerializer{ + Proc: snapshot.Proc, Action: model.SysCtlAction(sce.Action).String(), FilePosition: sce.FilePosition, Name: sce.Name, From 9fee0f0178eece9de2517f259c2c4a7c301367d4 Mon Sep 17 00:00:00 2001 From: gui774ume Date: Mon, 3 Mar 2025 13:54:10 +0100 Subject: [PATCH 2/2] [CWS] review changes --- pkg/config/setup/system_probe_cws.go | 2 +- pkg/security/probe/sysctl/snapshot.go | 11 ++++++----- pkg/security/probe/sysctl/snapshot_test.go | 11 +++-------- pkg/util/kernel/fs.go | 6 ++++++ 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/pkg/config/setup/system_probe_cws.go b/pkg/config/setup/system_probe_cws.go index 80359bc1aedd3..0d6e72b8b4366 100644 --- a/pkg/config/setup/system_probe_cws.go +++ b/pkg/config/setup/system_probe_cws.go @@ -119,7 +119,7 @@ func initCWSSystemProbeConfig(cfg pkgconfigmodel.Config) { cfg.BindEnvAndSetDefault("runtime_security_config.hash_resolver.replace", map[string]string{}) // CWS - SysCtl snapshot - cfg.BindEnvAndSetDefault("runtime_security_config.sysctl_snapshot.enabled", false) + cfg.BindEnvAndSetDefault("runtime_security_config.sysctl_snapshot.enabled", true) cfg.BindEnvAndSetDefault("runtime_security_config.sysctl_snapshot.period", "1h") // CWS - UserSessions diff --git a/pkg/security/probe/sysctl/snapshot.go b/pkg/security/probe/sysctl/snapshot.go index ce1b5913bdfa7..cc62dd43a3528 100644 --- a/pkg/security/probe/sysctl/snapshot.go +++ b/pkg/security/probe/sysctl/snapshot.go @@ -11,6 +11,7 @@ package sysctl import ( "encoding/json" "fmt" + "github.com/DataDog/datadog-agent/pkg/util/kernel" "io/fs" "os" "path" @@ -92,7 +93,7 @@ func (s *Snapshot) Snapshot() error { // snapshotProcSys recursively reads files in /proc/sys and builds a nested JSON structure. func (s *Snapshot) snapshotProcSys() error { - return filepath.Walk("/proc/sys", func(file string, info fs.FileInfo, err error) error { + return filepath.Walk(kernel.HostProc("/sys"), func(file string, info fs.FileInfo, err error) error { if err != nil { return err } @@ -104,11 +105,11 @@ func (s *Snapshot) snapshotProcSys() error { // Skip if mode doesn't allow reading mode := info.Mode() - if mode&0400 == 0 && mode&0040 == 0 && mode&0004 == 0 { + if mode&0444 == 0 { return nil } - relPath, err := filepath.Rel("/proc", file) + relPath, err := filepath.Rel(kernel.ProcFSRoot(), file) if err != nil { return err } @@ -125,13 +126,13 @@ func (s *Snapshot) snapshotProcSys() error { // snapshotSys reads security relevant files from the /sys filesystem func (s *Snapshot) snapshotSys() error { - lockdownValue, err := readFileContent("/sys/kernel/security/lockdown") + lockdownValue, err := readFileContent(kernel.HostSys("/kernel/security/lockdown")) if err != nil { return err } s.InsertSnapshotEntry(s.Sys, "kernel/security/lockdown", lockdownValue) - lsmValue, err := readFileContent("/sys/kernel/security/lsm") + lsmValue, err := readFileContent(kernel.HostSys("/kernel/security/lsm")) if err != nil { return err } diff --git a/pkg/security/probe/sysctl/snapshot_test.go b/pkg/security/probe/sysctl/snapshot_test.go index d8852618d8649..db2673bbb584e 100644 --- a/pkg/security/probe/sysctl/snapshot_test.go +++ b/pkg/security/probe/sysctl/snapshot_test.go @@ -8,7 +8,7 @@ package sysctl import ( - "reflect" + "github.com/stretchr/testify/assert" "testing" ) @@ -85,9 +85,7 @@ func TestSnapshotEvent_ToJSON(t *testing.T) { t.Errorf("ToJSON() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(string(got), tt.want) { - t.Errorf("ToJSON() got = %v, want %v", string(got), tt.want) - } + assert.JSONEqf(t, tt.want, string(got), "ToJSON() error") }) } } @@ -247,10 +245,7 @@ func TestSnapshot_InsertSnapshotEntry(t *testing.T) { t.Errorf("InsertSnapshotEntry - ToJSON error: %v", err) return } - if !reflect.DeepEqual(string(got), tt.output) { - t.Errorf("InsertSnapshotEntry got = %v, want %v", string(got), tt.output) - } - + assert.JSONEqf(t, string(got), tt.output, "InsertSnapshotEntry error") }) } } diff --git a/pkg/util/kernel/fs.go b/pkg/util/kernel/fs.go index cbb9b843b8c2f..008350b022543 100644 --- a/pkg/util/kernel/fs.go +++ b/pkg/util/kernel/fs.go @@ -65,6 +65,12 @@ func HostProc(combineWith ...string) string { return filepath.Join(ProcFSRoot(), filepath.Join(combineWith...)) } +// HostSys returns the location of a host's sysfs. This can and will be +// overridden when running inside a container. +func HostSys(combineWith ...string) string { + return filepath.Join(SysFSRoot(), filepath.Join(combineWith...)) +} + // RootNSPID returns the current PID from the root namespace var RootNSPID = funcs.Memoize(func() (int, error) { pidPath := filepath.Join(ProcFSRoot(), "self")