Skip to content

Commit 7dfabd0

Browse files
Mikaayensongithub-actions[bot]
authored andcommitted
Validate against beats and integrations schemas (#2524)
(cherry picked from commit 6011544)
1 parent c0a7fdb commit 7dfabd0

File tree

2 files changed

+55
-50
lines changed

2 files changed

+55
-50
lines changed

detection_rules/rule_validators.py

Lines changed: 53 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -40,31 +40,36 @@ def validate(self, data: QueryRuleData, meta: RuleMeta) -> None:
4040
packages_manifest = load_integrations_manifests()
4141
package_integrations = TOMLRuleContents.get_packaged_integrations(data, meta, packages_manifest)
4242

43+
# validate the query against fields within beats
44+
self.validate_stack_combos(data, meta)
45+
4346
if package_integrations:
4447
# validate the query against related integration fields
4548
self.validate_integration(data, meta, package_integrations)
46-
else:
47-
for stack_version, mapping in meta.get_validation_stack_versions().items():
48-
beats_version = mapping['beats']
49-
ecs_version = mapping['ecs']
50-
err_trailer = f'stack: {stack_version}, beats: {beats_version}, ecs: {ecs_version}'
51-
52-
beat_types, beat_schema, schema = self.get_beats_schema(data.index or [],
53-
beats_version, ecs_version)
54-
55-
try:
56-
kql.parse(self.query, schema=schema)
57-
except kql.KqlParseError as exc:
58-
message = exc.error_msg
59-
trailer = err_trailer
60-
if "Unknown field" in message and beat_types:
61-
trailer = f"\nTry adding event.module or event.dataset to specify beats module\n\n{trailer}"
62-
63-
raise kql.KqlParseError(exc.error_msg, exc.line, exc.column, exc.source,
64-
len(exc.caret.lstrip()), trailer=trailer) from None
65-
except Exception:
66-
print(err_trailer)
67-
raise
49+
50+
def validate_stack_combos(self, data: QueryRuleData, meta: RuleMeta) -> None:
51+
"""Validate the query against ECS and beats schemas across stack combinations."""
52+
for stack_version, mapping in meta.get_validation_stack_versions().items():
53+
beats_version = mapping['beats']
54+
ecs_version = mapping['ecs']
55+
err_trailer = f'stack: {stack_version}, beats: {beats_version}, ecs: {ecs_version}'
56+
57+
beat_types, beat_schema, schema = self.get_beats_schema(data.index or [],
58+
beats_version, ecs_version)
59+
60+
try:
61+
kql.parse(self.query, schema=schema)
62+
except kql.KqlParseError as exc:
63+
message = exc.error_msg
64+
trailer = err_trailer
65+
if "Unknown field" in message and beat_types:
66+
trailer = f"\nTry adding event.module or event.dataset to specify beats module\n\n{trailer}"
67+
68+
raise kql.KqlParseError(exc.error_msg, exc.line, exc.column, exc.source,
69+
len(exc.caret.lstrip()), trailer=trailer) from None
70+
except Exception:
71+
print(err_trailer)
72+
raise
6873

6974
def validate_integration(self, data: QueryRuleData, meta: RuleMeta, package_integrations: List[dict]) -> None:
7075
"""Validate the query, called from the parent which contains [metadata] information."""
@@ -105,7 +110,8 @@ def validate_integration(self, data: QueryRuleData, meta: RuleMeta, package_inte
105110
f"{stack_version=}, {ecs_version=}"
106111
)
107112
error_fields[field] = {"error": exc, "trailer": trailer}
108-
print(f"\nWarning: `{field}` in `{data.name}` not found in schema. {trailer}")
113+
if data.get("notify", False):
114+
print(f"\nWarning: `{field}` in `{data.name}` not found in schema. {trailer}")
109115
else:
110116
raise kql.KqlParseError(exc.error_msg, exc.line, exc.column, exc.source,
111117
len(exc.caret.lstrip()), trailer=trailer) from None
@@ -154,30 +160,34 @@ def validate(self, data: 'QueryRuleData', meta: RuleMeta) -> None:
154160
packages_manifest = load_integrations_manifests()
155161
package_integrations = TOMLRuleContents.get_packaged_integrations(data, meta, packages_manifest)
156162

163+
# validate the query against fields within beats
164+
self.validate_stack_combos(data, meta)
165+
157166
if package_integrations:
158167
# validate the query against related integration fields
159168
self.validate_integration(data, meta, package_integrations)
160169

161-
else:
162-
for stack_version, mapping in meta.get_validation_stack_versions().items():
163-
beats_version = mapping['beats']
164-
ecs_version = mapping['ecs']
165-
endgame_version = mapping['endgame']
166-
err_trailer = f'stack: {stack_version}, beats: {beats_version},' \
167-
f'ecs: {ecs_version}, endgame: {endgame_version}'
168-
169-
beat_types, beat_schema, schema = self.get_beats_schema(data.index or [],
170-
beats_version, ecs_version)
171-
endgame_schema = self.get_endgame_schema(data.index, endgame_version)
172-
eql_schema = ecs.KqlSchema2Eql(schema)
170+
def validate_stack_combos(self, data: QueryRuleData, meta: RuleMeta) -> None:
171+
"""Validate the query against ECS and beats schemas across stack combinations."""
172+
for stack_version, mapping in meta.get_validation_stack_versions().items():
173+
beats_version = mapping['beats']
174+
ecs_version = mapping['ecs']
175+
endgame_version = mapping['endgame']
176+
err_trailer = f'stack: {stack_version}, beats: {beats_version},' \
177+
f'ecs: {ecs_version}, endgame: {endgame_version}'
178+
179+
beat_types, beat_schema, schema = self.get_beats_schema(data.index or [],
180+
beats_version, ecs_version)
181+
endgame_schema = self.get_endgame_schema(data.index, endgame_version)
182+
eql_schema = ecs.KqlSchema2Eql(schema)
173183

174-
# validate query against the beats and eql schema
175-
self.validate_query_with_schema(data=data, schema=eql_schema, err_trailer=err_trailer,
176-
beat_types=beat_types)
184+
# validate query against the beats and eql schema
185+
self.validate_query_with_schema(data=data, schema=eql_schema, err_trailer=err_trailer,
186+
beat_types=beat_types)
177187

178-
if endgame_schema:
179-
# validate query against the endgame schema
180-
self.validate_query_with_schema(data=data, schema=endgame_schema, err_trailer=err_trailer)
188+
if endgame_schema:
189+
# validate query against the endgame schema
190+
self.validate_query_with_schema(data=data, schema=endgame_schema, err_trailer=err_trailer)
181191

182192
def validate_integration(self, data: QueryRuleData, meta: RuleMeta, package_integrations: List[dict]) -> None:
183193
"""Validate an EQL query while checking TOMLRule against integration schemas."""
@@ -195,7 +205,6 @@ def validate_integration(self, data: QueryRuleData, meta: RuleMeta, package_inte
195205
package_version = integration_schema_data['package_version']
196206
integration_schema = integration_schema_data['schema']
197207
stack_version = integration_schema_data['stack_version']
198-
endgame_version = integration_schema_data['endgame_version']
199208

200209
if stack_version != current_stack_version:
201210
# reset the combined schema for each stack version
@@ -223,17 +232,11 @@ def validate_integration(self, data: QueryRuleData, meta: RuleMeta, package_inte
223232
f"{stack_version=}, {ecs_version=}"
224233
)
225234
error_fields[field] = {"error": exc, "trailer": trailer}
226-
print(f"\nWarning: `{field}` in `{data.name}` not found in schema. {trailer}")
235+
if data.get("notify", False):
236+
print(f"\nWarning: `{field}` in `{data.name}` not found in schema. {trailer}")
227237
else:
228238
raise exc
229239

230-
# Still need to check endgame if it's in the index
231-
endgame_schema = self.get_endgame_schema(data.index, endgame_version)
232-
if endgame_schema:
233-
# validate query against the endgame schema
234-
err_trailer = f'stack: {stack_version}, endgame: {endgame_version}'
235-
self.validate_query_with_schema(data=data, schema=endgame_schema, err_trailer=err_trailer)
236-
237240
# don't error on fields that are in another integration schema
238241
for field in list(error_fields.keys()):
239242
if field in combined_schema:

tests/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
"""Shared resources for tests."""
77

8+
import os
89
import unittest
910
from typing import Union
1011

@@ -17,6 +18,7 @@ class BaseRuleTest(unittest.TestCase):
1718

1819
@classmethod
1920
def setUpClass(cls):
21+
os.environ["DR_NOTIFY_INTEGRATION_UPDATE_AVAILABLE"] = "1"
2022
rc = RuleCollection.default()
2123
cls.all_rules = rc.rules
2224
cls.rule_lookup = rc.id_map

0 commit comments

Comments
 (0)