Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/rules/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ func TestRunSequenceRuleWithPsUUIDLink(t *testing.T) {
e2 := &event.Event{
Seq: 2,
Type: event.CreateFile,
Timestamp: time.Now(),
Timestamp: time.Now().Add(time.Second),
Name: "CreateFile",
Tid: 2484,
PID: uint32(os.Getpid()),
Expand Down
93 changes: 53 additions & 40 deletions pkg/rules/sequence.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ type sequenceState struct {
// smu guards the states map
smu sync.RWMutex

// lastMatch is the timestamp of the last matched event.
// The purpose is to enforce temporal monotonicity
lastMatch time.Time

psnap ps.Snapshotter
}

Expand Down Expand Up @@ -370,6 +374,7 @@ func (s *sequenceState) clear() {
s.spanDeadlines = make(map[fsm.State]*time.Timer)
s.isPartialsBreached.Store(false)
partialsPerSequence.Delete(s.name)
s.lastMatch = time.Time{}
}

func (s *sequenceState) clearLocked() {
Expand All @@ -391,15 +396,17 @@ func (s *sequenceState) next(seqID int) bool {
if seqID == 0 {
return true
}

var next bool
s.smu.RLock()
defer s.smu.RUnlock()
for n := 0; n < seqID; n++ {
for n := range seqID {
next = s.states[n]
if !next {
break
}
}

return next && !s.inDeadline.Load() && !s.inExpired.Load()
}

Expand Down Expand Up @@ -454,53 +461,59 @@ func (s *sequenceState) runSequence(e *event.Event) bool {
continue
}

// prevent running the filter if the expression
// can't be matched against the current event
if !expr.IsEvaluable(e) {
s.mu.RLock()
matches := expr.IsEvaluable(e) && s.filter.RunSequence(e, i, s.partials, false)
s.mu.RUnlock()

if !matches {
continue
}

s.mu.RLock()
matches := s.filter.RunSequence(e, i, s.partials, false)
s.mu.RUnlock()
// enforce temporal monotonicity check for ordered sequences
if !s.seq.IsUnordered && !s.lastMatch.IsZero() && !e.Timestamp.After(s.lastMatch) {
// this event is older than or equal to the previous matched slot
continue
}

// append the partial and transition state machine
if matches {
s.addPartial(i, e, false)
err := s.matchTransition(i, e)
if err != nil {
matchTransitionErrors.Add(1)
log.Warnf("match transition failure: %v", err)
}
// now try to match all pending out-of-order
// events from downstream sequence slots if
// the previous match hasn't reached terminal
// state
if s.seq.IsUnordered && s.currentState() != sequenceTerminalState {
s.mu.RLock()
for seqID := range s.partials {
for _, evt := range s.partials[seqID] {
if !evt.ContainsMeta(event.RuleSequenceOOOKey) {
continue
}
// try to initialize process state before evaluating the event
if evt.PS == nil {
_, evt.PS = s.psnap.Find(evt.PID)
}
matches = s.filter.RunSequence(evt, seqID, s.partials, false)
// transition the state machine
if matches {
err := s.matchTransition(seqID, evt)
if err != nil {
matchTransitionErrors.Add(1)
log.Warnf("out of order match transition failure: %v", err)
}
evt.RemoveMeta(event.RuleSequenceOOOKey)
}
s.addPartial(i, e, false)
err := s.matchTransition(i, e)
if err != nil {
matchTransitionErrors.Add(1)
log.Warnf("match transition failure: %v", err)
}
if !s.seq.IsUnordered {
s.lastMatch = e.Timestamp
}
// now try to match all pending out-of-order
// events from downstream sequence slots if
// the previous match hasn't reached terminal
// state
if s.seq.IsUnordered && s.currentState() != sequenceTerminalState {
s.mu.RLock()
for seqID := range s.partials {
for _, evt := range s.partials[seqID] {
if !evt.ContainsMeta(event.RuleSequenceOOOKey) {
continue
}
// try to initialize process state before evaluating the event
if evt.PS == nil {
_, evt.PS = s.psnap.Find(evt.PID)
}
matches = s.filter.RunSequence(evt, seqID, s.partials, false)
if !matches {
continue
}
// transition the state machine
err := s.matchTransition(seqID, evt)
if err != nil {
matchTransitionErrors.Add(1)
log.Warnf("out of order match transition failure: %v", err)
}
evt.RemoveMeta(event.RuleSequenceOOOKey)
}
s.mu.RUnlock()
}
s.mu.RUnlock()
}

// if both the terminal state is reached and the partials
Expand Down
88 changes: 48 additions & 40 deletions pkg/rules/sequence_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ func TestSequenceState(t *testing.T) {
assert.Equal(t, "evt.name = CreateProcess AND ps.name = cmd.exe", ss.expr(ss.initialState))

e1 := &event.Event{
Type: event.CreateProcess,
Name: "CreateProcess",
Tid: 2484,
PID: 859,
Type: event.CreateProcess,
Name: "CreateProcess",
Tid: 2484,
PID: 859,
Timestamp: time.Now(),
PS: &pstypes.PS{
Name: "cmd.exe",
Exe: "C:\\Windows\\system32\\svchost.exe",
Expand All @@ -71,6 +72,22 @@ func TestSequenceState(t *testing.T) {
params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "powershell.exe"},
},
}

e2 := &event.Event{
Type: event.CreateFile,
Name: "CreateFile",
Tid: 2484,
PID: 4143,
Timestamp: time.Now().Add(time.Second * 5),
PS: &pstypes.PS{
Name: "cmd.exe",
Exe: "C:\\Windows\\system32\\svchost.exe",
},
Params: event.Params{
params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Temp\\dropper"},
},
}

require.True(t, ss.next(0))
require.False(t, ss.next(1))
require.NoError(t, ss.matchTransition(0, e1))
Expand All @@ -82,19 +99,17 @@ func TestSequenceState(t *testing.T) {
assert.False(t, ss.isInitialState())
assert.Equal(t, "evt.name = CreateFile AND file.path ICONTAINS temp", ss.expr(ss.currentState()))

e2 := &event.Event{
Type: event.CreateFile,
Name: "CreateFile",
Tid: 2484,
PID: 4143,
PS: &pstypes.PS{
Name: "cmd.exe",
Exe: "C:\\Windows\\system32\\svchost.exe",
},
e3 := &event.Event{
Type: event.CreateProcess,
Name: "CreateProcess",
Timestamp: time.Now().Add(time.Second * 10),
Tid: 2484,
PID: 4143,
Params: event.Params{
params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Temp\\dropper"},
params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: "C:\\Temp\\dropper.exe"},
},
}

// can't go to the next transitions as the expr hasn't matched
require.False(t, ss.next(2))
require.NoError(t, ss.matchTransition(1, e2))
Expand All @@ -108,15 +123,6 @@ func TestSequenceState(t *testing.T) {
assert.Equal(t, 2, ss.currentState())
assert.Equal(t, "evt.name = CreateProcess", ss.expr(ss.currentState()))

e3 := &event.Event{
Type: event.CreateProcess,
Name: "CreateProcess",
Tid: 2484,
PID: 4143,
Params: event.Params{
params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: "C:\\Temp\\dropper.exe"},
},
}
require.NoError(t, ss.matchTransition(2, e3))
ss.addPartial(2, e3, false)

Expand Down Expand Up @@ -214,7 +220,7 @@ func TestSimpleSequence(t *testing.T) {
}, {
Type: event.CreateFile,
Name: "CreateFile",
Timestamp: time.Now(),
Timestamp: time.Now().Add(time.Second),
Tid: 2484,
PID: 859,
Category: event.File,
Expand Down Expand Up @@ -242,7 +248,7 @@ func TestSimpleSequence(t *testing.T) {
}, {
Type: event.CreateFile,
Name: "CreateFile",
Timestamp: time.Now(),
Timestamp: time.Now().Add(time.Second),
Tid: 2484,
PID: 859,
Category: event.File,
Expand Down Expand Up @@ -410,7 +416,7 @@ func TestUnconstrainedSequenceMatches(t *testing.T) {
e2 := &event.Event{
Seq: 21,
Type: event.CreateProcess,
Timestamp: time.Now().Add(time.Second),
Timestamp: time.Now().Add(time.Second * 2),
Name: "CreateProcess",
Tid: 2484,
PID: 1859,
Expand All @@ -430,7 +436,7 @@ func TestUnconstrainedSequenceMatches(t *testing.T) {
e3 := &event.Event{
Type: event.CreateFile,
Seq: 25,
Timestamp: time.Now().Add(time.Second * time.Duration(2)),
Timestamp: time.Now().Add(time.Second * 3),
Name: "CreateFile",
Tid: 2484,
PID: 3859,
Expand Down Expand Up @@ -493,7 +499,7 @@ func TestSimpleSequenceDeadline(t *testing.T) {

e2 := &event.Event{
Type: event.CreateFile,
Timestamp: time.Now(),
Timestamp: time.Now().Add(time.Millisecond * 200),
Name: "CreateFile",
Tid: 2484,
PID: 859,
Expand Down Expand Up @@ -563,7 +569,7 @@ func TestSequenceMultiLinks(t *testing.T) {

e2 := &event.Event{
Type: event.CreateFile,
Timestamp: time.Now(),
Timestamp: time.Now().Add(time.Second),
Name: "CreateFile",
Tid: 2484,
PID: 859,
Expand Down Expand Up @@ -856,7 +862,7 @@ func TestSequenceExpire(t *testing.T) {
{
Seq: 2,
Type: event.CreateProcess,
Timestamp: time.Now(),
Timestamp: time.Now().Add(time.Second),
Category: event.Process,
Name: "CreateProcess",
Tid: 2484,
Expand Down Expand Up @@ -1029,11 +1035,12 @@ func TestSequenceBoundFieldsWithFunctions(t *testing.T) {
ss := newSequenceState(f, c, new(ps.SnapshotterMock))

e1 := &event.Event{
Type: event.CreateFile,
Name: "CreateFile",
Category: event.File,
Tid: 2484,
PID: 859,
Type: event.CreateFile,
Name: "CreateFile",
Category: event.File,
Timestamp: time.Now(),
Tid: 2484,
PID: 859,
PS: &pstypes.PS{
Name: "cmd.exe",
Exe: "C:\\Windows\\system32\\cmd.exe",
Expand All @@ -1045,11 +1052,12 @@ func TestSequenceBoundFieldsWithFunctions(t *testing.T) {
}

e2 := &event.Event{
Type: event.RegSetValue,
Name: "RegSetValue",
Category: event.Registry,
Tid: 2484,
PID: 859,
Type: event.RegSetValue,
Name: "RegSetValue",
Category: event.Registry,
Timestamp: time.Now().Add(time.Millisecond * 5),
Tid: 2484,
PID: 859,
PS: &pstypes.PS{
Name: "cmd.exe",
Exe: "C:\\Windows\\system32\\cmd.exe",
Expand Down
Loading