277 lines
9.6 KiB
Python
277 lines
9.6 KiB
Python
import re
|
|
import requests
|
|
import json
|
|
import os
|
|
from sigma.config.eventdict import event
|
|
from fnmatch import fnmatch
|
|
|
|
from sigma.backends.base import SingleTextQueryBackend
|
|
from sigma.backends.exceptions import NotSupportedError
|
|
from sigma.parser.modifiers.type import SigmaRegularExpressionModifier
|
|
from sigma.parser.condition import ConditionOR, ConditionAND, NodeSubexpression
|
|
|
|
from sigma.parser.modifiers.base import SigmaTypeModifier
|
|
from sigma.parser.modifiers.type import SigmaRegularExpressionModifier
|
|
|
|
|
|
class CarbonBlackWildcardHandlingMixin:
|
|
"""
|
|
Determine field mapping to keyword subfields depending on existence of wildcards in search values. Further,
|
|
provide configurability with backend parameters.
|
|
"""
|
|
|
|
# options = SingleTextQueryBackend.options + (
|
|
# ("keyword_field", None, "Keyword sub-field name", None),
|
|
# ("keyword_blacklist", None, "Fields that don't have a keyword subfield (wildcards * and ? allowed)", None)
|
|
# )
|
|
reContainsWildcard = re.compile("(?:(?<!\\\\)|\\\\\\\\)[*?]").search
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.matchKeyword = True
|
|
try:
|
|
self.blacklist = self.keyword_blacklist.split(",")
|
|
except AttributeError:
|
|
self.blacklist = list()
|
|
|
|
def containsWildcard(self, value):
|
|
"""Determine if value contains wildcard."""
|
|
if type(value) == str:
|
|
res = self.reContainsWildcard(value)
|
|
return res
|
|
else:
|
|
return False
|
|
|
|
|
|
class CarbonBlackQueryBackend(CarbonBlackWildcardHandlingMixin, SingleTextQueryBackend):
|
|
"""Converts Sigma rule into CarbonBlack query string. Only searches, no aggregations. Contributed by SOC Prime. https://socprime.com"""
|
|
identifier = "carbonblack"
|
|
active = True
|
|
|
|
# reEscape = re.compile("([\s+\\-=!(){}\\[\\]^\"~:/]|(?<!\\\\)\\\\(?![*?\\\\])|\\\\u|&&|\\|\\|)")
|
|
reEscape = re.compile("([\s\s+()\"])")
|
|
reClear = re.compile("[<>]")
|
|
andToken = " AND "
|
|
orToken = " OR "
|
|
notToken = " -"
|
|
subExpression = "(%s)"
|
|
listExpression = "(%s)"
|
|
listSeparator = " OR "
|
|
valueExpression = '%s'
|
|
typedValueExpression = {SigmaRegularExpressionModifier: "/%s/"}
|
|
nullExpression = "NOT _exists_:%s"
|
|
notNullExpression = "_exists_:%s"
|
|
mapExpression = "%s:%s"
|
|
mapListsSpecialHandling = False
|
|
escapeCharacters = [
|
|
'\\',
|
|
'"',
|
|
'\'',
|
|
'(',
|
|
')',
|
|
'[',
|
|
']',
|
|
'{',
|
|
'}',
|
|
',',
|
|
'=',
|
|
'<',
|
|
'>',
|
|
'&',
|
|
'|',
|
|
';',
|
|
':',
|
|
'-'
|
|
]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Initialize field mappings."""
|
|
super().__init__(*args, **kwargs)
|
|
self.category = None
|
|
self.excluded_fields = None
|
|
|
|
def cleanLeading(self, val):
|
|
if val.startswith("*"):
|
|
val = val[1:]
|
|
if val.endswith("*"):
|
|
val = val[:-1]
|
|
return val.strip()
|
|
|
|
def escapeCharacter(self, val):
|
|
for ch in self.escapeCharacters:
|
|
val = val.replace(ch, '\\' + ch)
|
|
return val
|
|
|
|
def unescapeCharacter(self, val):
|
|
for ch in self.escapeCharacters:
|
|
val = val.replace('\\' + ch, ch)
|
|
return val
|
|
|
|
def cleanWhitespace(self, val):
|
|
val = val.replace('*', ' AND ').replace(' ', ' ')
|
|
if re.match('\S+ \S', val):
|
|
matches = re.findall('(?:^|\(| )(.+?)(?:\)| OR| AND|$)', val)
|
|
for strMatch in matches:
|
|
if re.match('\S+ \S', strMatch):
|
|
strUnescapeMatch = self.unescapeCharacter(strMatch)
|
|
val = val.replace(strMatch, '"{}"'.format(strUnescapeMatch))
|
|
return val.strip()
|
|
|
|
def fixWildcards(self, val):
|
|
# prob a better way to do this with SigmaStartswithModifier/SigmaEndswithModifier? idk, fail fast!
|
|
if val.endswith("\\\\"):
|
|
val = val[:-1] + "*"
|
|
if val.startswith("\\\\") and not val.startswith("\\\\\\\\"):
|
|
val = val[2:]
|
|
return val
|
|
|
|
def cleanValue(self, val):
|
|
if "[1 to *]" in val:
|
|
self.reEscape = re.compile("([()])")
|
|
val = super().cleanValue(val)
|
|
val = val.strip()
|
|
# else:
|
|
# self.reEscape = re.compile("([\s\s+()])")
|
|
elif isinstance(val, str):
|
|
val = val.strip()
|
|
val = self.cleanLeading(val)
|
|
val = self.escapeCharacter(val)
|
|
val = self.cleanWhitespace(val)
|
|
val = self.fixWildcards(val)
|
|
return val
|
|
|
|
def cleanIPRange(self, value):
|
|
new_value = value
|
|
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 isinstance(new_value, list):
|
|
for index, vl in enumerate(new_value):
|
|
new_value[index] = self.cleanIPRange(vl)
|
|
|
|
return new_value
|
|
|
|
def generateValueNode(self, node):
|
|
result = self.valueExpression % (str(node))
|
|
if result == "" or result.isspace():
|
|
return '""'
|
|
else:
|
|
if self.matchKeyword: # don't quote search value on keyword field
|
|
return result
|
|
else:
|
|
return "%s" % result
|
|
|
|
def generateMapItemNode(self, node):
|
|
fieldname, value = node
|
|
if fieldname == "EventID" and (type(value) is str or type(value) is int):
|
|
value = str(value)
|
|
fieldname = self.generateEventKey(value)
|
|
value = self.generateEventValue(value)
|
|
if fieldname.lower() in self.excluded_fields:
|
|
return
|
|
else:
|
|
transformed_fieldname = self.fieldNameMapping(fieldname, value)
|
|
if transformed_fieldname == "ipaddr":
|
|
value = self.cleanIPRange(value)
|
|
if (
|
|
self.mapListsSpecialHandling == False
|
|
and type(value) in (str, int, list)
|
|
or self.mapListsSpecialHandling == True
|
|
and type(value) in (str, int)
|
|
):
|
|
# return self.mapExpression % (transformed_fieldname, self.generateNode(value))
|
|
if isinstance(value, list):
|
|
return self.generateNode(
|
|
[
|
|
self.mapExpression
|
|
% (transformed_fieldname, self.cleanValue(item))
|
|
for item in value
|
|
]
|
|
)
|
|
elif isinstance(value, str) or isinstance(value, int):
|
|
return self.mapExpression % (
|
|
transformed_fieldname,
|
|
self.generateNode(self.cleanValue(value)),
|
|
)
|
|
elif type(value) == list:
|
|
return self.generateMapItemListNode(transformed_fieldname, value)
|
|
elif isinstance(value, SigmaTypeModifier) and not isinstance(
|
|
value, SigmaRegularExpressionModifier
|
|
):
|
|
return self.generateMapItemTypedNode(transformed_fieldname, value)
|
|
elif value is None:
|
|
return self.nullExpression % (transformed_fieldname,)
|
|
else:
|
|
raise TypeError(
|
|
"Backend does not support map values of type " + str(type(value))
|
|
)
|
|
|
|
def generateNOTNode(self, node):
|
|
expression = super().generateNode(node.item)
|
|
if expression:
|
|
return "(%s%s)" % (self.notToken, expression)
|
|
|
|
# Function to upload watchlists through CB API
|
|
|
|
def postAPI(self, result, title, desc):
|
|
url = os.getenv("cbapi_watchlist")
|
|
body = {
|
|
"name": title,
|
|
"search_query": "q=" + str(result),
|
|
"description": desc,
|
|
"index_type": "events",
|
|
}
|
|
header = {"X-Auth-Token": os.getenv("APIToken")}
|
|
print(title)
|
|
x = requests.post(url, data=json.dumps(body), headers=header, verify=False)
|
|
print(x.text)
|
|
|
|
def generateEventKey(self, value):
|
|
if value in event:
|
|
return event[value][0]
|
|
else:
|
|
return 'eventid'
|
|
|
|
def generateEventValue(self, value):
|
|
if value in event:
|
|
return event[value][1]
|
|
else:
|
|
return ''
|
|
|
|
def generate(self, sigmaparser):
|
|
"""Method is called for each sigma rule and receives the parsed rule (SigmaParser)"""
|
|
title = sigmaparser.parsedyaml["title"]
|
|
desc = sigmaparser.parsedyaml["description"]
|
|
try:
|
|
self.category = sigmaparser.parsedyaml['logsource'].setdefault(
|
|
'category', None
|
|
)
|
|
self.counted = sigmaparser.parsedyaml.get('counted', None)
|
|
self.excluded_fields = [
|
|
item.lower()
|
|
for item in sigmaparser.config.config.get("excludedfields", [])
|
|
]
|
|
except KeyError:
|
|
self.category = None
|
|
for parsed in sigmaparser.condparsed:
|
|
query = self.generateQuery(parsed)
|
|
result = ""
|
|
|
|
if query is not None:
|
|
result += query
|
|
# self.postAPI(result,title,desc)
|
|
return result
|
|
# if self.category == "process_creation":
|
|
# for parsed in sigmaparser.condparsed:
|
|
# query = self.generateQuery(parsed)
|
|
# result = ""
|
|
|
|
# if query is not None:
|
|
# result += query
|
|
# return result
|
|
# else:
|
|
# raise NotSupportedError("Not supported logsource category.")
|