Skip to content

Commit 8f58df6

Browse files
Add validate support via env var
1 parent b0d3fb8 commit 8f58df6

File tree

3 files changed

+64
-31
lines changed

3 files changed

+64
-31
lines changed

detection_rules/rule.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -650,8 +650,6 @@ def validate(self, _: "QueryRuleData", __: RuleMeta) -> None:
650650
@cached
651651
def get_required_fields(self, index: str) -> list[dict[str, Any]]:
652652
"""Retrieves fields needed for the query along with type information from the schema."""
653-
if isinstance(self, ESQLValidator):
654-
return []
655653

656654
current_version = Version.parse(load_current_package_version(), optional_minor_and_patch=True)
657655
ecs_version = get_stack_schemas()[str(current_version)]["ecs"]

detection_rules/rule_validators.py

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -618,26 +618,44 @@ def validate_rule_type_configurations(self, data: EQLRuleData, meta: RuleMeta) -
618618
class ESQLValidator(QueryValidator):
619619
"""Validate specific fields for ESQL query event types."""
620620

621+
esql_unique_fields: list[str]
622+
621623
@cached_property
622624
def ast(self) -> None: # type: ignore[reportIncompatibleMethodOverride]
623625
return None
624626

625627
@cached_property
626628
def unique_fields(self) -> list[str]: # type: ignore[reportIncompatibleMethodOverride]
627-
"""Return a list of unique fields in the query."""
628-
# return empty list for ES|QL rules until ast is available (friendlier than raising error)
629-
return []
629+
"""Return a list of unique fields in the query. Requires remote validation to have occurred."""
630+
if not self.esql_unique_fields:
631+
return []
632+
return self.esql_unique_fields
630633

