From deff52ecae080311f014c68c1058eb239d5509c1 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Thu, 12 Feb 2026 20:29:18 +0100 Subject: [PATCH] fix(rules_engine): Collect matches for unconstrained sequences If the sequence is unconstrained and the partials don't have sequence links, we can collect the first matching event of each sequence slot. --- pkg/rules/sequence.go | 11 +++-- pkg/rules/sequence_test.go | 87 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/pkg/rules/sequence.go b/pkg/rules/sequence.go index 6ea41d386..6346e6dc8 100644 --- a/pkg/rules/sequence.go +++ b/pkg/rules/sequence.go @@ -505,8 +505,9 @@ func (s *sequenceState) runSequence(e *event.Event) bool { // if both the terminal state is reached and the partials // in the sequence state could be joined by the specified - // field(s), the rule has matched successfully, and we can - // collect all events involved in the rule match + // field(s) or the sequence is unconstrained, the rule has + // matched successfully, and we can collect all events involved + // in the rule match isTerminal := s.isTerminalState() if isTerminal { setMatch := func(seqID int, e *event.Event) { @@ -521,7 +522,11 @@ func (s *sequenceState) runSequence(e *event.Event) bool { for seqID := 0; seqID < len(s.partials); seqID++ { for _, outer := range s.partials[seqID] { for _, inner := range s.partials[seqID+1] { - if filter.CompareSeqLinks(outer.SequenceLinks(), inner.SequenceLinks()) { + switch { + case filter.CompareSeqLinks(outer.SequenceLinks(), inner.SequenceLinks()): + setMatch(seqID, outer) + setMatch(seqID+1, inner) + case !s.seq.IsConstrained() && !outer.ContainsMeta(event.RuleSequenceLinks) && !inner.ContainsMeta(event.RuleSequenceLinks): setMatch(seqID, outer) setMatch(seqID+1, inner) } diff --git a/pkg/rules/sequence_test.go b/pkg/rules/sequence_test.go index cbb0602cc..4517f953e 100644 --- a/pkg/rules/sequence_test.go +++ b/pkg/rules/sequence_test.go @@ -373,6 +373,93 @@ func TestSimpleSequenceMultiplePartials(t *testing.T) { assert.Equal(t, "C:\\Temp\\file.tmp", ss.matches[1].GetParamAsString(params.FilePath)) } +func TestUnconstrainedSequenceMatches(t *testing.T) { + log.SetLevel(log.DebugLevel) + + c := &config.FilterConfig{Name: "Command shell created a temp file"} + f := filter.New(` + sequence + maxspan 200ms + |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| + |evt.name = 'CreateFile' and file.path icontains 'temp'| + `, &config.Config{EventSource: config.EventSourceConfig{EnableFileIOEvents: true}, Filters: &config.Filters{}}) + require.NoError(t, f.Compile()) + + ss := newSequenceState(f, c, new(ps.SnapshotterMock)) + + e1 := &event.Event{ + Seq: 20, + Type: event.CreateProcess, + Timestamp: time.Now().Add(time.Second), + Name: "CreateProcess", + Tid: 2484, + PID: 859, + PS: &pstypes.PS{ + Name: "cmd.exe", + Exe: "C:\\Windows\\System32\\cmd.exe", + PID: 859, + Parent: &pstypes.PS{ + Name: "WmiPrvSE.exe", + }, + }, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(859)}, + }, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + } + e2 := &event.Event{ + Seq: 21, + Type: event.CreateProcess, + Timestamp: time.Now().Add(time.Second), + Name: "CreateProcess", + Tid: 2484, + PID: 1859, + PS: &pstypes.PS{ + Name: "cmd.exe", + Exe: "C:\\Windows\\System32\\cmd.exe", + PID: 1859, + Parent: &pstypes.PS{ + Name: "svchost.exe", + }, + }, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(859)}, + }, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + } + e3 := &event.Event{ + Type: event.CreateFile, + Seq: 25, + Timestamp: time.Now().Add(time.Second * time.Duration(2)), + Name: "CreateFile", + Tid: 2484, + PID: 3859, + Category: event.File, + PS: &pstypes.PS{ + Name: "cmd.exe", + Exe: "C:\\Windows\\system32\\cmd.exe", + PID: 3859, + }, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Temp\\file.tmp"}, + }, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + } + + require.False(t, ss.runSequence(e1)) + require.False(t, ss.runSequence(e2)) + assert.Len(t, ss.partials[0], 2) + assert.Len(t, ss.partials[1], 0) + require.True(t, ss.runSequence(e3)) + assert.Len(t, ss.partials[1], 1) + + require.Len(t, ss.matches, 2) + assert.Equal(t, uint32(859), ss.matches[0].PID) + assert.Equal(t, "WmiPrvSE.exe", ss.matches[0].PS.Parent.Name) + assert.Equal(t, uint32(3859), ss.matches[1].PID) + assert.Equal(t, "C:\\Temp\\file.tmp", ss.matches[1].GetParamAsString(params.FilePath)) +} + func TestSimpleSequenceDeadline(t *testing.T) { log.SetLevel(log.DebugLevel)