Skip to content

Commit 5f3e23d

Browse files
committed
feat(ql): Allow sequences with multi links
Multi link sequences can define two or more field whose values are extracted and used as a sequence join link.
1 parent 0ad7a08 commit 5f3e23d

4 files changed

Lines changed: 164 additions & 16 deletions

File tree

pkg/filter/ql/literal.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
package ql
2020

2121
import (
22-
"github.com/rabbitstack/fibratus/pkg/event"
23-
"github.com/rabbitstack/fibratus/pkg/filter/fields"
2422
"net"
2523
"reflect"
2624
"strconv"
2725
"strings"
2826
"time"
2927

28+
"github.com/rabbitstack/fibratus/pkg/event"
29+
"github.com/rabbitstack/fibratus/pkg/filter/fields"
30+
3031
"github.com/rabbitstack/fibratus/pkg/filter/ql/functions"
3132
)
3233

@@ -271,11 +272,11 @@ func (f *Function) validate() error {
271272
// SequenceExpr represents a single binary expression within the sequence.
272273
type SequenceExpr struct {
273274
Expr Expr
274-
// By contains the field literal if the sequence expression is constrained.
275-
By *FieldLiteral
275+
// By contains the expression link if the sequence is constrained.
276+
By *SequenceLink
276277
// BoundFields is a group of bound fields referenced in the sequence expression.
277278
BoundFields []*BoundFieldLiteral
278-
// Alias represents the sequence expression alias.
279+
// Alias represents the sequence expression alias when bound fields are used.
279280
Alias string
280281

281282
bitsets event.BitSets
@@ -381,10 +382,31 @@ func (e *SequenceExpr) HasBoundFields() bool {
381382
return len(e.BoundFields) > 0
382383
}
383384

385+
// SequenceLink represents a single or
386+
// a collection of fields that are used to
387+
// build the sequence join link.
388+
type SequenceLink struct {
389+
Fields []*FieldLiteral
390+
}
391+
392+
// IsCompound indicates if the sequence expression
393+
// uses multiple fields for the join link.
394+
func (l *SequenceLink) IsCompound() bool {
395+
return len(l.Fields) > 1
396+
}
397+
398+
// First returns the first field if the link is not compound.
399+
func (l *SequenceLink) First() string {
400+
if len(l.Fields) == 1 {
401+
return l.Fields[0].Value
402+
}
403+
return ""
404+
}
405+
384406
// Sequence is a collection of two or more sequence expressions.
385407
type Sequence struct {
386408
MaxSpan time.Duration
387-
By *FieldLiteral
409+
By *SequenceLink
388410
Expressions []SequenceExpr
389411
IsUnordered bool
390412
}

pkg/filter/ql/parser.go

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ package ql
2323
import (
2424
"errors"
2525
"fmt"
26-
"github.com/rabbitstack/fibratus/pkg/config"
27-
"github.com/rabbitstack/fibratus/pkg/filter/fields"
28-
"github.com/rabbitstack/fibratus/pkg/util/multierror"
2926
"net"
3027
"strconv"
3128
"strings"
3229
"time"
30+
31+
"github.com/rabbitstack/fibratus/pkg/config"
32+
"github.com/rabbitstack/fibratus/pkg/filter/fields"
33+
"github.com/rabbitstack/fibratus/pkg/util/multierror"
3334
)
3435

3536
// Parser builds the binary expression tree from the filter string.
@@ -71,18 +72,41 @@ func (p *Parser) ParseSequence() (*Sequence, error) {
7172
p.unscan()
7273
}
7374

74-
// parse optional global join
75+
// parse optional global link
7576
tok, _, _ = p.scanIgnoreWhitespace()
7677
if tok == By {
7778
tok, pos, lit := p.scanIgnoreWhitespace()
7879
if !fields.IsField(lit) {
7980
return nil, newParseError(tokstr(tok, lit), []string{"field"}, pos, p.expr)
8081
}
8182
var err error
82-
seq.By, err = p.parseField(lit)
83+
field, err := p.parseField(lit)
8384
if err != nil {
8485
return nil, err
8586
}
87+
88+
seqLink := &SequenceLink{Fields: []*FieldLiteral{field}}
89+
90+
// handle multiple join fields separated by comma
91+
for {
92+
if tok, _, _ := p.scanIgnoreWhitespace(); tok != Comma {
93+
p.unscan()
94+
break
95+
}
96+
97+
tok, pos, lit := p.scanIgnoreWhitespace()
98+
if !fields.IsField(lit) {
99+
return nil, newParseError(tokstr(tok, lit), []string{"field"}, pos, p.expr)
100+
}
101+
field, err := p.parseField(lit)
102+
if err != nil {
103+
return nil, err
104+
}
105+
106+
seqLink.Fields = append(seqLink.Fields, field)
107+
}
108+
109+
seq.By = seqLink
86110
} else {
87111
p.unscan()
88112
}
@@ -127,7 +151,7 @@ func (p *Parser) ParseSequence() (*Sequence, error) {
127151

128152
var seqexpr SequenceExpr
129153

130-
// parse sequence BY or AS constraints
154+
// parse sequence BY or AS constraints (links)
131155
tok, _, _ = p.scanIgnoreWhitespace()
132156
switch tok {
133157
case By:
@@ -139,7 +163,28 @@ func (p *Parser) ParseSequence() (*Sequence, error) {
139163
if err != nil {
140164
return nil, err
141165
}
142-
seqexpr = SequenceExpr{Expr: expr, By: field}
166+
167+
seqLink := &SequenceLink{Fields: []*FieldLiteral{field}}
168+
169+
// handle multiple join fields separated by comma
170+
for {
171+
if tok, _, _ := p.scanIgnoreWhitespace(); tok != Comma {
172+
p.unscan()
173+
break
174+
}
175+
176+
tok, pos, lit := p.scanIgnoreWhitespace()
177+
if !fields.IsField(lit) {
178+
return nil, newParseError(tokstr(tok, lit), []string{"field"}, pos, p.expr)
179+
}
180+
field, err := p.parseField(lit)
181+
if err != nil {
182+
return nil, err
183+
}
184+
185+
seqLink.Fields = append(seqLink.Fields, field)
186+
}
187+
seqexpr = SequenceExpr{Expr: expr, By: seqLink}
143188
case As:
144189
tok, pos, lit := p.scanIgnoreWhitespace()
145190
if tok != Ident {

pkg/filter/ql/parser_test.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ package ql
2020

2121
import (
2222
"errors"
23+
"fmt"
24+
"strings"
2325
"testing"
2426
"time"
2527

@@ -271,6 +273,31 @@ func TestParseSequence(t *testing.T) {
271273
time.Duration(0),
272274
true,
273275
},
276+
{
277+
`|evt.name = 'CreateProcess'| by ps.exe, ps.uuid
278+
|evt.name = 'CreateFile'| by file.name, ps.uuid
279+
`,
280+
nil,
281+
time.Duration(0),
282+
true,
283+
},
284+
{
285+
`by ps.exe, ps.uuid
286+
|evt.name = 'CreateProcess'|
287+
|evt.name = 'CreateFile'|
288+
`,
289+
nil,
290+
time.Duration(0),
291+
true,
292+
},
293+
{
294+
`|evt.name = 'CreateProcess'| by ps.exe,
295+
|evt.name = 'CreateFile'| by file.name, ps.uuid
296+
`,
297+
errors.New("expected field"),
298+
time.Duration(0),
299+
true,
300+
},
274301
{
275302

276303
`by ps.pid
@@ -336,7 +363,7 @@ func TestParseSequence(t *testing.T) {
336363
|evt.name = 'CreateProcess'| as e1
337364
|evt.name = 'CreateFile' and $e1.ps.ame = file.name |
338365
`,
339-
errors.New("expected field after bound ref"),
366+
errors.New("expected field/segment after bound ref"),
340367
time.Second * 30,
341368
false,
342369
},
@@ -352,8 +379,8 @@ func TestParseSequence(t *testing.T) {
352379
},
353380
{
354381

355-
`by ps.uuid
356-
maxspan 2m
382+
`maxspan 2m
383+
by ps.uuid
357384
|evt.name = 'CreateProcess'| by ps.uuid
358385
|evt.name = 'CreateFile'| by ps.uuid
359386
`,
@@ -372,6 +399,10 @@ func TestParseSequence(t *testing.T) {
372399
t.Errorf("%d. exp=%s got error=\n%v", i, tt.expr, err)
373400
}
374401

402+
if err != nil && tt.err != nil {
403+
assert.True(t, strings.Contains(err.Error(), tt.err.Error()), fmt.Sprintf("error '%v' should contain '%v'", err, tt.err))
404+
}
405+
375406
if seq != nil {
376407
if seq.MaxSpan != tt.maxSpan {
377408
t.Errorf("%d. exp=%s maxspan=%s got maxspan=%v", i, tt.expr, tt.maxSpan, seq.MaxSpan)

pkg/rules/sequence_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,56 @@ func TestSimpleSequenceDeadline(t *testing.T) {
443443
require.True(t, ss.runSequence(e2))
444444
}
445445

446+
func TestSequenceMultiLinks(t *testing.T) {
447+
log.SetLevel(log.DebugLevel)
448+
449+
c := &config.FilterConfig{Name: "Command shell created a temp file"}
450+
f := filter.New(`
451+
sequence
452+
maxspan 100ms
453+
|evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe, ps.pid
454+
|evt.name = 'CreateFile' and file.path icontains 'temp'| by file.path, ps.pid
455+
`, &config.Config{EventSource: config.EventSourceConfig{EnableFileIOEvents: true}, Filters: &config.Filters{}})
456+
require.NoError(t, f.Compile())
457+
458+
ss := newSequenceState(f, c, new(ps.SnapshotterMock))
459+
460+
e1 := &event.Event{
461+
Type: event.CreateProcess,
462+
Timestamp: time.Now(),
463+
Name: "CreateProcess",
464+
Tid: 2484,
465+
PID: 859,
466+
PS: &pstypes.PS{
467+
Name: "cmd.exe",
468+
Exe: "C:\\Windows\\system32\\svchost-temp.exe",
469+
},
470+
Params: event.Params{
471+
params.ProcessID: {Name: params.ProcessID, Type: params.Uint32, Value: uint32(4143)},
472+
},
473+
Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"},
474+
}
475+
require.False(t, ss.runSequence(e1))
476+
477+
e2 := &event.Event{
478+
Type: event.CreateFile,
479+
Timestamp: time.Now(),
480+
Name: "CreateFile",
481+
Tid: 2484,
482+
PID: 859,
483+
Category: event.File,
484+
PS: &pstypes.PS{
485+
Name: "cmd.exe",
486+
Exe: "C:\\Windows\\system32\\svchost.exe",
487+
},
488+
Params: event.Params{
489+
params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost-temp.exe"},
490+
},
491+
Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"},
492+
}
493+
require.True(t, ss.runSequence(e2))
494+
}
495+
446496
func TestComplexSequence(t *testing.T) {
447497
log.SetLevel(log.DebugLevel)
448498

0 commit comments

Comments
 (0)