-
Notifications
You must be signed in to change notification settings - Fork 521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add EQL rules and schema validation #297
Changes from 2 commits
ff78c55
5797689
fbf9c5b
3e16791
09e6016
d5db8fb
9d96ae9
192aa10
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
|
||
import click | ||
import kql | ||
import eql | ||
|
||
from . import ecs, beats | ||
from .attack import TACTICS, build_threat_map_entry, technique_lookup | ||
|
@@ -152,18 +153,58 @@ def validate(self, as_rule=False, versioned=False, query=True): | |
|
||
schema_cls.validate(contents, role=self.type) | ||
|
||
if query and self.query and self.contents['language'] == 'kuery': | ||
if query and self.query is not None: | ||
ecs_versions = self.metadata.get('ecs_version') | ||
indexes = self.contents.get("index", []) | ||
self._validate_kql(ecs_versions, indexes, self.query, self.name) | ||
|
||
if self.contents['language'] == 'kuery': | ||
self._validate_kql(ecs_versions, indexes, self.query, self.name) | ||
|
||
if self.contents['language'] == 'eql': | ||
self._validate_eql(ecs_versions, indexes, self.query, self.name) | ||
|
||
@staticmethod | ||
@cached | ||
def _validate_eql(ecs_versions, indexes, query, name): | ||
# validate against all specified schemas or the latest if none specified | ||
parsed = eql.parse_query(query) | ||
beat_types = [index.split("-")[0] for index in indexes if "beat-*" in index] | ||
beat_schema = beats.get_schema_from_kql(parsed, beat_types) if beat_types else None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be |
||
|
||
ecs_versions = ecs_versions or [ecs_versions] | ||
schemas = [] | ||
|
||
for version in ecs_versions: | ||
try: | ||
schemas.append(ecs.get_kql_schema(indexes=indexes, beat_schema=beat_schema, version=version)) | ||
except KeyError: | ||
raise KeyError('Unknown ecs schema version: {} in rule {}.\n' | ||
'Do you need to update schemas?'.format(version, name)) from None | ||
|
||
for schema in schemas: | ||
try: | ||
with ecs.KqlSchema2Eql(schema): | ||
eql.parse_query(query) | ||
|
||
except eql.EqlTypeMismatchError: | ||
raise | ||
|
||
except eql.EqlParseError as exc: | ||
message = exc.error_msg | ||
trailer = None | ||
if "Unknown field" in message and beat_types: | ||
trailer = "\nTry adding event.module and event.dataset to specify beats module" | ||
|
||
raise type(exc)(exc.error_msg, exc.line, exc.column, exc.source, | ||
len(exc.caret.lstrip()), trailer=trailer) from None | ||
|
||
@staticmethod | ||
@cached | ||
def _validate_kql(ecs_versions, indexes, query, name): | ||
# validate against all specified schemas or the latest if none specified | ||
parsed = kql.parse(query) | ||
beat_types = [index.split("-")[0] for index in indexes if "beat-*" in index] | ||
beat_schema = beats.get_schema_for_query(parsed, beat_types) if beat_types else None | ||
beat_schema = beats.get_schema_from_kql(parsed, beat_types) if beat_types else None | ||
|
||
if not ecs_versions: | ||
kql.parse(query, schema=ecs.get_kql_schema(indexes=indexes, beat_schema=beat_schema)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
# or more contributor license agreements. Licensed under the Elastic License; | ||
# you may not use this file except in compliance with the Elastic License. | ||
|
||
"""Definitions for rule metadata and schemas.""" | ||
|
||
import jsl | ||
from .v7_9 import ApiSchema79 | ||
|
||
|
||
# rule types | ||
EQL = "eql" | ||
|
||
|
||
class ApiSchema710(ApiSchema79): | ||
"""Schema for siem rule in API format.""" | ||
|
||
STACK_VERSION = "7.10" | ||
RULE_TYPES = ApiSchema79.RULE_TYPES + [EQL] | ||
|
||
type = jsl.StringField(enum=RULE_TYPES, required=True) | ||
|
||
# there might be a bug in jsl that requires us to redefine these here | ||
query_scope = ApiSchema79.query_scope | ||
saved_id_scope = ApiSchema79.saved_id_scope | ||
ml_scope = ApiSchema79.ml_scope | ||
threshold_scope = ApiSchema79.threshold_scope | ||
|
||
with jsl.Scope(EQL) as eql_scope: | ||
eql_scope.index = jsl.ArrayField(jsl.StringField(), required=False) | ||
eql_scope.query = jsl.StringField(required=True) | ||
eql_scope.language = jsl.StringField(enum=[EQL], required=True) | ||
rw-access marked this conversation as resolved.
Show resolved
Hide resolved
|
||
eql_scope.type = jsl.StringField(enum=[EQL], required=True) | ||
rw-access marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rw-access I believe this is what you wanted 👀 on, schema-wise? I can confirm that these are the EQL-specific fields and their correct types 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep, thanks! |
||
|
||
with jsl.Scope(jsl.DEFAULT_ROLE) as default_scope: | ||
default_scope.type = type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you change the
parsed_kql
property toThen you can use that here (and more consistently as needed)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i can't use it here because this is a static method and needs to be for caching to work.
but i did update the method regardless, even though it's never used