2017-02-13 22:14:40 +00:00
|
|
|
# Output backends for sigmac
|
|
|
|
|
|
|
|
import json
|
2017-02-22 21:47:12 +00:00
|
|
|
import re
|
|
|
|
import sigma
|
2017-02-13 22:14:40 +00:00
|
|
|
|
|
|
|
def getBackendList():
|
|
|
|
"""Return list of backend classes"""
|
|
|
|
return list(filter(lambda cls: type(cls) == type and issubclass(cls, BaseBackend) and cls.active, [item[1] for item in globals().items()]))
|
|
|
|
|
|
|
|
def getBackendDict():
|
|
|
|
return {cls.identifier: cls for cls in getBackendList() }
|
|
|
|
|
2017-02-22 21:47:12 +00:00
|
|
|
def getBackend(name):
|
|
|
|
try:
|
|
|
|
return getBackendDict()[name]
|
|
|
|
except KeyError as e:
|
|
|
|
raise LookupError("Backend not found") from e
|
|
|
|
|
2017-02-13 22:14:40 +00:00
|
|
|
class BaseBackend:
|
|
|
|
"""Base class for all backends"""
|
|
|
|
identifier = "base"
|
|
|
|
active = False
|
|
|
|
|
2017-03-06 21:07:04 +00:00
|
|
|
def __init__(self, sigmaconfig):
|
|
|
|
if not isinstance(sigmaconfig, (sigma.SigmaConfiguration, None)):
|
|
|
|
raise TypeError("SigmaConfiguration object expected")
|
|
|
|
self.sigmaconfig = sigmaconfig
|
|
|
|
|
2017-02-22 21:47:12 +00:00
|
|
|
def generate(self, parsed):
|
|
|
|
return self.generateNode(parsed.getParseTree())
|
|
|
|
|
|
|
|
def generateNode(self, node):
|
|
|
|
if type(node) == sigma.ConditionAND:
|
2017-03-01 20:47:51 +00:00
|
|
|
return self.generateANDNode(node)
|
2017-02-22 21:47:12 +00:00
|
|
|
elif type(node) == sigma.ConditionOR:
|
2017-03-01 20:47:51 +00:00
|
|
|
return self.generateORNode(node)
|
2017-02-22 21:47:12 +00:00
|
|
|
elif type(node) == sigma.ConditionNOT:
|
2017-03-01 20:47:51 +00:00
|
|
|
return self.generateNOTNode(node)
|
2017-02-22 21:47:12 +00:00
|
|
|
elif type(node) == sigma.NodeSubexpression:
|
2017-03-01 20:47:51 +00:00
|
|
|
return self.generateSubexpressionNode(node)
|
2017-02-22 21:47:12 +00:00
|
|
|
elif type(node) == tuple:
|
2017-03-01 20:47:51 +00:00
|
|
|
return self.generateMapItemNode(node)
|
2017-02-22 21:47:12 +00:00
|
|
|
elif type(node) in (str, int):
|
2017-03-01 20:47:51 +00:00
|
|
|
return self.generateValueNode(node)
|
2017-02-22 21:47:12 +00:00
|
|
|
elif type(node) == list:
|
2017-03-01 20:47:51 +00:00
|
|
|
return self.generateListNode(node)
|
2017-02-22 21:47:12 +00:00
|
|
|
else:
|
|
|
|
raise TypeError("Node type %s was not expected in Sigma parse tree" % (str(type(node))))
|
2017-02-13 22:14:40 +00:00
|
|
|
|
2017-03-01 20:47:51 +00:00
|
|
|
def generateANDNode(self, node):
|
|
|
|
raise NotImplementedError("Node type not implemented for this backend")
|
|
|
|
|
|
|
|
def generateORNode(self, node):
|
|
|
|
raise NotImplementedError("Node type not implemented for this backend")
|
|
|
|
|
|
|
|
def generateNOTNode(self, node):
|
|
|
|
raise NotImplementedError("Node type not implemented for this backend")
|
|
|
|
|
|
|
|
def generateSubexpressionNode(self, node):
|
|
|
|
raise NotImplementedError("Node type not implemented for this backend")
|
|
|
|
|
|
|
|
def generateListNode(self, node):
|
|
|
|
raise NotImplementedError("Node type not implemented for this backend")
|
|
|
|
|
|
|
|
def generateMapItemNode(self, node):
|
|
|
|
raise NotImplementedError("Node type not implemented for this backend")
|
|
|
|
|
|
|
|
def generateValueNode(self, node):
|
|
|
|
raise NotImplementedError("Node type not implemented for this backend")
|
|
|
|
|
|
|
|
class ElasticsearchQuerystringBackend(BaseBackend):
|
|
|
|
"""Converts Sigma rule into Elasticsearch query string. Only searches, no aggregations."""
|
|
|
|
identifier = "es-qs"
|
|
|
|
active = True
|
2017-03-02 11:07:07 +00:00
|
|
|
reEscape = re.compile("([+\\-=!(){}\\[\\]^\"~:\\\\/]|&&|\\|\\|)")
|
2017-03-01 20:47:51 +00:00
|
|
|
reClear = re.compile("[<>]")
|
|
|
|
|
|
|
|
def cleanValue(self, val):
|
|
|
|
val = self.reEscape.sub("\\\\\g<1>", val)
|
|
|
|
return self.reClear.sub("", val)
|
|
|
|
|
|
|
|
def generateANDNode(self, node):
|
|
|
|
return " AND ".join([self.generateNode(val) for val in node])
|
|
|
|
|
|
|
|
def generateORNode(self, node):
|
|
|
|
return " OR ".join([self.generateNode(val) for val in node])
|
|
|
|
|
|
|
|
def generateNOTNode(self, node):
|
|
|
|
return "NOT " + self.generateNode(node.item)
|
|
|
|
|
|
|
|
def generateSubexpressionNode(self, node):
|
|
|
|
return "(%s)" % self.generateNode(node.items)
|
|
|
|
|
|
|
|
def generateListNode(self, node):
|
|
|
|
if not set([type(value) for value in node]).issubset({str, int}):
|
|
|
|
raise TypeError("List values must be strings or numbers")
|
|
|
|
return "(%s)" % (" ".join([self.generateNode(value) for value in node]))
|
|
|
|
|
|
|
|
def generateMapItemNode(self, node):
|
|
|
|
key, value = node
|
|
|
|
if type(value) not in (str, int, list):
|
|
|
|
raise TypeError("Map values must be strings, numbers or lists, not " + str(type(value)))
|
2017-03-06 21:07:04 +00:00
|
|
|
return "%s:%s" % (self.sigmaconfig.get_fieldmapping(key), self.generateNode(value))
|
2017-03-01 20:47:51 +00:00
|
|
|
|
|
|
|
def generateValueNode(self, node):
|
|
|
|
return "\"%s\"" % (self.cleanValue(str(node)))
|
|
|
|
|
2017-02-13 22:14:40 +00:00
|
|
|
class ElasticsearchDSLBackend(BaseBackend):
|
|
|
|
"""Converts Sigma rule into Elasticsearch DSL query (JSON)."""
|
|
|
|
identifier = "es-dsl"
|
2017-03-02 21:55:45 +00:00
|
|
|
active = False
|
2017-02-13 22:14:40 +00:00
|
|
|
|
|
|
|
class KibanaBackend(ElasticsearchDSLBackend):
|
|
|
|
"""Converts Sigma rule into Kibana JSON Configurations."""
|
|
|
|
identifier = "kibana"
|
2017-03-02 21:55:45 +00:00
|
|
|
active = False
|
2017-02-13 22:14:40 +00:00
|
|
|
|
|
|
|
class SplunkBackend(BaseBackend):
|
|
|
|
"""Converts Sigma rule into Splunk Search Processing Language (SPL)."""
|
|
|
|
identifier = "splunk"
|
2017-03-02 22:34:12 +00:00
|
|
|
active = True
|
2017-03-04 09:37:35 +00:00
|
|
|
reEscape = re.compile('(["\\\\])')
|
2017-03-02 22:34:12 +00:00
|
|
|
|
|
|
|
def cleanValue(self, val):
|
|
|
|
return self.reEscape.sub("\\\\\g<1>", val)
|
|
|
|
|
|
|
|
def generateANDNode(self, node):
|
2017-03-04 09:37:35 +00:00
|
|
|
return " ".join([self.generateNode(val) for val in node])
|
2017-03-02 22:34:12 +00:00
|
|
|
|
|
|
|
def generateORNode(self, node):
|
|
|
|
return " OR ".join([self.generateNode(val) for val in node])
|
|
|
|
|
|
|
|
def generateNOTNode(self, node):
|
|
|
|
return "NOT " + self.generateNode(node.item)
|
|
|
|
|
|
|
|
def generateSubexpressionNode(self, node):
|
|
|
|
return "(%s)" % self.generateNode(node.items)
|
|
|
|
|
|
|
|
def generateListNode(self, node):
|
|
|
|
if not set([type(value) for value in node]).issubset({str, int}):
|
|
|
|
raise TypeError("List values must be strings or numbers")
|
|
|
|
return "(%s)" % (" ".join([self.generateNode(value) for value in node]))
|
|
|
|
|
|
|
|
def generateMapItemNode(self, node):
|
|
|
|
key, value = node
|
|
|
|
if type(value) in (str, int):
|
2017-03-06 21:07:04 +00:00
|
|
|
return '%s=%s' % (self.sigmaconfig.get_fieldmapping(key), self.generateNode(value))
|
2017-03-02 22:34:12 +00:00
|
|
|
elif type(value) == list:
|
2017-03-06 21:07:04 +00:00
|
|
|
return "(" + (" OR ".join(['%s=%s' % (self.sigmaconfig.get_fieldmapping(key), self.generateValueNode(item)) for item in value])) + ")"
|
2017-03-02 22:34:12 +00:00
|
|
|
else:
|
|
|
|
raise TypeError("Map values must be strings, numbers or lists, not " + str(type(value)))
|
|
|
|
|
|
|
|
def generateValueNode(self, node):
|
|
|
|
return "\"%s\"" % (self.cleanValue(str(node)))
|
2017-02-13 22:14:40 +00:00
|
|
|
|
|
|
|
class NullBackend(BaseBackend):
|
|
|
|
"""Does nothing, for debugging purposes."""
|
|
|
|
identifier = "null"
|
2017-03-02 21:55:45 +00:00
|
|
|
active = False
|
2017-02-22 21:47:12 +00:00
|
|
|
|
|
|
|
def generate(self, parsed):
|
|
|
|
pass
|