Skip to content

Commit 6279563

Browse files
authored
chore(rules): Redesign rule actions (#221)
1 parent af99162 commit 6279563

22 files changed

Lines changed: 328 additions & 550 deletions

pkg/alertsender/alert.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const (
3434
Normal Severity = iota
3535
// Medium designates alert's medium level
3636
Medium
37+
// High designates alert's high level
38+
High
3739
// Critical designates alert's critical level
3840
Critical
3941
)
@@ -45,6 +47,8 @@ func (s Severity) String() string {
4547
return "low"
4648
case Medium:
4749
return "medium"
50+
case High:
51+
return "high"
4852
case Critical:
4953
return "critical"
5054
default:
@@ -59,7 +63,9 @@ func ParseSeverityFromString(sever string) Severity {
5963
return Normal
6064
case "medium", "Medium", "MEDIUM":
6165
return Medium
62-
case "critical", "Critical", "high", "High", "HIGH":
66+
case "high", "High", "HIGH":
67+
return High
68+
case "critical", "Critical", "CRITICAL":
6369
return Critical
6470
default:
6571
return Normal

pkg/config/_fixtures/filters/default.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44
- TE
55
rules:
66
- name: only network category
7+
description: this rule matches all network signals
78
condition: kevt.category = 'net'
9+
severity: low
10+
output: >
11+
`%ps.exe` attempted to reach out to `%net.sip` IP address
12+
action:
13+
- kill:
14+
pid: ps.pid
815
min-engine-version: 2.0.0
916

1017
- group: rouge processes

pkg/config/_fixtures/filters/invalid_filter_action.yml

Lines changed: 0 additions & 8 deletions
This file was deleted.

pkg/config/_fixtures/filters/invalid_filter_action_values.yml

Lines changed: 0 additions & 8 deletions
This file was deleted.

pkg/config/filters.go

Lines changed: 71 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ package config
2020

2121
import (
2222
"bytes"
23-
"encoding/base64"
2423
"fmt"
2524
"github.com/Masterminds/sprig/v3"
2625
"github.com/rabbitstack/fibratus/pkg/kevent"
@@ -34,6 +33,7 @@ import (
3433
u "net/url"
3534
"os"
3635
"path/filepath"
36+
"strconv"
3737
"strings"
3838
"text/template"
3939
)
@@ -43,30 +43,13 @@ type FilterConfig struct {
4343
Name string `json:"name" yaml:"name"`
4444
Description string `json:"description" yaml:"description"`
4545
Condition string `json:"condition" yaml:"condition"`
46-
Action string `json:"action" yaml:"action"`
46+
Action []FilterAction `json:"action" yaml:"action"`
47+
Output string `json:"output" yaml:"output"`
48+
Severity string `json:"severity" yaml:"severity"`
4749
Labels map[string]string `json:"labels" yaml:"labels"`
4850
MinEngineVersion string `json:"min-engine-version" yaml:"min-engine-version"`
4951
}
5052

51-
// parseTmpl ensures the correctness of the rule
52-
// action template by trying to parse the template
53-
// string from the base64 payload.
54-
func (f FilterConfig) parseTmpl(resource string) error {
55-
if f.Action == "" {
56-
return nil
57-
}
58-
decoded, err := base64.StdEncoding.DecodeString(f.Action)
59-
if err != nil {
60-
return err
61-
}
62-
tmpl, err := template.New(f.Name).Funcs(FilterFuncMap()).Parse(string(decoded))
63-
if err != nil {
64-
return cleanupParseError(resource, err)
65-
}
66-
var bb bytes.Buffer
67-
return cleanupParseError(resource, tmpl.Execute(&bb, tmplData()))
68-
}
69-
7053
// FilterGroup represents the container for filters.
7154
type FilterGroup struct {
7255
Name string `json:"group" yaml:"group"`
@@ -77,18 +60,61 @@ type FilterGroup struct {
7760
Labels map[string]string `json:"labels" yaml:"labels"`
7861
}
7962

80-
// IsDisabled determines if this group is disabled.
81-
func (g FilterGroup) IsDisabled() bool { return g.Enabled != nil && !*g.Enabled }
63+
// FilterAction wraps all possible filter actions.
64+
type FilterAction any
65+
66+
// KillAction defines an action for killing the process
67+
// indicates by the filter field expression.
68+
type KillAction struct {
69+
// Pid indicates the field for which
70+
// the process id is resolved
71+
Pid string `json:"pid" yaml:"pid"`
72+
}
73+
74+
func (a KillAction) PidToInt(pid string) uint32 {
75+
n, err := strconv.Atoi(pid)
76+
if err != nil {
77+
return 0
78+
}
79+
return uint32(n)
80+
}
81+
82+
const (
83+
killActionID = "kill"
84+
)
85+
86+
// DecodeActions converts raw YAML map to
87+
// typed action structures.
88+
func (f FilterConfig) DecodeActions() ([]any, error) {
89+
actions := make([]any, 0, len(f.Action))
90+
91+
dec := func(m map[string]any, o any) error {
92+
err := decode(m, &o)
93+
if err != nil {
94+
return err
95+
}
96+
actions = append(actions, o)
97+
return nil
98+
}
8299

83-
func (g FilterGroup) validate(resource string) error {
84-
for _, filter := range g.Rules {
85-
if err := filter.parseTmpl(resource); err != nil {
86-
return fmt.Errorf("invalid %q rule action: %v", filter.Name, err)
100+
for _, act := range f.Action {
101+
m, ok := act.(map[string]any)
102+
if !ok {
103+
continue
104+
}
105+
if _, ok := m[killActionID]; ok {
106+
var kill KillAction
107+
if err := dec(m, kill); err != nil {
108+
return nil, err
109+
}
87110
}
88111
}
89-
return nil
112+
return actions, nil
90113
}
91114

115+
// IsDisabled determines if this group is disabled.
116+
func (g FilterGroup) IsDisabled() bool { return g.Enabled != nil && !*g.Enabled }
117+
92118
// Hash calculates the filter group hash.
93119
func (g FilterGroup) Hash() uint32 {
94120
return hashers.FnvUint32([]byte(g.Name))
@@ -132,6 +158,21 @@ type Macro struct {
132158
List []string `json:"list" yaml:"list"`
133159
}
134160

161+
// ActionContext is the convenient structure
162+
// for grouping the event that resulted in
163+
// matched filter along with filter group
164+
// information.
165+
type ActionContext struct {
166+
// Events contains a single element for non-sequence
167+
// group policies or a list of ordered matched events
168+
// for sequence group policies
169+
Events []*kevent.Kevent
170+
// Filter represents the filter that matched the event
171+
Filter *FilterConfig
172+
// Group represents the group where the filter is declared
173+
Group FilterGroup
174+
}
175+
135176
const (
136177
rulesFromPaths = "filters.rules.from-paths"
137178
rulesFromURLs = "filters.rules.from-urls"
@@ -307,40 +348,24 @@ func decodeFilterGroups(resource string, b []byte) ([]FilterGroup, error) {
307348
"%v in %s: %v", rawGroup, resource, multierror.Wrap(errs...))
308349
}
309350
}
310-
// convert filter action template to
311-
// base64 before executing the global
312-
// template. The rendered template yields
313-
// a yaml payload with template directives
314-
// expanded
315-
b, err = encodeFilterActions(b)
316-
if err != nil {
317-
return nil, err
318-
}
351+
// render template
319352
b, err = renderTmpl(resource, b)
320353
if err != nil {
321354
return nil, err
322355
}
323-
324356
// now unmarshal into typed group slice
325357
var groups []FilterGroup
326358
if err := yaml.Unmarshal(b, &groups); err != nil {
327359
return nil, err
328360
}
329-
// try to validate filter action template
330-
for _, group := range groups {
331-
err := group.validate(resource)
332-
if err != nil {
333-
return nil, err
334-
}
335-
}
336361
return groups, nil
337362
}
338363

339364
// renderTmpl executes templating directives in the
340365
// file group yaml file. It returns the byte slice
341366
// with yaml content after template expansion.
342367
func renderTmpl(filename string, b []byte) ([]byte, error) {
343-
tmpl, err := template.New(filename).Funcs(FilterFuncMap()).Parse(string(b))
368+
tmpl, err := template.New(filename).Funcs(sprig.FuncMap()).Parse(string(b))
344369
if err != nil {
345370
return nil, cleanupParseError(filename, err)
346371
}
@@ -379,111 +404,3 @@ func cleanupParseError(filename string, err error) error {
379404
}
380405
return fmt.Errorf("syntax error in (%s) at %s: %s", location, key, errMsg)
381406
}
382-
383-
// ActionContext is the convenient structure
384-
// for grouping the event that resulted in
385-
// matched filter along with filter group
386-
// information.
387-
type ActionContext struct {
388-
Kevt *kevent.Kevent
389-
// Kevts contains matched events for sequence group
390-
// policies indexed by `k` + the slot number of the
391-
// rule that produced a partial match
392-
Kevts map[string]*kevent.Kevent
393-
// Events contains a single element for non-sequence
394-
// group policies or a list of ordered matched events
395-
// for sequence group policies
396-
Events []*kevent.Kevent
397-
Filter *FilterConfig
398-
Group FilterGroup
399-
}
400-
401-
// FilterFuncMap returns the template func map
402-
// populated with some useful template functions
403-
// that can be used in rule actions.
404-
func FilterFuncMap() template.FuncMap {
405-
f := sprig.TxtFuncMap()
406-
407-
extra := template.FuncMap{
408-
// This is a placeholder for the functions that might be
409-
// late-bound to a template. By declaring them here, we
410-
// can still execute the template associated with the
411-
// filter action to ensure template syntax is correct
412-
"emit": func(ctx *ActionContext, title string, text string, args ...string) string { return "" },
413-
"kill": func(pid uint32) string { return "" },
414-
}
415-
416-
for k, v := range extra {
417-
f[k] = v
418-
}
419-
420-
return f
421-
}
422-
423-
func tmplData() *ActionContext {
424-
return &ActionContext{
425-
Filter: &FilterConfig{},
426-
Group: FilterGroup{},
427-
Kevt: kevent.Empty(),
428-
Events: make([]*kevent.Kevent, 0),
429-
Kevts: make(map[string]*kevent.Kevent),
430-
}
431-
}
432-
433-
const (
434-
actionNode = "action"
435-
defNode = "def"
436-
fromStringsNode = "from-strings"
437-
rulesNode = "rules"
438-
)
439-
440-
// encodeFilterActions convert the filter action template
441-
// to base64 payload. Because we only want to execute
442-
// the action template when a filter matches in runtime,
443-
// encoding the template to base64 prevents the Go templating
444-
// engine from expanding the template in parse time, when we
445-
// first load all the filter groups.
446-
func encodeFilterActions(buf []byte) ([]byte, error) {
447-
var yn yaml.Node
448-
if err := yaml.Unmarshal(buf, &yn); err != nil {
449-
return nil, err
450-
}
451-
452-
// for each group
453-
for _, n := range yn.Content[0].Content {
454-
// for each group node
455-
for i, gn := range n.Content {
456-
// sequence groups action
457-
if gn.Value == actionNode && n.Content[i+1].Value != "" {
458-
n.Content[i+1].Value =
459-
base64.StdEncoding.EncodeToString([]byte(n.Content[i+1].Value))
460-
}
461-
if gn.Value == fromStringsNode {
462-
log.Warnf("`from-strings` attribute is deprecated and will be " +
463-
"removed in future versions. Please consider switching to `rules` attribute")
464-
}
465-
if gn.Value == fromStringsNode || gn.Value == rulesNode {
466-
content := n.Content[i+1]
467-
// for each node in from-strings
468-
for _, s := range content.Content {
469-
for j, e := range s.Content {
470-
if e.Value == defNode {
471-
log.Warnf("`def` attribute is deprecated and will be " +
472-
"removed in future versions. Please consider switching to `condition` attribute")
473-
}
474-
if e.Value == actionNode && s.Content[j+1].Value != "" {
475-
s.Content[j+1].Value =
476-
base64.StdEncoding.EncodeToString([]byte(s.Content[j+1].Value))
477-
}
478-
}
479-
}
480-
}
481-
}
482-
}
483-
484-
b, err := yaml.Marshal(&yn)
485-
if err != nil {
486-
return nil, err
487-
}
488-
return b, nil
489-
}

0 commit comments

Comments
 (0)