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-05-26 21:42:49 +00:00
### Generic base classes
2017-02-13 22:14:40 +00:00
class BaseBackend :
""" Base class for all backends """
identifier = " base "
active = False
2017-03-17 22:28:06 +00:00
index_field = None # field name that is used to address indices
2017-02-13 22:14:40 +00:00
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-03-17 22:28:06 +00:00
self . sigmaconfig . set_backend ( self )
2017-03-06 21:07:04 +00:00
2017-02-22 21:47:12 +00:00
def generate ( self , parsed ) :
2017-04-16 22:11:20 +00:00
result = self . generateNode ( parsed . parsedSearch )
if parsed . parsedAgg :
result + = self . generateAggregation ( parsed . parsedAgg )
return result
2017-02-22 21:47:12 +00:00
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 " )
2017-03-29 21:18:47 +00:00
def generateAggregation ( self , agg ) :
raise NotImplementedError ( " Aggregations not implemented for this backend " )
2017-06-02 21:43:45 +00:00
class SingleTextQueryBackend ( BaseBackend ) :
""" Base class for backends that generate one text-based expression from a Sigma rule """
identifier = " base-textquery "
active = False
2017-05-26 21:42:49 +00:00
2017-06-02 21:43:45 +00:00
# the following class variables define the generation and behavior of queries from a parse tree some are prefilled with default values that are quite usual
reEscape = None # match characters that must be quoted
escapeSubst = " \\ \\ \ g<1> " # Substitution that is applied to characters/strings matched for escaping by reEscape
reClear = None # match characters that are cleaned out completely
andToken = None # Token used for linking expressions with logical AND
orToken = None # Same for OR
notToken = None # Same for NOT
subExpression = None # Syntax for subexpressions, usually parenthesis around it. %s is inner expression
listExpression = None # Syntax for lists, %s are list items separated with listSeparator
listSeparator = None # Character for separation of list items
valueExpression = None # Expression of values, %s represents value
mapExpression = None # Syntax for field/value conditions. First %s is key, second is value
mapListsSpecialHandling = False # Same handling for map items with list values as for normal values (strings, integers) if True, generateMapItemListNode method is called with node
mapListValueExpression = None # Syntax for field/value condititons where map value is a list
2017-03-01 20:47:51 +00:00
def cleanValue ( self , val ) :
2017-06-02 21:43:45 +00:00
if self . reEscape :
val = self . reEscape . sub ( self . escapeSubst , val )
if self . reClear :
val = self . reClear . sub ( " " , val )
return val
2017-03-01 20:47:51 +00:00
def generateANDNode ( self , node ) :
2017-06-02 21:43:45 +00:00
return self . andToken . join ( [ self . generateNode ( val ) for val in node ] )
2017-03-01 20:47:51 +00:00
def generateORNode ( self , node ) :
2017-06-02 21:43:45 +00:00
return self . orToken . join ( [ self . generateNode ( val ) for val in node ] )
2017-03-01 20:47:51 +00:00
def generateNOTNode ( self , node ) :
2017-06-02 21:43:45 +00:00
return self . notToken + self . generateNode ( node . item )
2017-03-01 20:47:51 +00:00
def generateSubexpressionNode ( self , node ) :
2017-06-02 21:43:45 +00:00
return self . subExpression % self . generateNode ( node . items )
2017-03-01 20:47:51 +00:00
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 " )
2017-06-02 21:43:45 +00:00
return self . listExpression % ( self . listSeparator . join ( [ self . generateNode ( value ) for value in node ] ) )
2017-03-01 20:47:51 +00:00
def generateMapItemNode ( self , node ) :
key , value = node
2017-06-02 21:43:45 +00:00
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 . 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 ) :
return self . mapListValueExpression % ( key , self . generateNode ( value ) )
2017-03-01 20:47:51 +00:00
def generateValueNode ( self , node ) :
2017-06-02 21:43:45 +00:00
return self . valueExpression % ( self . cleanValue ( str ( node ) ) )
### Backends for specific SIEMs
class ElasticsearchQuerystringBackend ( SingleTextQueryBackend ) :
""" Converts Sigma rule into Elasticsearch query string. Only searches, no aggregations. """
identifier = " es-qs "
active = True
reEscape = re . compile ( " ([+ \\ -=!() {} \\ [ \\ ]^ \" ~: \\ \\ /]|&&| \\ | \\ |) " )
reClear = re . compile ( " [<>] " )
andToken = " AND "
orToken = " OR "
notToken = " NOT "
subExpression = " ( %s ) "
listExpression = " ( %s ) "
listSeparator = " "
valueExpression = " \" %s \" "
mapExpression = " %s : %s "
mapListsSpecialHandling = False
2017-03-01 20:47:51 +00:00
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
2017-06-02 21:43:45 +00:00
class LogPointBackend ( SingleTextQueryBackend ) :
2017-03-18 10:12:06 +00:00
""" Converts Sigma rule into LogPoint query """
identifier = " logpoint "
active = True
2017-06-02 21:43:45 +00:00
reEscape = re . compile ( ' ([ " \\ \\ ]) ' )
reClear = None
andToken = " "
orToken = " OR "
notToken = " - "
subExpression = " ( %s ) "
listExpression = " [ %s ] "
listSeparator = " , "
valueExpression = " \" %s \" "
mapExpression = " %s = %s "
mapListsSpecialHandling = True
mapListValueExpression = " %s IN %s "
2017-03-18 10:12:06 +00:00
2017-06-19 13:21:29 +00:00
def generateAggregation ( self , agg ) :
if agg == None :
return " "
if agg . groupfield == None :
return " | chart %s ( %s ) as val | search val %s %s " % ( agg . aggfunc_notrans , agg . aggfield , agg . cond_op , agg . condition )
else :
return " | chart %s ( %s ) as val by %s | search val %s %s " % ( agg . aggfunc_notrans , agg . aggfield , agg . groupfield , agg . cond_op , agg . condition )
2017-06-02 21:43:45 +00:00
class SplunkBackend ( SingleTextQueryBackend ) :
2017-02-13 22:14:40 +00:00
""" Converts Sigma rule into Splunk Search Processing Language (SPL). """
identifier = " splunk "
2017-03-02 22:34:12 +00:00
active = True
2017-03-17 22:28:06 +00:00
index_field = " index "
2017-03-02 22:34:12 +00:00
2017-06-02 21:43:45 +00:00
reEscape = re . compile ( ' ([ " \\ \\ ]) ' )
reClear = None
andToken = " "
orToken = " OR "
notToken = " NOT "
subExpression = " ( %s ) "
listExpression = " ( %s ) "
listSeparator = " "
valueExpression = " \" %s \" "
mapExpression = " %s = %s "
mapListsSpecialHandling = False
mapListValueExpression = " %s IN %s "
def generateMapItemListNode ( self , node ) :
return " ( " + ( " OR " . join ( [ ' %s = %s ' % ( key , self . generateValueNode ( item ) ) for item in value ] ) ) + " ) "
2017-02-13 22:14:40 +00:00
2017-03-29 21:18:47 +00:00
def generateAggregation ( self , agg ) :
if agg == None :
return " "
if agg . groupfield == None :
return " | stats %s ( %s ) as val | search val %s %s " % ( agg . aggfunc_notrans , agg . aggfield , agg . cond_op , agg . condition )
else :
return " | stats %s ( %s ) as val by %s | search val %s %s " % ( agg . aggfunc_notrans , agg . aggfield , agg . groupfield , agg . cond_op , agg . condition )
2017-05-26 21:42:49 +00:00
### Backends for developement purposes
2017-03-06 21:47:30 +00:00
class FieldnameListBackend ( BaseBackend ) :
""" List all fieldnames from given Sigma rules for creation of a field mapping configuration. """
identifier = " fieldlist "
active = True
2017-02-22 21:47:12 +00:00
def generate ( self , parsed ) :
2017-03-29 21:18:47 +00:00
return " \n " . join ( sorted ( set ( list ( flatten ( self . generateNode ( parsed . parsedSearch ) ) ) ) ) )
2017-03-06 21:47:30 +00:00
def generateANDNode ( self , node ) :
return [ self . generateNode ( val ) for val in node ]
def generateORNode ( self , node ) :
return self . generateANDNode ( node )
def generateNOTNode ( self , node ) :
return self . generateNode ( node . item )
def generateSubexpressionNode ( self , node ) :
return 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 [ 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-23 23:48:32 +00:00
return [ key ]
2017-03-06 21:47:30 +00:00
def generateValueNode ( self , node ) :
return [ ]
# Helpers
def flatten ( l ) :
for i in l :
if type ( i ) == list :
yield from flatten ( i )
else :
yield i