Skip to content

Commit b0dabe0

Browse files
committed
feat(rule-engine): Match all strategy
The match all rule engine strategy permits a single event to trigger multiple rules.
1 parent fc38e3e commit b0dabe0

File tree

7 files changed

+70
-45
lines changed

7 files changed

+70
-45
lines changed

configs/fibratus.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ filament:
131131
# For local file system rule paths, it is possible to use the glob expression to load the
132132
# rules from different directory locations.
133133
filters:
134+
# Indicates if the rule engine match all strategy is enabled. When the match all strategy
135+
# is enabled, a single event can trigger multiple rules.
136+
match-all: true
137+
134138
rules:
135139
# Indicates if the rule engine is enabled and rules loaded
136140
enabled: true

pkg/config/config_windows.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ func (c *Config) addFlags() {
380380
c.flags.StringSlice(rulesFromPaths, []string{filepath.Join(dir, "*")}, "Comma-separated list of rules files")
381381
c.flags.StringSlice(macrosFromPaths, []string{filepath.Join(dir, "Macros", "*")}, "Comma-separated list of macro files")
382382
c.flags.StringSlice(rulesFromURLs, []string{}, "Comma-separated list of rules URL resources")
383+
c.flags.Bool(matchAll, true, "Indicates if the match all strategy is enabled for the rule engine. If the match all strategy is enabled, a single event can trigger multiple rules")
383384
}
384385
if c.opts.capture {
385386
c.flags.StringP(kcapFile, "o", "", "The path of the output kcap file")

pkg/config/filters.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,13 @@ func (f FilterConfig) HasLabel(l string) bool { return f.Labels[l] != "" }
102102

103103
// Filters contains references to rule and macro definitions.
104104
type Filters struct {
105-
Rules Rules `json:"rules" yaml:"rules"`
106-
Macros Macros `json:"macros" yaml:"macros"`
107-
macros map[string]*Macro
108-
filters []*FilterConfig
105+
Rules Rules `json:"rules" yaml:"rules"`
106+
Macros Macros `json:"macros" yaml:"macros"`
107+
// MatchAll indicates if the match all strategy is enabled for the rule engine.
108+
// If the match all strategy is enabled, a single event can trigger multiple rules.
109+
MatchAll bool `json:"match-all" yaml:"match-all"`
110+
macros map[string]*Macro
111+
filters []*FilterConfig
109112
}
110113

111114
// FiltersWithMacros builds the filter config with the map of
@@ -241,13 +244,15 @@ const (
241244
rulesFromPaths = "filters.rules.from-paths"
242245
rulesFromURLs = "filters.rules.from-urls"
243246
macrosFromPaths = "filters.macros.from-paths"
247+
matchAll = "filters.match-all"
244248
)
245249

246250
func (f *Filters) initFromViper(v *viper.Viper) {
247251
f.Rules.Enabled = v.GetBool(rulesEnabled)
248252
f.Rules.FromPaths = v.GetStringSlice(rulesFromPaths)
249253
f.Rules.FromURLs = v.GetStringSlice(rulesFromURLs)
250254
f.Macros.FromPaths = v.GetStringSlice(macrosFromPaths)
255+
f.MatchAll = v.GetBool(matchAll)
251256
}
252257

253258
func (f Filters) HasMacros() bool { return len(f.macros) > 0 }

pkg/config/filters_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func TestLoadRulesFromPaths(t *testing.T) {
3737
},
3838
},
3939
Macros{FromPaths: nil},
40+
false,
4041
map[string]*Macro{},
4142
[]*FilterConfig{},
4243
}
@@ -78,6 +79,7 @@ func TestLoadRulesFromPathsWithTemplate(t *testing.T) {
7879
},
7980
},
8081
Macros{FromPaths: nil},
82+
false,
8183
map[string]*Macro{},
8284
[]*FilterConfig{},
8385
}
@@ -116,6 +118,7 @@ func TestLoadGroupsFromURLs(t *testing.T) {
116118
},
117119
},
118120
Macros{FromPaths: nil},
121+
false,
119122
map[string]*Macro{},
120123
[]*FilterConfig{},
121124
}

