From 51df5ad8764cd6896a3ef83ad388aebc136d5815 Mon Sep 17 00:00:00 2001 From: vh Date: Tue, 6 Oct 2020 15:07:52 +0300 Subject: [PATCH 01/20] Added: Sumo Logic CSE Rule Backend Updated: Mapping depence on logsource Azure Sentinel Query Backend MDATP: query with few logsources CROWDSTRIKE: fix generateMapItemTypedNode --- tools/config/sumologic-cse.yml | 185 +++++++++++++++ tools/config/sysmon.yml | 9 + tools/sigma/backends/ala.py | 107 ++++++--- tools/sigma/backends/carbonblack.py | 7 +- tools/sigma/backends/humio.py | 12 +- tools/sigma/backends/mdatp.py | 187 ++++++++++----- tools/sigma/backends/splunk.py | 9 + tools/sigma/backends/sumologic.py | 342 ++++++++++++++++++++++------ tools/sigma/backends/sysmon.py | 2 +- tools/sigma/config/mapping.py | 8 + 10 files changed, 702 insertions(+), 166 deletions(-) create mode 100644 tools/config/sumologic-cse.yml create mode 100644 tools/config/sysmon.yml diff --git a/tools/config/sumologic-cse.yml b/tools/config/sumologic-cse.yml new file mode 100644 index 000000000..893e83bec --- /dev/null +++ b/tools/config/sumologic-cse.yml @@ -0,0 +1,185 @@ +title: SumoLogic +order: 20 +backends: + - sumologic-cse + - sumologic-cse-rule +# Sumulogic mapping depends on customer configuration. Adapt to your context! +# typically rule on _sourceCategory, _index or Field Extraction Rules (FER) +# supposing existing FER for service, metdata_vendor, EventID +logsources: + unix: + product: unix + index: UNIX + linux: + product: linux + index: Linux + linux-sshd: + product: linux + service: sshd + index: Linux + linux-auth: + product: linux + service: auth + index: Linux + linux-clamav: + product: linux + service: clamav + index: Linux + windows: + product: windows + index: Windows + conditions: + metdata_vendor: Microsoft + windows-sysmon: + product: windows + service: sysmon + conditions: + metdata_vendor: Microsoft + index: Windows + windows-security: + product: windows + service: security + conditions: + metdata_vendor: Microsoft + index: Security + windows-powershell: + product: windows + service: powershell + conditions: + metdata_vendor: Microsoft + index: Powershell + windows-system: + product: windows + service: system + conditions: + metdata_vendor: Microsoft + index: Windows + windows-dhcp: + product: windows + service: dhcp + conditions: + metdata_vendor: Microsoft + index: Windows + microsoft: + product: gsuite + index: gsuite + apache: + product: apache + service: apache + index: Apache + apache2: + product: apache + index: Apache + nginx: + product: nginx + index: Nginx + cisco: + product: cisco + index: Cisco + webserver: + category: webserver + index: WEBSERVER + firewall: + category: firewall + index: FIREWALL + firewall2: + product: firewall + index: FIREWALL + network-dns: + category: dns + index: DNS + network-dns2: + product: dns + index: DNS + proxy: + category: proxy + index: PROXY + vpn: + product: vpn + index: network + antivirus: + product: antivirus + index: ANTIVIRUS + azure: + product: azure + index: Azure + conditions: + metdata_vendor: Microsoft + azuread: + product: azuread + index: Azure AD + conditions: + metdata_vendor: Microsoft + zeek: + product: zeek + index: zeek + application-sql: + product: sql + index: DATABASE + application-python: + product: python + index: APPLICATIONS + application-django: + product: django + index: Django + application-rails: + product: rails + index: RAILS + application-spring: + product: spring + index: SPRING +# if no index, search in all indexes + +fieldmappings: + EventID: metadata_deviceEventId + event_id: metadata_deviceEventId + Image: baseImage + event_data.Image: baseImage + TargetImage: changeTarget + EventType: changeType + CommandLine: commandLine + Commandline: commandLine + process.args: commandLine + event_data.CommandLine: commandLine + Description: description + + DestinationHostname: dstDevice_hostname + DestinationIp: dstDevice_ip + dst_ip: dstDevice_ip + dst_mac: dstDevice_mac + DestinationPort: dstPort + dst_port: dstPort + + FileName: file_basename + Imphash: file_hash_imphash + hash: file_hash_imphash + file_hash: file_hash_imphash +# :file_hash_md5 +# :file_hash_sha1 +# :file_hash_sha256 + + Path: file_path + path: file_path + + LogonType: logonType + ModuleType: moduleType + ObjectType: objectType + + ParentImage: parentBaseImage + event_data.ParentImage: parentBaseImage + ParentCommandLine: parentCommandLine + ParentProcessName: parentPid + + SourceHostname: srcDevice_hostname + SourceIp: srcDevice_ip + src_ip: srcDevice_ip + src_mac: srcDevice_mac + SourcePort: srcPort + src_port: srcPort + User: user_username + User-Agent: http_userAgent + Protocol: http_url_protocol + + destination.domain: http_url_rootDomain + domain: http_url_rootDomain + diff --git a/tools/config/sysmon.yml b/tools/config/sysmon.yml new file mode 100644 index 000000000..bf0a3e0b9 --- /dev/null +++ b/tools/config/sysmon.yml @@ -0,0 +1,9 @@ +title: Sysmon +order: 20 +backends: + - sysmon +fieldmappings: + event_id: EventID + event_data.ParentImage: ParentImage + event_data.CommandLine: CommandLine + event_data.Image: Image \ No newline at end of file diff --git a/tools/sigma/backends/ala.py b/tools/sigma/backends/ala.py index 3bbbec544..f70a9dbf1 100644 --- a/tools/sigma/backends/ala.py +++ b/tools/sigma/backends/ala.py @@ -110,62 +110,95 @@ class AzureLogAnalyticsBackend(SingleTextQueryBackend): if isinstance(val, str): if "*" in val[1:-1]: # value contains * inside string - use regex match op = "matches regex" - val = re.sub('\\*', '.*', val) + val = re.sub('(\\\\\*|\*)', '.*', val) if "\\" in val: - return "%s \"(?i)%s\"" % (op, val) - return "%s \"(?i)%s\"" % (op, val) + val = "@'(?i)%s'" % (val) + else: + val = "'(?i)%s'" % (val) + return "%s %s" % (op, self.cleanValue(val)) elif val.startswith("*") or val.endswith("*"): - op = "contains" + if val.startswith("*") and val.endswith("*"): + op = "contains" + elif val.startswith("*"): + op = "endswith" + elif val.endswith("*"): + op = "startswith" val = re.sub('([".^$]|(?![*?]))', '\g<1>', val) - val = re.sub('\\*', '', val) + val = re.sub('(\\\\\*|\*)', '.*', val) val = re.sub('\\?', '.', val) - # if "\\" in val: - # return "%s @\"%s\"" % (op, val) - return "%s \"%s\"" % (op, val) - # elif "\\" in val: - # return "%s @\"%s\"" % (op, val) - return "%s \"%s\"" % (op, val) + if "\\" in val: + return "%s @'%s'" % (op, self.cleanValue(val)) + return "%s '%s'" % (op, self.cleanValue(val)) + elif "\\" in val: + return "%s @'%s'" % (op, self.cleanValue(val)) + return "%s \"%s\"" % (op, self.cleanValue(val)) - def generate(self, sigmaparser): - self.table = None - self.category = sigmaparser.parsedyaml['logsource'].setdefault('category', None) - self.product = sigmaparser.parsedyaml['logsource'].setdefault('product', None) - self.service = sigmaparser.parsedyaml['logsource'].setdefault('service', None) - - detection = sigmaparser.parsedyaml.get("detection", {}) - if "keywords" in detection.keys(): - return super().generate(sigmaparser) - - if self.category == "process_creation": - self.table = "SecurityEvent" - self.eventid = "1" - elif self.service == "security": - self.table = "SecurityEvent" - elif self.service == "sysmon": + def getTable(self, sigmaparser): + if self.category == "process_creation" and len(set(sigmaparser.values.keys()) - {"Image", "ParentImage", + "CommandLine"}) == 0: + self.table = "SecurityEvent | where EventID == 4688 " + self.eventid = "4688" + elif self.category == "process_creation": self.table = "SysmonEvent" - elif self.service == "powershell": + self.eventid = "1" + elif self.service and self.service.lower() == "security": + self.table = "SecurityEvent" + elif self.service and self.service.lower() == "sysmon": + self.table = "SysmonEvent" + elif self.service and self.service.lower() == "powershell": self.table = "Event" - elif self.service == "office365": + elif self.service and self.service.lower() == "office365": self.table = "OfficeActivity" - elif self.service == "azuread": + elif self.service and self.service.lower() == "azuread": self.table = "AuditLogs" - elif self.service == "azureactivity": + elif self.service and self.service.lower() == "azureactivity": self.table = "AzureActivity" else: if self.service: if "-" in self.service: - self.table = "-".join([item.title() for item in self.service.split("-")]) + self.table = "-".join([item.capitalize() for item in self.service.split("-")]) elif "_" in self.service: - self.table = "_".join([item.title() for item in self.service.split("_")]) + self.table = "_".join([item.capitalize() for item in self.service.split("_")]) else: - self.table = self.service.title() + if self.service.islower() or self.service.isupper(): + self.table = self.service.capitalize() + else: + self.table = self.service elif self.product: if "-" in self.product: - self.table = "-".join([item.title() for item in self.product.split("-")]) + self.table = "-".join([item.capitalize() for item in self.product.split("-")]) elif "_" in self.product: - self.table = "_".join([item.title() for item in self.product.split("_")]) + self.table = "_".join([item.capitalize() for item in self.product.split("_")]) else: - self.table = self.product.title() + if self.product.islower() or self.product.isupper(): + self.table = self.product.capitalize() + else: + self.table = self.product + elif self.category: + if "-" in self.category: + self.table = "-".join([item.capitalize() for item in self.category.split("-")]) + elif "_" in self.category: + self.table = "_".join([item.capitalize() for item in self.category.split("_")]) + else: + if self.category.islower() or self.category.isupper(): + self.table = self.category.capitalize() + else: + self.table = self.category + + def generate(self, sigmaparser): + try: + self.category = sigmaparser.parsedyaml['logsource'].setdefault('category', None) + self.product = sigmaparser.parsedyaml['logsource'].setdefault('product', None) + self.service = sigmaparser.parsedyaml['logsource'].setdefault('service', None) + except KeyError: + self.category = None + self.product = None + self.service = None + detection = sigmaparser.parsedyaml.get("detection", {}) + if "keywords" in detection.keys(): + return super().generate(sigmaparser) + if self.table is None: + self.getTable(sigmaparser) return super().generate(sigmaparser) diff --git a/tools/sigma/backends/carbonblack.py b/tools/sigma/backends/carbonblack.py index 860badb5a..975381620 100644 --- a/tools/sigma/backends/carbonblack.py +++ b/tools/sigma/backends/carbonblack.py @@ -44,8 +44,7 @@ class CarbonBlackWildcardHandlingMixin: class CarbonBlackQueryBackend(CarbonBlackWildcardHandlingMixin, SingleTextQueryBackend): - """Converts Sigma rule into CarbonBlack query string. Only searches, no aggregations.""" - + """Converts Sigma rule into CarbonBlack query string. Only searches, no aggregations. Contributed by SOC Prime. https://socprime.com""" identifier = "carbonblack" active = True @@ -133,13 +132,13 @@ class CarbonBlackQueryBackend(CarbonBlackWildcardHandlingMixin, SingleTextQueryB def cleanIPRange(self, value): new_value = value - if type(new_value) is str and value.find('*'): + if isinstance(new_value, str) and value.find('*'): sub = value.count('.') if value[-2:] == '.*': value = value[:-2] min_ip = value + '.0' * (4 - sub) new_value = min_ip + '/' + str(8 * (4 - sub)) - elif type(new_value) is list: + elif isinstance(new_value, list): for index, vl in enumerate(new_value): new_value[index] = self.cleanIPRange(vl) diff --git a/tools/sigma/backends/humio.py b/tools/sigma/backends/humio.py index 21577e151..6347d49f3 100644 --- a/tools/sigma/backends/humio.py +++ b/tools/sigma/backends/humio.py @@ -23,7 +23,7 @@ from .base import SingleTextQueryBackend from .mixins import MultiRuleOutputMixin class HumioBackend(SingleTextQueryBackend): - """Converts Sigma rule into Humio query.""" + """Converts Sigma rule into Humio query. Contributed by SOC Prime. https://socprime.com""" identifier = "humio" active = True @@ -117,23 +117,23 @@ class HumioBackend(SingleTextQueryBackend): # return (" | " + " | ".join([self.regexExpression % (key, self.cleanValue(item)) for item in value]) + " | ") if not set([type(val) for val in value]).issubset({str, int}): raise TypeError("List values must be strings or numbers") - return (" or ".join(['%s=%s' % (key, self.generateValueNode(item)) for item in value])) + return " or ".join(['%s=%s' % (key, self.generateValueNode(item)) for item in value]) def generateAggregation(self, agg): - if agg == None: + if agg is None: return "" if agg.aggfunc == SigmaAggregationParser.AGGFUNC_NEAR: raise NotImplementedError("The 'near' aggregation operator is not yet implemented for this backend") - if agg.groupfield == None: + if agg.groupfield is None: if agg.aggfunc_notrans == 'count': - if agg.aggfield == None : + if agg.aggfield is None : return " | val := count() | val %s %s" % (agg.cond_op, agg.condition) else: agg.aggfunc_notrans = 'dc' return " | count(field=%s, distinct=true, as=val) | val %s %s" % (agg.aggfield or "", agg.cond_op, agg.condition) else: if agg.aggfunc_notrans == 'count': - if agg.aggfield == None : + if agg.aggfield is None : return " | val := count(field=%s) | val %s %s" % (agg.groupfield or "", agg.cond_op, agg.condition) else: agg.aggfunc_notrans = 'dc' diff --git a/tools/sigma/backends/mdatp.py b/tools/sigma/backends/mdatp.py index ad5d0960a..ab57d49d5 100644 --- a/tools/sigma/backends/mdatp.py +++ b/tools/sigma/backends/mdatp.py @@ -18,6 +18,10 @@ import re from functools import wraps from .base import SingleTextQueryBackend from .exceptions import NotSupportedError +from ..parser.modifiers.base import SigmaTypeModifier +from ..parser.modifiers.transform import SigmaContainsModifier, SigmaStartswithModifier, SigmaEndswithModifier +from ..parser.modifiers.type import SigmaRegularExpressionModifier + def wrapper(method): @wraps(method) @@ -132,6 +136,24 @@ class WindowsDefenderATPBackend(SingleTextQueryBackend): } } + def generateANDNode(self, node): + generated = [ self.generateNode(val) for val in node ] + filtered = [] + for g in generated: + if g and g.startswith("ActionType"): + if not any([i for i in filtered if i.startswith("ActionType")]): + filtered.append(g) + else: + continue + elif g is not None: + filtered.append(g) + if filtered: + if self.sort_condition_lists: + filtered = sorted(filtered) + return self.andToken.join(filtered) + else: + return None + def id_mapping(self, src): """Identity mapping, source == target field name""" return src @@ -186,25 +208,95 @@ class WindowsDefenderATPBackend(SingleTextQueryBackend): return (("InititatingProcessAccountName", self.default_value_mapping(src_value))) def generate(self, sigmaparser): - self.table = None - self.category = sigmaparser.parsedyaml['logsource'].get('category') - self.product = sigmaparser.parsedyaml['logsource'].get('product') - self.service = sigmaparser.parsedyaml['logsource'].get('service') + self.tables = [] + try: + self.category = sigmaparser.parsedyaml['logsource'].setdefault('category', None) + self.product = sigmaparser.parsedyaml['logsource'].setdefault('product', None) + self.service = sigmaparser.parsedyaml['logsource'].setdefault('service', None) + except KeyError: + self.category = None + self.product = None + self.service = None if (self.category, self.product, self.service) == ("process_creation", "windows", None): - self.table = "DeviceProcessEvents" + self.tables.append("DeviceProcessEvents") + self.current_table = "DeviceProcessEvents" elif (self.category, self.product, self.service) == (None, "windows", "powershell"): - self.table = "DeviceEvents" + self.tables.append("DeviceEvents") + self.current_table = "DeviceEvents" self.orToken = ", " + elif (self.category, self.product, self.service) == (None, "windows", "security"): + self.tables.append("DeviceAlertEvents") + self.current_table = "DeviceAlertEvents" return super().generate(sigmaparser) def generateBefore(self, parsed): - if self.table is None: + if not any(self.tables): raise NotSupportedError("No MDATP table could be determined from Sigma rule") - if self.table == "DeviceEvents" and self.service == "powershell": - return "%s | where tostring(extractjson('$.Command', AdditionalFields)) in~ " % self.table - return "%s | where " % self.table + # if self.tables in "DeviceEvents" and self.service == "powershell": + # return "%s | where tostring(extractjson('$.Command', AdditionalFields)) in~ " % self.tables + if len(self.tables) == 1: + if self.tables[0] == "DeviceEvents" and self.service == "powershell": + return "%s | where tostring(extractjson('$.Command', AdditionalFields)) in~ " % self.tables + return "%s | where " % self.tables[0] + else: + if "DeviceEvents" in self.tables and self.service == "powershell": + return "union %s | where tostring(extractjson('$.Command', AdditionalFields)) in~ " % ", ".join(self.tables) + return "union %s | where " % ", ".join(self.tables) + + def generateORNode(self, node): + generated = super().generateORNode(node) + if generated: + return "%s" % generated + return generated + + + def mapEventId(self, event_id): + if self.product == "windows": + if self.service == "sysmon" and event_id == 1 \ + or self.service == "security" and event_id == 4688: # Process Execution + self.tables.append("DeviceProcessEvents") + self.current_table = "DeviceProcessEvents" + return None + elif self.service == "sysmon" and event_id == 3: # Network Connection + self.tables.append("DeviceNetworkEvents") + self.current_table = "DeviceNetworkEvents" + return None + elif self.service == "sysmon" and event_id == 7: # Image Load + self.tables.append("DeviceImageLoadEvents") + self.current_table = "DeviceImageLoadEvents" + return None + elif self.service == "sysmon" and event_id == 8: # Create Remote Thread + self.tables.append("DeviceEvents") + self.current_table = "DeviceEvents" + return "ActionType == \"CreateRemoteThreadApiCall\"" + elif self.service == "sysmon" and event_id == 11: # File Creation + self.tables.append("DeviceFileEvents") + self.current_table = "DeviceFileEvents" + return "ActionType == \"FileCreated\"" + elif self.service == "sysmon" and event_id == 23: # File Deletion + self.tables.append("DeviceFileEvents") + self.current_table = "DeviceFileEvents" + return "ActionType == \"FileDeleted\"" + elif self.service == "sysmon" and event_id == 12: # Create/Delete Registry Value + self.tables.append("DeviceRegistryEvents") + self.current_table = "DeviceRegistryEvents" + return None + elif self.service == "sysmon" and event_id == 13 \ + or self.service == "security" and event_id == 4657: # Set Registry Value + self.tables.append("DeviceRegistryEvents") + self.current_table = "DeviceRegistryEvents" + return "ActionType == \"RegistryValueSet\"" + elif self.service == "security" and event_id == 4624: + self.tables.append("DeviceLogonEvents") + self.current_table = "DeviceLogonEvents" + return None + else: + if not self.tables: + raise NotSupportedError("No sysmon Event ID provided") + else: + raise NotSupportedError("No mapping for Event ID %s" % event_id) @wrapper def generateMapItemNode(self, node): @@ -213,67 +305,56 @@ class WindowsDefenderATPBackend(SingleTextQueryBackend): and creates an appropriate table reference. """ key, value = node - # handle map items with values list like multiple OR-chained conditions - if type(value) == list: - return self.generateORNode([(key, v) for v in value]) - elif key == "EventID": # EventIDs are not reflected in condition but in table selection - if self.product == "windows": - if self.service == "sysmon" and value == 1 \ - or self.service == "security" and value == 4688: # Process Execution - self.table = "DeviceProcessEvents" - return None - elif self.service == "sysmon" and value == 3: # Network Connection - self.table = "DeviceNetworkEvents" - return None - elif self.service == "sysmon" and value == 7: # Image Load - self.table = "DeviceImageLoadEvents" - return None - elif self.service == "sysmon" and value == 8: # Create Remote Thread - self.table = "DeviceEvents" - return "ActionType == \"CreateRemoteThreadApiCall\"" - elif self.service == "sysmon" and value == 11: # File Creation - self.table = "DeviceFileEvents" - return "ActionType == \"FileCreated\"" - elif self.service == "sysmon" and value == 23: # File Deletion - self.table = "DeviceFileEvents" - return "ActionType == \"FileDeleted\"" - elif self.service == "sysmon" and value == 12: # Create/Delete Registry Value - self.table = "DeviceRegistryEvents" - return None - elif self.service == "sysmon" and value == 13 \ - or self.service == "security" and value == 4657: # Set Registry Value - self.table = "DeviceRegistryEvents" - return "ActionType == \"RegistryValueSet\"" - elif self.service == "security" and value == 4624: - self.table = "DeviceLogonEvents" + if key == "EventID": + # EventIDs are not reflected in condition but in table selection + if isinstance(value, str) or isinstance(value, int): + value = int(value) if isinstance(value, str) else value + return self.mapEventId(value) + elif isinstance(value, list): + return_payload = [] + for event_id in value: + res = self.mapEventId(event_id) + if res: + return_payload.append(res) + if len(return_payload) == 1: + return return_payload[0] + elif not any(return_payload): return None else: - if not self.table: - raise NotSupportedError("No sysmon Event ID provided") - else: - raise NotSupportedError("No mapping for Event ID %s" % value) + return "(%s)" % self.generateORNode( + [(key, v) for v in value] + ) + if type(value) == list: # handle map items with values list like multiple OR-chained conditions + return "(%s)" % self.generateORNode( + [(key, self.cleanValue(v)) for v in value] + ) elif type(value) in (str, int): # default value processing try: - mapping = self.fieldMappings[self.table][key] + mapping = self.fieldMappings[self.current_table][key] except KeyError: - raise NotSupportedError("No mapping defined for field '%s' in '%s'" % (key, self.table)) + raise NotSupportedError("No mapping defined for field '%s' in '%s'" % (key, self.tables)) if len(mapping) == 1: mapping = mapping[0] if type(mapping) == str: return mapping elif callable(mapping): - conds = mapping(key, value) + conds = mapping(key, self.cleanValue(value)) return self.andToken.join(["{} {}".format(*cond) for cond in conds]) elif len(mapping) == 2: result = list() - # iterate mapping and mapping source value synchronously over key and value - for mapitem, val in zip(mapping, node): + for mapitem, val in zip(mapping, node): # iterate mapping and mapping source value synchronously over key and value if type(mapitem) == str: result.append(mapitem) elif callable(mapitem): - result.append(mapitem(val)) + result.append(mapitem(self.cleanValue(val))) return "{} {}".format(*result) else: raise TypeError("Backend does not support map values of type " + str(type(value))) + elif isinstance(value, SigmaTypeModifier): + try: + mapping = self.fieldMappings[self.current_table][key] + except KeyError: + raise NotSupportedError("No mapping defined for field '%s' in '%s'" % (key, self.tables)) + return self.generateMapItemTypedNode(mapping[0], value) return super().generateMapItemNode(node) diff --git a/tools/sigma/backends/splunk.py b/tools/sigma/backends/splunk.py index 75658343a..a2db0261a 100644 --- a/tools/sigma/backends/splunk.py +++ b/tools/sigma/backends/splunk.py @@ -175,6 +175,9 @@ class SplunkXMLBackend(SingleTextQueryBackend, MultiRuleOutputMixin): class CrowdStrikeBackend(SplunkBackend): """Converts Sigma rule into CrowdStrike Search Processing Language (SPL).""" identifier = "crowdstrike" + typedValueExpression = { + SigmaRegularExpressionModifier: 'regex field=%s "%s"' + } def generate(self, sigmaparser): lgs = sigmaparser.parsedyaml.get("logsource") @@ -208,3 +211,9 @@ class CrowdStrikeBackend(SplunkBackend): return super().generate(sigmaparser) else: raise NotImplementedError("Not supported logsources!") + + def generateMapItemTypedNode(self, fieldname, value): + if isinstance(value, SigmaRegularExpressionModifier): + return self.typedValueExpression.get(type(value)) % (fieldname, value) + else: + return super().generateMapItemTypedNode(fieldname=fieldname, value=value) \ No newline at end of file diff --git a/tools/sigma/backends/sumologic.py b/tools/sigma/backends/sumologic.py index 0613c7fbf..b2fb05ed6 100644 --- a/tools/sigma/backends/sumologic.py +++ b/tools/sigma/backends/sumologic.py @@ -13,11 +13,14 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . - +import json +import os import re -import sigma -from sigma.parser.condition import ConditionOR -from .base import SingleTextQueryBackend +import sys + +from sigma.backends.base import SingleTextQueryBackend +from sigma.backends.exceptions import NotSupportedError +from sigma.parser.condition import ConditionOR, SigmaAggregationParser # Sumo specifics # https://help.sumologic.com/05Search/Search-Query-Language @@ -29,13 +32,13 @@ from .base import SingleTextQueryBackend class SumoLogicBackend(SingleTextQueryBackend): - """Converts Sigma rule into SumoLogic query""" + """Converts Sigma rule into SumoLogic query. Contributed by SOC Prime. https://socprime.com""" identifier = "sumologic" active = True config_required = False default_config = ["sysmon", "sumologic"] - index_field = "_index" + index_field = "_sourceCategory" reClear = None andToken = " AND " orToken = " OR " @@ -59,22 +62,46 @@ class SumoLogicBackend(SingleTextQueryBackend): agg.groupfield = 'hostname' if agg.aggfunc_notrans == 'count() by': agg.aggfunc_notrans = 'count by' - if agg.aggfunc == sigma.parser.condition.SigmaAggregationParser.AGGFUNC_NEAR: + if agg.aggfunc == SigmaAggregationParser.AGGFUNC_NEAR: raise NotImplementedError("The 'near' aggregation operator is not yet implemented for this backend") - # WIP - # ex: - # (QUERY) | timeslice 5m - # | count_distinct(process) _timeslice,hostname - # | where _count_distinct > 5 - # return " | timeslice %s | count_distinct(%s) %s | where _count_distinct > 0" % (self.interval, agg.aggfunc_notrans or "", agg.aggfield or "", agg.groupfield or "") - # return " | timeslice %s | count_distinct(%s) %s | where _count_distinct %s %s" % (self.interval, agg.aggfunc_notrans, agg.aggfield or "", agg.groupfield or "", agg.cond_op, agg.condition) - if not agg.groupfield: - # 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: - return " | %s by %s | where _count %s %s" % (agg.aggfunc_notrans, agg.groupfield or "", agg.cond_op, agg.condition) + if self.keypresent: + if not agg.groupfield: + if agg.aggfield: + agg.aggfunc_notrans = "count_distinct" + return " \n| %s(%s) \n| where _count_distinct %s %s" % ( + agg.aggfunc_notrans, agg.aggfield, agg.cond_op, agg.condition) + else: + return " \n| %s | where _count %s %s" % ( + agg.aggfunc_notrans, agg.cond_op, agg.condition) + elif agg.groupfield: + if agg.aggfield: + agg.aggfunc_notrans = "count_distinct" + return " \n| %s(%s) by %s \n| where _count_distinct %s %s" % ( + agg.aggfunc_notrans, agg.aggfield, agg.groupfield, agg.cond_op, agg.condition) + else: + return " \n| %s by %s \n| where _count %s %s" % ( + agg.aggfunc_notrans, agg.groupfield, agg.cond_op, agg.condition) + else: + return " \n| %s | where _count %s %s" % (agg.aggfunc_notrans, 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) + if not agg.groupfield: + if agg.aggfield: + agg.aggfunc_notrans = "count_distinct" + return " \n| parse \"[%s=*]\" as searched nodrop\n| %s(searched) \n| where _count_distinct %s %s" % ( + agg.aggfield, agg.aggfunc_notrans, agg.cond_op, agg.condition) + else: + return " \n| %s | where _count %s %s" % ( + agg.aggfunc_notrans, agg.cond_op, agg.condition) + elif agg.groupfield: + if agg.aggfield: + agg.aggfunc_notrans = "count_distinct" + return " \n| parse \"[%s=*]\" as searched nodrop\n| parse \"[%s=*]\" as grpd nodrop\n| %s(searched) by grpd \n| where _count_distinct %s %s" % ( + agg.aggfield, agg.groupfield, agg.aggfunc_notrans, agg.cond_op, agg.condition) + else: + return " \n| parse \"[%s=*]\" as grpd nodrop\n| %s by grpd \n| where _count %s %s" % ( + agg.groupfield, agg.aggfunc_notrans, agg.cond_op, agg.condition) + else: + return " \n| %s | where _count %s %s" % (agg.aggfunc_notrans, agg.cond_op, agg.condition) def generateBefore(self, parsed): # not required but makes query faster, especially if no FER or _index/_sourceCategory @@ -126,13 +153,17 @@ class SumoLogicBackend(SingleTextQueryBackend): if '|' in result: return result else: - return "(" + result + ")" + return result def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # TODO/FIXME! depending on deployment configuration, existing FER must be populate here (or backend config?) - # aFL = ["EventID"] - aFL = ["_index", "_sourceCategory", "_view", "EventID", "sourcename", "CommandLine", "NewProcessName", "Image", "ParentImage", "ParentCommandLine", "ParentProcessName"] + aFL = ["_sourceCategory", "_view", "_sourceName"] + if self.sigmaconfig.config.get("afl_fields"): + self.keypresent = True + aFL.extend(self.sigmaconfig.config.get("afl_fields")) + else: + self.keypresent = False for item in self.sigmaconfig.fieldmappings.values(): if item.target_type is list: aFL.extend(item.target) @@ -146,25 +177,22 @@ class SumoLogicBackend(SingleTextQueryBackend): # Clearing values from special characters. # Sumologic: only removing '*' (in quotes, is litteral. without, is wildcard) and '"' - def CleanNode(self, node): - search_ptrn = re.compile(r"[*\"\\]") - replace_ptrn = re.compile(r"[*\"\\]") - match = search_ptrn.search(str(node)) - new_node = list() - if match: - replaced_str = replace_ptrn.sub('*', node) - node = [x for x in replaced_str.split('*') if x] - new_node.extend(node) - else: - new_node.append(node) - node = new_node + + def cleanNode(self, node, key=None): + if "*" in node and key and not re.search("[\s]", node): + return node + elif "*" in node and not key: + return [x for x in node.split("*") if x] return node # Clearing values from special characters. def generateMapItemNode(self, node): key, value = node if key in self.allowedFieldsList: - if not self.mapListsSpecialHandling and type(value) in ( + if key in ["_sourceCategory", "_sourceName"]: + value = "*%s*" % value.lower() + return self.mapExpression % (key, value) + elif not self.mapListsSpecialHandling and type(value) in ( str, int, list) or self.mapListsSpecialHandling and type(value) in (str, int): if key in ("LogName", "source"): self.logname = value @@ -181,29 +209,29 @@ class SumoLogicBackend(SingleTextQueryBackend): str, int, list) or self.mapListsSpecialHandling and type(value) in (str, int): if type(value) is str: new_value = list() - value = self.CleanNode(value) + value = self.cleanNode(value) if type(value) == list: - new_value.append(self.andToken.join([self.valueExpression % val for val in value])) + new_value.append(self.andToken.join([self.cleanValue(val) for val in value])) else: new_value.append(value) if len(new_value) == 1: if self.generateANDNode(new_value): - return "(" + 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) + ")" + return self.generateORNode(new_value) else: return self.generateValueNode(value) elif type(value) is list: new_value = list() for item in value: - item = self.CleanNode(item) + item = self.cleanNode(item) if type(item) is list and len(item) == 1: - new_value.append(self.valueExpression % item[0]) + new_value.append(item[0]) elif type(item) is list: - new_value.append(self.andToken.join([self.valueExpression % val for val in item])) + new_value.append(self.andToken.join([self.cleanValue(val) for val in item])) else: new_value.append(item) return self.generateORNode(new_value) @@ -217,35 +245,25 @@ class SumoLogicBackend(SingleTextQueryBackend): # => OK only if field entry with list, not string # => generateNode: call cleanValue def cleanValue(self, val, key=''): - # in sumologic, if key, can use wildcard outside of double quotes. if inside, it's litteral - if key: - val = re.sub(r'\"', '\\"', str(val)) - val = re.sub(r'(.+)\*(.+)', '"\g<1>"*"\g<2>"', val, 0) - val = re.sub(r'^\*', '*"', val) - val = re.sub(r'\*$', '"*', 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 - # double escape if end quote - if val.endswith('\\"*') and not val.endswith('\\\\"*'): - val = re.sub(r'\\"\*$', '\\\\\\"*', 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 and not '_index' in key and not '_sourceCategory' in key and not '_view' in key: - val = '"%s"' % val + if isinstance(val, str): + val = re.sub("[^\\\"](\")", "\\\"", val) + if re.search("[\W\s]", val):# and not val.startswith('"') and not val.endswith('"'): # or "\\" in node in [] or "/" in node: + return self.valueExpression % val return val # for keywords values with space def generateValueNode(self, node, key=''): - cV = self.cleanValue(str(node), key) + cV = self.cleanNode(str(node), key) if type(node) is int: return cV + if type(cV) is list: + return "(%s)" % "AND".join([self.cleanValue(item) for item in cV]) if 'AND' in node and cV: return "(" + cV + ")" - else: + elif isinstance(node, str) and node.startswith('"') and node.endswith('"'): return cV + else: + return self.cleanValue(cV) def generateMapItemListNode(self, key, value): itemslist = list() @@ -256,15 +274,209 @@ class SumoLogicBackend(SingleTextQueryBackend): itemslist.append('%s' % (self.generateValueNode(item))) return "(" + " OR ".join(itemslist) + ")" - # generateORNode algorithm for ArcSightBackend & SumoLogicBackend class. + # generateORNode algorithm for SumoLogicBackend class. def generateORNode(self, node): if type(node) == ConditionOR and all(isinstance(item, str) for item in node): new_value = list() for value in node: - value = self.CleanNode(value) + value = self.cleanNode(value) if type(value) is list: new_value.append(self.andToken.join([self.valueExpression % val for val in value])) else: new_value.append(value) return "(" + self.orToken.join([self.generateNode(val) for val in new_value]) + ")" return "(" + self.orToken.join([self.generateNode(val) for val in node]) + ")" + + +class SumoLogicCSE(SumoLogicBackend): + """Converts Sigma rule into SumoLogic CSE query. Contributed by SOC Prime. https://socprime.com""" + identifier = "sumologic-cse" + active = True + config_required = False + default_config = ["sysmon"] + + index_field = "metdata_product" + reClear = None + #reEscape = re.compile('[\\\\"]') + andToken = " and " + orToken = " or " + notToken = "!" + subExpression = "(%s)" + listExpression = "(%s)" + listSeparator = ", " + valueExpression = "\"%s\"" + nullExpression = "isEmpty(%s)" + notNullExpression = "!isEmpty(%s)" + mapExpression = "%s=%s" + mapListsSpecialHandling = True + mapListValueExpression = "%s IN %s" + interval = None + logname = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.allowedFieldsList.extend(["metdata_product", "metdata_vendor"]) + + def cleanValue(self, val, key=''): + if key == 'metadata_deviceEventId' or val.isdigit(): + return val + return self.valueExpression % val + + def cleanNode(self, node, key=None): + return node + + # Clearing values from special characters. + def generateMapItemNode(self, node): + key, value = node + if key: + if not self.mapListsSpecialHandling and type(value) in ( + str, int, list) or self.mapListsSpecialHandling and type(value) in (str, int): + if key in ("LogName", "source"): + self.logname = value + # 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) + elif value is None: + return self.nullExpression % (key,) + else: + raise TypeError("Backend does not support map values of type " + str(type(value))) + raise TypeError("Backend does not support query without key.") + + def generateMapItemListNode(self, key, value): + if len(value) == 1: + return self.mapExpression % (key, value[0]) + return "%s IN (%s)" % (key, ", ".join([self.cleanValue(item, key) for item in value])) + + +class SumoLogicCSERule(SumoLogicCSE): + """Converts Sigma rule into SumoLogic CSE query""" + identifier = "sumologic-cse-rule" + active = True + + def __init__(self, *args, **kwargs): + """Initialize field mappings""" + super().__init__(*args, **kwargs) + self.techniques = self._load_mitre_file("techniques") + self.allowedCategories = ["Threat Intelligence", "Initial Access", "Execution", "Persistence", "Privilege Escalation", + "Defense Evasion", "Credential Access", "Discovery", "Lateral Movement", "Collection", + "Command and Control", "Exfiltration", "Impact"] + self.defaultCategory = "Unknown/Other" + self.results = [] + + def find_technique(self, key_ids): + for key_id in set(key_ids): + if not key_id: + continue + for technique in self.techniques: + if key_id == technique.get("technique_id", ""): + yield technique + + def _load_mitre_file(self, mitre_type): + try: + backend_dir = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "config", "mitre")) + path = os.path.join(backend_dir, "{}.json".format(mitre_type)) + with open(path) as config_file: + config = json.load(config_file) + return config + except (IOError, OSError) as e: + print("Failed to open {} configuration file '%s': %s".format(path, str(e)), file=sys.stderr) + return [] + except json.JSONDecodeError as e: + print("Failed to parse {} configuration file '%s' as valid YAML: %s" % (path, str(e)), file=sys.stderr) + return [] + + def skip_tactics_or_techniques(self, src_technics, src_tactics): + tactics = set() + technics = set() + + local_storage_techniques = {item["technique_id"]: item for item in self.find_technique(src_technics)} + + for key_id in src_technics: + src_tactic = local_storage_techniques.get(key_id, {}).get("tactic") + if not src_tactic: + continue + src_tactic = set(src_tactic) + + for item in src_tactics: + if item in src_tactic: + technics.add(key_id) + tactics.add(item) + + return sorted(tactics), sorted(technics) + + def parse_severity(self, old_severity): + if old_severity.lower() == "critical": + return "high" + return old_severity + + def get_tactics_and_techniques(self, tags): + tactics = list() + technics = list() + + for tag in tags: + tag = tag.replace("attack.", "") + if re.match("[t][0-9]{4}", tag, re.IGNORECASE): + technics.append(tag.title()) + elif re.match("[s][0-9]{4}", tag, re.IGNORECASE): + continue + else: + if "_" in tag: + tag = tag.replace("_", " ") + tag = tag.title() + tactics.append(tag) + + return tactics, technics + + def map_risk_score(self, level): + if level == "critical": + return 5 + elif level == "high": + return 4 + elif level == "medium": + return 3 + elif level == "low": + return 2 + return 1 + + def create_rule(self, config): + tags = config.get("tags", []) + + tactics, technics = self.get_tactics_and_techniques(tags) + tactics, technics = self.skip_tactics_or_techniques(technics, tactics) + tactics = list(map(lambda s: s.replace(" ", ""), tactics)) + score = self.map_risk_score(config.get("level", "medium")) + rule = { + "name": "{} by {}".format(config.get("title"), config.get('author')), + "description": "{} {}".format(config.get("description"), "Technique: {}.".format(",".join(technics))), + "enabled": True, + "expression": """{}""".format(config.get("translation", "")), + "assetField": "device_hostname", + "score": score, + "stream": "record" + } + if tactics and tactics[0] in self.allowedCategories: + rule.update({"category": tactics[0]}) + else: + rule.update({"category": "Unknown/Other"}) + self.results.append(rule) + #return json.dumps(rule, indent=4, sort_keys=False) + + def generate(self, sigmaparser): + translation = super().generate(sigmaparser) + if translation: + configs = sigmaparser.parsedyaml + configs.update({"translation": translation}) + rule = self.create_rule(configs) + return rule + else: + raise NotSupportedError("No table could be determined from Sigma rule") + + def finalize(self): + if len(self.results) == 1: + return json.dumps(self.results[0], indent=4, sort_keys=False) + elif len(self.results) > 1: + return json.dumps(self.results, indent=4, sort_keys=False) + + + diff --git a/tools/sigma/backends/sysmon.py b/tools/sigma/backends/sysmon.py index 12171dc34..66832d576 100644 --- a/tools/sigma/backends/sysmon.py +++ b/tools/sigma/backends/sysmon.py @@ -14,7 +14,7 @@ class SysmonConfigBackend(SingleTextQueryBackend, MultiRuleOutputMixin): orToken = " OR " notToken = "NOT " subExpression = "(%s)" - config_required = False + config_required = True INCLUDE = "include" EXCLUDE = "exclude" conditionDict = { diff --git a/tools/sigma/config/mapping.py b/tools/sigma/config/mapping.py index a03976834..f857f0887 100644 --- a/tools/sigma/config/mapping.py +++ b/tools/sigma/config/mapping.py @@ -207,6 +207,14 @@ class FieldMappingChain(object): def resolve(self, key, value, sigmaparser): if type(self.fieldmappings) == str: # one field mapping return (self.fieldmappings, value) + elif isinstance(self.fieldmappings, ConditionalFieldMapping): + logsource = sigmaparser.parsedyaml.get("logsource") + condition = self.fieldmappings.conditions + for source_type, logsource_item in logsource.items(): + if condition.get(source_type) and condition.get(source_type, {}).get(logsource_item): + new_field = condition.get(source_type, {}).get(logsource_item) + self.fieldmappings.default = new_field + return self.fieldmappings.resolve(self.fieldmappings.source, value, sigmaparser) elif isinstance(self.fieldmappings, SimpleFieldMapping): return self.fieldmappings.resolve(key, value, sigmaparser) elif type(self.fieldmappings) == set: From f45e45d736246c7d4339bf77c1ec2cf4bbf23366 Mon Sep 17 00:00:00 2001 From: vh Date: Tue, 20 Oct 2020 18:13:53 +0300 Subject: [PATCH 02/20] Fix: Import SigmaRegularExpressionModifier in the splunk backend. --- tools/sigma/backends/splunk.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/sigma/backends/splunk.py b/tools/sigma/backends/splunk.py index a2db0261a..c537cb6c4 100644 --- a/tools/sigma/backends/splunk.py +++ b/tools/sigma/backends/splunk.py @@ -18,6 +18,8 @@ import re import sigma from .base import SingleTextQueryBackend from .mixins import MultiRuleOutputMixin +from sigma.parser.modifiers.type import SigmaRegularExpressionModifier + class SplunkBackend(SingleTextQueryBackend): """Converts Sigma rule into Splunk Search Processing Language (SPL).""" From 383823f49a55f50474f554989fcb1c31cf8ef231 Mon Sep 17 00:00:00 2001 From: vh Date: Wed, 21 Oct 2020 10:12:17 +0300 Subject: [PATCH 03/20] Fix: added default value of current_table --- tools/sigma/backends/mdatp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/sigma/backends/mdatp.py b/tools/sigma/backends/mdatp.py index ab57d49d5..32c3b3a8b 100644 --- a/tools/sigma/backends/mdatp.py +++ b/tools/sigma/backends/mdatp.py @@ -135,6 +135,7 @@ class WindowsDefenderATPBackend(SingleTextQueryBackend): "User": (self.decompose_user, ), } } + self.current_table = "" def generateANDNode(self, node): generated = [ self.generateNode(val) for val in node ] From 2fb7dd5e99e0e8f41bb3f37c2c62ffe91c58de7d Mon Sep 17 00:00:00 2001 From: Thomas Patzke Date: Fri, 23 Oct 2020 15:31:00 +0200 Subject: [PATCH 04/20] Fixes * Removed Splunk regex query * Added test for sumologic-cse backend --- Makefile | 1 + tools/sigma/backends/splunk.py | 14 +++----------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 8439b5dd7..1bd3a4698 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,7 @@ test-sigmac: $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t netwitness -c tools/config/netwitness.yml rules/ > /dev/null $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t netwitness-epl -c netwitness-epl rules/ > /dev/null $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t sumologic -O rulecomment -c tools/config/sumologic.yml rules/ > /dev/null + $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t sumologic-cse -O rulecomment -c tools/config/sumologic.yml rules/ > /dev/null $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t humio -O rulecomment -c tools/config/humio.yml rules/ > /dev/null $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t crowdstrike -O rulecomment -c tools/config/crowdstrike.yml rules/ > /dev/null $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t sql -c sysmon rules/ > /dev/null diff --git a/tools/sigma/backends/splunk.py b/tools/sigma/backends/splunk.py index c537cb6c4..c2bfb96b5 100644 --- a/tools/sigma/backends/splunk.py +++ b/tools/sigma/backends/splunk.py @@ -18,8 +18,6 @@ import re import sigma from .base import SingleTextQueryBackend from .mixins import MultiRuleOutputMixin -from sigma.parser.modifiers.type import SigmaRegularExpressionModifier - class SplunkBackend(SingleTextQueryBackend): """Converts Sigma rule into Splunk Search Processing Language (SPL).""" @@ -70,7 +68,7 @@ class SplunkBackend(SingleTextQueryBackend): agg.aggfunc_notrans = 'dc' return " | eventstats %s(%s) as val by %s | search val %s %s" % (agg.aggfunc_notrans, agg.aggfield or "", agg.groupfield or "", agg.cond_op, agg.condition) - + def generate(self, sigmaparser): """Method is called for each sigma rule and receives the parsed rule (SigmaParser)""" columns = list() @@ -108,7 +106,7 @@ class SplunkBackend(SingleTextQueryBackend): result += fields return result - + class SplunkXMLBackend(SingleTextQueryBackend, MultiRuleOutputMixin): """Converts Sigma rule into XML used for Splunk Dashboard Panels""" identifier = "splunkxml" @@ -177,9 +175,6 @@ class SplunkXMLBackend(SingleTextQueryBackend, MultiRuleOutputMixin): class CrowdStrikeBackend(SplunkBackend): """Converts Sigma rule into CrowdStrike Search Processing Language (SPL).""" identifier = "crowdstrike" - typedValueExpression = { - SigmaRegularExpressionModifier: 'regex field=%s "%s"' - } def generate(self, sigmaparser): lgs = sigmaparser.parsedyaml.get("logsource") @@ -215,7 +210,4 @@ class CrowdStrikeBackend(SplunkBackend): raise NotImplementedError("Not supported logsources!") def generateMapItemTypedNode(self, fieldname, value): - if isinstance(value, SigmaRegularExpressionModifier): - return self.typedValueExpression.get(type(value)) % (fieldname, value) - else: - return super().generateMapItemTypedNode(fieldname=fieldname, value=value) \ No newline at end of file + return super().generateMapItemTypedNode(fieldname=fieldname, value=value) \ No newline at end of file From e30237c5c5f4652a670854b6ae7a25089ab259c9 Mon Sep 17 00:00:00 2001 From: Thomas Patzke Date: Fri, 23 Oct 2020 19:30:59 +0200 Subject: [PATCH 05/20] Fixed test configuration --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1bd3a4698..62c3e8af4 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,7 @@ test-sigmac: $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t netwitness -c tools/config/netwitness.yml rules/ > /dev/null $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t netwitness-epl -c netwitness-epl rules/ > /dev/null $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t sumologic -O rulecomment -c tools/config/sumologic.yml rules/ > /dev/null - $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t sumologic-cse -O rulecomment -c tools/config/sumologic.yml rules/ > /dev/null + $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t sumologic-cse -O rulecomment -c tools/config/sumologic-cse.yml rules/ > /dev/null $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t humio -O rulecomment -c tools/config/humio.yml rules/ > /dev/null $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t crowdstrike -O rulecomment -c tools/config/crowdstrike.yml rules/ > /dev/null $(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t sql -c sysmon rules/ > /dev/null From f0e89b0c8c10ad9b4ad74741ec76dde31a2dbd8a Mon Sep 17 00:00:00 2001 From: Thomas Patzke Date: Fri, 23 Oct 2020 19:49:55 +0200 Subject: [PATCH 06/20] Fixed: typecheck in sumologig-cse --- tools/sigma/backends/sumologic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/sigma/backends/sumologic.py b/tools/sigma/backends/sumologic.py index b2fb05ed6..04e13039e 100644 --- a/tools/sigma/backends/sumologic.py +++ b/tools/sigma/backends/sumologic.py @@ -318,7 +318,7 @@ class SumoLogicCSE(SumoLogicBackend): self.allowedFieldsList.extend(["metdata_product", "metdata_vendor"]) def cleanValue(self, val, key=''): - if key == 'metadata_deviceEventId' or val.isdigit(): + if key == 'metadata_deviceEventId' or isinstance(val, int) or val.isdigit(): return val return self.valueExpression % val From 16d63cc5d2c2177e0b35f9e1eaf3094a2f9c0817 Mon Sep 17 00:00:00 2001 From: Thomas Patzke Date: Fri, 23 Oct 2020 20:17:06 +0200 Subject: [PATCH 07/20] Decreased coverage requirement --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 62c3e8af4..20eab02b6 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ clearcov: rm -f .coverage finish: - $(COVERAGE) report --fail-under=90 + $(COVERAGE) report --fail-under=80 rm -f $(TMPOUT) test-rules: From 75637324e0f94ccfcc9eb8dd50c0f8168be492b0 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Fri, 23 Oct 2020 23:44:48 +0200 Subject: [PATCH 08/20] feat: cover newest emotet campaigns --- rules/windows/process_creation/win_susp_powershell_enc_cmd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/rules/windows/process_creation/win_susp_powershell_enc_cmd.yml b/rules/windows/process_creation/win_susp_powershell_enc_cmd.yml index a2a4ad534..a384047e8 100644 --- a/rules/windows/process_creation/win_susp_powershell_enc_cmd.yml +++ b/rules/windows/process_creation/win_susp_powershell_enc_cmd.yml @@ -40,6 +40,7 @@ detection: - '* -e* IAB*' - '* -e* UwB*' - '* -e* cwB*' + - '*.exe -ENCOD *' falsepositive1: CommandLine: '* -ExecutionPolicy remotesigned *' condition: selection and not falsepositive1 From dd0d1d053c31af02f378f4e337e3d6110a741b5c Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Mon, 2 Nov 2020 11:11:18 +0100 Subject: [PATCH 09/20] rule: WebLogic exploit CVE-2020-14882 --- .../web_cve_2020_14882_weblogic_exploit.yml | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 rules/web/web_cve_2020_14882_weblogic_exploit.yml diff --git a/rules/web/web_cve_2020_14882_weblogic_exploit.yml b/rules/web/web_cve_2020_14882_weblogic_exploit.yml new file mode 100644 index 000000000..16a800344 --- /dev/null +++ b/rules/web/web_cve_2020_14882_weblogic_exploit.yml @@ -0,0 +1,29 @@ +title: Oracle WebLogic Exploit CVE-2020-14882 +id: 85d466b0-d74c-4514-84d3-2bdd3327588b +status: experimental +description: Detects exploitation attempts on WebLogic servers +author: Florian Roth +date: 2020/11/02 +references: + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-14882 + - https://isc.sans.edu/diary/26734 +logsource: + category: webserver +detection: + selection: + c-uri|contains|all: + - '/console/images/%252E%252E%252F' + - '.exec(' + condition: selection +fields: + - c-ip + - c-dns +falsepositives: + - Unknown +level: high +tags: + - attack.t1100 # an old one + - attack.t1190 + - attack.initial_access + - cve.2020-14882 + - attack.t1505.003 From 31241d9bbdde78575293ec97da83e193b902b47c Mon Sep 17 00:00:00 2001 From: Thomas Patzke Date: Mon, 2 Nov 2020 22:57:01 +0100 Subject: [PATCH 10/20] Removed ES query tests --- .github/workflows/sigma-test.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/sigma-test.yml b/.github/workflows/sigma-test.yml index ee0c317a5..28931b92e 100644 --- a/.github/workflows/sigma-test.yml +++ b/.github/workflows/sigma-test.yml @@ -23,18 +23,9 @@ jobs: run: | python -m pip install --upgrade pip pip install -r tools/requirements.txt -r tools/requirements-devel.txt - wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add - - sudo apt install -y apt-transport-https - echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic.list - sudo apt update - sudo apt install -y elasticsearch - sudo systemctl start elasticsearch - name: Test Sigma Tools and Rules run: | make test - - name: Test Generated Elasticsearch Query Strings - run: | - make test-backend-es-qs - name: Test SQL(ite) Backend run: | make test-backend-sql From f848bb912cd1cfd4303a113e12d8066304385c8a Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Tue, 3 Nov 2020 10:39:40 +0100 Subject: [PATCH 11/20] rule: reworked weblogic CVE-2020-14882 rule --- rules/web/web_cve_2020_14882_weblogic_exploit.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rules/web/web_cve_2020_14882_weblogic_exploit.yml b/rules/web/web_cve_2020_14882_weblogic_exploit.yml index 16a800344..eaf668eda 100644 --- a/rules/web/web_cve_2020_14882_weblogic_exploit.yml +++ b/rules/web/web_cve_2020_14882_weblogic_exploit.yml @@ -4,16 +4,17 @@ status: experimental description: Detects exploitation attempts on WebLogic servers author: Florian Roth date: 2020/11/02 +modified: 2020/11/03 references: - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-14882 - https://isc.sans.edu/diary/26734 + - https://twitter.com/jas502n/status/1321416053050667009?s=20 logsource: category: webserver detection: selection: - c-uri|contains|all: - - '/console/images/%252E%252E%252F' - - '.exec(' + c-uri|contains: + - '/console/images/%252E%252E%252Fconsole.portal' condition: selection fields: - c-ip @@ -26,4 +27,3 @@ tags: - attack.t1190 - attack.initial_access - cve.2020-14882 - - attack.t1505.003 From 4a5b2d642e7dca3e1d99ca9ec16bc0b35b517311 Mon Sep 17 00:00:00 2001 From: bczyz1 <56651710+bczyz1@users.noreply.github.com> Date: Tue, 3 Nov 2020 14:46:29 +0100 Subject: [PATCH 12/20] Fix typo in win_apt_lazarus_session_hijack.yml --- .../process_creation/win_apt_lazarus_session_highjack.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rules/windows/process_creation/win_apt_lazarus_session_highjack.yml b/rules/windows/process_creation/win_apt_lazarus_session_highjack.yml index ce5e14cc3..bf8fcd819 100644 --- a/rules/windows/process_creation/win_apt_lazarus_session_highjack.yml +++ b/rules/windows/process_creation/win_apt_lazarus_session_highjack.yml @@ -8,7 +8,7 @@ tags: - attack.defense_evasion - attack.t1036 # an old one - attack.t1036.005 -author: Trent Liffick (@tliffick) +author: Trent Liffick (@tliffick), Bartlomiej Czyz (@bczyz1) date: 2020/06/03 logsource: category: process_creation @@ -16,7 +16,7 @@ logsource: detection: selection: Image: - - '*\mstdc.exe' + - '*\msdtc.exe' - '*\gpvc.exe' filter: Image: From 908023fa660a06df622a777fe23fbed9bcc1c097 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Wed, 4 Nov 2020 16:43:35 +0100 Subject: [PATCH 13/20] rule: added second expression --- rules/web/web_cve_2020_14882_weblogic_exploit.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rules/web/web_cve_2020_14882_weblogic_exploit.yml b/rules/web/web_cve_2020_14882_weblogic_exploit.yml index eaf668eda..14afc0d12 100644 --- a/rules/web/web_cve_2020_14882_weblogic_exploit.yml +++ b/rules/web/web_cve_2020_14882_weblogic_exploit.yml @@ -4,17 +4,19 @@ status: experimental description: Detects exploitation attempts on WebLogic servers author: Florian Roth date: 2020/11/02 -modified: 2020/11/03 +modified: 2020/11/04 references: - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-14882 - https://isc.sans.edu/diary/26734 - https://twitter.com/jas502n/status/1321416053050667009?s=20 + - https://twitter.com/sudo_sudoka/status/1323951871078223874 logsource: category: webserver detection: selection: c-uri|contains: - '/console/images/%252E%252E%252Fconsole.portal' + - '/console/css/%2e' condition: selection fields: - c-ip From c3785d6dc78eee19d43c52f0224492876734cf90 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Thu, 5 Nov 2020 16:44:33 +0100 Subject: [PATCH 14/20] rule: FPs with WmiPrvSE rule --- .../process_creation/win_wmiprvse_spawning_process.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rules/windows/process_creation/win_wmiprvse_spawning_process.yml b/rules/windows/process_creation/win_wmiprvse_spawning_process.yml index fcabfdb70..aafe963ea 100644 --- a/rules/windows/process_creation/win_wmiprvse_spawning_process.yml +++ b/rules/windows/process_creation/win_wmiprvse_spawning_process.yml @@ -3,7 +3,7 @@ id: d21374ff-f574-44a7-9998-4a8c8bf33d7d description: Detects wmiprvse spawning processes status: experimental date: 2019/08/15 -modified: 2019/11/10 +modified: 2020/11/05 author: Roberto Rodriguez @Cyb3rWard0g references: - https://github.com/Cyb3rWard0g/ThreatHunter-Playbook/tree/master/playbooks/windows/02_execution/T1047_windows_management_instrumentation/wmi_win32_process_create_remote.md @@ -19,7 +19,10 @@ detection: filter: - LogonId: '0x3e7' # LUID 999 for SYSTEM - User: 'NT AUTHORITY\SYSTEM' # if we don't have LogonId data, fallback on username detection + - Image|endswith: + - '\WmiPrvSE.exe' + - '\WerFault.exe' condition: selection and not filter falsepositives: - Unknown -level: critical +level: high From 31639366cd99c547b5c4335dff59213a02db26e7 Mon Sep 17 00:00:00 2001 From: Olivier Caillault Date: Thu, 5 Nov 2020 22:30:12 +0100 Subject: [PATCH 15/20] Fix unicode error in sigma2attack --- tools/sigma/sigma2attack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/sigma/sigma2attack.py b/tools/sigma/sigma2attack.py index 5543d6eee..9b27126a8 100755 --- a/tools/sigma/sigma2attack.py +++ b/tools/sigma/sigma2attack.py @@ -21,7 +21,7 @@ def main(): num_rules_used = 0 for rule_file in rule_files: try: - rule = yaml.safe_load(open(rule_file).read()) + rule = yaml.safe_load(open(rule_file, encoding="utf-8").read()) except yaml.YAMLError: sys.stderr.write("Ignoring rule " + rule_file + " (parsing failed)\n") continue From bf5d40eec3427f14af9ff9381780495e55266cbe Mon Sep 17 00:00:00 2001 From: Hendrik Date: Thu, 5 Nov 2020 23:34:25 +0100 Subject: [PATCH 16/20] New Backend - Kibana NDJSON Tested against 7.9.3 --- tools/config/ecs-proxy.yml | 1 + tools/sigma/backends/elasticsearch.py | 122 ++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/tools/config/ecs-proxy.yml b/tools/config/ecs-proxy.yml index 0659f7c34..2aa441a17 100644 --- a/tools/config/ecs-proxy.yml +++ b/tools/config/ecs-proxy.yml @@ -6,6 +6,7 @@ backends: - es-rule - corelight_elasticsearch-rule - kibana + - kibana-ndjson - xpack-watcher - elastalert - elastalert-dsl diff --git a/tools/sigma/backends/elasticsearch.py b/tools/sigma/backends/elasticsearch.py index d22a0c37e..1ad8d5282 100644 --- a/tools/sigma/backends/elasticsearch.py +++ b/tools/sigma/backends/elasticsearch.py @@ -1331,3 +1331,125 @@ class ElasticSearchRuleBackend(ElasticsearchQuerystringBackend): if references: rule.update({"references": references}) return json.dumps(rule) + +class KibanaNdjsonBackend(ElasticsearchQuerystringBackend, MultiRuleOutputMixin): + """Converts Sigma rule into Kibana JSON Configuration files (searches only).""" + identifier = "kibana-ndjson" + active = True + options = ElasticsearchQuerystringBackend.options + ( + ("output", "import", "Output format: import = JSON file manually imported in Kibana, curl = Shell script that imports queries in Kibana via curl (jq is additionally required)", "output_type"), + ("es", "localhost:9200", "Host and port of Elasticsearch instance", None), + ("index", ".kibana", "Kibana index", None), + ("prefix", "Sigma: ", "Title prefix of Sigma queries", None), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.kibanaconf = list() + self.indexsearch = set() + + def generate(self, sigmaparser): + description = sigmaparser.parsedyaml.setdefault("description", "") + + columns = list() + try: + for field in sigmaparser.parsedyaml["fields"]: + mapped = sigmaparser.config.get_fieldmapping(field).resolve_fieldname(field, sigmaparser) + if type(mapped) == str: + columns.append(mapped) + elif type(mapped) == list: + columns.extend(mapped) + else: + raise TypeError("Field mapping must return string or list") + except KeyError: # no 'fields' attribute + pass + + indices = sigmaparser.get_logsource().index + if len(indices) == 0: # fallback if no index is given + indices = ["*"] + + for parsed in sigmaparser.condparsed: + result = self.generateNode(parsed.parsedSearch) + + for index in indices: + rulename = self.getRuleName(sigmaparser) + if len(indices) > 1: # add index names if rule must be replicated because of ambigiuous index patterns + raise NotSupportedError("Multiple target indices are not supported by Kibana") + else: + title = self.prefix + sigmaparser.parsedyaml["title"] + + self.indexsearch.add( + "export {indexvar}=$(curl -s '{es}/{index}/_search?q=index-pattern.title:{indexpattern}' | jq -r '.hits.hits[0]._id | ltrimstr(\"index-pattern:\")')".format( + es=self.es, + index=self.index, + indexpattern=index.replace("*", "\\*"), + indexvar=self.index_variable_name(index) + ) + ) + self.kibanaconf.append({ + "id": rulename, + "type": "search", + "attributes": { + "title": title, + "description": description, + "hits": 0, + "columns": columns, + "sort": ["@timestamp", "desc"], + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": { + "index": index, + "filter": [], + "highlight": { + "pre_tags": ["@kibana-highlighted-field@"], + "post_tags": ["@/kibana-highlighted-field@"], + "fields": { "*":{} }, + "require_field_match": False, + "fragment_size": 2147483647 + }, + "query": { + "query_string": { + "query": result, + "analyze_wildcard": True + } + } + } + } + }, + "references": [ + { + "id": index, + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ] + }) + + def finalize(self): + if self.output_type == "import": # output format that can be imported via Kibana UI + for item in self.kibanaconf: # JSONize kibanaSavedObjectMeta.searchSourceJSON + item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON'] = json.dumps(item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON']) + if self.kibanaconf: + for item in self.kibanaconf: + return json.dumps(item) + elif self.output_type == "curl": + for item in self.indexsearch: + return item + for item in self.kibanaconf: + item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON']['index'] = "$" + self.index_variable_name(item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON']['index']) # replace index pattern with reference to variable that will contain Kibana index UUID at script runtime + item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON'] = json.dumps(item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON']) # Convert it to JSON string as expected by Kibana + item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON'] = item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON'].replace("\\", "\\\\") # Add further escaping for escaped quotes for shell + return "curl -s -XPUT -H 'Content-Type: application/json' --data-binary @- '{es}/{index}/doc/{doc_id}' < Date: Thu, 5 Nov 2020 23:37:01 +0100 Subject: [PATCH 17/20] Updating attack navigator version to v4.0 --- tools/sigma/sigma2attack.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/sigma/sigma2attack.py b/tools/sigma/sigma2attack.py index 9b27126a8..165d077fc 100755 --- a/tools/sigma/sigma2attack.py +++ b/tools/sigma/sigma2attack.py @@ -61,7 +61,10 @@ def main(): "maxValue": curr_max_technique_count, "minValue": 0 }, - "version": "2.2", + "versions": { + "navigator": "4.0", + "layer": "4.0" + }, "techniques": scores, } From 96e90fbff2f40b6af7df383ba55977f143260878 Mon Sep 17 00:00:00 2001 From: Hendrik Date: Fri, 6 Nov 2020 12:43:52 +0100 Subject: [PATCH 18/20] Fix recursion of rules --- tools/sigma/backends/elasticsearch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/sigma/backends/elasticsearch.py b/tools/sigma/backends/elasticsearch.py index 1ad8d5282..c8a7acb53 100644 --- a/tools/sigma/backends/elasticsearch.py +++ b/tools/sigma/backends/elasticsearch.py @@ -1430,8 +1430,11 @@ class KibanaNdjsonBackend(ElasticsearchQuerystringBackend, MultiRuleOutputMixin) for item in self.kibanaconf: # JSONize kibanaSavedObjectMeta.searchSourceJSON item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON'] = json.dumps(item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON']) if self.kibanaconf: + ndjson = "" for item in self.kibanaconf: - return json.dumps(item) + ndjson += json.dumps(item) + ndjson += "\n" + return ndjson elif self.output_type == "curl": for item in self.indexsearch: return item From 7e742cc049806ae9252c3418a3631f16abfa2d07 Mon Sep 17 00:00:00 2001 From: Hendrik Date: Mon, 9 Nov 2020 08:42:35 +0100 Subject: [PATCH 19/20] kibana-ndjson for all configs which already have kibana --- tools/config/ecs-cloudtrail.yml | 1 + tools/config/ecs-dns.yml | 1 + tools/config/ecs-zeek-corelight.yml | 1 + tools/config/ecs-zeek-elastic-beats-implementation.yml | 1 + tools/config/filebeat-defaultindex.yml | 1 + tools/config/helk.yml | 1 + tools/config/logstash-defaultindex.yml | 1 + tools/config/logstash-linux.yml | 1 + tools/config/logstash-windows.yml | 1 + tools/config/logstash-zeek-default-json.yml | 1 + tools/config/winlogbeat-modules-enabled.yml | 1 + tools/config/winlogbeat-old.yml | 1 + tools/config/winlogbeat.yml | 1 + 13 files changed, 13 insertions(+) diff --git a/tools/config/ecs-cloudtrail.yml b/tools/config/ecs-cloudtrail.yml index fe9419bd4..5a1368635 100644 --- a/tools/config/ecs-cloudtrail.yml +++ b/tools/config/ecs-cloudtrail.yml @@ -5,6 +5,7 @@ backends: - es-dsl - es-rule - kibana + - kibana-ndjson - xpack-watcher - elastalert - elastalert-dsl diff --git a/tools/config/ecs-dns.yml b/tools/config/ecs-dns.yml index d41c06398..fddfc32eb 100644 --- a/tools/config/ecs-dns.yml +++ b/tools/config/ecs-dns.yml @@ -5,6 +5,7 @@ backends: - es-dsl - elasticsearch-rule - kibana + - kibana-ndjson - xpack-watcher - elastalert - elastalert-dsl diff --git a/tools/config/ecs-zeek-corelight.yml b/tools/config/ecs-zeek-corelight.yml index 0707a7f72..5bf7dab3b 100644 --- a/tools/config/ecs-zeek-corelight.yml +++ b/tools/config/ecs-zeek-corelight.yml @@ -8,6 +8,7 @@ backends: - elasticsearch-rule - corelight_elasticsearch-rule - kibana + - kibana-ndjson - corelight_kibana - xpack-watcher - corelight_xpack-watcher diff --git a/tools/config/ecs-zeek-elastic-beats-implementation.yml b/tools/config/ecs-zeek-elastic-beats-implementation.yml index cd999bb51..ac9b8a45c 100644 --- a/tools/config/ecs-zeek-elastic-beats-implementation.yml +++ b/tools/config/ecs-zeek-elastic-beats-implementation.yml @@ -5,6 +5,7 @@ backends: - es-dsl - elasticsearch-rule - kibana + - kibana-ndjson - xpack-watcher - elastalert - elastalert-dsl diff --git a/tools/config/filebeat-defaultindex.yml b/tools/config/filebeat-defaultindex.yml index 940e34f9b..eb30975da 100644 --- a/tools/config/filebeat-defaultindex.yml +++ b/tools/config/filebeat-defaultindex.yml @@ -5,6 +5,7 @@ backends: - es-dsl - es-rule - kibana + - kibana-ndjson - xpack-watcher - elastalert - elastalert-dsl diff --git a/tools/config/helk.yml b/tools/config/helk.yml index 7042b25f2..c6077fa77 100644 --- a/tools/config/helk.yml +++ b/tools/config/helk.yml @@ -5,6 +5,7 @@ backends: - es-dsl - es-rule - kibana + - kibana-ndjson - xpack-watcher - elastalert - elastalert-dsl diff --git a/tools/config/logstash-defaultindex.yml b/tools/config/logstash-defaultindex.yml index eb566f041..107ca4d92 100644 --- a/tools/config/logstash-defaultindex.yml +++ b/tools/config/logstash-defaultindex.yml @@ -5,6 +5,7 @@ backends: - es-dsl - es-rule - kibana + - kibana-ndjson - xpack-watcher - elastalert - elastalert-dsl diff --git a/tools/config/logstash-linux.yml b/tools/config/logstash-linux.yml index e15e2050d..1ad673fed 100644 --- a/tools/config/logstash-linux.yml +++ b/tools/config/logstash-linux.yml @@ -5,6 +5,7 @@ backends: - es-dsl - es-rule - kibana + - kibana-ndjson - xpack-watcher - elastalert - elastalert-dsl diff --git a/tools/config/logstash-windows.yml b/tools/config/logstash-windows.yml index d21a846bd..317abd9f0 100644 --- a/tools/config/logstash-windows.yml +++ b/tools/config/logstash-windows.yml @@ -5,6 +5,7 @@ backends: - es-dsl - es-rule - kibana + - kibana-ndjson - xpack-watcher - elastalert - elastalert-dsl diff --git a/tools/config/logstash-zeek-default-json.yml b/tools/config/logstash-zeek-default-json.yml index 6915fe14d..e6b1d14ee 100644 --- a/tools/config/logstash-zeek-default-json.yml +++ b/tools/config/logstash-zeek-default-json.yml @@ -5,6 +5,7 @@ backends: - es-dsl - elasticsearch-rule - kibana + - kibana-ndjson - xpack-watcher - elastalert - elastalert-dsl diff --git a/tools/config/winlogbeat-modules-enabled.yml b/tools/config/winlogbeat-modules-enabled.yml index 4009a9bde..292f8d0d6 100644 --- a/tools/config/winlogbeat-modules-enabled.yml +++ b/tools/config/winlogbeat-modules-enabled.yml @@ -5,6 +5,7 @@ backends: - es-dsl - es-rule - kibana + - kibana-ndjson - xpack-watcher - elastalert - elastalert-dsl diff --git a/tools/config/winlogbeat-old.yml b/tools/config/winlogbeat-old.yml index f60c49b84..3955c35a5 100644 --- a/tools/config/winlogbeat-old.yml +++ b/tools/config/winlogbeat-old.yml @@ -5,6 +5,7 @@ backends: - es-dsl - es-rule - kibana + - kibana-ndjson - xpack-watcher - elastalert - elastalert-dsl diff --git a/tools/config/winlogbeat.yml b/tools/config/winlogbeat.yml index 3bc1824e4..4b13103dd 100644 --- a/tools/config/winlogbeat.yml +++ b/tools/config/winlogbeat.yml @@ -5,6 +5,7 @@ backends: - es-dsl - es-rule - kibana + - kibana-ndjson - xpack-watcher - elastalert - elastalert-dsl From f6c0fb2d3330810afacc1e9cb486a2ae1505e059 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Mon, 9 Nov 2020 16:34:12 +0100 Subject: [PATCH 20/20] fix: FPs with notepad++ GUP rule --- rules/windows/process_creation/win_susp_gup.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rules/windows/process_creation/win_susp_gup.yml b/rules/windows/process_creation/win_susp_gup.yml index aaeacc966..19acad192 100644 --- a/rules/windows/process_creation/win_susp_gup.yml +++ b/rules/windows/process_creation/win_susp_gup.yml @@ -10,6 +10,7 @@ tags: - attack.t1073 # an old one author: Florian Roth date: 2019/02/06 +modified: 2020/11/09 logsource: category: process_creation product: windows @@ -17,11 +18,11 @@ detection: selection: Image: '*\GUP.exe' filter: - Image: - - 'C:\Users\\*\AppData\Local\Notepad++\updater\gup.exe' - - 'C:\Users\\*\AppData\Roaming\Notepad++\updater\gup.exe' - - 'C:\Program Files\Notepad++\updater\gup.exe' - - 'C:\Program Files (x86)\Notepad++\updater\gup.exe' + Image|endswith: + - ':\Users\\*\AppData\Local\Notepad++\updater\GUP.exe' + - ':\Users\\*\AppData\Roaming\Notepad++\updater\GUP.exe' + - ':\Program Files\Notepad++\updater\GUP.exe' + - ':\Program Files (x86)\Notepad++\updater\GUP.exe' condition: selection and not filter falsepositives: - Execution of tools named GUP.exe and located in folders different than Notepad++\updater