Skip to content

Commit

Permalink
[CWS] Add status to kill action report (#30100)
Browse files Browse the repository at this point in the history
  • Loading branch information
YoannGh authored Oct 17, 2024
1 parent bf79a29 commit 33d480e
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 46 deletions.
62 changes: 39 additions & 23 deletions pkg/security/probe/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,48 @@ import (
"github.com/DataDog/datadog-agent/pkg/security/utils"
)

// KillActionStatus defines the status of a kill action
type KillActionStatus string

const (
// KillActionStatusPerformed indicates the kill action was performed
KillActionStatusPerformed KillActionStatus = "performed"
// KillActionStatusRuleDisarmed indicates the kill action was skipped because the rule was disarmed
KillActionStatusRuleDisarmed KillActionStatus = "rule_disarmed"
)

// KillActionReport defines a kill action reports
type KillActionReport struct {
sync.RWMutex

Signal string
Scope string
Pid uint32
CreatedAt time.Time
DetectedAt time.Time
KilledAt time.Time
ExitedAt time.Time
Signal string
Scope string
Status KillActionStatus
CreatedAt time.Time
DetectedAt time.Time
KilledAt time.Time
ExitedAt time.Time
DisarmerType string

// internal
Pid uint32
resolved bool
rule *rules.Rule
}

// JKillActionReport used to serialize date
// easyjson:json
type JKillActionReport struct {
Type string `json:"type"`
Signal string `json:"signal"`
Scope string `json:"scope"`
CreatedAt utils.EasyjsonTime `json:"created_at"`
DetectedAt utils.EasyjsonTime `json:"detected_at"`
KilledAt utils.EasyjsonTime `json:"killed_at"`
ExitedAt *utils.EasyjsonTime `json:"exited_at,omitempty"`
TTR string `json:"ttr,omitempty"`
Type string `json:"type"`
Signal string `json:"signal"`
Scope string `json:"scope"`
Status string `json:"status"`
DisarmerType string `json:"disarmer_type,omitempty"`
CreatedAt utils.EasyjsonTime `json:"created_at"`
DetectedAt utils.EasyjsonTime `json:"detected_at"`
KilledAt *utils.EasyjsonTime `json:"killed_at,omitempty"`
ExitedAt *utils.EasyjsonTime `json:"exited_at,omitempty"`
TTR string `json:"ttr,omitempty"`
}

// IsResolved return if the action is resolved
Expand All @@ -53,7 +67,7 @@ func (k *KillActionReport) IsResolved() bool {
defer k.RUnlock()

// for sigkill wait for exit
return k.Signal != "SIGKILL" || k.resolved
return k.Signal != "SIGKILL" || k.resolved || k.Status == KillActionStatusRuleDisarmed
}

// ToJSON marshal the action
Expand All @@ -62,13 +76,15 @@ func (k *KillActionReport) ToJSON() ([]byte, error) {
defer k.RUnlock()

jk := JKillActionReport{
Type: rules.KillAction,
Signal: k.Signal,
Scope: k.Scope,
CreatedAt: utils.NewEasyjsonTime(k.CreatedAt),
DetectedAt: utils.NewEasyjsonTime(k.DetectedAt),
KilledAt: utils.NewEasyjsonTime(k.KilledAt),
ExitedAt: utils.NewEasyjsonTimeIfNotZero(k.ExitedAt),
Type: rules.KillAction,
Signal: k.Signal,
Scope: k.Scope,
Status: string(k.Status),
DisarmerType: k.DisarmerType,
CreatedAt: utils.NewEasyjsonTime(k.CreatedAt),
DetectedAt: utils.NewEasyjsonTime(k.DetectedAt),
KilledAt: utils.NewEasyjsonTimeIfNotZero(k.KilledAt),
ExitedAt: utils.NewEasyjsonTimeIfNotZero(k.ExitedAt),
}

if !k.ExitedAt.IsZero() {
Expand Down
30 changes: 26 additions & 4 deletions pkg/security/probe/actions_easyjson.go

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

37 changes: 26 additions & 11 deletions pkg/security/probe/process_killer.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ func (p *ProcessKiller) KillAndReport(kill *rules.KillDefinition, rule *rules.Ru
return false
}

scope := "process"
switch kill.Scope {
case "container", "process":
scope = kill.Scope
}

if p.useDisarmers.Load() {
var disarmer *ruleDisarmer
p.ruleDisarmersLock.Lock()
Expand All @@ -184,13 +190,27 @@ func (p *ProcessKiller) KillAndReport(kill *rules.KillDefinition, rule *rules.Ru
}
p.ruleDisarmersLock.Unlock()

onActionBlockedByDisarmer := func(dt disarmerType) {
seclog.Warnf("skipping kill action of rule `%s` because it has been disarmed", rule.ID)
ev.ActionReports = append(ev.ActionReports, &KillActionReport{
Scope: scope,
Signal: kill.Signal,
Status: KillActionStatusRuleDisarmed,
DisarmerType: string(dt),
CreatedAt: ev.ProcessContext.ExecTime,
DetectedAt: ev.ResolveEventTime(),
Pid: ev.ProcessContext.Pid,
rule: rule,
})
}

if disarmer.container.enabled {
if containerID := ev.FieldHandlers.ResolveContainerID(ev, ev.ContainerContext); containerID != "" {
if !disarmer.allow(disarmer.containerCache, containerID, func() {
disarmer.disarmedCount[containerDisarmerType]++
seclog.Warnf("disarming kill action of rule `%s` because more than %d different containers triggered it in the last %s", rule.ID, disarmer.container.capacity, disarmer.container.period)
}) {
seclog.Warnf("skipping kill action of rule `%s` because it has been disarmed", rule.ID)
onActionBlockedByDisarmer(containerDisarmerType)
return false
}
}
Expand All @@ -202,18 +222,12 @@ func (p *ProcessKiller) KillAndReport(kill *rules.KillDefinition, rule *rules.Ru
disarmer.disarmedCount[executableDisarmerType]++
seclog.Warnf("disarmed kill action of rule `%s` because more than %d different executables triggered it in the last %s", rule.ID, disarmer.executable.capacity, disarmer.executable.period)
}) {
seclog.Warnf("skipping kill action of rule `%s` because it has been disarmed", rule.ID)
onActionBlockedByDisarmer(executableDisarmerType)
return false
}
}
}

scope := "process"
switch kill.Scope {
case "container", "process":
scope = kill.Scope
}

pids, paths, err := p.getProcesses(scope, ev, entry)
if err != nil {
log.Errorf("unable to kill: %s", err)
Expand Down Expand Up @@ -255,10 +269,11 @@ func (p *ProcessKiller) KillAndReport(kill *rules.KillDefinition, rule *rules.Ru
report := &KillActionReport{
Scope: scope,
Signal: kill.Signal,
Pid: ev.ProcessContext.Pid,
Status: KillActionStatusPerformed,
CreatedAt: ev.ProcessContext.ExecTime,
DetectedAt: ev.ResolveEventTime(),
KilledAt: killedAt,
Pid: ev.ProcessContext.Pid,
rule: rule,
}
ev.ActionReports = append(ev.ActionReports, report)
Expand Down Expand Up @@ -440,8 +455,8 @@ const (
type disarmerType string

const (
containerDisarmerType = disarmerType("container")
executableDisarmerType = disarmerType("executable")
containerDisarmerType disarmerType = "container"
executableDisarmerType disarmerType = "executable"
)

type ruleDisarmer struct {
Expand Down
29 changes: 22 additions & 7 deletions pkg/security/tests/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ func TestActionKill(t *testing.T) {
if el, err := jsonpath.JsonPathLookup(obj, `$.agent.rule_actions[?(@.signal == 'SIGUSR2')]`); err != nil || el == nil || len(el.([]interface{})) == 0 {
t.Errorf("element not found %s => %v", string(msg.Data), err)
}
if el, err := jsonpath.JsonPathLookup(obj, `$.agent.rule_actions[?(@.status == 'performed')]`); err != nil || el == nil || len(el.([]interface{})) == 0 {
t.Errorf("element not found %s => %v", string(msg.Data), err)
}
})

return nil
Expand Down Expand Up @@ -182,6 +185,9 @@ func TestActionKill(t *testing.T) {
if el, err := jsonpath.JsonPathLookup(obj, `$.agent.rule_actions[?(@.exited_at =~ /20.*/)]`); err != nil || el == nil || len(el.([]interface{})) == 0 {
t.Errorf("element not found %s => %v", string(msg.Data), err)
}
if el, err := jsonpath.JsonPathLookup(obj, `$.agent.rule_actions[?(@.status == 'performed')]`); err != nil || el == nil || len(el.([]interface{})) == 0 {
t.Errorf("element not found %s => %v", string(msg.Data), err)
}
})

return nil
Expand Down Expand Up @@ -331,6 +337,9 @@ func TestActionKillRuleSpecific(t *testing.T) {
if el, err := jsonpath.JsonPathLookup(obj, `$.agent.rule_actions[?(@.exited_at =~ /20.*/)]`); err != nil || el == nil || len(el.([]interface{})) == 0 {
t.Errorf("element not found %s => %v", string(msg.Data), err)
}
if el, err := jsonpath.JsonPathLookup(obj, `$.agent.rule_actions[?(@.status == 'performed')]`); err != nil || el == nil || len(el.([]interface{})) == 0 {
t.Errorf("element not found %s => %v", string(msg.Data), err)
}
})

return nil
Expand Down Expand Up @@ -399,14 +408,17 @@ func testActionKillDisarm(t *testing.T, test *testModule, sleep, syscallTester s
if el, err := jsonpath.JsonPathLookup(obj, `$.agent.rule_actions[?(@.exited_at =~ /20.*/)]`); err != nil || el == nil || len(el.([]interface{})) == 0 {
t.Errorf("element not found %s => %v", string(msg.Data), err)
}
if el, err := jsonpath.JsonPathLookup(obj, `$.agent.rule_actions[?(@.status == 'performed')]`); err != nil || el == nil || len(el.([]interface{})) == 0 {
t.Errorf("element not found %s => %v", string(msg.Data), err)
}
})

return nil
}, retry.Delay(200*time.Millisecond), retry.Attempts(30), retry.DelayType(retry.FixedDelay))
assert.NoError(t, err)
}

