2019-10-26 20:45:48 +00:00
|
|
|
# LimaCharlie backend for sigmac created by LimaCharlie.io
|
|
|
|
# Copyright 2019 Refraction Point, Inc
|
|
|
|
|
|
|
|
# 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/>.
|
|
|
|
|
|
|
|
import re
|
|
|
|
import yaml
|
2019-10-28 15:49:05 +00:00
|
|
|
from collections import namedtuple
|
2019-10-26 20:45:48 +00:00
|
|
|
from .base import BaseBackend
|
|
|
|
from sigma.parser.modifiers.base import SigmaTypeModifier
|
|
|
|
from sigma.parser.modifiers.type import SigmaRegularExpressionModifier
|
|
|
|
|
2019-10-26 21:30:50 +00:00
|
|
|
# A few helper functions for cases where field mapping cannot be done
|
|
|
|
# as easily one by one, or can be done more efficiently.
|
|
|
|
def _windowsEventLogFieldName(fieldName):
|
|
|
|
if 'EventID' == fieldName:
|
|
|
|
return 'Event/System/EventID'
|
|
|
|
return 'Event/EventData/%s' % (fieldName,)
|
|
|
|
|
2019-11-01 16:47:53 +00:00
|
|
|
def _mapProcessCreationOperations(node):
|
|
|
|
# Here we fix some common pitfalls found in rules
|
2019-11-05 13:33:21 +00:00
|
|
|
# in a consistent fashion (already processed to D&R rule).
|
2019-11-01 16:47:53 +00:00
|
|
|
|
|
|
|
# First fixup is looking for a specific path prefix
|
|
|
|
# based on a specific drive letter. There are many cases
|
|
|
|
# where the driver letter can change or where the early
|
|
|
|
# boot process refers to it as "\Device\HarddiskVolume1\".
|
|
|
|
if ("starts with" == node["op"] and
|
|
|
|
"event/FILE_PATH" == node["path"] and
|
|
|
|
node["value"].lower().startswith("c:\\")):
|
|
|
|
node["op"] = "matches"
|
|
|
|
node["re"] = "^(?:(?:.:)|(?:\\\\Device\\\\HarddiskVolume.))\\\\%s" % (re.escape(node["value"][3:]),)
|
|
|
|
del(node["value"])
|
|
|
|
|
|
|
|
return node
|
|
|
|
|
2019-10-26 20:45:48 +00:00
|
|
|
# We support many different log sources so we keep different mapping depending
|
|
|
|
# on the log source and category.
|
2019-10-26 21:30:50 +00:00
|
|
|
# The mapping key is product/category/service.
|
2019-10-28 15:49:05 +00:00
|
|
|
# The mapping value is tuple like:
|
2019-10-27 17:17:15 +00:00
|
|
|
# - top-level parameters
|
2019-10-26 21:30:50 +00:00
|
|
|
# - pre-condition is a D&R rule node filtering relevant events.
|
|
|
|
# - field mappings is a dict with a mapping or a callable to convert the field name.
|
2019-10-31 18:40:41 +00:00
|
|
|
# Individual mapping values can also be callabled(fieldname, value) returning a new fieldname and value.
|
2019-10-26 21:30:50 +00:00
|
|
|
# - isAllStringValues is a bool indicating whether all values should be converted to string.
|
2019-10-31 18:40:41 +00:00
|
|
|
# - keywordField is the field name to alias for keywords if supported or None if not.
|
2019-11-01 16:47:53 +00:00
|
|
|
# - postOpMapper is a callback that can modify an operation once it has been generated.
|
2019-10-28 15:49:05 +00:00
|
|
|
SigmaLCConfig = namedtuple('SigmaLCConfig', [
|
|
|
|
'topLevelParams',
|
|
|
|
'preConditions',
|
|
|
|
'fieldMappings',
|
|
|
|
'isAllStringValues',
|
2019-10-31 16:15:07 +00:00
|
|
|
'keywordField',
|
2019-11-01 16:47:53 +00:00
|
|
|
'postOpMapper',
|
2019-10-28 15:49:05 +00:00
|
|
|
])
|
2019-10-26 20:45:48 +00:00
|
|
|
_allFieldMappings = {
|
2019-10-28 15:49:05 +00:00
|
|
|
"windows/process_creation/": SigmaLCConfig(
|
|
|
|
topLevelParams = {
|
|
|
|
"events": [
|
|
|
|
"NEW_PROCESS",
|
|
|
|
"EXISTING_PROCESS",
|
|
|
|
]
|
|
|
|
},
|
|
|
|
preConditions = {
|
|
|
|
"op": "is windows",
|
|
|
|
},
|
|
|
|
fieldMappings = {
|
|
|
|
"CommandLine": "event/COMMAND_LINE",
|
|
|
|
"Image": "event/FILE_PATH",
|
|
|
|
"ParentImage": "event/PARENT/FILE_PATH",
|
|
|
|
"ParentCommandLine": "event/PARENT/COMMAND_LINE",
|
|
|
|
"User": "event/USER_NAME",
|
|
|
|
# This field is redundant in LC, it seems to always be used with Image
|
|
|
|
# so we will ignore it.
|
2019-10-31 18:40:41 +00:00
|
|
|
"OriginalFileName": lambda fn, fv: ("event/FILE_PATH", "*" + fv),
|
2019-10-28 15:49:05 +00:00
|
|
|
# Custom field names coming from somewhere unknown.
|
|
|
|
"NewProcessName": "event/FILE_PATH",
|
|
|
|
"ProcessCommandLine": "event/COMMAND_LINE",
|
|
|
|
# Another one-off command line.
|
|
|
|
"Command": "event/COMMAND_LINE",
|
|
|
|
},
|
|
|
|
isAllStringValues = False,
|
2019-11-01 16:47:53 +00:00
|
|
|
keywordField = "event/COMMAND_LINE",
|
|
|
|
postOpMapper = _mapProcessCreationOperations
|
2019-10-28 15:49:05 +00:00
|
|
|
),
|
|
|
|
"windows//": SigmaLCConfig(
|
|
|
|
topLevelParams = {
|
|
|
|
"target": "log",
|
|
|
|
"log type": "wel",
|
|
|
|
},
|
|
|
|
preConditions = None,
|
|
|
|
fieldMappings = _windowsEventLogFieldName,
|
|
|
|
isAllStringValues = True,
|
2019-11-01 16:47:53 +00:00
|
|
|
keywordField = None,
|
|
|
|
postOpMapper = None
|
2019-10-28 15:49:05 +00:00
|
|
|
),
|
|
|
|
"windows_defender//": SigmaLCConfig(
|
|
|
|
topLevelParams = {
|
|
|
|
"target": "log",
|
|
|
|
"log type": "wel",
|
|
|
|
},
|
|
|
|
preConditions = None,
|
|
|
|
fieldMappings = _windowsEventLogFieldName,
|
|
|
|
isAllStringValues = True,
|
2019-11-01 16:47:53 +00:00
|
|
|
keywordField = None,
|
|
|
|
postOpMapper = None
|
2019-10-28 15:49:05 +00:00
|
|
|
),
|
|
|
|
"dns//": SigmaLCConfig(
|
|
|
|
topLevelParams = {
|
|
|
|
"event": "DNS_REQUEST",
|
|
|
|
},
|
|
|
|
preConditions = None,
|
|
|
|
fieldMappings = {
|
|
|
|
"query": "event/DOMAIN_NAME",
|
|
|
|
},
|
|
|
|
isAllStringValues = False,
|
2019-11-01 16:47:53 +00:00
|
|
|
keywordField = None,
|
|
|
|
postOpMapper = None
|
2019-10-28 15:49:05 +00:00
|
|
|
),
|
|
|
|
"linux//": SigmaLCConfig(
|
|
|
|
topLevelParams = {
|
|
|
|
"events": [
|
|
|
|
"NEW_PROCESS",
|
|
|
|
"EXISTING_PROCESS",
|
|
|
|
]
|
|
|
|
},
|
|
|
|
preConditions = {
|
|
|
|
"op": "is linux",
|
|
|
|
},
|
|
|
|
fieldMappings = {
|
|
|
|
"exe": "event/FILE_PATH",
|
|
|
|
"type": None,
|
|
|
|
},
|
|
|
|
isAllStringValues = False,
|
2019-11-01 16:47:53 +00:00
|
|
|
keywordField = 'event/COMMAND_LINE',
|
|
|
|
postOpMapper = None
|
2019-10-31 16:15:07 +00:00
|
|
|
),
|
2019-10-28 15:49:05 +00:00
|
|
|
"unix//": SigmaLCConfig(
|
|
|
|
topLevelParams = {
|
|
|
|
"events": [
|
|
|
|
"NEW_PROCESS",
|
|
|
|
"EXISTING_PROCESS",
|
|
|
|
]
|
|
|
|
},
|
|
|
|
preConditions = {
|
|
|
|
"op": "is linux",
|
|
|
|
},
|
|
|
|
fieldMappings = {
|
|
|
|
"exe": "event/FILE_PATH",
|
|
|
|
"type": None,
|
|
|
|
},
|
|
|
|
isAllStringValues = False,
|
2019-11-01 16:47:53 +00:00
|
|
|
keywordField = 'event/COMMAND_LINE',
|
|
|
|
postOpMapper = None
|
2019-10-31 16:15:07 +00:00
|
|
|
),
|
2019-10-28 15:49:05 +00:00
|
|
|
"netflow//": SigmaLCConfig(
|
|
|
|
topLevelParams = {
|
|
|
|
"event": "NETWORK_CONNECTIONS",
|
|
|
|
},
|
|
|
|
preConditions = None,
|
|
|
|
fieldMappings = {
|
|
|
|
"destination.port": "event/NETWORK_ACTIVITY/DESTINATION/PORT",
|
|
|
|
"source.port": "event/NETWORK_ACTIVITY/SOURCE/PORT",
|
|
|
|
},
|
|
|
|
isAllStringValues = False,
|
2019-11-01 16:47:53 +00:00
|
|
|
keywordField = None,
|
|
|
|
postOpMapper = None
|
2019-10-31 16:15:07 +00:00
|
|
|
),
|
2019-10-26 20:45:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class LimaCharlieBackend(BaseBackend):
|
|
|
|
"""Converts Sigma rule into LimaCharlie D&R rules. Contributed by LimaCharlie. https://limacharlie.io"""
|
|
|
|
identifier = "limacharlie"
|
|
|
|
active = True
|
2019-11-03 22:32:50 +00:00
|
|
|
config_required = False
|
|
|
|
default_config = ["limacharlie"]
|
2019-10-26 20:45:48 +00:00
|
|
|
|
|
|
|
def generate(self, sigmaparser):
|
|
|
|
# Take the log source information and figure out which set of mappings to use.
|
2019-10-26 22:30:40 +00:00
|
|
|
ruleConfig = sigmaparser.parsedyaml
|
|
|
|
ls_rule = ruleConfig['logsource']
|
2019-10-26 20:45:48 +00:00
|
|
|
try:
|
|
|
|
category = ls_rule['category']
|
|
|
|
except KeyError:
|
2019-10-26 21:30:50 +00:00
|
|
|
category = ""
|
2019-10-26 20:45:48 +00:00
|
|
|
try:
|
|
|
|
product = ls_rule['product']
|
|
|
|
except KeyError:
|
2019-10-26 21:30:50 +00:00
|
|
|
product = ""
|
2019-10-26 21:59:33 +00:00
|
|
|
# try:
|
|
|
|
# service = ls_rule['service']
|
|
|
|
# except KeyError:
|
|
|
|
# service = ""
|
2019-10-26 22:37:13 +00:00
|
|
|
|
2019-10-26 21:59:33 +00:00
|
|
|
# Don't use service for now, most Windows Event Logs
|
|
|
|
# uses a different service with no category, since we
|
|
|
|
# treat all Windows Event Logs together we can ignore
|
|
|
|
# the service.
|
|
|
|
service = ""
|
2019-10-26 20:45:48 +00:00
|
|
|
|
2019-10-28 15:49:05 +00:00
|
|
|
# See if we have a definition for the source combination.
|
2019-10-26 21:09:39 +00:00
|
|
|
mappingKey = "%s/%s/%s" % (product, category, service)
|
2019-11-01 16:47:53 +00:00
|
|
|
topFilter, preCond, mappings, isAllStringValues, keywordField, postOpMapper = _allFieldMappings.get(mappingKey, tuple([None, None, None, None, None, None]))
|
2019-10-26 20:45:48 +00:00
|
|
|
if mappings is None:
|
2019-10-26 21:59:33 +00:00
|
|
|
raise NotImplementedError("Log source %s/%s/%s not supported by backend." % (product, category, service))
|
2019-10-26 20:45:48 +00:00
|
|
|
|
2019-10-26 22:37:13 +00:00
|
|
|
# Field name conversions.
|
2019-10-26 20:45:48 +00:00
|
|
|
self._fieldMappingInEffect = mappings
|
2019-10-26 22:37:13 +00:00
|
|
|
|
|
|
|
# LC event type pre-selector for the type of data.
|
2019-10-26 20:45:48 +00:00
|
|
|
self._preCondition = preCond
|
2019-10-26 22:37:13 +00:00
|
|
|
|
|
|
|
# Are all the values treated as strings?
|
2019-10-26 21:30:50 +00:00
|
|
|
self._isAllStringValues = isAllStringValues
|
2019-10-26 20:45:48 +00:00
|
|
|
|
2019-10-27 20:28:54 +00:00
|
|
|
# Are we supporting keywords full text search?
|
2019-10-31 16:15:07 +00:00
|
|
|
self._keywordField = keywordField
|
2019-10-27 20:28:54 +00:00
|
|
|
|
2019-11-01 16:47:53 +00:00
|
|
|
# Call to fixup all operations after the fact.
|
|
|
|
self._postOpMapper = postOpMapper
|
|
|
|
|
2019-10-26 22:37:13 +00:00
|
|
|
# Call the original generation code.
|
2019-10-26 22:30:40 +00:00
|
|
|
detectComponent = super().generate(sigmaparser)
|
2019-10-26 22:37:13 +00:00
|
|
|
|
|
|
|
# We expect a string (yaml) as output, so if
|
|
|
|
# we get anything else we assume it's a core
|
|
|
|
# library value and just return it as-is.
|
2019-10-26 22:30:40 +00:00
|
|
|
if not isinstance( detectComponent, str):
|
|
|
|
return detectComponent
|
|
|
|
|
|
|
|
# This redundant to deserialize it right after
|
|
|
|
# generating the yaml, but we try to use the parent
|
|
|
|
# official class code as much as possible for future
|
|
|
|
# compatibility.
|
|
|
|
detectComponent = yaml.safe_load(detectComponent)
|
2019-10-28 15:49:05 +00:00
|
|
|
|
|
|
|
# Check that we got a proper node and not just a string
|
|
|
|
# which we don't really know what to do with.
|
|
|
|
if not isinstance(detectComponent, dict):
|
|
|
|
raise NotImplementedError("Selection combination not supported.")
|
|
|
|
|
|
|
|
# Apply top level filter.
|
|
|
|
detectComponent.update(topFilter)
|
|
|
|
|
|
|
|
# Now prepare the Response component.
|
2019-10-26 22:30:40 +00:00
|
|
|
respondComponents = [{
|
|
|
|
"action": "report",
|
|
|
|
"name": ruleConfig["title"],
|
|
|
|
}]
|
|
|
|
|
2019-10-26 22:37:13 +00:00
|
|
|
# Add a lot of the metadata available to the report.
|
2019-10-26 22:30:40 +00:00
|
|
|
if ruleConfig.get("tags", None) is not None:
|
2019-10-28 16:31:50 +00:00
|
|
|
respondComponents[0].setdefault("metadata", {})["tags"] = ruleConfig["tags"]
|
2019-10-26 22:30:40 +00:00
|
|
|
|
|
|
|
if ruleConfig.get("description", None) is not None:
|
2019-10-28 16:31:50 +00:00
|
|
|
respondComponents[0].setdefault("metadata", {})["description"] = ruleConfig["description"]
|
2019-10-26 22:30:40 +00:00
|
|
|
|
|
|
|
if ruleConfig.get("references", None) is not None:
|
2019-10-28 16:31:50 +00:00
|
|
|
respondComponents[0].setdefault("metadata", {})["references"] = ruleConfig["references"]
|
2019-10-26 22:30:40 +00:00
|
|
|
|
|
|
|
if ruleConfig.get("level", None) is not None:
|
2019-10-28 16:31:50 +00:00
|
|
|
respondComponents[0].setdefault("metadata", {})["level"] = ruleConfig["level"]
|
2019-10-26 22:30:40 +00:00
|
|
|
|
|
|
|
if ruleConfig.get("author", None) is not None:
|
2019-10-28 16:31:50 +00:00
|
|
|
respondComponents[0].setdefault("metadata", {})["author"] = ruleConfig["author"]
|
2019-10-26 22:30:40 +00:00
|
|
|
|
2019-10-26 22:37:13 +00:00
|
|
|
# Assemble it all as a single, complete D&R rule.
|
2019-10-26 22:30:40 +00:00
|
|
|
return yaml.safe_dump({
|
|
|
|
"detect": detectComponent,
|
|
|
|
"respond": respondComponents,
|
|
|
|
})
|
2019-10-26 20:45:48 +00:00
|
|
|
|
|
|
|
def generateQuery(self, parsed):
|
2019-10-26 22:37:13 +00:00
|
|
|
# We override the generateQuery function because
|
|
|
|
# we generate proper JSON structures internally
|
|
|
|
# and only convert to string (yaml) once the
|
|
|
|
# whole thing is assembled.
|
2019-10-26 20:45:48 +00:00
|
|
|
result = self.generateNode(parsed.parsedSearch)
|
2019-10-31 16:15:07 +00:00
|
|
|
|
2019-10-26 20:45:48 +00:00
|
|
|
if self._preCondition is not None:
|
|
|
|
result = {
|
|
|
|
"op": "and",
|
|
|
|
"rules": [
|
|
|
|
self._preCondition,
|
|
|
|
result,
|
|
|
|
]
|
|
|
|
}
|
2019-11-01 16:47:53 +00:00
|
|
|
if self._postOpMapper is not None:
|
|
|
|
result = self._postOpMapper(result)
|
2019-10-26 20:45:48 +00:00
|
|
|
return yaml.safe_dump(result)
|
|
|
|
|
|
|
|
def generateANDNode(self, node):
|
|
|
|
generated = [ self.generateNode(val) for val in node ]
|
|
|
|
filtered = [ g for g in generated if g is not None ]
|
2019-10-28 15:49:05 +00:00
|
|
|
if not filtered:
|
2019-10-26 20:45:48 +00:00
|
|
|
return None
|
2019-10-31 16:15:07 +00:00
|
|
|
|
|
|
|
# Map any possible keywords.
|
|
|
|
filtered = self._mapKeywordVals(filtered)
|
|
|
|
|
2019-10-28 15:49:05 +00:00
|
|
|
if 1 == len(filtered):
|
2019-11-01 16:47:53 +00:00
|
|
|
if self._postOpMapper is not None:
|
|
|
|
filtered[0] = self._postOpMapper(filtered[0])
|
2019-10-28 15:49:05 +00:00
|
|
|
return filtered[0]
|
2019-11-01 16:47:53 +00:00
|
|
|
result = {
|
2019-10-28 15:49:05 +00:00
|
|
|
"op": "and",
|
|
|
|
"rules": filtered,
|
|
|
|
}
|
2019-11-01 16:47:53 +00:00
|
|
|
if self._postOpMapper is not None:
|
|
|
|
result = self._postOpMapper(result)
|
|
|
|
return result
|
2019-10-26 20:45:48 +00:00
|
|
|
|
|
|
|
def generateORNode(self, node):
|
2019-10-26 21:59:33 +00:00
|
|
|
generated = [self.generateNode(val) for val in node]
|
|
|
|
filtered = [g for g in generated if g is not None]
|
2019-10-28 15:49:05 +00:00
|
|
|
if not filtered:
|
2019-10-26 20:45:48 +00:00
|
|
|
return None
|
2019-10-31 16:15:07 +00:00
|
|
|
|
|
|
|
# Map any possible keywords.
|
|
|
|
filtered = self._mapKeywordVals(filtered)
|
|
|
|
|
2019-10-28 15:49:05 +00:00
|
|
|
if 1 == len(filtered):
|
2019-11-01 16:47:53 +00:00
|
|
|
if self._postOpMapper is not None:
|
|
|
|
filtered[0] = self._postOpMapper(filtered[0])
|
2019-10-28 15:49:05 +00:00
|
|
|
return filtered[0]
|
2019-11-01 16:47:53 +00:00
|
|
|
result = {
|
2019-10-28 15:49:05 +00:00
|
|
|
"op": "or",
|
|
|
|
"rules": filtered,
|
|
|
|
}
|
2019-11-01 16:47:53 +00:00
|
|
|
if self._postOpMapper is not None:
|
|
|
|
result = self._postOpMapper(result)
|
|
|
|
return result
|
2019-10-26 20:45:48 +00:00
|
|
|
|
|
|
|
def generateNOTNode(self, node):
|
|
|
|
generated = self.generateNode(node.item)
|
2019-10-28 15:49:05 +00:00
|
|
|
if generated is None:
|
2019-10-26 20:45:48 +00:00
|
|
|
return None
|
2019-10-28 15:49:05 +00:00
|
|
|
if not isinstance(generated, dict):
|
|
|
|
raise NotImplementedError("Not operator not available on non-dict nodes.")
|
2019-11-01 16:47:53 +00:00
|
|
|
generated["not"] = not generated.get("not", False)
|
2019-10-28 15:49:05 +00:00
|
|
|
return generated
|
2019-10-26 20:45:48 +00:00
|
|
|
|
|
|
|
def generateSubexpressionNode(self, node):
|
2019-10-28 15:49:05 +00:00
|
|
|
return self.generateNode(node.items)
|
2019-10-26 20:45:48 +00:00
|
|
|
|
|
|
|
def generateListNode(self, node):
|
|
|
|
return [self.generateNode(value) for value in node]
|
|
|
|
|
|
|
|
def generateMapItemNode(self, node):
|
|
|
|
fieldname, value = node
|
|
|
|
|
2019-10-31 18:40:41 +00:00
|
|
|
fieldNameAndValCallback = None
|
|
|
|
|
2019-10-26 21:30:50 +00:00
|
|
|
# The mapping can be a dictionary of mapping or a callable
|
|
|
|
# to get the correct value.
|
|
|
|
if callable(self._fieldMappingInEffect):
|
|
|
|
fieldname = self._fieldMappingInEffect(fieldname)
|
|
|
|
else:
|
2019-10-26 21:59:33 +00:00
|
|
|
try:
|
2019-10-31 18:40:41 +00:00
|
|
|
# The mapping can also be a callable that will
|
|
|
|
# return a mapped key AND value.
|
|
|
|
if callable(self._fieldMappingInEffect[fieldname]):
|
|
|
|
fieldNameAndValCallback = self._fieldMappingInEffect[fieldname]
|
|
|
|
else:
|
|
|
|
fieldname = self._fieldMappingInEffect[fieldname]
|
2019-10-26 21:59:33 +00:00
|
|
|
except:
|
2019-10-26 21:30:50 +00:00
|
|
|
raise NotImplementedError("Field name %s not supported by backend." % (fieldname,))
|
2019-10-26 20:45:48 +00:00
|
|
|
|
2019-10-26 21:59:33 +00:00
|
|
|
# If fieldname returned is None, it's a special case where we
|
|
|
|
# ignore the node.
|
|
|
|
if fieldname is None:
|
|
|
|
return None
|
|
|
|
|
2019-10-26 20:45:48 +00:00
|
|
|
if isinstance(value, (int, str)):
|
2019-10-31 18:40:41 +00:00
|
|
|
if fieldNameAndValCallback is not None:
|
|
|
|
fieldname, value = fieldNameAndValCallback(fieldname, value)
|
2019-10-26 20:45:48 +00:00
|
|
|
op, newVal = self._valuePatternToLcOp(value)
|
2019-10-31 02:25:14 +00:00
|
|
|
newOp = {
|
2019-10-26 20:45:48 +00:00
|
|
|
"op": op,
|
|
|
|
"path": fieldname,
|
2019-10-26 21:30:50 +00:00
|
|
|
"case sensitive": False,
|
2019-10-26 20:45:48 +00:00
|
|
|
}
|
2019-10-31 02:25:14 +00:00
|
|
|
if op == "matches":
|
|
|
|
newOp["re"] = newVal
|
|
|
|
else:
|
|
|
|
newOp["value"] = newVal
|
2019-11-01 16:47:53 +00:00
|
|
|
if self._postOpMapper is not None:
|
|
|
|
newOp = self._postOpMapper(newOp)
|
2019-10-31 02:25:14 +00:00
|
|
|
return newOp
|
2019-10-26 20:45:48 +00:00
|
|
|
elif isinstance(value, list):
|
|
|
|
subOps = []
|
|
|
|
for v in value:
|
2019-10-31 18:40:41 +00:00
|
|
|
if fieldNameAndValCallback is not None:
|
|
|
|
fieldname, v = fieldNameAndValCallback(fieldname, v)
|
2019-10-26 20:45:48 +00:00
|
|
|
op, newVal = self._valuePatternToLcOp(v)
|
2019-10-31 02:25:14 +00:00
|
|
|
newOp = {
|
2019-10-26 20:45:48 +00:00
|
|
|
"op": op,
|
|
|
|
"path": fieldname,
|
2019-10-26 21:30:50 +00:00
|
|
|
"case sensitive": False,
|
2019-10-31 02:25:14 +00:00
|
|
|
}
|
|
|
|
if op == "matches":
|
|
|
|
newOp["re"] = newVal
|
|
|
|
else:
|
|
|
|
newOp["value"] = newVal
|
2019-11-01 16:47:53 +00:00
|
|
|
if self._postOpMapper is not None:
|
|
|
|
newOp = self._postOpMapper(newOp)
|
2019-10-31 02:25:14 +00:00
|
|
|
subOps.append(newOp)
|
2019-10-26 20:45:48 +00:00
|
|
|
if 1 == len(subOps):
|
|
|
|
return subOps[0]
|
|
|
|
return {
|
|
|
|
"op": "or",
|
|
|
|
"rules": subOps
|
|
|
|
}
|
|
|
|
elif isinstance(value, SigmaTypeModifier):
|
|
|
|
if isinstance(value, SigmaRegularExpressionModifier):
|
2019-10-31 18:40:41 +00:00
|
|
|
if fieldNameAndValCallback is not None:
|
|
|
|
fieldname, value = fieldNameAndValCallback(fieldname, value)
|
2019-11-01 16:47:53 +00:00
|
|
|
result = {
|
2019-10-26 20:45:48 +00:00
|
|
|
"op": "matches",
|
|
|
|
"path": fieldname,
|
|
|
|
"re": re.compile(value),
|
|
|
|
}
|
2019-11-01 16:47:53 +00:00
|
|
|
if self._postOpMapper is not None:
|
|
|
|
result = self._postOpMapper(result)
|
|
|
|
return result
|
2019-10-26 20:45:48 +00:00
|
|
|
else:
|
|
|
|
raise TypeError("Backend does not support TypeModifier: %s" % (str(type(value))))
|
|
|
|
elif value is None:
|
2019-10-31 18:40:41 +00:00
|
|
|
if fieldNameAndValCallback is not None:
|
|
|
|
fieldname, value = fieldNameAndValCallback(fieldname, value)
|
2019-11-01 16:47:53 +00:00
|
|
|
result = {
|
2019-10-26 20:45:48 +00:00
|
|
|
"op": "exists",
|
|
|
|
"not": True,
|
|
|
|
"path": fieldname,
|
|
|
|
}
|
2019-11-01 16:47:53 +00:00
|
|
|
if self._postOpMapper is not None:
|
|
|
|
result = self._postOpMapper(result)
|
|
|
|
return result
|
2019-10-26 20:45:48 +00:00
|
|
|
else:
|
|
|
|
raise TypeError("Backend does not support map values of type " + str(type(value)))
|
|
|
|
|
|
|
|
def generateValueNode(self, node):
|
2019-10-26 20:54:08 +00:00
|
|
|
return node
|
2019-10-26 20:45:48 +00:00
|
|
|
|
|
|
|
def _valuePatternToLcOp(self, val):
|
2019-10-26 22:37:13 +00:00
|
|
|
# Here we convert the string values supported by Sigma that
|
|
|
|
# can include wildcards into either proper values (string or int)
|
|
|
|
# or into altered values to be functionally equivalent using
|
|
|
|
# a few different LC D&R rule operators.
|
|
|
|
|
2019-11-05 13:33:21 +00:00
|
|
|
# No point evaluating non-strings.
|
2019-10-26 20:45:48 +00:00
|
|
|
if not isinstance(val, str):
|
2019-10-26 21:30:50 +00:00
|
|
|
return ("is", str(val) if self._isAllStringValues else val)
|
2019-10-31 02:25:14 +00:00
|
|
|
|
|
|
|
# Is there any wildcard in this string? If not, we can short circuit.
|
|
|
|
if "*" not in val and "?" not in val:
|
|
|
|
return ("is", val)
|
|
|
|
|
|
|
|
# Now we do a small optimization for the shortcut operators
|
2019-11-05 13:33:21 +00:00
|
|
|
# available in LC. We try to see if the wildcards are around
|
|
|
|
# the main value, but NOT within. If that's the case we can
|
|
|
|
# use the "starts with", "ends with" or "contains" operators.
|
2019-10-31 02:25:14 +00:00
|
|
|
isStartsWithWildcard = False
|
|
|
|
isEndsWithWildcard = False
|
|
|
|
tmpVal = val
|
|
|
|
if tmpVal.startswith("*"):
|
|
|
|
isStartsWithWildcard = True
|
|
|
|
tmpVal = tmpVal[1:]
|
2019-10-31 02:34:29 +00:00
|
|
|
if tmpVal.endswith("*") and not (tmpVal.endswith("\\*") and not tmpVal.endswith("\\\\*")):
|
2019-10-31 02:25:14 +00:00
|
|
|
isEndsWithWildcard = True
|
2019-10-31 20:29:31 +00:00
|
|
|
if tmpVal.endswith("\\\\*"):
|
|
|
|
# An extra \ had to be there so it didn't escapte the
|
|
|
|
# *, but since we plan on removing the *, we can also
|
|
|
|
# remove one \.
|
|
|
|
tmpVal = tmpVal[:-2]
|
|
|
|
else:
|
|
|
|
tmpVal = tmpVal[:-1]
|
2019-10-31 02:25:14 +00:00
|
|
|
|
|
|
|
# Check to see if there are any other wildcards. If there are
|
|
|
|
# we cannot use our shortcuts.
|
|
|
|
if "*" not in tmpVal and "?" not in tmpVal:
|
|
|
|
if isStartsWithWildcard and isEndsWithWildcard:
|
|
|
|
return ("contains", tmpVal)
|
|
|
|
|
|
|
|
if isStartsWithWildcard:
|
|
|
|
return ("ends with", tmpVal)
|
|
|
|
|
|
|
|
if isEndsWithWildcard:
|
|
|
|
return ("starts with", tmpVal)
|
|
|
|
|
|
|
|
# This is messy, but it is accurate in generating a RE based on
|
|
|
|
# the simplified wildcard system, while also supporting the
|
|
|
|
# escaping of those wildcards.
|
|
|
|
segments = []
|
|
|
|
tmpVal = val
|
|
|
|
while True:
|
|
|
|
nEscapes = 0
|
|
|
|
for i in range(len(tmpVal)):
|
|
|
|
# We keep a running count of backslash escape
|
|
|
|
# characters we see so that if we meet a wildcard
|
|
|
|
# we can tell whether the wildcard is escaped
|
|
|
|
# (with odd number of escapes) or if it's just a
|
|
|
|
# backslash literal before a wildcard (even number).
|
|
|
|
if "\\" == tmpVal[i]:
|
|
|
|
nEscapes += 1
|
|
|
|
continue
|
|
|
|
|
|
|
|
if "*" == tmpVal[i]:
|
|
|
|
if 0 == nEscapes:
|
|
|
|
segments.append(re.escape(tmpVal[:i]))
|
|
|
|
segments.append(".*")
|
|
|
|
elif nEscapes % 2 == 0:
|
|
|
|
segments.append(re.escape(tmpVal[:i - nEscapes]))
|
|
|
|
segments.append(tmpVal[i - nEscapes:i])
|
|
|
|
segments.append(".*")
|
|
|
|
else:
|
|
|
|
segments.append(re.escape(tmpVal[:i - nEscapes]))
|
|
|
|
segments.append(tmpVal[i - nEscapes:i + 1])
|
|
|
|
tmpVal = tmpVal[i + 1:]
|
|
|
|
break
|
|
|
|
|
|
|
|
if "?" == tmpVal[i]:
|
|
|
|
if 0 == nEscapes:
|
|
|
|
segments.append(re.escape(tmpVal[:i]))
|
|
|
|
segments.append(".")
|
|
|
|
elif nEscapes % 2 == 0:
|
|
|
|
segments.append(re.escape(tmpVal[:i - nEscapes]))
|
|
|
|
segments.append(tmpVal[i - nEscapes:i])
|
|
|
|
segments.append(".")
|
|
|
|
else:
|
|
|
|
segments.append(re.escape(tmpVal[:i - nEscapes]))
|
|
|
|
segments.append(tmpVal[i - nEscapes:i + 1])
|
|
|
|
tmpVal = tmpVal[i + 1:]
|
|
|
|
break
|
|
|
|
|
|
|
|
nEscapes = 0
|
|
|
|
else:
|
|
|
|
segments.append(re.escape(tmpVal))
|
|
|
|
break
|
|
|
|
|
|
|
|
val = ''.join(segments)
|
|
|
|
|
2019-10-31 16:15:07 +00:00
|
|
|
return ("matches", val)
|
|
|
|
|
|
|
|
def _mapKeywordVals(self, values):
|
2019-10-31 18:40:41 +00:00
|
|
|
# This function ensures that the list of values passed
|
|
|
|
# are proper D&R operations, if they are strings it indicates
|
|
|
|
# they were requested as keyword matches. We only support
|
2019-11-05 13:33:21 +00:00
|
|
|
# keyword matches when specified in the config. We generally just
|
2019-10-31 18:40:41 +00:00
|
|
|
# map them to the most common field in LC that makes sense.
|
2019-10-31 16:15:07 +00:00
|
|
|
mapped = []
|
|
|
|
|
|
|
|
for val in values:
|
2019-11-05 13:33:21 +00:00
|
|
|
# Non-keywords are just passed through.
|
2019-10-31 16:15:07 +00:00
|
|
|
if not isinstance(val, str):
|
|
|
|
mapped.append(val)
|
|
|
|
continue
|
|
|
|
|
|
|
|
if self._keywordField is None:
|
|
|
|
raise NotImplementedError("Full-text keyboard searches not supported.")
|
|
|
|
|
|
|
|
# This seems to be indicative only of "keywords" which are mostly
|
|
|
|
# representative of full-text searches. We don't suport that but
|
|
|
|
# in some data sources we can alias them to an actual field.
|
|
|
|
op, newVal = self._valuePatternToLcOp(val)
|
|
|
|
newOp = {
|
|
|
|
"op": op,
|
|
|
|
"path": self._keywordField,
|
|
|
|
}
|
|
|
|
if op == "matches":
|
|
|
|
newOp["re"] = newVal
|
|
|
|
else:
|
|
|
|
newOp["value"] = newVal
|
|
|
|
mapped.append(newOp)
|
|
|
|
|
|
|
|
return mapped
|