diff --git a/config_parser.go b/config_parser.go index 4d65f70..86f6ba4 100644 --- a/config_parser.go +++ b/config_parser.go @@ -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 diff --git a/evaluator/evaluate.go b/evaluator/evaluate.go index 0167311..8af8674 100644 --- a/evaluator/evaluate.go +++ b/evaluator/evaluate.go @@ -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 diff --git a/evaluator/evaluate_search.go b/evaluator/evaluate_search.go index b96cbe5..8ddc7b8 100644 --- a/evaluator/evaluate_search.go +++ b/evaluator/evaluate_search.go @@ -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 { @@ -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 } @@ -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 } @@ -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) } }, diff --git a/evaluator/fieldmappings.go b/evaluator/fieldmappings.go new file mode 100644 index 0000000..b6e7350 --- /dev/null +++ b/evaluator/fieldmappings.go @@ -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 +} diff --git a/evaluator/fieldmappings_test.go b/evaluator/fieldmappings_test.go new file mode 100644 index 0000000..81b388e --- /dev/null +++ b/evaluator/fieldmappings_test.go @@ -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") + } +} diff --git a/evaluator/options.go b/evaluator/options.go index 3cf5427..05fbb48 100644 --- a/evaluator/options.go +++ b/evaluator/options.go @@ -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() } }