diff --git a/condition_parser.go b/condition_parser.go index 6ac63a8..e976156 100644 --- a/condition_parser.go +++ b/condition_parser.go @@ -9,7 +9,8 @@ import ( var ( searchExprLexer = lexer.Must(lexer.Regexp(`(?P(?i)(1 of them)|(all of them)|(1 of)|(all of))` + `|(?P\*?[a-zA-Z_]+\*[a-zA-Z0-9_*]*)` + - `|(?P[a-zA-Z_][a-zA-Z0-9_]*)` + + // `|(?P[a-zA-Z_][a-zA-Z0-9_]*)` + + `|(?P[a-zA-Z_][a-zA-Z0-9_.]*)` + `|(?P(?i)and|or|not|[()])` + // TODO: this never actually matches anything because they get matched as a SearchIdentifier instead. However this isn't currently a problem because we don't parse anything in the Grammar as an Operator (we just use string constants which don't care about Operator vs SearchIdentifier) `|(?P=|!=|<=|>=|<|>)` + `|(?P0|[1-9][0-9]*)` + diff --git a/evaluator/evaluate.go b/evaluator/evaluate.go index 7cf033b..50ad887 100644 --- a/evaluator/evaluate.go +++ b/evaluator/evaluate.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/bradleyjkemp/sigma-go" "github.com/bradleyjkemp/sigma-go/evaluator/modifiers" ) @@ -85,12 +86,66 @@ type Result struct { // Event should be some form a map[string]interface{} or map[string]string type Event interface{} +func flattenJSON(data interface{}, prefix string, result map[string]interface{}) { + switch v := data.(type) { + case map[string]interface{}: + for k, val := range v { + newKey := k + if prefix != "" { + newKey = prefix + "." + k + } + flattenJSON(val, newKey, result) + } + + case []interface{}: + if len(v) == 0 { + result[prefix] = v + return + } + + if _, isMap := v[0].(map[string]interface{}); isMap { + fieldsMap := make(map[string][]interface{}) + for _, item := range v { + if mapItem, ok := item.(map[string]interface{}); ok { + for k, val := range mapItem { + key := prefix + "." + k + fieldsMap[key] = append(fieldsMap[key], val) + } + } + } + for k, val := range fieldsMap { + result[k] = val + } + } else { + result[prefix] = v + } + + case string: + switch v { + case "False", "false": + result[prefix] = false + case "True", "true": + result[prefix] = true + default: + result[prefix] = v + } + default: + result[prefix] = v + } +} + +func getNestedValue(data map[string]interface{}, path string) interface{} { + result := make(map[string]interface{}) + flattenJSON(data, "", result) + return result[path] +} + func eventValue(e Event, key string) interface{} { switch evt := e.(type) { case map[string]string: return evt[key] case map[string]interface{}: - return evt[key] + return getNestedValue(evt, key) default: return "" } diff --git a/evaluator/evaluate_search.go b/evaluator/evaluate_search.go index 43fbb5f..400ff41 100644 --- a/evaluator/evaluate_search.go +++ b/evaluator/evaluate_search.go @@ -4,13 +4,14 @@ import ( "context" "encoding/json" "fmt" - "github.com/PaesslerAG/jsonpath" - "github.com/bradleyjkemp/sigma-go" - "github.com/bradleyjkemp/sigma-go/evaluator/modifiers" "path" "reflect" "regexp" "strings" + + "github.com/PaesslerAG/jsonpath" + "github.com/bradleyjkemp/sigma-go" + "github.com/bradleyjkemp/sigma-go/evaluator/modifiers" ) func (rule RuleEvaluator) evaluateSearchExpression(search sigma.SearchExpr, searchResults func(string) bool) bool { @@ -174,7 +175,8 @@ func (rule *RuleEvaluator) GetFieldValuesFromEvent(field string, event Event) ([ var actualValues []interface{} if len(rule.fieldmappings[field]) == 0 { // No FieldMapping exists so use the name directly from the rule - actualValues = []interface{}{eventValue(event, field)} + // actualValues = []interface{}{eventValue(event, field)} + actualValues = toGenericSlice(eventValue(event, field)) } else { // FieldMapping does exist so check each of the possible mapped names instead of the name from the rule for _, mapping := range rule.fieldmappings[field] { diff --git a/rule_parser.go b/rule_parser.go index 821efd3..1951666 100644 --- a/rule_parser.go +++ b/rule_parser.go @@ -63,9 +63,14 @@ func (d *Detection) UnmarshalYAML(node *yaml.Node) error { return err } case "timeframe": - if err := node.Decode(&d.Timeframe); err != nil { + if t, err := time.ParseDuration(value.Value); err != nil { return err + } else { + d.Timeframe = t } + // if err := node.Decode(&d.Timeframe); err != nil { + // return err + // } default: search := Search{} if err := search.UnmarshalYAML(value); err != nil { diff --git a/rule_parser_test.go b/rule_parser_test.go index 68fcf79..366a697 100644 --- a/rule_parser_test.go +++ b/rule_parser_test.go @@ -1,13 +1,14 @@ package sigma import ( - "github.com/google/go-cmp/cmp/cmpopts" "io/ioutil" "os" "path/filepath" "strings" "testing" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/bradleyjkemp/cupaloy/v2" "github.com/google/go-cmp/cmp" "gopkg.in/yaml.v3" @@ -39,6 +40,32 @@ func TestParseRule(t *testing.T) { } } +func TestParseRule2(t *testing.T) { + err := filepath.Walk("./testdata/", func(path string, info os.FileInfo, err error) error { + if !strings.HasSuffix(path, "aws_enum_backup.rule.yml") { + return nil + } + + t.Run(strings.TrimSuffix(filepath.Base(path), "aws_enum_backup.rule.yml"), func(t *testing.T) { + contents, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("failed reading test input: %v", err) + } + + rule, err := ParseRule(contents) + if err != nil { + t.Fatalf("error parsing rule: %v", err) + } + + cupaloy.New(cupaloy.SnapshotSubdirectory("testdata")).SnapshotT(t, rule) + }) + return nil + }) + if err != nil { + t.Fatal(err) + } +} + func TestMarshalRule(t *testing.T) { err := filepath.Walk("./testdata/", func(path string, info os.FileInfo, err error) error { if !strings.HasSuffix(path, ".rule.yml") { diff --git a/testdata/aws_enum_backup.rule.yml b/testdata/aws_enum_backup.rule.yml new file mode 100644 index 0000000..baefa3a --- /dev/null +++ b/testdata/aws_enum_backup.rule.yml @@ -0,0 +1,35 @@ +# Copied from https://github.com/SigmaHQ/sigma/blob/b062d8ad650054cd20836d5ba38031090b8d8c33/unsupported/cloud/aws_enum_backup.yml#L29 +# under license https://github.com/Neo23x0/sigma/blob/master/LICENSE.Detection.Rules.md +title: Potential Backup Enumeration on AWS +id: 76255e09-755e-4675-8b6b-dbce9842cd2a +status: unsupported +description: Detects potential enumeration activity targeting an AWS instance backups +references: + - https://unit42.paloaltonetworks.com/compromised-cloud-compute-credentials/ +author: Janantha Marasinghe +date: 2022/12/13 +modified: 2023/03/24 +tags: + - attack.discovery + - attack.t1580 +logsource: + product: aws + service: cloudtrail +detection: + selection: + eventSource: 'ec2.amazonaws.com' + eventName: + - 'GetPasswordData' + - 'GetEbsEncryptionByDefault' + - 'GetEbsDefaultKmsKeyId' + - 'GetBucketReplication' + - 'DescribeVolumes' + - 'DescribeVolumesModifications' + - 'DescribeSnapshotAttribute' + - 'DescribeSnapshotTierStatus' + - 'DescribeImages' + timeframe: 10m + condition: selection | count() > 5 +falsepositives: + - Unknown +level: medium \ No newline at end of file