2017-02-13 23:14:40 +01:00
# Output backends for sigmac
2018-07-24 00:01:16 +02:00
# Copyright 2018 Thomas Patzke
2017-12-07 21:55:43 +01:00
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2017-02-13 23:14:40 +01:00
2017-02-22 22:47:12 +01:00
import re
2020-05-08 13:41:52 +03:00
from functools import wraps
2018-07-20 23:30:32 +02:00
from . base import SingleTextQueryBackend
from . exceptions import NotSupportedError
2017-02-13 23:14:40 +01:00
2020-05-08 13:41:52 +03:00
def wrapper ( method ) :
@wraps ( method )
def _impl ( self , method_args ) :
key , value , * _ = method_args
if ' .keyword ' in key :
key = key . split ( ' .keyword ' ) [ 0 ]
if key not in self . skip_fields :
method_output = method ( self , method_args )
return method_output
else :
return
return _impl
2018-05-21 23:07:47 +02:00
class WindowsDefenderATPBackend ( SingleTextQueryBackend ) :
2020-05-08 13:41:52 +03:00
""" Converts Sigma rule into Microsoft Defender ATP Hunting Queries. """
identifier = " mdatp "
2018-05-21 23:07:47 +02:00
active = True
2019-04-22 23:40:21 +02:00
config_required = False
2018-05-21 23:07:47 +02:00
2019-02-02 00:18:58 +01:00
# \ -> \\
# \* -> \*
# \\* -> \\*
reEscape = re . compile ( ' ( " |(?<! \\ \\ ) \\ \\ (?![*? \\ \\ ])) ' )
2018-05-21 23:07:47 +02:00
reClear = None
andToken = " and "
orToken = " or "
notToken = " not "
subExpression = " ( %s ) "
listExpression = " ( %s ) "
listSeparator = " , "
valueExpression = " \" %s \" "
nullExpression = " isnull( %s ) "
notNullExpression = " isnotnull( %s ) "
mapExpression = " %s == %s "
mapListsSpecialHandling = True
mapListValueExpression = " %s in %s "
2020-05-08 13:41:52 +03:00
skip_fields = {
" Description " ,
" _exists_ " ,
" FileVersion " ,
" Product " ,
" Company " ,
" ParentProcessName " ,
" ParentCommandLine "
}
2018-05-21 23:07:47 +02:00
def __init__ ( self , * args , * * kwargs ) :
""" Initialize field mappings """
super ( ) . __init__ ( * args , * * kwargs )
self . fieldMappings = { # mapping between Sigma and ATP field names
2020-05-02 14:31:02 +01:00
# Supported values:
# (field name mapping, value mapping): distinct mappings for field name and value, may be a string (direct mapping) or function maps name/value to ATP target value
# (mapping function,): receives field name and value as parameter, return list of 2 element tuples (destination field name and value)
# (replacement, ): Replaces field occurrence with static string
" DeviceProcessEvents " : {
" AccountName " : ( self . id_mapping , self . default_value_mapping ) ,
" CommandLine " : ( " ProcessCommandLine " , self . default_value_mapping ) ,
" Command " : ( " ProcessCommandLine " , self . default_value_mapping ) ,
" DeviceName " : ( self . id_mapping , self . default_value_mapping ) ,
" EventType " : ( " ActionType " , self . default_value_mapping ) ,
" Image " : ( " FolderPath " , self . default_value_mapping ) ,
" ImageLoaded " : ( " FolderPath " , self . default_value_mapping ) ,
" LogonType " : ( self . id_mapping , self . logontype_mapping ) ,
" NewProcessName " : ( " FolderPath " , self . default_value_mapping ) ,
" ParentImage " : ( " InitiatingProcessFolderPath " , self . default_value_mapping ) ,
" SourceImage " : ( " InitiatingProcessFolderPath " , self . default_value_mapping ) ,
" TargetImage " : ( " FolderPath " , self . default_value_mapping ) ,
" User " : ( self . decompose_user , ) ,
} ,
" DeviceEvents " : {
" TargetFilename " : ( " FolderPath " , self . default_value_mapping ) ,
2020-05-02 14:46:55 +01:00
" TargetImage " : ( " FolderPath " , self . default_value_mapping ) ,
2020-06-30 14:49:29 +01:00
2020-05-02 14:46:55 +01:00
" Image " : ( " InitiatingProcessFolderPath " , self . default_value_mapping ) ,
2020-05-02 14:31:02 +01:00
" User " : ( self . decompose_user , ) ,
} ,
" DeviceRegistryEvents " : {
" TargetObject " : ( " RegistryKey " , self . default_value_mapping ) ,
" ObjectValueName " : ( " RegistryValueName " , self . default_value_mapping ) ,
" Details " : ( " RegistryValueData " , self . default_value_mapping ) ,
2020-06-30 14:49:29 +01:00
" EventType " : ( " ActionType " , self . default_value_mapping ) ,
2020-05-02 14:46:55 +01:00
" Image " : ( " InitiatingProcessFolderPath " , self . default_value_mapping ) ,
2020-05-02 14:31:02 +01:00
" User " : ( self . decompose_user , ) ,
} ,
" DeviceFileEvents " : {
" TargetFilename " : ( " FolderPath " , self . default_value_mapping ) ,
" TargetFileName " : ( " FolderPath " , self . default_value_mapping ) ,
" Image " : ( " InitiatingProcessFolderPath " , self . default_value_mapping ) ,
" User " : ( self . decompose_user , ) ,
} ,
" DeviceNetworkEvents " : {
" Initiated " : ( " RemotePort " , self . default_value_mapping ) ,
2020-06-05 23:03:52 +02:00
" Protocol " : ( " RemoteProtocol " , self . default_value_mapping ) ,
2020-05-02 14:31:02 +01:00
" DestinationPort " : ( " RemotePort " , self . default_value_mapping ) ,
" DestinationIp " : ( " RemoteIP " , self . default_value_mapping ) ,
" DestinationIsIpv6 " : ( " RemoteIP has \" : \" " , ) ,
" SourcePort " : ( " LocalPort " , self . default_value_mapping ) ,
" SourceIp " : ( " LocalIP " , self . default_value_mapping ) ,
" DestinationHostname " : ( " RemoteUrl " , self . default_value_mapping ) ,
2020-06-30 14:49:29 +01:00
" EventType " : ( " ActionType " , self . default_value_mapping ) ,
2020-05-02 14:31:02 +01:00
" Image " : ( " InitiatingProcessFolderPath " , self . default_value_mapping ) ,
" User " : ( self . decompose_user , ) ,
} ,
" DeviceImageLoadEvents " : {
" ImageLoaded " : ( " FolderPath " , self . default_value_mapping ) ,
2020-06-30 14:49:29 +01:00
" EventType " : ( " ActionType " , self . default_value_mapping ) ,
2020-05-02 14:31:02 +01:00
" Image " : ( " InitiatingProcessFolderPath " , self . default_value_mapping ) ,
" User " : ( self . decompose_user , ) ,
}
}
2018-05-21 23:07:47 +02:00
def id_mapping ( self , src ) :
""" Identity mapping, source == target field name """
return src
def default_value_mapping ( self , val ) :
op = " == "
2019-05-15 21:25:53 +02:00
if type ( val ) == str :
if " * " in val [ 1 : - 1 ] : # value contains * inside string - use regex match
op = " matches regex "
val = re . sub ( ' ([ " .^$]| \\ \\ (?![*?])) ' , ' \\ \\ \ g<1> ' , val )
val = re . sub ( ' \\ * ' , ' .* ' , val )
val = re . sub ( ' \\ ? ' , ' . ' , val )
else : # value possibly only starts and/or ends with *, use prefix/postfix match
if val . endswith ( " * " ) and val . startswith ( " * " ) :
op = " contains "
val = self . cleanValue ( val [ 1 : - 1 ] )
elif val . endswith ( " * " ) :
op = " startswith "
val = self . cleanValue ( val [ : - 1 ] )
elif val . startswith ( " * " ) :
op = " endswith "
val = self . cleanValue ( val [ 1 : ] )
2018-05-21 23:07:47 +02:00
return " %s \" %s \" " % ( op , val )
def logontype_mapping ( self , src ) :
""" Value mapping for logon events to reduced ATP LogonType set """
logontype_mapping = {
2020-05-02 14:31:02 +01:00
2 : " Interactive " ,
3 : " Network " ,
4 : " Batch " ,
5 : " Service " ,
7 : " Interactive " , # unsure
8 : " Network " ,
9 : " Interactive " , # unsure
10 : " Remote interactive (RDP) logons " , # really the value?
11 : " Interactive "
}
2018-05-21 23:07:47 +02:00
try :
return logontype_mapping [ int ( src ) ]
except KeyError :
raise NotSupportedError ( " Logon type %d unknown and can ' t be mapped " % src )
def decompose_user ( self , src_field , src_value ) :
""" Decompose domain \\ user User field of Sysmon events into ATP InitiatingProcessAccountDomain and InititatingProcessAccountName. """
reUser = re . compile ( " ^(.*?) \\ \\ (.*)$ " )
m = reUser . match ( src_value )
if m :
domain , user = m . groups ( )
2020-05-02 14:31:02 +01:00
return ( ( " InitiatingProcessAccountDomain " , self . default_value_mapping ( domain ) ) , ( " InititatingProcessAccountName " , self . default_value_mapping ( user ) ) )
2018-05-21 23:07:47 +02:00
else : # assume only user name is given if backslash is missing
2020-05-02 14:37:37 +01:00
return ( ( " InititatingProcessAccountName " , self . default_value_mapping ( src_value ) ) )
2018-05-21 23:07:47 +02:00
def generate ( self , sigmaparser ) :
self . table = None
2020-06-05 23:33:51 +02:00
self . category = sigmaparser . parsedyaml [ ' logsource ' ] . get ( ' category ' )
self . product = sigmaparser . parsedyaml [ ' logsource ' ] . get ( ' product ' )
self . service = sigmaparser . parsedyaml [ ' logsource ' ] . get ( ' service ' )
2018-05-21 23:07:47 +02:00
2019-01-14 23:54:05 +01:00
if ( self . category , self . product , self . service ) == ( " process_creation " , " windows " , None ) :
2020-05-08 13:41:52 +03:00
self . table = " DeviceProcessEvents "
2019-08-20 14:33:08 -07:00
elif ( self . category , self . product , self . service ) == ( None , " windows " , " powershell " ) :
2020-05-08 13:41:52 +03:00
self . table = " DeviceEvents "
2019-08-20 14:33:08 -07:00
self . orToken = " , "
2019-01-14 23:54:05 +01:00
2018-09-06 00:31:40 +02:00
return super ( ) . generate ( sigmaparser )
2018-05-21 23:07:47 +02:00
def generateBefore ( self , parsed ) :
if self . table is None :
2020-05-08 13:41:52 +03:00
raise NotSupportedError ( " No MDATP table could be determined from Sigma rule " )
if self . table == " DeviceEvents " and self . service == " powershell " :
2019-08-20 14:33:08 -07:00
return " %s | where tostring(extractjson( ' $.Command ' , AdditionalFields)) in~ " % self . table
2018-05-21 23:07:47 +02:00
return " %s | where " % self . table
2020-05-08 13:41:52 +03:00
@wrapper
2018-05-21 23:07:47 +02:00
def generateMapItemNode ( self , node ) :
"""
ATP queries refer to event tables instead of Windows logging event identifiers. This method catches conditions that refer to this field
and creates an appropriate table reference.
"""
key , value = node
2020-05-02 14:31:02 +01:00
# handle map items with values list like multiple OR-chained conditions
if type ( value ) == list :
return self . generateORNode ( [ ( key , v ) for v in value ] )
2018-05-21 23:07:47 +02:00
elif key == " EventID " : # EventIDs are not reflected in condition but in table selection
if self . product == " windows " :
if self . service == " sysmon " and value == 1 \
2020-05-02 14:31:02 +01:00
or self . service == " security " and value == 4688 : # Process Execution
2020-05-08 13:41:52 +03:00
self . table = " DeviceProcessEvents "
2018-05-21 23:07:47 +02:00
return None
2020-05-02 14:31:02 +01:00
elif self . service == " sysmon " and value == 3 : # Network Connection
2020-05-08 13:41:52 +03:00
self . table = " DeviceNetworkEvents "
2018-05-21 23:07:47 +02:00
return None
2020-05-02 14:31:02 +01:00
elif self . service == " sysmon " and value == 7 : # Image Load
2020-05-08 13:41:52 +03:00
self . table = " DeviceImageLoadEvents "
2018-05-21 23:07:47 +02:00
return None
2020-05-02 14:31:02 +01:00
elif self . service == " sysmon " and value == 8 : # Create Remote Thread
2020-05-08 13:41:52 +03:00
self . table = " DeviceEvents "
2018-07-10 22:49:38 +02:00
return " ActionType == \" CreateRemoteThreadApiCall \" "
2020-05-02 14:31:02 +01:00
elif self . service == " sysmon " and value == 11 : # File Creation
2020-05-08 13:41:52 +03:00
self . table = " DeviceFileEvents "
2020-05-02 14:31:02 +01:00
return " ActionType == \" FileCreated \" "
2020-05-02 17:31:50 +01:00
elif self . service == " sysmon " and value == 23 : # File Deletion
self . table = " DeviceFileEvents "
return " ActionType == \" FileDeleted \" "
2020-05-02 14:31:02 +01:00
elif self . service == " sysmon " and value == 12 : # Create/Delete Registry Value
self . table = " DeviceRegistryEvents "
2018-05-21 23:07:47 +02:00
return None
elif self . service == " sysmon " and value == 13 \
2020-05-02 14:31:02 +01:00
or self . service == " security " and value == 4657 : # Set Registry Value
2020-05-08 13:41:52 +03:00
self . table = " DeviceRegistryEvents "
2018-07-10 22:49:38 +02:00
return " ActionType == \" RegistryValueSet \" "
2018-05-21 23:07:47 +02:00
elif self . service == " security " and value == 4624 :
2020-05-08 13:41:52 +03:00
self . table = " DeviceLogonEvents "
2018-05-21 23:07:47 +02:00
return None
2020-05-02 14:31:02 +01:00
else :
if not self . table :
raise NotSupportedError ( " No sysmon Event ID provided " )
else :
raise NotSupportedError ( " No mapping for Event ID %s " % value )
2018-05-21 23:07:47 +02:00
elif type ( value ) in ( str , int ) : # default value processing
try :
2020-05-02 14:31:02 +01:00
mapping = self . fieldMappings [ self . table ] [ key ]
2018-05-21 23:07:47 +02:00
except KeyError :
2020-05-02 14:31:02 +01:00
raise NotSupportedError ( " No mapping defined for field ' %s ' in ' %s ' " % ( key , self . table ) )
2018-05-21 23:07:47 +02:00
if len ( mapping ) == 1 :
mapping = mapping [ 0 ]
if type ( mapping ) == str :
return mapping
elif callable ( mapping ) :
conds = mapping ( key , value )
2020-05-02 14:31:02 +01:00
return self . andToken . join ( [ " {} {} " . format ( * cond ) for cond in conds ] )
2018-05-21 23:07:47 +02:00
elif len ( mapping ) == 2 :
result = list ( )
2020-05-02 14:31:02 +01:00
# iterate mapping and mapping source value synchronously over key and value
for mapitem , val in zip ( mapping , node ) :
2018-05-21 23:07:47 +02:00
if type ( mapitem ) == str :
result . append ( mapitem )
elif callable ( mapitem ) :
result . append ( mapitem ( val ) )
return " {} {} " . format ( * result )
else :
raise TypeError ( " Backend does not support map values of type " + str ( type ( value ) ) )
return super ( ) . generateMapItemNode ( node )