diff --git a/detection_rules/rule.py b/detection_rules/rule.py index 0c293141b70..ceec6ce98e8 100644 --- a/detection_rules/rule.py +++ b/detection_rules/rule.py @@ -935,8 +935,14 @@ def validates_esql_data(self, data: dict[str, Any], **_: Any) -> None: query_lower = data["query"].lower() # Combine both patterns using an OR operator and compile the regex + # The first part matches the metadata fields in the from clause by allowing one or + # multiple indices and any order of the metadata fields + # The second part matches the stats command with the by clause combined_pattern = re.compile( - r"(from\s+\S+\s+metadata\s+_id,\s*_version,\s*_index)|(\bstats\b.*?\bby\b)", re.DOTALL + r"(from\s+(?:\S+\s*,\s*)*\S+\s+metadata\s+" + r"(?:_id|_version|_index)(?:,\s*(?:_id|_version|_index)){2})" + r"|(\bstats\b.*?\bby\b)", + re.DOTALL, ) # Ensure that non-aggregate queries have metadata @@ -948,7 +954,9 @@ def validates_esql_data(self, data: dict[str, Any], **_: Any) -> None: ) # Enforce KEEP command for ESQL rules - if "| keep" not in query_lower: + # Match | followed by optional whitespace/newlines and then 'keep' + keep_pattern = re.compile(r"\|\s*keep\b", re.IGNORECASE | re.DOTALL) + if not keep_pattern.search(query_lower): raise ValidationError( f"Rule: {data['name']} does not contain a 'keep' command -> Add a 'keep' command to the query." )