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 from .mixins import MultiRuleOutputMixin from sigma.parser.modifiers.transform import SigmaContainsModifier, SigmaStartswithModifier, SigmaEndswithModifier from sigma.parser.modifiers.type import SigmaRegularExpressionModifier from ..parser.modifiers.base import SigmaTypeModifier gUnsupportedCategories = {} def convert_sigma_level_to_uberagent_risk_score(level): """Converts the given Sigma rule level to uberAgent ESA RiskScore property.""" levels = { "critical": 100, "high": 75, "medium": 50, "low": 25 } if level in levels: return levels[level] return 0 def convert_sigma_name_to_uberagent_tag(name): """Converts the given Sigma rule name to uberAgent ESA Tag property.""" tag = name.lower().replace(" ", "-") tag = re.sub(r"-{2,}", "-", tag, 0, re.IGNORECASE) return tag def convert_sigma_category_to_uberagent_event_type(category): categories = { "process_creation": "Process.Start", "image_load": "Image.Load", "dns": "Dns.Query", "dns_query": "Dns.Query", "network_connection": "Net.Any", "firewall": "Net.Any", "create_remote_thread": "Process.CreateRemoteThread", "registry_event": "Reg.Any" } if category in categories: return categories[category] if category in gUnsupportedCategories: gUnsupportedCategories[category] += 1 else: gUnsupportedCategories[category] = 1 return None def is_sigma_category_supported(category): """Returns whether uberAgent ESA knows the given category or not.""" return convert_sigma_category_to_uberagent_event_type(category) is not None class IgnoreTypedModifierException(Exception): """ IgnoreTypedModifierException Helper class to ignore exceptions of type identifiers that are not yet supported. """ pass class IgnoreFieldException(Exception): """ IgnoreFieldException Helper class to ignore exceptions of specific fields that are not yet supported. """ pass class IgnoreAggregationException(Exception): """ IgnoreAggregationException Helper class to ignore exceptions of aggregation rules that are not yet supported. """ class MalformedRuleException(Exception): """ MalformedRuleException Helper class to ignore exceptions of malformed rules. """ pass class ActivityMonitoringRule: """ ActivityMonitoringRule This class wraps a [ActivityMonitoringRule] configuration block. """ def __init__(self): self.name = "" self.event_type = None self.tag = "" self.query = "" self.risk_score = 0 self.description = "" self.sigma_level = "" # Specifies the properties that are being evaluated and send to the backend # if an Activity Monitoring rule is matched. self.generic_properties = { "Process.": [ "Process.Hash.MD5", "Process.Hash.SHA1", "Process.Hash.SHA256", "Process.Hash.IMP" ], "Image.": [ "Image.Name", "Image.Path", "Image.Hash.MD5", "Image.Hash.SHA1", "Image.Hash.SHA256", "Image.Hash.IMP" ], "Net.": [ "Net.Target.Ip", "Net.Target.Name", "Net.Target.Port", "Net.Target.Protocol", "Net.Source.Ip", "Net.Source.Port", ], "Reg.": [ "Reg.Key.Path", "Reg.Key.Path.New", "Reg.Key.Path.Old", "Reg.Key.Name", "Reg.Parent.Key.Path", "Reg.Value.Name", "Reg.File.Name", "Reg.Key.Sddl", "Reg.Key.Hive", "Reg.Key.Target" ], "Dns.": [ "Dns.QueryRequest", "Dns.QueryResponse" ] } def set_query(self, query): """Sets the generated query.""" self.query = query def set_name(self, name): """Sets the RuleName.""" self.name = name def set_tag(self, tag): """Sets the Tag property.""" self.tag = tag def set_event_type(self, event_type): """Sets the EventType property.""" self.event_type = event_type def set_risk_score(self, risk_score): """Sets the RiskScore property.""" self.risk_score = risk_score def set_sigma_level(self, level): """Sets the Sigma rule level.""" self.sigma_level = level def set_description(self, description): """Set the Description property.""" self.description = description def _prefixed_tag(self): prefixes = { "Process.Start": "proc-start" } if self.event_type not in prefixes: return self.tag return "{}-{}".format(prefixes[self.event_type], self.tag) def __str__(self): """Builds and returns the [ActivityMonitoringRule] configuration block.""" result = "[ActivityMonitoringRule]\n" # The Description is optional. if len(self.description) > 0: for description_line in self.description.splitlines(): result += "# {}\n".format(description_line) # Make sure all required properties have at least a value that is somehow usable. if self.event_type is None: raise MalformedRuleException() if len(self.tag) == 0: raise MalformedRuleException() if len(self.name) == 0: raise MalformedRuleException() if len(self.query) == 0: raise MalformedRuleException() result += "RuleName = {}\n".format(self.name) result += "EventType = {}\n".format(self.event_type) result += "Tag = {}\n".format(self._prefixed_tag()) # The RiskScore is optional. # Set it, if a risk_score value is present. if self.risk_score > 0: result += "RiskScore = {}\n".format(self.risk_score) result += "Query = {}\n".format(self.query) if self.event_type == "Reg.Any": result += "Hive = HKLM,HKU\n" counter = 1 for event_type_prefix in self.generic_properties: if self.event_type.startswith(event_type_prefix): for prop in self.generic_properties[event_type_prefix]: # Generic properties are limited to 10. if counter > 10: break result += "GenericProperty{} = {}\n".format(counter, prop) counter += 1 return result def get_parser_properties(sigmaparser): title = sigmaparser.parsedyaml['title'] level = sigmaparser.parsedyaml['level'] description = sigmaparser.parsedyaml['description'] condition = sigmaparser.parsedyaml['detection']['condition'] logsource = sigmaparser.parsedyaml['logsource'] category = '' if 'category' in logsource: category = logsource['category'].lower() product = '' if 'product' in logsource: product = logsource['product'].lower() service = '' if 'service' in logsource: service = logsource['service'].lower() return product, category, service, title, level, condition, description def write_file_header(f, level): f.write("#\n") f.write("# The rules are generated from the Sigma GitHub repository at https://github.com/Neo23x0/sigma\n") f.write("# Follow these steps to get the latest rules from the repository with Python\n") f.write("# 1. Clone the repository locally\n") f.write("# 2. Using a commandline, change working directory to the just cloned repository\n") f.write("# 3. Run sigmac -I --target uberagent -r rules/\n") f.write("#\n") f.write("# The rules in this file are marked with sigma-level: {}\n".format(level)) f.write("#\n\n") class uberAgentBackend(SingleTextQueryBackend): """Converts Sigma rule into uberAgent ESA's process tagging rules.""" identifier = "uberagent" active = True config_required = False rule = None current_category = None # # SingleTextQueryBackend # andToken = " and " orToken = " or " notToken = "not " subExpression = "(%s)" listExpression = "[%s]" listSeparator = ", " valueExpression = "\"%s\"" nullExpression = "%s == ''" notNullExpression = "%s != ''" mapExpression = "%s == %s" mapListsSpecialHandling = True mapListValueExpression = "%s in %s" # Syntax for swapping wildcard conditions: Adding \ as escape character # Wildcard conditions are based on modifiers such as contains, # startswith, endswith mapWildcard = "%s like r%s" # # uberAgent field mapping # fieldMapping = { "commandline": "Process.CommandLine", "image": "Process.Path", "originalfilename": "Process.Name", "imageloaded": "Image.Path", "imagepath": "Image.Path", "parentcommandline": "Parent.CommandLine", "parentprocessname": "Parent.Name", "parentimage": "Parent.Path", "path": "Process.Path", "processcommandline": "Process.CommandLine", "command": "Process.CommandLine", "processname": "Process.Name", "user": "Process.User", "username": "Process.User", "company": "Process.Company" } fieldMappingPerCategory = { "process_creation": { "sha1": "Process.Hash.SHA1", "imphash": "Process.Hash.IMP", "childimage": "Process.Path" # Not yet supported. # "signed": "Process.IsSigned" }, "image_load": { "sha1": "Image.Hash.SHA1", "imphash": "Image.Hash.IMP", "childimage": "Image.Path" # Not yet supported. # "signed": "Image.IsSigned" }, "dns": { "query": "Dns.QueryRequest", "answer": "Dns.QueryResponse" }, "dns_query": { "queryname": "Dns.QueryRequest", }, "network_connection": { "destinationport": "Net.Target.Port", "destinationip": "Net.Target.Ip", "destinationhostname": "Net.Target.Name", "destinationisipv6": "Net.Target.IpIsV6", "sourceport": "Net.Source.Port" }, "firewall": { "destination.port": "Net.Target.Port", "dst_ip": "Net.Target.Ip", "src_ip": "Net.Source.Ip" }, "create_remote_thread": { "targetimage": "Process.Path", "startmodule": "Thread.StartModule", "startfunction": "Thread.StartFunctionName" }, "registry_event": { "targetobject": "Reg.Key.Target", "newname": "Reg.Key.Path.New" } } # We ignore some fields that we don't support yet but we don't want them to # throw errors in the console since we are aware of this. ignoreFieldList = [ "description", "product", "logonid", "integritylevel", "currentdirectory", "parentintegritylevel", "eventid", "parentuser", "parent_domain", "signed", "parentofparentimage", "record_type", # Related to network (DNS). "querystatus", # Related to network (DNS). "initiated", # Related to network connections. Seen as string 'true' / 'false'. "action", # Related to firewall category. "targetprocessaddress", "sourceimage", "eventtype", "details" ] rules = [] def fieldNameMapping(self, fieldname, value): key = fieldname.lower() if self.current_category is not None: if self.current_category in self.fieldMappingPerCategory: if key in self.fieldMappingPerCategory[self.current_category]: return self.fieldMappingPerCategory[self.current_category][key] if key not in self.fieldMapping: if key in self.ignoreFieldList: raise IgnoreFieldException() else: raise NotImplementedError( 'The field name %s in category %s is not implemented.' % (fieldname, self.current_category)) return self.fieldMapping[key] def generateQuery(self, parsed): if parsed.parsedAgg: raise IgnoreAggregationException() return self.generateNode(parsed.parsedSearch) def generate(self, sigmaparser): """Method is called for each sigma rule and receives the parsed rule (SigmaParser)""" product, category, service, title, level, condition, description = get_parser_properties(sigmaparser) # Do not generate a rule if the given category is unsupported by now. if not is_sigma_category_supported(category): return "" # We support windows rules and generic rules that don't have a specific product specifier - such as DNS. if product not in ["windows", ""]: return "" self.current_category = category try: rule = ActivityMonitoringRule() query = super().generate(sigmaparser) if len(query) > 0: rule.set_name(title) rule.set_tag(convert_sigma_name_to_uberagent_tag(title)) rule.set_event_type(convert_sigma_category_to_uberagent_event_type(category)) rule.set_query(query) rule.set_risk_score(convert_sigma_level_to_uberagent_risk_score(level)) rule.set_sigma_level(level) rule.set_description(description) self.rules.append(rule) print("Generated rule <{}>.. [level: {}]".format(rule.name, level)) except IgnoreTypedModifierException: return "" except IgnoreAggregationException: return "" except IgnoreFieldException: return "" except MalformedRuleException: return "" def serialize_file(self, name, level): count = 0 with open(name, "w", encoding='utf8') as file: write_file_header(file, level) for rule in self.rules: try: serialized_rule = str(rule) if rule.sigma_level == level: file.write(serialized_rule + "\n") count = count + 1 except MalformedRuleException: continue file.close() return count def finalize(self): count_critical = self.serialize_file("uberAgent-ESA-am-sigma-critical.conf", "critical") count_high = self.serialize_file("uberAgent-ESA-am-sigma-high.conf", "high") count_low = self.serialize_file("uberAgent-ESA-am-sigma-low.conf", "low") count_medium = self.serialize_file("uberAgent-ESA-am-sigma-medium.conf", "medium") print("Generated {} activity monitoring rules..".format(len(self.rules))) print( "This includes {} critical rules, {} high rules, {} medium rules and {} low rules..".format(count_critical, count_high, count_medium, count_low)) print("There are %d unsupported categories." % len(gUnsupportedCategories)) for category in gUnsupportedCategories: print("Category %s has %d unsupported rules." % (category, gUnsupportedCategories[category])) def generateTypedValueNode(self, node): raise IgnoreTypedModifierException() def generateMapItemTypedNode(self, fieldname, value): raise IgnoreTypedModifierException() def generateMapItemListNode(self, key, value): return "(" + (" or ".join([self.mapWildcard % (key, self.generateValueNode(item)) for item in value])) + ")" def generateMapItemNode(self, node): fieldname, value = node transformed_fieldname = self.fieldNameMapping(fieldname, value) if value is None: return self.nullExpression % (transformed_fieldname,) has_wildcard = re.search(r"((\\(\*|\?|\\))|\*|\?|_|%)", self.generateNode(value)) if "," in self.generateNode(value) and not has_wildcard: return self.mapListValueExpression % (transformed_fieldname, self.generateNode(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 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 cleanValue(self, val): if not isinstance(val, str): return str(val) # Single backlashes which are not in front of * or ? are doubled val = re.sub(r"(?