@@ -618,26 +618,44 @@ def validate_rule_type_configurations(self, data: EQLRuleData, meta: RuleMeta) -
618618class 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
0 commit comments