Skip to content

Commit

Permalink
[CSM] Track image tags of syscalls in activity trees (#27483)
Browse files Browse the repository at this point in the history
Co-authored-by: safchain <[email protected]>
  • Loading branch information
Gui774ume and safchain authored Nov 19, 2024
1 parent 8dc9452 commit 83e319e
Show file tree
Hide file tree
Showing 12 changed files with 316 additions and 20 deletions.
7 changes: 7 additions & 0 deletions pkg/security/secl/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,13 @@ func (e *Event) AddToFlags(flag uint32) {
e.Flags |= flag
}

// ResetAnomalyDetectionEvent removes the anomaly detection event flag
func (e *Event) ResetAnomalyDetectionEvent() {
if e.IsAnomalyDetectionEvent() {
e.RemoveFromFlags(EventFlagsAnomalyDetectionEvent)
}
}

// RemoveFromFlags remove a flag to the event
func (e *Event) RemoveFromFlags(flag uint32) {
e.Flags ^= flag
Expand Down
7 changes: 7 additions & 0 deletions pkg/security/seclwin/model/model.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pkg/security/security_profile/activity_tree/activity_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ func (at *ActivityTree) insertEvent(event *model.Event, dryRun bool, insertMissi
case model.BindEventType:
return node.InsertBindEvent(event, imageTag, generationType, at.Stats, dryRun), nil
case model.SyscallsEventType:
return node.InsertSyscalls(event, at.SyscallsMask), nil
return node.InsertSyscalls(event, imageTag, at.SyscallsMask, at.Stats, dryRun), nil
case model.ExitEventType:
// Update the exit time of the process (this is purely informative, do not rely on timestamps to detect
// execed children)
Expand Down Expand Up @@ -936,7 +936,7 @@ func (at *ActivityTree) ExtractSyscalls(arch string) []string {

at.visit(func(processNode *ProcessNode) {
for _, s := range processNode.Syscalls {
sycallKey := utils.SyscallKey{Arch: arch, ID: s}
sycallKey := utils.SyscallKey{Arch: arch, ID: s.Syscall}
syscall, ok := utils.Syscalls[sycallKey]
if ok {
syscalls = append(syscalls, syscall)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ func (at *ActivityTree) prepareFileNode(f *FileNode, data *utils.Graph, prefix s
func (at *ActivityTree) prepareSyscallsNode(p *ProcessNode, data *utils.Graph) utils.GraphID {
label := "<<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"5\">"
for _, s := range p.Syscalls {
label += "<TR><TD>" + model.Syscall(s).String() + "</TD></TR>"
label += "<TR><TD>" + model.Syscall(s.Syscall).String() + "</TD></TR>"
}
label += "</TABLE>>"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func protoDecodeProcessActivityNode(parent ProcessNodeParent, pan *adproto.Proce
DNSNames: make(map[string]*DNSNode, len(pan.DnsNames)),
IMDSEvents: make(map[model.IMDSEvent]*IMDSNode, len(pan.ImdsEvents)),
Sockets: make([]*SocketNode, 0, len(pan.Sockets)),
Syscalls: make([]int, 0, len(pan.Syscalls)),
Syscalls: make([]*SyscallNode, 0, len(pan.Syscalls)),
ImageTags: pan.ImageTags,
}

Expand Down Expand Up @@ -74,7 +74,7 @@ func protoDecodeProcessActivityNode(parent ProcessNodeParent, pan *adproto.Proce
}

for _, sysc := range pan.Syscalls {
ppan.Syscalls = append(ppan.Syscalls, int(sysc))
ppan.Syscalls = append(ppan.Syscalls, NewSyscallNode(int(sysc), "", Unknown))
}

return ppan
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func processActivityNodeToProto(pan *ProcessNode) *adproto.ProcessActivityNode {
}

for _, sysc := range pan.Syscalls {
ppan.Syscalls = append(ppan.Syscalls, uint32(sysc))
ppan.Syscalls = append(ppan.Syscalls, uint32(sysc.Syscall))
}

return ppan
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Stats struct {
DNSNodes int64
SocketNodes int64
IMDSNodes int64
SyscallNodes int64

counts map[model.EventType]*statsPerEventType
}
Expand Down Expand Up @@ -72,6 +73,7 @@ func (stats *Stats) ApproximateSize() int64 {
total += stats.DNSNodes * int64(unsafe.Sizeof(DNSNode{})) // 24
total += stats.SocketNodes * int64(unsafe.Sizeof(SocketNode{})) // 40
total += stats.IMDSNodes * int64(unsafe.Sizeof(IMDSNode{}))
total += stats.SyscallNodes * int64(unsafe.Sizeof(SyscallNode{}))
return total
}

Expand Down
35 changes: 26 additions & 9 deletions pkg/security/security_profile/activity_tree/process_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type ProcessNode struct {
IMDSEvents map[model.IMDSEvent]*IMDSNode

Sockets []*SocketNode
Syscalls []int
Syscalls []*SyscallNode
Children []*ProcessNode
}

Expand Down Expand Up @@ -200,20 +200,29 @@ func (pn *ProcessNode) Matches(entry *model.Process, matchArgs bool, normalize b
}

// InsertSyscalls inserts the syscall of the process in the dump
func (pn *ProcessNode) InsertSyscalls(e *model.Event, syscallMask map[int]int) bool {
func (pn *ProcessNode) InsertSyscalls(e *model.Event, imageTag string, syscallMask map[int]int, stats *Stats, dryRun bool) bool {
var hasNewSyscalls bool
newSyscallLoop:
for _, newSyscall := range e.Syscalls.Syscalls {
for _, existingSyscall := range pn.Syscalls {
if existingSyscall == int(newSyscall) {
if existingSyscall.Syscall == int(newSyscall) {
if imageTag != "" && !slices.Contains(existingSyscall.ImageTags, imageTag) {
existingSyscall.ImageTags = append(existingSyscall.ImageTags, imageTag)
}
continue newSyscallLoop
}
}

pn.Syscalls = append(pn.Syscalls, int(newSyscall))
syscallMask[int(newSyscall)] = int(newSyscall)
hasNewSyscalls = true
if dryRun {
// exit early
break
}
pn.Syscalls = append(pn.Syscalls, NewSyscallNode(int(newSyscall), imageTag, Runtime))
syscallMask[int(newSyscall)] = int(newSyscall)
stats.SyscallNodes++
}

return hasNewSyscalls
}

Expand Down Expand Up @@ -389,6 +398,9 @@ func (pn *ProcessNode) TagAllNodes(imageTag string) {
for _, sock := range pn.Sockets {
sock.appendImageTag(imageTag)
}
for _, scall := range pn.Syscalls {
scall.appendImageTag(imageTag)
}
for _, child := range pn.Children {
child.TagAllNodes(imageTag)
}
Expand Down Expand Up @@ -449,16 +461,21 @@ func (pn *ProcessNode) EvictImageTag(imageTag string, DNSNames *utils.StringKeys
}
pn.Sockets = newSockets

newSyscalls := []*SyscallNode{}
for _, scall := range pn.Syscalls {
if shouldRemove := scall.evictImageTag(imageTag); !shouldRemove {
newSyscalls = append(newSyscalls, scall)
SyscallsMask[scall.Syscall] = scall.Syscall
}
}
pn.Syscalls = newSyscalls

newChildren := []*ProcessNode{}
for _, child := range pn.Children {
if shouldRemoveNode := child.EvictImageTag(imageTag, DNSNames, SyscallsMask); !shouldRemoveNode {
newChildren = append(newChildren, child)
}
}
pn.Children = newChildren

for _, id := range pn.Syscalls {
SyscallsMask[id] = id
}
return false
}
46 changes: 46 additions & 0 deletions pkg/security/security_profile/activity_tree/syscalls_node.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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 activitytree holds activitytree related files
package activitytree

// SyscallNode is used to store a syscall node
type SyscallNode struct {
ImageTags []string
GenerationType NodeGenerationType

Syscall int
}

func (sn *SyscallNode) appendImageTag(imageTag string) {
sn.ImageTags, _ = AppendIfNotPresent(sn.ImageTags, imageTag)
}

func (sn *SyscallNode) evictImageTag(imageTag string) bool {
imageTags, removed := removeImageTagFromList(sn.ImageTags, imageTag)
if !removed {
return false
}
if len(imageTags) == 0 {
return true
}
sn.ImageTags = imageTags
return false
}

// NewSyscallNode returns a new SyscallNode instance
func NewSyscallNode(syscall int, imageTag string, generationType NodeGenerationType) *SyscallNode {
var imageTags []string
if len(imageTag) != 0 {
imageTags = append(imageTags, imageTag)
}
return &SyscallNode{
Syscall: syscall,
GenerationType: generationType,
ImageTags: imageTags,
}
}
27 changes: 27 additions & 0 deletions pkg/security/security_profile/profile/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,10 @@ func (m *SecurityProfileManager) LookupEventInProfiles(event *model.Event) {
globalEventTypeProfilState := profile.GetGlobalEventTypeState(event.GetEventType())
if globalEventTypeProfilState == model.UnstableEventType {
m.incrementEventFilteringStat(event.GetEventType(), model.UnstableEventType, NA)
// The anomaly flag can be set in kernel space by our eBPF programs (currently applies only to syscalls), reset
// the anomaly flag if the user space profile considers it to not be an anomaly. Here, when a version is unstable,
// we don't want to generate anomalies for this profile anymore.
event.ResetAnomalyDetectionEvent()
return
}

Expand All @@ -778,6 +782,13 @@ func (m *SecurityProfileManager) LookupEventInProfiles(event *model.Event) {
case model.NoProfile, model.ProfileAtMaxSize, model.UnstableEventType:
// an error occurred or we are in unstable state
// do not link the profile to avoid sending anomalies

// The anomaly flag can be set in kernel space by our eBPF programs (currently applies only to syscalls), reset
// the anomaly flag if the user space profile considers it to not be an anomaly.
// We can also get a syscall anomaly detection kernel space for runc, which is ignored in the activity tree
// (i.e. tryAutolearn returns NoProfile) because "runc" can't be a root node.
event.ResetAnomalyDetectionEvent()

return
case model.AutoLearning, model.WorkloadWarmup:
// the event was either already in the profile, or has just been inserted
Expand All @@ -798,12 +809,20 @@ func (m *SecurityProfileManager) LookupEventInProfiles(event *model.Event) {
if err != nil {
// ignore, evaluation failed
m.incrementEventFilteringStat(event.GetEventType(), model.NoProfile, NA)

// The anomaly flag can be set in kernel space by our eBPF programs (currently applies only to syscalls), reset
// the anomaly flag if the user space profile considers it to not be an anomaly.
event.ResetAnomalyDetectionEvent()
return
}
FillProfileContextFromProfile(&event.SecurityProfileContext, profile, imageTag, profileState)
if found {
event.AddToFlags(model.EventFlagsSecurityProfileInProfile)
m.incrementEventFilteringStat(event.GetEventType(), profileState, InProfile)

// The anomaly flag can be set in kernel space by our eBPF programs (currently applies only to syscalls), reset
// the anomaly flag if the user space profile considers it to not be an anomaly.
event.ResetAnomalyDetectionEvent()
} else {
m.incrementEventFilteringStat(event.GetEventType(), profileState, NotInProfile)
if m.canGenerateAnomaliesFor(event) {
Expand Down Expand Up @@ -851,11 +870,19 @@ func (m *SecurityProfileManager) tryAutolearn(profile *SecurityProfile, ctx *Ver
globalEventTypeState := profile.GetGlobalEventTypeState(event.GetEventType())
if globalEventTypeState == model.StableEventType && m.canGenerateAnomaliesFor(event) {
event.AddToFlags(model.EventFlagsAnomalyDetectionEvent)
} else {
// The anomaly flag can be set in kernel space by our eBPF programs (currently applies only to syscalls), reset
// the anomaly flag if the user space profile considers it to not be an anomaly: there is a new entry and no
// previous version is in stable state.
event.ResetAnomalyDetectionEvent()
}

m.incrementEventFilteringStat(event.GetEventType(), profileState, NotInProfile)
} else { // no newEntry
m.incrementEventFilteringStat(event.GetEventType(), profileState, InProfile)
// The anomaly flag can be set in kernel space by our eBPF programs (currently applies only to syscalls), reset
// the anomaly flag if the user space profile considers it to not be an anomaly
event.ResetAnomalyDetectionEvent()
}
return profileState
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/security/tests/activity_dumps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,10 +351,10 @@ func TestActivityDumps(t *testing.T) {
var exitOK, bindOK bool
for _, node := range nodes {
for _, s := range node.Syscalls {
if s == int(model.SysExit) || s == int(model.SysExitGroup) {
if s.Syscall == int(model.SysExit) || s.Syscall == int(model.SysExitGroup) {
exitOK = true
}
if s == int(model.SysBind) {
if s.Syscall == int(model.SysBind) {
bindOK = true
}
}
Expand Down
Loading

0 comments on commit 83e319e

Please sign in to comment.