diff --git a/tools/config/athena.yml b/tools/config/athena.yml new file mode 100644 index 000000000..601567f5d --- /dev/null +++ b/tools/config/athena.yml @@ -0,0 +1,10 @@ +title: AWS Athena +order: 20 +backends: + - athena +fieldmappings: + eventName: eventname + eventSource: eventsource + eventType: eventtype + userIdentity.type: useridentity.type + userIdentity.sessionContext.sessionIssuer.type: sessionissuer diff --git a/tools/config/carbon-black-eedr.yml b/tools/config/carbon-black-eedr.yml index 0e7c4fff6..da98001d2 100644 --- a/tools/config/carbon-black-eedr.yml +++ b/tools/config/carbon-black-eedr.yml @@ -31,6 +31,7 @@ fieldmappings: - netconn_ipv6 DestinationPort: netconn_port Device: device_name + EventID: event_id FileName: - process_name - process_original_filename diff --git a/tools/config/crowdstrike.yml b/tools/config/crowdstrike.yml index 25309412a..f1eb55934 100644 --- a/tools/config/crowdstrike.yml +++ b/tools/config/crowdstrike.yml @@ -16,4 +16,18 @@ fieldmappings: EventID: EventID CommandLine: Commandline Command_Line: Commandline + cmdline: Commandline Image: ImageFileName + TargetFilename: TargetFilename + TaskName: TaskName + Image: ImageFileName + image: ImageFileName + image_path: ImageFileName + OriginalFileName: ImageFileName + sha1: SHA1HashData + user: UserName + TaskName: TaskName + ParentImage: ParentBaseFileName + parent_image: ParentBaseFileName + ServiceName: ServiceName + TargetFilename: TargetFileName diff --git a/tools/sigma/backends/athena.py b/tools/sigma/backends/athena.py new file mode 100644 index 000000000..81957ac0e --- /dev/null +++ b/tools/sigma/backends/athena.py @@ -0,0 +1,304 @@ +# Output backends for sigmac +# Copyright 2019 Jayden Zheng +# Copyright 2020 Jonas Hagg +# Copyright 2021 wagga (https://github.com/wagga40/) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. + +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +import re +import sigma +from sigma.backends.base import SingleTextQueryBackend +from sigma.parser.condition import SigmaAggregationParser, NodeSubexpression, ConditionAND, ConditionOR, ConditionNOT +from sigma.parser.exceptions import SigmaParseError +class SQLBackend(SingleTextQueryBackend): + """Converts Sigma rule into SQL query""" + identifier = "athena" + active = True + + andToken = " AND " # Token used for linking expressions with logical AND + orToken = " OR " # Same for OR + notToken = "NOT " # Same for NOT + subExpression = "(%s)" # Syntax for subexpressions, usually parenthesis around it. %s is inner expression + listExpression = "(%s)" # Syntax for lists, %s are list items separated with listSeparator + listSeparator = ", " # Character for separation of list items + valueExpression = "\'%s\'" # Expression of values, %s represents value + nullExpression = "-%s=*" # Expression of queries for null values or non-existing fields. %s is field name + notNullExpression = "%s=*" # Expression of queries for not null values. %s is field name + mapExpression = "%s = %s" # Syntax for field/value conditions. First %s is fieldname, second is value + mapMulti = "%s IN %s" # Syntax for field/value conditions. First %s is fieldname, second is value + mapWildcard = "%s LIKE %s ESCAPE \'\\\'"# Syntax for swapping wildcard conditions: Adding \ as escape character + mapSource = "%s=%s" # Syntax for sourcetype + mapListsSpecialHandling = False # Same handling for map items with list values as for normal values (strings, integers) if True, generateMapItemListNode method is called with node + mapListValueExpression = "%s OR %s" # Syntax for field/value condititons where map value is a list + mapLength = "(%s %s)" + + options = SingleTextQueryBackend.options + ( + ("table", "eventlog", "Use this option to specify table name.", None), + ("select", "*", "Use this option to specify fields you want to select. Example: \"--backend-option select=xxx,yyy\"", None), + ("selection", False, "Use this option to enable fields selection from Sigma rules.", None), + ) + + selection_enabled = False + + + def __init__(self, sigmaconfig, options): + super().__init__(sigmaconfig) + + if "table" in options: + self.table = options["table"] + else: + self.table = "eventlog" + + if "select" in options and options["select"]: + self.select_fields = options["select"].split(',') + else: + self.select_fields = list() + + if "selection" in options: + self.selection_enabled = True + + def generateANDNode(self, node): + generated = [ self.generateNode(val) for val in node ] + filtered = [ g for g in generated if g is not None ] + if filtered: + return self.andToken.join(filtered) + else: + return None + + def generateORNode(self, node): + generated = [ self.generateNode(val) for val in node ] + filtered = [ g for g in generated if g is not None ] + if filtered: + return self.orToken.join(filtered) + else: + return None + + def generateNOTNode(self, node): + generated = self.generateNode(node.item) + if generated is not None: + return self.notToken + generated + else: + return None + + def generateSubexpressionNode(self, node): + generated = self.generateNode(node.items) + if generated: + return self.subExpression % generated + else: + return None + + def generateListNode(self, node): + if not set([type(value) for value in node]).issubset({str, int}): + raise TypeError("List values must be strings or numbers") + return self.listExpression % (self.listSeparator.join([self.generateNode(value) for value in node])) + + def generateMapItemNode(self, node): + fieldname, value = node + transformed_fieldname = self.fieldNameMapping(fieldname, value) + + has_wildcard = re.search(r"((\\(\*|\?|\\))|\*|\?|_|%)", self.generateNode(value)) + + if "," in self.generateNode(value) and not has_wildcard: + return self.mapMulti % (transformed_fieldname, self.generateNode(value)) + elif "LENGTH" in transformed_fieldname: + return self.mapLength % (transformed_fieldname, value) + elif type(value) == list: + return self.generateMapItemListNode(transformed_fieldname, value) + elif self.mapListsSpecialHandling == False and type(value) in (str, int, list) or self.mapListsSpecialHandling == True and type(value) in (str, int): + if has_wildcard: + return self.mapWildcard % (transformed_fieldname, self.generateNode(value)) + else: + return self.mapExpression % (transformed_fieldname, self.generateNode(value)) + elif "sourcetype" in transformed_fieldname: + return self.mapSource % (transformed_fieldname, self.generateNode(value)) + elif has_wildcard: + return self.mapWildcard % (transformed_fieldname, self.generateNode(value)) + else: + raise TypeError("Backend does not support map values of type " + str(type(value))) + + def generateMapItemListNode(self, key, value): + return "(" + (" OR ".join([self.mapWildcard % (key, self.generateValueNode(item)) for item in value])) + ")" + + def generateValueNode(self, node): + return self.valueExpression % (self.cleanValue(str(node))) + + def generateNULLValueNode(self, node): + return self.nullExpression % (node.item) + + def generateNotNULLValueNode(self, node): + return self.notNullExpression % (node.item) + + def fieldNameMapping(self, fieldname, value): + """ + Alter field names depending on the value(s). Backends may use this method to perform a final transformation of the field name + in addition to the field mapping defined in the conversion configuration. The field name passed to this method was already + transformed from the original name given in the Sigma rule. + """ + return fieldname + + def generate(self, sigmaparser): + """Method is called for each sigma rule and receives the parsed rule (SigmaParser)""" + fields = list() + + # First add fields specified in the rule + try: + for field in sigmaparser.parsedyaml["fields"]: + mapped = sigmaparser.config.get_fieldmapping(field).resolve_fieldname(field, sigmaparser) + if type(mapped) == str: + fields.append(mapped) + elif type(mapped) == list: + fields.extend(mapped) + else: + raise TypeError("Field mapping must return string or list") + + except KeyError: # no 'fields' attribute + pass + + # Then add fields specified in the backend configuration + fields.extend(self.select_fields) + + # In case select is specified in backend option, we want to enable selection + if len(self.select_fields) > 0: + self.selection_enabled = True + + # Finally, in case fields is empty, add the default value + if not fields: + fields = list("*") + + for parsed in sigmaparser.condparsed: + if self.selection_enabled: + query = self._generateQueryWithFields(parsed, fields) + else: + query = self.generateQuery(parsed) + before = self.generateBefore(parsed) + after = self.generateAfter(parsed) + + result = "" + if before is not None: + result = before + if query is not None: + result += query + if after is not None: + result += after + + return result + + def cleanValue(self, val): + if not isinstance(val, str): + return str(val) + + #Single backlashes which are not in front of * or ? are doulbed + val = re.sub(r"(? full text search + #False: no subexpression found, where a full text search is needed + + def _evaluateCondition(condition): + #Helper function to evaluate conditions + if type(condition) not in [ConditionAND, ConditionOR, ConditionNOT]: + raise NotImplementedError("Error in recursive Search logic") + + results = [] + for elem in condition.items: + if isinstance(elem, NodeSubexpression): + results.append(self._recursiveFtsSearch(elem)) + if isinstance(elem, ConditionNOT): + results.append(_evaluateCondition(elem)) + if isinstance(elem, tuple): + results.append(False) + if type(elem) in (str, int, list): + return True + return any(results) + + if type(subexpression) in [str, int, list]: + return True + elif type(subexpression) in [tuple]: + return False + + if not isinstance(subexpression, NodeSubexpression): + raise NotImplementedError("Error in recursive Search logic") + + if isinstance(subexpression.items, NodeSubexpression): + return self._recursiveFtsSearch(subexpression.items) + elif type(subexpression.items) in [ConditionAND, ConditionOR, ConditionNOT]: + return _evaluateCondition(subexpression.items) diff --git a/tools/sigma/backends/carbonblack.py b/tools/sigma/backends/carbonblack.py index cf5175777..20325e6e1 100644 --- a/tools/sigma/backends/carbonblack.py +++ b/tools/sigma/backends/carbonblack.py @@ -157,6 +157,7 @@ class CarbonBlackQueryBackend(CarbonBlackWildcardHandlingMixin, SingleTextQueryB def generateMapItemNode(self, node): fieldname, value = node + value = str(value) if fieldname == "EventID" and (type(value) is str or type(value) is int): fieldname = self.generateEventKey(value) value = self.generateEventValue(value) diff --git a/tools/sigma/backends/splunk.py b/tools/sigma/backends/splunk.py index c2bfb96b5..ca294f5c3 100644 --- a/tools/sigma/backends/splunk.py +++ b/tools/sigma/backends/splunk.py @@ -178,7 +178,7 @@ class CrowdStrikeBackend(SplunkBackend): def generate(self, sigmaparser): lgs = sigmaparser.parsedyaml.get("logsource") - if lgs.get("product") == "windows" and (lgs.get("service") == "sysmon" or lgs.get("category") == "process_creation"): + if lgs.get("product") == "windows" and (lgs.get("service") == "sysmon" or lgs.get("category") == "process_creation" or lgs.get("service") == "security"): fieldmappings = sigmaparser.config.fieldmappings detections = sigmaparser.definitions all_fields = dict() @@ -210,4 +210,4 @@ class CrowdStrikeBackend(SplunkBackend): raise NotImplementedError("Not supported logsources!") def generateMapItemTypedNode(self, fieldname, value): - return super().generateMapItemTypedNode(fieldname=fieldname, value=value) \ No newline at end of file + return super().generateMapItemTypedNode(fieldname=fieldname, value=value)