From d13e8d7bd30771a50ba7e404d7ec75bf8a22aae5 Mon Sep 17 00:00:00 2001 From: nikotin Date: Thu, 7 Jun 2018 16:18:23 +0300 Subject: [PATCH] Added ArcSight & Qualys backends --- tools/config/arcsight.yml | 102 +++++++++++++++++ tools/config/qualys.yml | 17 +++ tools/sigma/backends.py | 229 ++++++++++++++++++++++++++++++++++++++ tools/sigmac | 10 ++ 4 files changed, 358 insertions(+) create mode 100644 tools/config/arcsight.yml create mode 100644 tools/config/qualys.yml diff --git a/tools/config/arcsight.yml b/tools/config/arcsight.yml new file mode 100644 index 000000000..7e6a15fff --- /dev/null +++ b/tools/config/arcsight.yml @@ -0,0 +1,102 @@ +logsources: + linux: + product: linux + conditions: + deviceVendor: Unix + linux-sshd: + product: linux + service: sshd + conditions: + deviceVendor: Unix + linux-auth: + product: linux + service: auth + conditions: + deviceVendor: Unix + linux-clamav: + product: linux + service: clamav + conditions: + deviceVendor: Unix + windows-dns: + product: windows + service: dns-server + conditions: + deviceVendor: Microsoft + deviceProduct: DNS-Server + windows-pc: + product: windows + service: powershell-classic + conditions: + deviceVendor: Microsoft + windows-sys: + product: windows + service: sysmon + conditions: + deviceVendor: Microsoft + deviceProduct: Sysmon + windows-sec: + product: windows + service: security + conditions: + deviceVendor: Microsoft + deviceProduct: Microsoft Windows + windows-power: + product: windows + service: powershell + conditions: + deviceVendor: Microsoft + windows-system: + product: windows + service: system + conditions: + deviceVendor: Microsoft + windows-driver: + product: windows + service: driver-framework + conditions: + deviceVendor: Microsoft + windows-app: + product: windows + service: application + conditions: + deviceVendor: Microsoft + proxy: + category: proxy + conditions: + categoryDeviceGroup: /Proxy + python: + product: python + conditions: + deviceProduct: Python + categoryDeviceGroup: /Application + ruby_on_rails: + product: ruby_on_rails + conditions: + deviceProduct: Ruby on Rails + categoryDeviceGroup: /Application + spring: + product: spring + conditions: + deviceProduct: Spring + categoryDeviceGroup: /Application + apache: + product: apache + conditions: + deviceProduct: Apache + categoryDeviceGroup: /Application + firewall: + product: firewall + conditions: + categoryDeviceGroup: /Firewall + +fieldmappings: + EventID: externalId + dst: + - destinationAddress + dst_ip: + - destinationAddress + src: + - sourceAddress + src_ip: + - sourceAddress diff --git a/tools/config/qualys.yml b/tools/config/qualys.yml new file mode 100644 index 000000000..7c2efbb7a --- /dev/null +++ b/tools/config/qualys.yml @@ -0,0 +1,17 @@ +fieldmappings: + dst: + - network.remote.address.ip + dst_ip: + - network.remote.address.ip + src: + - network.local.address.ip + src_ip: + - network.local.address.ip + file_hash: + - file.hash.md5 + - file.hash.sha256 + NewProcessName: process.name + ServiceName: process.name + ServiceFileName: process.name + TargetObject: registry.path + diff --git a/tools/sigma/backends.py b/tools/sigma/backends.py index 82c2f01d2..6f21700ba 100644 --- a/tools/sigma/backends.py +++ b/tools/sigma/backends.py @@ -982,3 +982,232 @@ class BackendError(Exception): class NotSupportedError(BackendError): """Exception is raised if some output is required that is not supported by the target language.""" pass + +class PartialMatchError(Exception): + pass + +class FullMatchError(Exception): + pass + +class ArcSightBackend(SingleTextQueryBackend): + """ + Converts Sigma rule into ArcSight saved search. + Contributed by SOC Prime. https://socprime.com + """ + identifier = "as" + active = True + andToken = " AND " + orToken = " OR " + notToken = " NOT " + subExpression = "(%s)" + listExpression = "(%s)" + listSeparator = " OR " + valueExpression = "\"%s\"" + containsExpression = "%s CONTAINS %s" + nullExpression = "NOT _exists_:%s" + notNullExpression = "_exists_:%s" + mapExpression = "%s = %s" + mapListsSpecialHandling = True + mapListValueExpression = "%s = %s" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + aFL = ["deviceVendor", "categoryDeviceGroup", "deviceProduct"] + for item in self.sigmaconfig.fieldmappings.values(): + if item.target_type is list: + aFL.extend(item.target) + else: + aFL.append(item.target) + self.allowedFieldsList = list(set(aFL)) + + # Skip logsource value from sigma document for separate path. + def generateCleanValueNodeLogsource(self, value): + return self.valueExpression % (self.cleanValue(str(value))) + + # Clearing values from special characters. + def CleanNode(self, node): + search_ptrn = re.compile(r"[\/\\@?#&_%*',\(\)\" ]") + replace_ptrn = re.compile(r"[ \/\\@?#&_%*',\(\)\" ]") + match = search_ptrn.search(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 + return node + + # Clearing values from special characters. + def generateMapItemNode(self, node): + 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): + return self.mapExpression % (key, self.generateCleanValueNodeLogsource(value)) + elif type(value) is list: + return self.generateMapItemListNode(key, value) + else: + raise TypeError("Backend does not support map values of type " + str(type(value))) + else: + if self.mapListsSpecialHandling == False and type(value) in ( + str, int, list) or self.mapListsSpecialHandling == True and type(value) in (str, int): + if type(value) is str: + new_value = list() + value = self.CleanNode(value) + if type(value) == list: + new_value.append(self.andToken.join([self.valueExpression % val for val in value])) + else: + new_value.append(value) + if len(new_value)==1: + return "(" + self.generateANDNode(new_value) + ")" + else: + 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) + if type(item) is list and len(item) == 1: + new_value.append(self.valueExpression % item[0]) + elif type(item) is list: + new_value.append(self.andToken.join([self.valueExpression % val for val in item])) + else: + new_value.append(item) + return self.generateORNode(new_value) + else: + raise TypeError("Backend does not support map values of type " + str(type(value))) + + # for keywords values with space + def generateValueNode(self, node): + if type(node) is int: + return self.cleanValue(str(node)) + if 'AND' in node: + return "(" + self.cleanValue(str(node)) + ")" + else: + return self.cleanValue(str(node)) + + # collect elements of Arcsight search using OR + def generateMapItemListNode(self, key, value): + itemslist = list() + for item in value: + if key in self.allowedFieldsList: + itemslist.append('%s = %s' % (key, self.generateValueNode(item))) + else: + itemslist.append('%s' % (self.generateValueNode(item))) + return " OR ".join(itemslist) + + # prepare of tail for every translate + def generate(self, sigmaparser): + """Method is called for each sigma rule and receives the parsed rule (SigmaParser)""" + const_title = ' AND type != 2 | rex field = flexString1 mode=sed "s//Sigma: {}/g"' + for parsed in sigmaparser.condparsed: + self.output.print(self.generateQuery(parsed) + const_title.format(sigmaparser.parsedyaml["title"])) + + # Add "( )" for values + def generateSubexpressionNode(self, node): + return self.subExpression % self.generateNode(node.items) + + # generateORNode algorithm for ArcSightBackend class. + def generateORNode(self, node): + if type(node) == sigma.parser.ConditionOR and all(isinstance(item, str) for item in node): + new_value = list() + for value in node: + 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 QualysBackend(SingleTextQueryBackend): + """ + Converts Sigma rule into Qualys saved search. + Contributed by SOC Prime. https://socprime.com + """ + identifier = "qualys" + active = True + andToken = " and " + orToken = " or " + notToken = "not " + subExpression = "(%s)" + listExpression = "%s" + listSeparator = " " + valueExpression = "%s" + nullExpression = "%s is null" + notNullExpression = "not (%s is null)" + mapExpression = "%s:`%s`" + mapListsSpecialHandling = True + PartialMatchFlag = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + fl = [] + for item in self.sigmaconfig.fieldmappings.values(): + if item.target_type == list: + fl.extend(item.target) + else: + fl.append(item.target) + self.allowedFieldsList = list(set(fl)) + + + def generateORNode(self, node): + new_list = [] + for val in node: + if type(val) == tuple and not(val[0] in self.allowedFieldsList): + pass + # self.PartialMatchFlag = True + else: + new_list.append(val) + + return self.orToken.join([self.generateNode(val) for val in new_list]) + + def generateANDNode(self, node): + new_list = [] + for val in node: + if type(val) == tuple and not(val[0] in self.allowedFieldsList): + self.PartialMatchFlag = True + else: + new_list.append(val) + return self.andToken.join([self.generateNode(val) for val in new_list]) + + def generateMapItemNode(self, node): + key, value = node + if self.mapListsSpecialHandling == False and type(value) in (str, int, list) or self.mapListsSpecialHandling == True and type(value) in (str, int): + if key in self.allowedFieldsList: + return self.mapExpression % (key, self.generateNode(value)) + else: + return self.generateNode(value) + elif type(value) == list: + return self.generateMapItemListNode(key, value) + else: + raise TypeError("Backend does not support map values of type " + str(type(value))) + + def generateMapItemListNode(self, key, value): + itemslist = [] + for item in value: + if key in self.allowedFieldsList: + itemslist.append('%s:`%s`' % (key, self.generateValueNode(item))) + else: + itemslist.append('%s' % (self.generateValueNode(item))) + return "(" + (" or ".join(itemslist)) + ")" + + def generate(self, sigmaparser): + """Method is called for each sigma rule and receives the parsed rule (SigmaParser)""" + all_keys = set() + + for parsed in sigmaparser.condparsed: + if self.generateQuery(parsed) == "()": + self.PartialMatchFlag = None + sigmaparser_parsedyaml = sigmaparser.parsedyaml + if self.PartialMatchFlag == True: + raise PartialMatchError(self.generateQuery(parsed)) + elif self.PartialMatchFlag == None: + raise FullMatchError(self.generateQuery(parsed)) + else: + print(self.generateQuery(parsed)) + \ No newline at end of file diff --git a/tools/sigmac b/tools/sigmac index 74fd5d1d5..645115012 100755 --- a/tools/sigmac +++ b/tools/sigmac @@ -165,6 +165,16 @@ for sigmafile in get_inputs(cmdargs.inputs, cmdargs.recurse): error = 42 if not cmdargs.defer_abort: sys.exit(error) + except backends.PartialMatchError as e: + print("%s" % (str(e),), file=sys.stderr) + error = 80 + if not cmdargs.defer_abort: + sys.exit(error) + except backends.FullMatchError as e: + print("Full Mismatch Error", file=sys.stderr) + error = 90 + if not cmdargs.defer_abort: + sys.exit(error) finally: try: f.close()