246 lines
5.8 KiB
Python
246 lines
5.8 KiB
Python
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
# or more contributor license agreements. Licensed under the Elastic License
|
|
# 2.0; you may not use this file except in compliance with the Elastic License
|
|
# 2.0.
|
|
|
|
import re
|
|
from string import Template
|
|
|
|
from eql.ast import BaseNode
|
|
from eql.errors import EqlCompileError
|
|
from eql.utils import is_number, is_string
|
|
|
|
__all__ = (
|
|
"KqlNode",
|
|
"Value",
|
|
"Null",
|
|
"Number",
|
|
"Boolean",
|
|
"List",
|
|
"Expression",
|
|
"String",
|
|
"Wildcard",
|
|
"NotValue",
|
|
"OrValues",
|
|
"AndValues",
|
|
"AndExpr",
|
|
"OrExpr",
|
|
"NotExpr",
|
|
"FieldComparison",
|
|
"Field",
|
|
"FieldRange",
|
|
"NestedQuery",
|
|
"Exists",
|
|
)
|
|
|
|
|
|
class KqlNode(BaseNode):
|
|
def optimize(self, recursive=True):
|
|
from .optimizer import Optimizer
|
|
return Optimizer().walk(self)
|
|
|
|
def _render(self):
|
|
return BaseNode.render(self)
|
|
|
|
def render(self, precedence=None, **kwargs):
|
|
"""Render an EQL node and add parentheses to support orders of operation."""
|
|
rendered = self._render(**kwargs)
|
|
if precedence is not None and self.precedence is not None and self.precedence > precedence:
|
|
return '({})'.format(rendered)
|
|
return rendered
|
|
|
|
|
|
class Value(KqlNode):
|
|
__slots__ = "value",
|
|
precedence = 1
|
|
|
|
def __init__(self, value):
|
|
self.value = value
|
|
|
|
@classmethod
|
|
def from_python(cls, value):
|
|
if value is None:
|
|
return Null()
|
|
elif isinstance(value, bool):
|
|
return Boolean(value)
|
|
elif is_number(value):
|
|
return Number(value)
|
|
elif is_string(value):
|
|
return String(value)
|
|
else:
|
|
raise EqlCompileError("Unknown type {} for value {}".format(type(value).__name__, value))
|
|
|
|
|
|
class Null(Value):
|
|
def __init__(self, value=None):
|
|
Value.__init__(self, None)
|
|
|
|
def _render(self):
|
|
return "null"
|
|
|
|
|
|
class Number(Value):
|
|
def _render(self):
|
|
return str(self.value)
|
|
|
|
|
|
class Boolean(Value):
|
|
def _render(self):
|
|
return 'true' if self.value else 'false'
|
|
|
|
|
|
class String(Value):
|
|
unescapable = re.compile(r'^[^\\():<>"*{} \t\r\n]+$')
|
|
escapes = {"\t": "\\t", "\r": "\\r", "\"": "\\\""}
|
|
|
|
def _render(self):
|
|
# pass through as-is since nothing needs to be escaped
|
|
if self.unescapable.match(self.value) is not None:
|
|
return str(self.value)
|
|
|
|
regex = r"[{}]".format("".join(re.escape(s) for s in sorted(self.escapes)))
|
|
return '"{}"'.format(re.sub(regex, lambda r: self.escapes[r.group()], self.value))
|
|
|
|
|
|
class Wildcard(Value):
|
|
escapes = {"\t": "\\t", "\r": "\\r"}
|
|
slash_escaped = r'''^\\():<>"*{} '''
|
|
|
|
def _render(self):
|
|
escaped = []
|
|
for char in self.value:
|
|
if char in self.slash_escaped:
|
|
escaped.append("\\")
|
|
escaped.append(char)
|
|
elif char in self.escapes:
|
|
escaped.append(self.escapes[char])
|
|
else:
|
|
escaped.append(char)
|
|
return ''.join(escaped)
|
|
|
|
|
|
class List(KqlNode):
|
|
__slots__ = "items",
|
|
precedence = Value.precedence + 1
|
|
operator = ""
|
|
template = Template("$items")
|
|
|
|
def __init__(self, items):
|
|
self.items = items
|
|
KqlNode.__init__(self)
|
|
|
|
@property
|
|
def delims(self):
|
|
return {"items": " {} ".format(self.operator)}
|
|
|
|
def __eq__(self, other):
|
|
from .optimizer import Optimizer
|
|
from functools import cmp_to_key
|
|
if type(self) == type(other):
|
|
a = list(self.items)
|
|
b = list(other.items)
|
|
a.sort(key=cmp_to_key(Optimizer.sort_key))
|
|
b.sort(key=cmp_to_key(Optimizer.sort_key))
|
|
return a == b
|
|
|
|
return False
|
|
|
|
|
|
class NotValue(KqlNode):
|
|
__slots__ = "value",
|
|
template = Template("not $value")
|
|
precedence = Value.precedence + 1
|
|
|
|
def __init__(self, value):
|
|
self.value = value
|
|
KqlNode.__init__(self)
|
|
|
|
|
|
class AndValues(List):
|
|
precedence = List.precedence + 1
|
|
operator = "and"
|
|
|
|
|
|
class OrValues(List):
|
|
precedence = AndValues.precedence + 1
|
|
operator = "or"
|
|
|
|
|
|
class Field(KqlNode):
|
|
__slots__ = "name",
|
|
precedence = Value.precedence
|
|
template = Template("$name")
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
KqlNode.__init__(self)
|
|
|
|
@property
|
|
def path(self):
|
|
return self.name.split(".")
|
|
|
|
@classmethod
|
|
def from_path(cls, path):
|
|
dotted = ".".join(path)
|
|
return cls(dotted)
|
|
|
|
|
|
class Expression(KqlNode):
|
|
"""Intermediate node for class hierarchy."""
|
|
|
|
|
|
class FieldRange(Expression, KqlNode):
|
|
__slots__ = "field", "operator", "value",
|
|
precedence = Field.precedence
|
|
template = Template("$field $operator $value")
|
|
|
|
def __init__(self, field, operator, value):
|
|
self.field = field
|
|
self.operator = operator
|
|
self.value = value
|
|
|
|
|
|
class NestedQuery(Expression):
|
|
__slots__ = "field", "expr",
|
|
precedence = Field.precedence + 1
|
|
template = Template("$field:{$expr}")
|
|
|
|
def __init__(self, field, expr):
|
|
self.field = field
|
|
self.expr = expr
|
|
|
|
|
|
class FieldComparison(Expression):
|
|
__slots__ = "field", "value",
|
|
precedence = FieldRange.precedence
|
|
template = Template("$field:$value")
|
|
|
|
def __init__(self, field, value):
|
|
self.field = field
|
|
self.value = value
|
|
|
|
|
|
class Exists(KqlNode):
|
|
__slots__ = tuple()
|
|
precedence = FieldComparison.precedence
|
|
template = Template("*")
|
|
|
|
|
|
class NotExpr(Expression):
|
|
__slots__ = "expr",
|
|
precedence = FieldComparison.precedence + 1
|
|
template = Template("not $expr")
|
|
|
|
def __init__(self, expr):
|
|
self.expr = expr
|
|
|
|
|
|
class AndExpr(Expression, List):
|
|
precedence = NotExpr.precedence + 1
|
|
operator = "and"
|
|
|
|
|
|
class OrExpr(Expression, List):
|
|
precedence = AndExpr.precedence + 1
|
|
operator = "or"
|