Files
blue-team-tools/tools/sigma/backends/carbonblack.py
T
2021-11-01 16:05:34 +01:00

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.")