diff --git a/tools/sigma/backends/sumologic.py b/tools/sigma/backends/sumologic.py index e1e6cb70..b2d4f2eb 100644 --- a/tools/sigma/backends/sumologic.py +++ b/tools/sigma/backends/sumologic.py @@ -31,6 +31,8 @@ class SumoLogicBackend(SingleTextQueryBackend): """Converts Sigma rule into SumoLogic query""" identifier = "sumologic" active = True + #debug = True + debug = False index_field = "_index" #reEscape = re.compile('("|\\\\(?![*?]))') @@ -53,6 +55,12 @@ class SumoLogicBackend(SingleTextQueryBackend): def generateAggregation(self, agg): if agg == None: return "" + # lnx_shell_priv_esc_prep.yml + #print("DEBUG generateAggregation(): %s, %s, %s, %s" % (agg.aggfunc_notrans, agg.aggfield, agg.groupfield, agg.cond_op)) + if agg.groupfield == 'host': + agg.groupfield = 'hostname' + if agg.aggfunc_notrans == 'count() by': + agg.aggfunc_notrans = 'count by' if agg.aggfunc == sigma.parser.condition.SigmaAggregationParser.AGGFUNC_NEAR: raise NotImplementedError("The 'near' aggregation operator is not yet implemented for this backend") # WIP @@ -65,34 +73,78 @@ class SumoLogicBackend(SingleTextQueryBackend): if agg.groupfield == None: #return " | %s(%s) | when _count %s %s" % (agg.aggfunc_notrans, agg.aggfield or "", agg.cond_op, agg.condition) return " | %s %s | where _count %s %s" % (agg.aggfunc_notrans, agg.aggfield or "", agg.cond_op, agg.condition) + elif agg.groupfield != None: + return " | %s by %s | where _count %s %s" % (agg.aggfunc_notrans, agg.groupfield or "", agg.cond_op, agg.condition) else: - return " | %s %s by %s | where _count %s %s" % (agg.aggfunc_notrans, agg.aggfield or "", agg.groupfield or "", agg.cond_op, agg.condition) + return " | %s(%s) by %s | where _count %s %s" % (agg.aggfunc_notrans, agg.aggfield or "", agg.groupfield or "", agg.cond_op, agg.condition) def generateBefore(self, parsed): # not required but makes query faster, especially if no FER or _index/_sourceCategory + if self.debug: + print("DEBUG generateBefore(): %s, %s, %s, %s" % (self.logname, self.indices, self.product, self.service)) if self.logname: return "%s " % self.logname - if self.service: - return "%s %s " % (self.product, self.service) + # FIXME! don't get backend config mapping through generate() => mapping inside script + if not self.indices and self.product == 'windows' and self.service: + return "_index=WINDOWS %s " % (self.service) + if not self.indices and self.product == 'windows': + return "_index=WINDOWS " + if not self.indices and self.product == 'linux' and self.service == 'auditd': + return "_index=AUDITD " + if not self.indices and self.product == 'linux' and self.service == 'osqueryd': + return "_index=OSQUERY " + if not self.indices and self.product == 'linux': + return "_index=LINUX " + if self.product == 'antivirus': + return "_index=ANTIVIRUS " + if self.category == 'firewall': + return "_index=FIREWALL " + if self.indices: + return "_index=%s " % self.indices return "" def generate(self, sigmaparser): try: self.product = sigmaparser.parsedyaml['logsource']['product'] # OS or Software - self.service = sigmaparser.parsedyaml['logsource']['service'] # Channel except KeyError: self.product = None + try: + self.service = sigmaparser.parsedyaml['logsource']['service'] # Channel + except KeyError: self.service = None - + try: + self.category = sigmaparser.parsedyaml['logsource']['category'] # Channel + except KeyError: + self.category = None + # FIXME! don't get backend config mapping + if self.debug: + print("DEBUG sigmaconfig: %s" % self.sigmaconfig) + self.indices = sigmaparser.get_logsource().index + if self.debug: + print("DEBUG indices: %s" % self.indices) + if len(self.indices) == 0: + self.indices = None try: self.interval = sigmaparser.parsedyaml['detection']['timeframe'] except: pass for parsed in sigmaparser.condparsed: + if self.debug: + print("DEBUG generate0: %s" % parsed) query = self.generateQuery(parsed) + # FIXME! exclude if expression is regexp but anyway, not directly supported. + # Not doing if aggregation ('| count') or key ('=') + if not (query.startswith('"') and query.endswith('"')) and not (query.startswith('(') and query.endswith(')')) and not ('|' in query) and not ('=' in query): + query = '"%s"' % query + if self.debug: + print("DEBUG generate1q: %s" % query) before = self.generateBefore(parsed) + if self.debug: + print("DEBUG generate1b: %s" % before) after = self.generateAfter(parsed) + if self.debug: + print("DEBUG generate1a: %s" % after) result = "" if before is not None: @@ -103,7 +155,11 @@ class SumoLogicBackend(SingleTextQueryBackend): result += after # adding parenthesis here in case 2 rules are aggregated together - ex: win_possible_applocker_bypass - return "(" + result + ")" + # but does not work if count, where or other piped statements... + if '|' in result: + return result + else: + return "(" + result + ")" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -124,9 +180,12 @@ class SumoLogicBackend(SingleTextQueryBackend): # Clearing values from special characters. # Sumologic: only removing '*' (in quotes, is litteral. without, is wildcard) and '"' def CleanNode(self, node): + if self.debug: + print("DEBUG CleanNode0: %s" % node) #search_ptrn = re.compile(r"[\/@?#&%*\(\)\"]") - search_ptrn = re.compile(r"[*]") - replace_ptrn = re.compile(r"[*]") + #search_ptrn = re.compile(r"[*\"\\]") + search_ptrn = re.compile(r"[*\"\\]") + replace_ptrn = re.compile(r"[*\"\\]") match = search_ptrn.search(str(node)) new_node = list() if match: @@ -136,17 +195,30 @@ class SumoLogicBackend(SingleTextQueryBackend): else: new_node.append(node) node = new_node + if self.debug: + print("DEBUG CleanNode1: %s" % node) return node # Clearing values from special characters. def generateMapItemNode(self, node): + if self.debug: + try: + print("DEBUG generateMapItemNode0: %s" % node) + except TypeError: + print("EXCEPT generateMapItemNode0") key, value = node if key in self.allowedFieldsList: if self.mapListsSpecialHandling == False and type(value) in ( str, int, list) or self.mapListsSpecialHandling == True and type(value) in (str, int): if key in ("LogName","source"): self.logname = value - return self.mapExpression % (key, value) + if self.debug: + try: + print("DEBUG generateMapItemNode1: %s" % node) + except TypeError: + print("EXCEPT generateMapItemNode1") + # need cleanValue if sigma entry with single quote + return self.mapExpression % (key, self.cleanValue(value, key)) elif type(value) is list: return self.generateMapItemListNode(key, value) else: @@ -162,7 +234,11 @@ class SumoLogicBackend(SingleTextQueryBackend): else: new_value.append(value) if len(new_value)==1: - return "(" + self.generateANDNode(new_value) + ")" + if self.generateANDNode(new_value): + return "(" + self.generateANDNode(new_value) + ")" + else: + # if after cleaning node, it is empty but there is AND statement... make it true. + return "true" else: return "(" + self.generateORNode(new_value) + ")" else: @@ -184,50 +260,76 @@ class SumoLogicBackend(SingleTextQueryBackend): raise TypeError("Backend does not support map values of type " + str(type(value))) # from mixins.py - #FIXME! input in simple quotes are not passing through this function. ex: rules/windows/sysmon/sysmon_vul_java_remote_debugging.yml, rules/apt/apt_sofacy_zebrocy.yml + # input in simple quotes are not passing through this function. ex: rules/windows/sysmon/sysmon_vul_java_remote_debugging.yml, rules/apt/apt_sofacy_zebrocy.yml # => OK only if field entry with list, not string + # => generateNode: call cleanValue def cleanValue(self, val, key = ''): - print("DEBUG cleanValue0: %s" % val) + if self.debug: + print("DEBUG cleanValue0: %s" % val) if self.reEscape: val = self.reEscape.sub(self.escapeSubst, val) if self.reClear: val = self.reClear.sub("", val) # in sumologic, if key, can use wildcard outside of double quotes. if inside, it's litteral if key: - val = re.sub(r'(.+?)\*(.+?)', '\g<1>"*"\g<2>', val, 0) + # FIXME! few rules like apt_unidentified_nov_18.yml trigger an exception on following line??? + val = re.sub(r'\"', '\\"', val) + val = re.sub(r'(.+)\*(.+)', '"\g<1>"*"\g<2>"', val, 0) val = re.sub(r'^\*', '*"', val) val = re.sub(r'\*$', '"*', val) + if self.debug: + print("DEBUG cleanValue0a: %s" % val) # if unbalanced wildcard? if val.startswith('*"') and not (val.endswith('"*') or val.endswith('"')): val = val + '"' if val.endswith('"*') and not (val.startswith('*"') or val.startswith('"')): val = '"' + val + if self.debug: + print("DEBUG cleanValue0b: %s" % val) # double escape if end quote if val.endswith('\\"*') and not val.endswith('\\\\"*'): val = re.sub(r'\\"\*$', '\\\\\\"*', val) - print("DEBUG cleanValue1: %s" % val) + if self.debug: + print("DEBUG cleanValue0c: %s" % val) + #if not key and not (val.startswith('"') and val.endswith('"')) and not (val.startswith('(') and val.endswith(')')) and not ('|' in val) and val: + # apt_babyshark.yml + if not (val.startswith('"') and val.endswith('"')) and not (val.startswith('(') and val.endswith(')')) and not ('|' in val) and not ('*' in val) and val: + val = '"%s"' % val + if self.debug: + print("DEBUG cleanValue1: %s" % val) return val # for keywords values with space def generateValueNode(self, node, key = ''): + if self.debug: + print("DEBUG generateValueNode0: %s, %s" % (node, key)) + cV = self.cleanValue(str(node), key) + if self.debug: + print("DEBUG generateValueNode1: %s, %s" % (node, key)) if type(node) is int: - return self.cleanValue(str(node), key) - if 'AND' in node: - return "(" + self.cleanValue(str(node), key) + ")" + return cV + if 'AND' in node and cV: + return "(" + cV + ")" else: - return self.cleanValue(str(node), key) + return cV def generateMapItemListNode(self, key, value): + if self.debug: + print("DEBUG generateMapItemListNode0: %s, %s" % (key, value)) itemslist = list() for item in value: if key in self.allowedFieldsList: itemslist.append('%s = %s' % (key, self.generateValueNode(item, key))) else: itemslist.append('%s' % (self.generateValueNode(item))) + if self.debug: + print("DEBUG generateMapItemListNode1: %s, %s" % (key, value)) return "(" + " OR ".join(itemslist) + ")" # generateORNode algorithm for ArcSightBackend & SumoLogicBackend class. def generateORNode(self, node): + if self.debug: + print("DEBUG generateORNode0: %s" % node) if type(node) == ConditionOR and all(isinstance(item, str) for item in node): new_value = list() for value in node: @@ -236,7 +338,11 @@ class SumoLogicBackend(SingleTextQueryBackend): new_value.append(self.andToken.join([self.valueExpression % val for val in value])) else: new_value.append(value) + if self.debug: + print("DEBUG generateORNode1: %s" % node) return "(" + self.orToken.join([self.generateNode(val) for val in new_value]) + ")" + if self.debug: + print("DEBUG generateORNode1b: %s" % node) return "(" + self.orToken.join([self.generateNode(val) for val in node]) + ")" def fieldNameMapping(self, fieldname, value):