feat: logging support in ruleset

This commit is contained in:
Toby 2024-02-23 14:13:35 -08:00
parent 465373eaf1
commit 7353a16358
6 changed files with 68 additions and 32 deletions

View File

@ -254,6 +254,7 @@ func runMain(cmd *cobra.Command, args []string) {
logger.Fatal("failed to load rules", zap.Error(err)) logger.Fatal("failed to load rules", zap.Error(err))
} }
rsConfig := &ruleset.BuiltinConfig{ rsConfig := &ruleset.BuiltinConfig{
Logger: &rulesetLogger{},
GeoSiteFilename: config.Ruleset.GeoSite, GeoSiteFilename: config.Ruleset.GeoSite,
GeoIpFilename: config.Ruleset.GeoIp, GeoIpFilename: config.Ruleset.GeoIp,
} }
@ -371,14 +372,6 @@ func (l *engineLogger) UDPStreamAction(info ruleset.StreamInfo, action ruleset.A
zap.Bool("noMatch", noMatch)) zap.Bool("noMatch", noMatch))
} }
func (l *engineLogger) MatchError(info ruleset.StreamInfo, err error) {
logger.Error("match error",
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()),
zap.Error(err))
}
func (l *engineLogger) ModifyError(info ruleset.StreamInfo, err error) { func (l *engineLogger) ModifyError(info ruleset.StreamInfo, err error) {
logger.Error("modify error", logger.Error("modify error",
zap.Int64("id", info.ID), zap.Int64("id", info.ID),
@ -408,6 +401,26 @@ func (l *engineLogger) AnalyzerErrorf(streamID int64, name string, format string
zap.String("msg", fmt.Sprintf(format, args...))) zap.String("msg", fmt.Sprintf(format, args...)))
} }
type rulesetLogger struct{}
func (l *rulesetLogger) Log(info ruleset.StreamInfo, name string) {
logger.Info("ruleset log",
zap.String("name", name),
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()),
zap.Any("props", info.Props))
}
func (l *rulesetLogger) MatchError(info ruleset.StreamInfo, name string, err error) {
logger.Error("ruleset match error",
zap.String("name", name),
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()),
zap.Error(err))
}
func envOrDefaultString(key, def string) string { func envOrDefaultString(key, def string) string {
if v := os.Getenv(key); v != "" { if v := os.Getenv(key); v != "" {
return v return v

View File

@ -41,7 +41,6 @@ type Logger interface {
UDPStreamPropUpdate(info ruleset.StreamInfo, close bool) UDPStreamPropUpdate(info ruleset.StreamInfo, close bool)
UDPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool) UDPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool)
MatchError(info ruleset.StreamInfo, err error)
ModifyError(info ruleset.StreamInfo, err error) ModifyError(info ruleset.StreamInfo, err error)
AnalyzerDebugf(streamID int64, name string, format string, args ...interface{}) AnalyzerDebugf(streamID int64, name string, format string, args ...interface{})

View File

@ -148,10 +148,7 @@ func (s *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.Ass
s.virgin = false s.virgin = false
s.logger.TCPStreamPropUpdate(s.info, false) s.logger.TCPStreamPropUpdate(s.info, false)
// Match properties against ruleset // Match properties against ruleset
result, err := s.ruleset.Match(s.info) result := s.ruleset.Match(s.info)
if err != nil {
s.logger.MatchError(s.info, err)
}
action := result.Action action := result.Action
if action != ruleset.ActionMaybe && action != ruleset.ActionModify { if action != ruleset.ActionMaybe && action != ruleset.ActionModify {
verdict := actionToTCPVerdict(action) verdict := actionToTCPVerdict(action)

View File

@ -201,10 +201,7 @@ func (s *udpStream) Feed(udp *layers.UDP, rev bool, uc *udpContext) {
s.virgin = false s.virgin = false
s.logger.UDPStreamPropUpdate(s.info, false) s.logger.UDPStreamPropUpdate(s.info, false)
// Match properties against ruleset // Match properties against ruleset
result, err := s.ruleset.Match(s.info) result := s.ruleset.Match(s.info)
if err != nil {
s.logger.MatchError(s.info, err)
}
action := result.Action action := result.Action
if action == ruleset.ActionModify { if action == ruleset.ActionModify {
// Call the modifier instance // Call the modifier instance
@ -214,6 +211,7 @@ func (s *udpStream) Feed(udp *layers.UDP, rev bool, uc *udpContext) {
s.logger.ModifyError(s.info, errInvalidModifier) s.logger.ModifyError(s.info, errInvalidModifier)
action = ruleset.ActionMaybe action = ruleset.ActionMaybe
} else { } else {
var err error
uc.Packet, err = udpMI.Process(udp.Payload) uc.Packet, err = udpMI.Process(udp.Payload)
if err != nil { if err != nil {
// Modifier error, fallback to maybe // Modifier error, fallback to maybe

View File

@ -23,6 +23,7 @@ import (
type ExprRule struct { type ExprRule struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Action string `yaml:"action"` Action string `yaml:"action"`
Log bool `yaml:"log"`
Modifier ModifierEntry `yaml:"modifier"` Modifier ModifierEntry `yaml:"modifier"`
Expr string `yaml:"expr"` Expr string `yaml:"expr"`
} }
@ -45,7 +46,8 @@ func ExprRulesFromYAML(file string) ([]ExprRule, error) {
// compiledExprRule is the internal, compiled representation of an expression rule. // compiledExprRule is the internal, compiled representation of an expression rule.
type compiledExprRule struct { type compiledExprRule struct {
Name string Name string
Action Action Action *Action // fallthrough if nil
Log bool
ModInstance modifier.Instance ModInstance modifier.Instance
Program *vm.Program Program *vm.Program
} }
@ -55,6 +57,7 @@ var _ Ruleset = (*exprRuleset)(nil)
type exprRuleset struct { type exprRuleset struct {
Rules []compiledExprRule Rules []compiledExprRule
Ans []analyzer.Analyzer Ans []analyzer.Analyzer
Logger Logger
GeoMatcher *geo.GeoMatcher GeoMatcher *geo.GeoMatcher
} }
@ -62,25 +65,31 @@ func (r *exprRuleset) Analyzers(info StreamInfo) []analyzer.Analyzer {
return r.Ans return r.Ans
} }
func (r *exprRuleset) Match(info StreamInfo) (MatchResult, error) { func (r *exprRuleset) Match(info StreamInfo) MatchResult {
env := streamInfoToExprEnv(info) env := streamInfoToExprEnv(info)
for _, rule := range r.Rules { for _, rule := range r.Rules {
v, err := vm.Run(rule.Program, env) v, err := vm.Run(rule.Program, env)
if err != nil { if err != nil {
return MatchResult{ // Log the error and continue to the next rule.
Action: ActionMaybe, r.Logger.MatchError(info, rule.Name, err)
}, fmt.Errorf("rule %q failed to run: %w", rule.Name, err) continue
} }
if vBool, ok := v.(bool); ok && vBool { if vBool, ok := v.(bool); ok && vBool {
return MatchResult{ if rule.Log {
Action: rule.Action, r.Logger.Log(info, rule.Name)
ModInstance: rule.ModInstance, }
}, nil if rule.Action != nil {
return MatchResult{
Action: *rule.Action,
ModInstance: rule.ModInstance,
}
}
} }
} }
// No match
return MatchResult{ return MatchResult{
Action: ActionMaybe, Action: ActionMaybe,
}, nil }
} }
// CompileExprRules compiles a list of expression rules into a ruleset. // CompileExprRules compiles a list of expression rules into a ruleset.
@ -97,9 +106,16 @@ func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier
} }
// Compile all rules and build a map of analyzers that are used by the rules. // Compile all rules and build a map of analyzers that are used by the rules.
for _, rule := range rules { for _, rule := range rules {
action, ok := actionStringToAction(rule.Action) if rule.Action == "" && !rule.Log {
if !ok { return nil, fmt.Errorf("rule %q must have at least one of action or log", rule.Name)
return nil, fmt.Errorf("rule %q has invalid action %q", rule.Name, rule.Action) }
var action *Action
if rule.Action != "" {
a, ok := actionStringToAction(rule.Action)
if !ok {
return nil, fmt.Errorf("rule %q has invalid action %q", rule.Name, rule.Action)
}
action = &a
} }
visitor := &idVisitor{Identifiers: make(map[string]bool)} visitor := &idVisitor{Identifiers: make(map[string]bool)}
patcher := &idPatcher{} patcher := &idPatcher{}
@ -145,9 +161,10 @@ func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier
cr := compiledExprRule{ cr := compiledExprRule{
Name: rule.Name, Name: rule.Name,
Action: action, Action: action,
Log: rule.Log,
Program: program, Program: program,
} }
if action == ActionModify { if action != nil && *action == ActionModify {
mod, ok := fullModMap[rule.Modifier.Name] mod, ok := fullModMap[rule.Modifier.Name]
if !ok { if !ok {
return nil, fmt.Errorf("rule %q uses unknown modifier %q", rule.Name, rule.Modifier.Name) return nil, fmt.Errorf("rule %q uses unknown modifier %q", rule.Name, rule.Modifier.Name)
@ -168,6 +185,7 @@ func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier
return &exprRuleset{ return &exprRuleset{
Rules: compiledRules, Rules: compiledRules,
Ans: depAns, Ans: depAns,
Logger: config.Logger,
GeoMatcher: geoMatcher, GeoMatcher: geoMatcher,
}, nil }, nil
} }
@ -284,6 +302,10 @@ func (p *idPatcher) Visit(node *ast.Node) {
switch (*node).(type) { switch (*node).(type) {
case *ast.CallNode: case *ast.CallNode:
callNode := (*node).(*ast.CallNode) callNode := (*node).(*ast.CallNode)
if callNode.Func == nil {
// Ignore invalid call nodes
return
}
switch callNode.Func.Name { switch callNode.Func.Name {
case "cidr": case "cidr":
cidrStringNode, ok := callNode.Arguments[1].(*ast.StringNode) cidrStringNode, ok := callNode.Arguments[1].(*ast.StringNode)

View File

@ -90,10 +90,17 @@ type Ruleset interface {
Analyzers(StreamInfo) []analyzer.Analyzer Analyzers(StreamInfo) []analyzer.Analyzer
// Match matches a stream against the ruleset and returns the result. // Match matches a stream against the ruleset and returns the result.
// It must be safe for concurrent use by multiple workers. // It must be safe for concurrent use by multiple workers.
Match(StreamInfo) (MatchResult, error) Match(StreamInfo) MatchResult
}
// Logger is the logging interface for the ruleset.
type Logger interface {
Log(info StreamInfo, name string)
MatchError(info StreamInfo, name string, err error)
} }
type BuiltinConfig struct { type BuiltinConfig struct {
Logger Logger
GeoSiteFilename string GeoSiteFilename string
GeoIpFilename string GeoIpFilename string
} }