Skip to content

Commit

Permalink
Add basic support for FieldMappings
Browse files Browse the repository at this point in the history
  • Loading branch information
Bradley Kemp committed Oct 1, 2020
1 parent 8e879b2 commit 8fd532f
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 14 deletions.
17 changes: 16 additions & 1 deletion config_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,26 @@ type Config struct {
}

type FieldMapping struct {
SourceName string // The name that appears in a Sigma rule
TargetNames []string // The name(s) that appear in the events being matched
// TODO: support conditional mappings?
}

func (f *FieldMapping) UnmarshalYAML(value *yaml.Node) error {
switch value.Kind {
case yaml.ScalarNode:
f.TargetNames = []string{value.Value}

case yaml.SequenceNode:
var values []string
err := value.Decode(&values)
if err != nil {
return err
}
f.TargetNames = values
}
return nil
}

type LogsourceMapping struct {
Logsource `yaml:",inline"` // Matches the logsource field in Sigma rules
Index LogsourceIndexes // The index(es) that should be used
Expand Down
5 changes: 3 additions & 2 deletions evaluator/evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (

type RuleEvaluator struct {
sigma.Rule
config []sigma.Config
indexes []string // the list of indexes that this rule should be applied to. Computed from the Logsource field in the rule and any config that's supplied.
config []sigma.Config
indexes []string // the list of indexes that this rule should be applied to. Computed from the Logsource field in the rule and any config that's supplied.
fieldmappings map[string][]string // a compiled mapping from rule fieldnames to possible event fieldnames

count func(ctx context.Context, gb GroupedByValues) float64 // TODO: how to pass an event timestamp here and enable running rules on historical events?
average func(ctx context.Context, gb GroupedByValues, value float64) float64
Expand Down
38 changes: 27 additions & 11 deletions evaluator/evaluate_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,19 @@ func (rule RuleEvaluator) evaluateSearch(search sigma.Search, event map[string]i
panic("keywords unsupported")
}

// A Search is a series of "does this field match this value" conditions
// all need to match, for the Search to evaluate to true
for _, matcher := range search.FieldMatchers {
andValues := false
// A field matcher can specify multiple values to match against
// either the field should match all of these values or it should match any of them
allValuesMustMatch := false
fieldModifiers := matcher.Modifiers
if len(matcher.Modifiers) > 0 && fieldModifiers[len(fieldModifiers)-1] == "all" {
andValues = true
allValuesMustMatch = true
fieldModifiers = fieldModifiers[:len(fieldModifiers)-1]
}

// field matchers can specify modifiers (FieldName|modifier1|modifier2) which change the matching behaviour
valueMatcher := baseMatcher
for _, name := range fieldModifiers {
if modifiers[name] == nil {
Expand All @@ -51,16 +56,31 @@ func (rule RuleEvaluator) evaluateSearch(search sigma.Search, event map[string]i
valueMatcher = modifiers[name](valueMatcher)
}

matched := andValues
fieldMatched := allValuesMustMatch
for _, value := range matcher.Values {
if andValues {
matched = matched && valueMatcher(event[matcher.Field], value)
// There are multiple possible event fields that each value needs to be compared against
var valueMatches bool
if len(rule.fieldmappings[matcher.Field]) == 0 {
// No FieldMapping exists so use the name directly from the rule
valueMatches = valueMatcher(event[matcher.Field], value)
} else {
matched = matched || valueMatcher(event[matcher.Field], value)
// FieldMapping does exist so check each of the possible mapped names instead of the name from the rule
for _, field := range rule.fieldmappings[matcher.Field] {
valueMatches = valueMatcher(event[field], value)
if valueMatches {
break
}
}
}

if allValuesMustMatch {
fieldMatched = fieldMatched && valueMatches
} else {
fieldMatched = fieldMatched || valueMatches
}
}

if !matched {
if !fieldMatched {
// this field didn't match so the overall matcher doesn't match
return false
}
Expand All @@ -73,7 +93,6 @@ func (rule RuleEvaluator) evaluateSearch(search sigma.Search, event map[string]i
type valueMatcher func(actual interface{}, expected string) bool

func baseMatcher(actual interface{}, expected string) bool {
//fmt.Printf("=(%s, %s)\n", actual, expected)
return fmt.Sprintf("%v", actual) == expected
}

Expand All @@ -82,19 +101,16 @@ type valueModifier func(next valueMatcher) valueMatcher
var modifiers = map[string]valueModifier{
"contains": func(next valueMatcher) valueMatcher {
return func(actual interface{}, expected string) bool {
//fmt.Printf("contains(%s, %s)\n", actual, expected)
return strings.Contains(fmt.Sprintf("%v", actual), expected)
}
},
"endswith": func(next valueMatcher) valueMatcher {
return func(actual interface{}, expected string) bool {
//fmt.Printf("endswith(%s, %s)\n", actual, expected)
return strings.HasSuffix(fmt.Sprintf("%v", actual), expected)
}
},
"startswith": func(next valueMatcher) valueMatcher {
return func(actual interface{}, expected string) bool {
//fmt.Printf("startswith(%s, %s)\n", actual, expected)
return strings.HasPrefix(fmt.Sprintf("%v", actual), expected)
}
},
Expand Down
18 changes: 18 additions & 0 deletions evaluator/fieldmappings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package evaluator

func (rule *RuleEvaluator) calculateFieldMappings() {
if rule.config == nil {
return
}

mappings := map[string][]string{}

for _, config := range rule.config {
for field, mapping := range config.FieldMappings {
// TODO: trim duplicates and only care about fields that are actually checked by this rule
mappings[field] = append(mappings[field], mapping.TargetNames...)
}
}

rule.fieldmappings = mappings
}
46 changes: 46 additions & 0 deletions evaluator/fieldmappings_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package evaluator

import (
"context"
"testing"

"github.com/bradleyjkemp/sigma-go"
)

func TestRuleEvaluator_HandlesBasicFieldMappings(t *testing.T) {
rule := ForRule(sigma.Rule{
Logsource: sigma.Logsource{
Category: "category",
Product: "product",
Service: "service",
},
Detection: sigma.Detection{
Searches: map[string]sigma.Search{
"test": {
FieldMatchers: []sigma.FieldMatcher{{
Field: "name",
Values: []string{"value"},
}},
},
},
Conditions: []sigma.Condition{
{Search: sigma.SearchIdentifier{Name: "test"}}},
},
}, WithConfig(sigma.Config{
FieldMappings: map[string]sigma.FieldMapping{
"name": {TargetNames: []string{"mapped-name"}},
},
}))

if rule.Matches(context.Background(), map[string]interface{}{
"name": "value",
}) {
t.Error("If a field is mapped, the old name shouldn't be used")
}

if !rule.Matches(context.Background(), map[string]interface{}{
"mapped-name": "value",
}) {
t.Error("If a field is mapped, the mapped name should work")
}
}
1 change: 1 addition & 0 deletions evaluator/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ func WithConfig(config ...sigma.Config) Option {
// TODO: assert that the configs are in the correct order
e.config = append(e.config, config...)
e.calculateIndexes()
e.calculateFieldMappings()
}
}

0 comments on commit 8fd532f

Please sign in to comment.