pkg/config/schema_windows.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ var schema = `
146146
"filters": {
147147
"type": "object",
148148
"properties": {
149+
"match-all": {"type": "boolean"},
149150
"rules": {
150151
"type": "object",
151152
"properties": {
@@ -510,7 +511,7 @@ var rulesSchema = `
510511
"id": {"type": "string", "minLength": 36, "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},
511512
"version": {"type": "string", "minLength": 5, "pattern": "^([0-9]+.)([0-9]+.)([0-9]+)$"},
512513
"name": {"type": "string", "minLength": 3},
513-
"description": {"type": "string"},
514+
"description": {"type": "string"},
514515
"output": {"type": "string", "minLength": 5},
515516
"notes": {"type": "string"},
516517
"severity": {"type": "string", "enum": ["low", "medium", "high", "critical"]},

pkg/rules/engine.go

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ var (
5454
// the collection of compiled filters that are derived
5555
// from the loaded ruleset.
5656
type Engine struct {
57-
filters map[uint32][]*compiledFilter
57+
filters compiledFilters
5858
config *config.Config
5959
psnap ps.Snapshotter
6060

@@ -120,6 +120,29 @@ type compiledFilter struct {
120120
ss *sequenceState
121121
}
122122

123+
type compiledFilters map[uint32][]*compiledFilter
124+
125+
// collect collects all compiled filters for a
126+
// particular event type or category. If no filters
127+
// are found, the event is not asserted against the
128+
// ruleset.
129+
func (filters compiledFilters) collect(hashCache *hashCache, e *kevent.Kevent) []*compiledFilter {
130+
h := hashCache.typeHash(e)
131+
if h == 0 {
132+
h = hashCache.addTypeHash(e)
133+
}
134+
135+
if !hashCache.lookupCategory {
136+
return filters[h]
137+
}
138+
139+
c := hashCache.categoryHash(e)
140+
if c == 0 {
141+
c = hashCache.addCategoryHash(e)
142+
}
143+
return append(filters[h], filters[c]...)
144+
}
145+
123146
func newCompiledFilter(f filter.Filter, c *config.FilterConfig, ss *sequenceState) *compiledFilter {
124147
return &compiledFilter{filter: f, config: c, ss: ss}
125148
}
@@ -231,13 +254,14 @@ func (e *Engine) RegisterMatchFunc(fn RuleMatchFunc) {
231254
func (*Engine) CanEnqueue() bool { return true }
232255

233256
// ProcessEvent processes the system event against compiled filters.
234-
// Filter is the internal lingo to designate the rule condition. The
235-
// filters can be simple direct-event matchers or sequence states that
257+
// Filter is the internal lingo that designates a rule condition.
258+
// Filters can be simple direct-event matchers or sequence states that
236259
// track an ordered series of events over a short period of time.
237260
func (e *Engine) ProcessEvent(evt *kevent.Kevent) (bool, error) {
238261
if len(e.filters) == 0 {
239262
return true, nil
240263
}
264+
var matches bool
241265
if evt.IsTerminateProcess() {
242266
// expire all sequences if the
243267
// process referenced in any
@@ -246,45 +270,30 @@ func (e *Engine) ProcessEvent(evt *kevent.Kevent) (bool, error) {
246270
seq.expire(evt)
247271
}
248272
}
249-
filters := e.findFilters(evt)
273+
filters := e.filters.collect(e.hashCache, evt)
250274
for _, f := range filters {
251275
match := f.run(evt)
252-
if match {
253-
if f.isSequence() {
254-
e.appendMatch(f.config, f.ss.events()...)
255-
f.ss.clearLocked()
256-
} else {
257-
e.appendMatch(f.config, evt)
258-
}
259-
err := e.processActions()
260-
if err != nil {
261-
log.Errorf("unable to execute rule action: %v", err)
262-
}
263-
return true, nil
276+
if !match {
277+
continue
264278
}
265-
}
266-
return false, nil
267-
}
268-
269-
// findFilters collects all compiled filters for a
270-
// particular event type or category. If no filters
271-
// are found, the event is not asserted against the
272-
// ruleset.
273-
func (e *Engine) findFilters(evt *kevent.Kevent) []*compiledFilter {
274-
h := e.hashCache.typeHash(evt)
275-
if h == 0 {
276-
h = e.hashCache.addTypeHash(evt)
277-
}
278-
switch {
279-
case !e.hashCache.lookupCategory:
280-
return e.filters[h]
281-
default:
282-
c := e.hashCache.categoryHash(evt)
283-
if c == 0 {
284-
c = e.hashCache.addCategoryHash(evt)
279+
if f.isSequence() {
280+
e.appendMatch(f.config, f.ss.events()...)
281+
f.ss.clearLocked()
282+
} else {
283+
e.appendMatch(f.config, evt)
284+
}
285+
err := e.processActions()
286+
if err != nil {
287+
log.Errorf("unable to execute rule action: %v", err)
288+
}
289+
switch {
290+
case e.config.Filters.MatchAll:
291+
matches = true
292+
default:
293+
return true, nil
285294
}
286-
return append(e.filters[h], e.filters[c]...)
287295
}
296+
return matches, nil
288297
}
289298

290299
// processActions executes rule actions
@@ -310,6 +319,7 @@ func (e *Engine) processActions() error {
310319
if err != nil {
311320
return err
312321
}
322+
313323
for _, act := range actions {
314324
switch act.(type) {
315325
case config.KillAction:

pkg/rules/engine_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ func TestCompileIndexableFilters(t *testing.T) {
158158

159159
for _, tt := range tests {
160160
t.Run(tt.evt.Type.String(), func(t *testing.T) {
161-
assert.Len(t, e.findFilters(tt.evt), tt.wants)
161+
assert.Len(t, e.filters.collect(e.hashCache, tt.evt), tt.wants)
162162
})
163163
}
164164

@@ -333,8 +333,9 @@ func TestRunSimpleAndSequenceRules(t *testing.T) {
333333
log.SetLevel(log.DebugLevel)
334334

335335
expectedMatches := make(map[string][]uint64)
336-
337-
e := NewEngine(new(ps.SnapshotterMock), newConfig("_fixtures/simple_and_sequence_rules/*.yml"))
336+
c := newConfig("_fixtures/simple_and_sequence_rules/*.yml")
337+
c.Filters.MatchAll = true
338+
e := NewEngine(new(ps.SnapshotterMock), c)
338339
e.RegisterMatchFunc(func(f *config.FilterConfig, evts ...*kevent.Kevent) {
339340
ids := make([]uint64, 0)
340341
for _, evt := range evts {

0 commit comments

Comments
 (0)