Skip to content

Commit

Permalink
[CWS] sysctl_snapshot event
Browse files Browse the repository at this point in the history
  • Loading branch information
Gui774ume committed Mar 3, 2025
1 parent baf55a5 commit f61b324
Show file tree
Hide file tree
Showing 12 changed files with 615 additions and 63 deletions.
9 changes: 9 additions & 0 deletions docs/cloud-workload-security/backend_linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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 |
Expand Down
4 changes: 4 additions & 0 deletions docs/cloud-workload-security/backend_linux.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions pkg/config/setup/system_probe_cws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
9 changes: 9 additions & 0 deletions pkg/security/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"),
Expand Down
6 changes: 6 additions & 0 deletions pkg/security/events/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -105,6 +110,7 @@ func AllCustomRuleIDs() []string {
NoProcessContextErrorRuleID,
BrokenProcessLineageErrorRuleID,
InternalCoreDumpRuleID,
SysCtlSnapshotRuleID,
}
}

Expand Down
29 changes: 29 additions & 0 deletions pkg/security/probe/probe_ebpf.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,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"
Expand Down Expand Up @@ -596,6 +597,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)
}

Expand Down Expand Up @@ -1748,6 +1752,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 {
Expand Down
166 changes: 166 additions & 0 deletions pkg/security/probe/sysctl/snapshot.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit f61b324

Please sign in to comment.