631-
def validate(self, _: "QueryRuleData", __: RuleMeta) -> None: # type: ignore[reportIncompatibleMethodOverride]
634+
def validate(self, rule_data: "QueryRuleData", rule_meta: RuleMeta) -> None: # type: ignore[reportIncompatibleMethodOverride]
632635
"""Validate an ESQL query while checking TOMLRule."""
633-
# TODO
634-
# temporarily override to NOP until ES|QL query parsing is supported
635-
# if ENV VAR :
636-
# self.remote_validate_rule
637-
# else:
638-
# ESQLRuleData validation
639-
640-
# NOTE will go away
636+
if misc.getdefault("remote_esql_validation")():
637+
kibana_client = misc.get_kibana_client(
638+
api_key=misc.getdefault("api_key")(),
639+
cloud_id=misc.getdefault("cloud_id")(),
640+
kibana_url=misc.getdefault("kibana_url")(),
641+
space=misc.getdefault("space")(),
642+
ignore_ssl_errors=misc.getdefault("ignore_ssl_errors")(),
643+
)
644+
645+
elastic_client = misc.get_elasticsearch_client(
646+
api_key=misc.getdefault("api_key")(),
647+
cloud_id=misc.getdefault("cloud_id")(),
648+
elasticsearch_url=misc.getdefault("elasticsearch_url")(),
649+
ignore_ssl_errors=misc.getdefault("ignore_ssl_errors")(),
650+
)
651+
self.remote_validate_rule(
652+
kibana_client,
653+
elastic_client,
654+
rule_data.query,
655+
rule_meta,
656+
rule_data.rule_id,
657+
)
658+
641659
def validate_integration(
642660
self,
643661
_: QueryRuleData,
@@ -647,14 +665,14 @@ def validate_integration(
647665
# Disabling self.validate(data, meta)
648666
pass
649667

650-
def get_rule_integrations(self, contents: TOMLRuleContents) -> list[str]:
668+
def get_rule_integrations(self, metadata: RuleMeta) -> list[str]:
651669
"""Retrieve rule integrations from metadata."""
652670
rule_integrations: list[str] = []
653-
if contents.metadata.integration:
654-
if isinstance(contents.metadata.integration, list):
655-
rule_integrations = contents.metadata.integration
671+
if metadata.integration:
672+
if isinstance(metadata.integration, list):
673+
rule_integrations = metadata.integration
656674
else:
657-
rule_integrations = [contents.metadata.integration]
675+
rule_integrations = [metadata.integration]
658676
return rule_integrations
659677

660678
def prepare_integration_mappings(
@@ -823,14 +841,14 @@ def prepare_mappings(
823841
elastic_client: Elasticsearch,
824842
indices: list[str],
825843
stack_version: str,
826-
contents: TOMLRuleContents,
844+
metadata: RuleMeta,
827845
log: Callable[[str], None],
828846
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
829847
"""Prepare index mappings for the given indices and rule integrations."""
830848
existing_mappings, index_lookup = misc.get_existing_mappings(elastic_client, indices)
831849

832850
# Collect mappings for the integrations
833-
rule_integrations = self.get_rule_integrations(contents)
851+
rule_integrations = self.get_rule_integrations(metadata)
834852

835853
# Collect mappings for all relevant integrations for the given stack version
836854
package_manifests = load_integrations_manifests()
@@ -866,11 +884,29 @@ def prepare_mappings(
866884

867885
return existing_mappings, index_lookup, combined_mappings
868886

869-
def remote_validate_rule(
887+
def remote_validate_rule_contents(
870888
self, kibana_client: Kibana, elastic_client: Elasticsearch, contents: TOMLRuleContents, verbosity: int = 0
889+
) -> None:
890+
"""Remote validate a rule's ES|QL query using an Elastic Stack."""
891+
self.remote_validate_rule(
892+
kibana_client=kibana_client,
893+
elastic_client=elastic_client,
894+
query=contents.data.query, # type: ignore[reportUnknownVariableType]
895+
metadata=contents.metadata,
896+
rule_id=contents.data.rule_id,
897+
verbosity=verbosity,
898+
)
899+
900+
def remote_validate_rule( # noqa: PLR0913
901+
self,
902+
kibana_client: Kibana,
903+
elastic_client: Elasticsearch,
904+
query: str,
905+
metadata: RuleMeta,
906+
rule_id: str = "",
907+
verbosity: int = 0,
871908
) -> None:
872909
"""Uses remote validation from an Elastic Stack to validate ES|QL a given rule"""
873-
rule_id = contents.data.rule_id
874910

875911
def log(val: str) -> None:
876912
"""Log if verbosity is 1 or greater (1 corresponds to `-v` in pytest)"""
@@ -885,12 +921,12 @@ def log(val: str) -> None:
885921
stack_version = str(kibana_details["version"]["number"])
886922
log(f"Validating against {stack_version} stack")
887923

888-
indices_str, indices = utils.get_esql_query_indices(contents.data.query) # type: ignore[reportUnknownVariableType]
924+
indices_str, indices = utils.get_esql_query_indices(query) # type: ignore[reportUnknownVariableType]
889925
log(f"Extracted indices from query: {', '.join(indices)}")
890926

891927
# Get mappings for all matching existing index templates
892928
existing_mappings, index_lookup, combined_mappings = self.prepare_mappings(
893-
elastic_client, indices, stack_version, contents, log
929+
elastic_client, indices, stack_version, metadata, log
894930
)
895931
log(f"Collected mappings: {len(existing_mappings)}")
896932
log(f"Combined mappings prepared: {len(combined_mappings)}")
@@ -901,11 +937,10 @@ def log(val: str) -> None:
901937
utils.combine_dicts(combined_mappings, index_lookup["rule-ecs-index"])
902938

903939
# Replace all sources with the test indices
904-
query = contents.data.query # type: ignore[reportUnknownVariableType]
905940
query = query.replace(indices_str, full_index_str) # type: ignore[reportUnknownVariableType]
906941

907-
# TODO these query_columns are the unique fields
908942
query_columns = self.execute_query_against_indices(elastic_client, query, full_index_str, log) # type: ignore[reportUnknownVariableType]
943+
self.esql_unique_fields = query_columns
909944

910945
# Validate that all fields (columns) are either dynamic fields or correctly mapped
911946
# against the combined mapping of all the indices

tests/test_rules_remote.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,18 @@ def test_esql_rules(self):
4545
)
4646

4747
# Retrieve verbosity level from pytest
48-
verbosity = self._outcome.result.config.get_verbosity()
48+
verbosity: int = int(self._outcome.result.config.get_verbosity()) # type: ignore[reportIncompatibleMethodOverride]
4949

5050
failed_count = 0
51-
fail_list = []
51+
fail_list: list[str] = []
5252
max_retries = 3
5353
for r in esql_rules:
5454
print()
5555
retry_count = 0
5656
while retry_count < max_retries:
5757
try:
58-
validator = ESQLValidator(r.contents.data.query)
59-
validator.remote_validate_rule(kibana_client, elastic_client, r.contents, verbosity)
58+
validator = ESQLValidator(r.contents.data.query) # type: ignore[reportIncompatibleMethodOverride]
59+
validator.remote_validate_rule_contents(kibana_client, elastic_client, r.contents, verbosity)
6060
break
6161
except (ValueError, BadRequestError) as e:
6262
print(f"FAILURE: {e}")

0 commit comments

Comments
 (0)