testKillActionIgnored := func(t *testing.T, ruleID string, cmdFunc func(context.Context)) {
testKillActionDisarmed := func(t *testing.T, ruleID string, cmdFunc func(context.Context)) {
test.msgSender.flush()
err := test.GetEventSent(t, func() error {
cmdFunc(nil)
Expand All @@ -426,8 +438,11 @@ func testActionKillDisarm(t *testing.T, test *testModule, sleep, syscallTester s
validateMessageSchema(t, string(msg.Data))

jsonPathValidation(test, msg.Data, func(_ *testModule, obj interface{}) {
if _, err := jsonpath.JsonPathLookup(obj, `$.agent.rule_actions`); err == nil {
t.Errorf("unexpected rule action %s", string(msg.Data))
if el, err := jsonpath.JsonPathLookup(obj, `$.agent.rule_actions[?(@.signal == 'SIGKILL')]`); err != nil || el == nil || len(el.([]interface{})) == 0 {
t.Errorf("element not found %s => %v", string(msg.Data), err)
}
if el, err := jsonpath.JsonPathLookup(obj, `$.agent.rule_actions[?(@.status == 'rule_disarmed')]`); err != nil || el == nil || len(el.([]interface{})) == 0 {
t.Errorf("element not found %s => %v", string(msg.Data), err)
}
})

Expand All @@ -447,8 +462,8 @@ func testActionKillDisarm(t *testing.T, test *testModule, sleep, syscallTester s
})
}

// test that another executable dismars the kill action
testKillActionIgnored(t, "kill_action_disarm_executable", func(_ context.Context) {
// test that another executable disarms the kill action
testKillActionDisarmed(t, "kill_action_disarm_executable", func(_ context.Context) {
cmd := exec.Command(sleep, "1")
cmd.Env = []string{"TARGETTOKILL=1"}
_ = cmd.Run()
Expand Down Expand Up @@ -486,8 +501,8 @@ func testActionKillDisarm(t *testing.T, test *testModule, sleep, syscallTester s
}
defer newDockerInstance.stop()

// test that another container dismars the kill action
testKillActionIgnored(t, "kill_action_disarm_container", func(_ context.Context) {
// test that another container disarms the kill action
testKillActionDisarmed(t, "kill_action_disarm_container", func(_ context.Context) {
cmd := newDockerInstance.Command("env", []string{"-i", "-", "TARGETTOKILL=1", "sleep", "1"}, []string{})
_ = cmd.Run()
})
Expand Down
28 changes: 27 additions & 1 deletion pkg/security/tests/schemas/kill.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,30 +33,56 @@
"properties": {
"signal": {
"const": "SIGKILL"
},
"status": {
"const": "performed"
}
},
"required": [
"type",
"signal",
"scope",
"status",
"created_at",
"detected_at",
"killed_at",
"exited_at"
"exited_at",
"ttr"
]
},
{
"properties": {
"signal": {
"const": "SIGUSR2"
},
"status": {
"const": "performed"
}
},
"required": [
"type",
"signal",
"scope",
"status",
"created_at",
"detected_at",
"killed_at"
]
},
{
"properties": {
"status": {
"const": "rule_disarmed"
}
},
"required": [
"type",
"signal",
"scope",
"status",
"created_at",
"detected_at"
]
}
]
}

0 comments on commit 33d480e

Please sign in to comment.