192 lines
8.4 KiB
Python
192 lines
8.4 KiB
Python
import re
|
|
from datetime import datetime
|
|
|
|
import sigma
|
|
from sigma.backends.base import SingleTextQueryBackend
|
|
from sigma.backends.mixins import MultiRuleOutputMixin
|
|
|
|
from .exceptions import NotSupportedError
|
|
from ..parser.condition import SigmaAggregationParser
|
|
from ..parser.modifiers.base import SigmaTypeModifier
|
|
from ..parser.modifiers.transform import SigmaContainsModifier, SigmaStartswithModifier, SigmaEndswithModifier
|
|
from ..parser.modifiers.type import SigmaRegularExpressionModifier
|
|
|
|
comparative = ["greater_than",
|
|
"greater_equal",
|
|
"less_than",
|
|
"less_equal",
|
|
]
|
|
|
|
class ChronicleBackend(SingleTextQueryBackend):
|
|
"""Converts Sigma rule into Google Chronicle YARA-L. Contributed by SOC Prime. https://socprime.com"""
|
|
identifier = "chronicle"
|
|
active = True
|
|
andToken = " and "
|
|
#\\\
|
|
reEscape = re.compile('([\"]|(\\\\))')
|
|
reClear = re.compile('`')
|
|
|
|
orToken = " or "
|
|
notToken = "not "
|
|
subExpression = "(%s)"
|
|
valueExpression = "\"%s\""
|
|
mapExpression = "%s = %s"
|
|
listExpression = "(%s)"
|
|
listSeparator = " or "
|
|
config_required = True
|
|
mapListsSpecialHandling = True
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.defaultEventName = "event"
|
|
self.condition_name = None
|
|
self.parsed_detection = None
|
|
self.author = None
|
|
self.description = None
|
|
self.created = None
|
|
self.title = None
|
|
self.references = None
|
|
self.rule_count = 0
|
|
return super().__init__(*args, **kwargs)
|
|
|
|
def cleanValue(self, val):
|
|
if val and isinstance(val, str) and val.endswith("/"):
|
|
val = val.rstrip("/")
|
|
if val and isinstance(val, str) and val.startswith("\\"):
|
|
val = val.lstrip("\\")
|
|
return super().cleanValue(val)
|
|
|
|
def parseTitle(self, title):
|
|
new_title = re.sub(re.compile('[()*:;+!,\[\].?"-/]'), "", title.lower())
|
|
new_title = re.sub(re.compile('\s'), "_", new_title.lower())
|
|
index = 0
|
|
for i, title_char in enumerate(new_title):
|
|
if not title_char.isdigit():
|
|
index = i
|
|
break
|
|
new_title = new_title[index:]
|
|
new_title = new_title.strip("_")
|
|
return new_title
|
|
|
|
def generateMapItemNode(self, node):
|
|
fieldname, value = node
|
|
|
|
transformed_fieldname = self.fieldNameMapping(fieldname, value)
|
|
if type(value) in (str, int):
|
|
return self.regex_check(transformed_fieldname=transformed_fieldname, val=value)
|
|
elif type(value) == list:
|
|
return self.generateMapItemListNode(transformed_fieldname, value)
|
|
elif isinstance(value, SigmaTypeModifier):
|
|
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 createFinalRule(self, body):
|
|
# Spaces required in rule for structure
|
|
function_name = self.parseTitle(self.title)
|
|
if self.rule_count != 0:
|
|
function_name += "_part_{}".format(self.rule_count)
|
|
|
|
meta = """ meta:\n author = \"{author}\"\n description = \"{description}\"\n reference = \"{reference}\"\n version = \"0.01\"""".format(
|
|
author=self.author, description=self.description, reference=""
|
|
)
|
|
if self.created:
|
|
meta += "\n created = \"{}\"".format(self.created)
|
|
if any(self.logsource):
|
|
logsources = "\n ".join([f'{i} = "{j}"' for i, j in self.logsource.items() if i not in ("description", "definition")])
|
|
meta += "\n {}".format(logsources)
|
|
if self.tags:
|
|
tags = ", ".join([item.replace("attack.", "") for item in self.tags])
|
|
meta += "\n mitre = \"{}\"".format(tags)
|
|
condition_func = """ condition:\n {condition}""".format(condition=self.condition)
|
|
result = """rule {function_name} {{\n{meta}\n\n events:\n{function}\n\n{condition}\n}}""".format(
|
|
function_name=function_name,
|
|
meta=meta,
|
|
function=body,
|
|
condition=condition_func
|
|
)
|
|
self.rule_count += 1
|
|
return result
|
|
|
|
def fieldNameMapping(self, fieldname, value):
|
|
return f"${self.condition_name}.{fieldname}"
|
|
|
|
def regex_check(self, transformed_fieldname, val):
|
|
if val and isinstance(val, str) and '*' in val:
|
|
val = val.replace("\*", "*")
|
|
val = self.cleanValue(val)
|
|
val = val.replace("(", "\(")
|
|
val = val.replace(")", "\)")
|
|
val = re.compile(r'([+.?])').sub("\\\\\g<1>", val)
|
|
val = val.replace("*", ".*")
|
|
return f"re.regex({transformed_fieldname}, `{val}`)"
|
|
if val and isinstance(val, str):
|
|
return self.mapExpression % (transformed_fieldname, self.generateNode(val))
|
|
else:
|
|
return self.mapExpression % (transformed_fieldname, self.generateNode(val))
|
|
|
|
def generateMapItemListNode(self, fieldname, value):
|
|
list_query = []
|
|
for item in value:
|
|
updated_field_value = self.regex_check(transformed_fieldname=fieldname, val=item)
|
|
list_query.append(updated_field_value)
|
|
if len(list_query) > 1:
|
|
return "(" + " or ".join(list_query) + ")"
|
|
return list_query[0]
|
|
|
|
def generate(self, sigmaparser):
|
|
detection = sigmaparser.parsedyaml.get("detection")
|
|
condition_name = [item for item in detection.keys() if item not in ("condition", "keywords")]
|
|
if any(condition_name):
|
|
self.condition_name = condition_name[0]
|
|
else:
|
|
self.condition_name = "event"
|
|
self.author = sigmaparser.parsedyaml.get("author")
|
|
self.title = sigmaparser.parsedyaml.get("title")
|
|
description = "{} Author: {}.".format(sigmaparser.parsedyaml.get("description"), self.author)
|
|
description = description.replace("\\", "\\\\")
|
|
description = description.replace("\n", "")
|
|
self.description = description.replace('"', '\\"')
|
|
self.created = sigmaparser.parsedyaml.get("date", datetime.now().strftime("%Y-%m-%d"))
|
|
references = sigmaparser.parsedyaml.get("reference", [])
|
|
if not any(references):
|
|
references = sigmaparser.parsedyaml.get("references", [])
|
|
self.references = references
|
|
self.logsource = sigmaparser.parsedyaml.get("logsource") if sigmaparser.parsedyaml.get("logsource") else sigmaparser.parsedyaml.get("logsources", {})
|
|
self.tags = sigmaparser.parsedyaml.get("tags")
|
|
for parsed in sigmaparser.condparsed:
|
|
aggregation = None
|
|
translate = self.generateQuery(parsed)
|
|
self.condition = "${}".format(self.condition_name)
|
|
if parsed.parsedAgg:
|
|
translate = self.generateAggregation(agg=parsed.parsedAgg, body=translate)
|
|
return self.createFinalRule(body=translate)
|
|
|
|
def generateQuery(self, parsed):
|
|
result = self.generateNode(parsed.parsedSearch)
|
|
return result
|
|
|
|
def generateAggregation(self, agg, body):
|
|
if agg is None:
|
|
return ""
|
|
if agg.aggfunc == SigmaAggregationParser.AGGFUNC_NEAR:
|
|
raise NotImplementedError(
|
|
"The 'near' aggregation operator is not "
|
|
+ f"implemented for the %s backend" % self.identifier
|
|
)
|
|
if agg.aggfunc_notrans != 'count' and agg.aggfield is None:
|
|
raise NotSupportedError(
|
|
"The '%s' aggregation operator " % agg.aggfunc_notrans
|
|
+ "must have an aggregation field for the %s backend" % self.identifier
|
|
)
|
|
if agg.aggfunc_notrans == 'count':
|
|
if agg.groupfield:
|
|
self.condition = "${condition} and #target {op} {cond}".format(condition=self.condition_name,
|
|
field=agg.groupfield,
|
|
op=agg.cond_op,
|
|
cond=agg.condition)
|
|
body += "\n${condition}.{field} = $target".format(condition=self.condition_name, field=agg.groupfield,)
|
|
else:
|
|
self.condition = "#{} {} {}".format(self.condition_name, agg.cond_op, agg.condition)
|
|
return body |