Skip to content

Commit a034a33

Browse files
committed
fix(rule_engine): Enforce temporal monotonicity
Enforces temporal monotonicity by comparing the timestamp of the last matched partial in the previous slot.
1 parent 4894a7f commit a034a33

File tree

3 files changed

+102
-81
lines changed

3 files changed

+102
-81
lines changed

pkg/rules/engine_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ func TestRunSequenceRuleWithPsUUIDLink(t *testing.T) {
299299
e2 := &event.Event{
300300
Seq: 2,
301301
Type: event.CreateFile,
302-
Timestamp: time.Now(),
302+
Timestamp: time.Now().Add(time.Second),
303303
Name: "CreateFile",
304304
Tid: 2484,
305305
PID: uint32(os.Getpid()),

pkg/rules/sequence.go

Lines changed: 53 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ type sequenceState struct {
116116
// smu guards the states map
117117
smu sync.RWMutex
118118

119+
// lastMatch is the timestamp of the last matched event.
120+
// The purpose is to enforce temporal monotonicity
121+
lastMatch time.Time
122+
119123
psnap ps.Snapshotter
120124
}
121125

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

375380
func (s *sequenceState) clearLocked() {
@@ -391,15 +396,17 @@ func (s *sequenceState) next(seqID int) bool {
391396
if seqID == 0 {
392397
return true
393398
}
399+
394400
var next bool
395401
s.smu.RLock()
396402
defer s.smu.RUnlock()
397-
for n := 0; n < seqID; n++ {
403+
for n := range seqID {
398404
next = s.states[n]
399405
if !next {
400406
break
401407
}
402408
}
409+
403410
return next && !s.inDeadline.Load() && !s.inExpired.Load()
404411
}
405412

@@ -454,53 +461,59 @@ func (s *sequenceState) runSequence(e *event.Event) bool {
454461
continue
455462
}
456463

457-
// prevent running the filter if the expression
458-
// can't be matched against the current event
459-
if !expr.IsEvaluable(e) {
464+
s.mu.RLock()
465+
matches := expr.IsEvaluable(e) && s.filter.RunSequence(e, i, s.partials, false)
466+
s.mu.RUnlock()
467+
468+
if !matches {
460469
continue
461470
}
462471

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

467478
// append the partial and transition state machine
468-
if matches {
469-
s.addPartial(i, e, false)
470-
err := s.matchTransition(i, e)
471-
if err != nil {
472-
matchTransitionErrors.Add(1)
473-
log.Warnf("match transition failure: %v", err)
474-
}
475-
// now try to match all pending out-of-order
476-
// events from downstream sequence slots if
477-
// the previous match hasn't reached terminal
478-
// state
479-
if s.seq.IsUnordered && s.currentState() != sequenceTerminalState {
480-
s.mu.RLock()
481-
for seqID := range s.partials {
482-
for _, evt := range s.partials[seqID] {
483-
if !evt.ContainsMeta(event.RuleSequenceOOOKey) {
484-
continue
485-
}
486-
// try to initialize process state before evaluating the event
487-
if evt.PS == nil {
488-
_, evt.PS = s.psnap.Find(evt.PID)
489-
}
490-
matches = s.filter.RunSequence(evt, seqID, s.partials, false)
491-
// transition the state machine
492-
if matches {
493-
err := s.matchTransition(seqID, evt)
494-
if err != nil {
495-
matchTransitionErrors.Add(1)
496-
log.Warnf("out of order match transition failure: %v", err)
497-
}
498-
evt.RemoveMeta(event.RuleSequenceOOOKey)
499-
}
479+
s.addPartial(i, e, false)
480+
err := s.matchTransition(i, e)
481+
if err != nil {
482+
matchTransitionErrors.Add(1)
483+
log.Warnf("match transition failure: %v", err)
484+
}
485+
if !s.seq.IsUnordered {
486+
s.lastMatch = e.Timestamp
487+
}
488+
// now try to match all pending out-of-order
489+
// events from downstream sequence slots if
490+
// the previous match hasn't reached terminal
491+
// state
492+
if s.seq.IsUnordered && s.currentState() != sequenceTerminalState {
493+
s.mu.RLock()
494+
for seqID := range s.partials {
495+
for _, evt := range s.partials[seqID] {
496+
if !evt.ContainsMeta(event.RuleSequenceOOOKey) {
497+
continue
498+
}
499+
// try to initialize process state before evaluating the event
500+
if evt.PS == nil {
501+
_, evt.PS = s.psnap.Find(evt.PID)
502+
}
503+
matches = s.filter.RunSequence(evt, seqID, s.partials, false)
504+
if !matches {
505+
continue
500506
}
507+
// transition the state machine
508+
err := s.matchTransition(seqID, evt)
509+
if err != nil {
510+
matchTransitionErrors.Add(1)
511+
log.Warnf("out of order match transition failure: %v", err)
512+
}
513+
evt.RemoveMeta(event.RuleSequenceOOOKey)
501514
}
502-
s.mu.RUnlock()
503515
}
516+
s.mu.RUnlock()
504517
}
505518

506519
// if both the terminal state is reached and the partials

pkg/rules/sequence_test.go

Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,11 @@ func TestSequenceState(t *testing.T) {
5858
assert.Equal(t, "evt.name = CreateProcess AND ps.name = cmd.exe", ss.expr(ss.initialState))
5959

6060
e1 := &event.Event{
61-
Type: event.CreateProcess,
62-
Name: "CreateProcess",
63-
Tid: 2484,
64-
PID: 859,
61+
Type: event.CreateProcess,
62+
Name: "CreateProcess",
63+
Tid: 2484,
64+
PID: 859,
65+
Timestamp: time.Now(),
6566
PS: &pstypes.PS{
6667
Name: "cmd.exe",
6768
Exe: "C:\\Windows\\system32\\svchost.exe",
@@ -71,6 +72,22 @@ func TestSequenceState(t *testing.T) {
7172
params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "powershell.exe"},
7273
},
7374
}
75+
76+
e2 := &event.Event{
77+
Type: event.CreateFile,
78+
Name: "CreateFile",
79+
Tid: 2484,
80+
PID: 4143,
81+
Timestamp: time.Now().Add(time.Second * 5),
82+
PS: &pstypes.PS{
83+
Name: "cmd.exe",
84+
Exe: "C:\\Windows\\system32\\svchost.exe",
85+
},
86+
Params: event.Params{
87+
params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Temp\\dropper"},
88+
},
89+
}
90+
7491
require.True(t, ss.next(0))
7592
require.False(t, ss.next(1))
7693
require.NoError(t, ss.matchTransition(0, e1))
@@ -82,19 +99,17 @@ func TestSequenceState(t *testing.T) {
8299
assert.False(t, ss.isInitialState())
83100
assert.Equal(t, "evt.name = CreateFile AND file.path ICONTAINS temp", ss.expr(ss.currentState()))
84101

85-
e2 := &event.Event{
86-
Type: event.CreateFile,
87-
Name: "CreateFile",
88-
Tid: 2484,
89-
PID: 4143,
90-
PS: &pstypes.PS{
91-
Name: "cmd.exe",
92-
Exe: "C:\\Windows\\system32\\svchost.exe",
93-
},
102+
e3 := &event.Event{
103+
Type: event.CreateProcess,
104+
Name: "CreateProcess",
105+
Timestamp: time.Now().Add(time.Second * 10),
106+
Tid: 2484,
107+
PID: 4143,
94108
Params: event.Params{
95-
params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Temp\\dropper"},
109+
params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: "C:\\Temp\\dropper.exe"},
96110
},
97111
}
112+
98113
// can't go to the next transitions as the expr hasn't matched
99114
require.False(t, ss.next(2))
100115
require.NoError(t, ss.matchTransition(1, e2))
@@ -108,15 +123,6 @@ func TestSequenceState(t *testing.T) {
108123
assert.Equal(t, 2, ss.currentState())
109124
assert.Equal(t, "evt.name = CreateProcess", ss.expr(ss.currentState()))
110125

111-
e3 := &event.Event{
112-
Type: event.CreateProcess,
113-
Name: "CreateProcess",
114-
Tid: 2484,
115-
PID: 4143,
116-
Params: event.Params{
117-
params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: "C:\\Temp\\dropper.exe"},
118-
},
119-
}
120126
require.NoError(t, ss.matchTransition(2, e3))
121127
ss.addPartial(2, e3, false)
122128

@@ -214,7 +220,7 @@ func TestSimpleSequence(t *testing.T) {
214220
}, {
215221
Type: event.CreateFile,
216222
Name: "CreateFile",
217-
Timestamp: time.Now(),
223+
Timestamp: time.Now().Add(time.Second),
218224
Tid: 2484,
219225
PID: 859,
220226
Category: event.File,
@@ -242,7 +248,7 @@ func TestSimpleSequence(t *testing.T) {
242248
}, {
243249
Type: event.CreateFile,
244250
Name: "CreateFile",
245-
Timestamp: time.Now(),
251+
Timestamp: time.Now().Add(time.Second),
246252
Tid: 2484,
247253
PID: 859,
248254
Category: event.File,
@@ -410,7 +416,7 @@ func TestUnconstrainedSequenceMatches(t *testing.T) {
410416
e2 := &event.Event{
411417
Seq: 21,
412418
Type: event.CreateProcess,
413-
Timestamp: time.Now().Add(time.Second),
419+
Timestamp: time.Now().Add(time.Second * 2),
414420
Name: "CreateProcess",
415421
Tid: 2484,
416422
PID: 1859,
@@ -430,7 +436,7 @@ func TestUnconstrainedSequenceMatches(t *testing.T) {
430436
e3 := &event.Event{
431437
Type: event.CreateFile,
432438
Seq: 25,
433-
Timestamp: time.Now().Add(time.Second * time.Duration(2)),
439+
Timestamp: time.Now().Add(time.Second * 3),
434440
Name: "CreateFile",
435441
Tid: 2484,
436442
PID: 3859,
@@ -493,7 +499,7 @@ func TestSimpleSequenceDeadline(t *testing.T) {
493499

494500
e2 := &event.Event{
495501
Type: event.CreateFile,
496-
Timestamp: time.Now(),
502+
Timestamp: time.Now().Add(time.Millisecond * 200),
497503
Name: "CreateFile",
498504
Tid: 2484,
499505
PID: 859,
@@ -563,7 +569,7 @@ func TestSequenceMultiLinks(t *testing.T) {
563569

564570
e2 := &event.Event{
565571
Type: event.CreateFile,
566-
Timestamp: time.Now(),
572+
Timestamp: time.Now().Add(time.Second),
567573
Name: "CreateFile",
568574
Tid: 2484,
569575
PID: 859,
@@ -856,7 +862,7 @@ func TestSequenceExpire(t *testing.T) {
856862
{
857863
Seq: 2,
858864
Type: event.CreateProcess,
859-
Timestamp: time.Now(),
865+
Timestamp: time.Now().Add(time.Second),
860866
Category: event.Process,
861867
Name: "CreateProcess",
862868
Tid: 2484,
@@ -1029,11 +1035,12 @@ func TestSequenceBoundFieldsWithFunctions(t *testing.T) {
10291035
ss := newSequenceState(f, c, new(ps.SnapshotterMock))
10301036

10311037
e1 := &event.Event{
1032-
Type: event.CreateFile,
1033-
Name: "CreateFile",
1034-
Category: event.File,
1035-
Tid: 2484,
1036-
PID: 859,
1038+
Type: event.CreateFile,
1039+
Name: "CreateFile",
1040+
Category: event.File,
1041+
Timestamp: time.Now(),
1042+
Tid: 2484,
1043+
PID: 859,
10371044
PS: &pstypes.PS{
10381045
Name: "cmd.exe",
10391046
Exe: "C:\\Windows\\system32\\cmd.exe",
@@ -1045,11 +1052,12 @@ func TestSequenceBoundFieldsWithFunctions(t *testing.T) {
10451052
}
10461053

10471054
e2 := &event.Event{
1048-
Type: event.RegSetValue,
1049-
Name: "RegSetValue",
1050-
Category: event.Registry,
1051-
Tid: 2484,
1052-
PID: 859,
1055+
Type: event.RegSetValue,
1056+
Name: "RegSetValue",
1057+
Category: event.Registry,
1058+
Timestamp: time.Now().Add(time.Millisecond * 5),
1059+
Tid: 2484,
1060+
PID: 859,
10531061
PS: &pstypes.PS{
10541062
Name: "cmd.exe",
10551063
Exe: "C:\\Windows\\system32\\cmd.exe",

0 commit comments

Comments
 (0)