diff --git a/detection_rules/rule.py b/detection_rules/rule.py index 06c645d814c..9163c68a358 100644 --- a/detection_rules/rule.py +++ b/detection_rules/rule.py @@ -914,8 +914,15 @@ def validates_esql_data(self, data, **kwargs): 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,\s*_version,\s*_index|_id,\s*_index,\s*_version|_version,\s*_id,\s*_index|' + r'_version,\s*_index,\s*_id|_index,\s*_id,\s*_version|_index,\s*_version,\s*_id))' + r'|(\bstats\b.*?\bby\b)', + re.DOTALL ) # Ensure that non-aggregate queries have metadata @@ -927,7 +934,9 @@ def validates_esql_data(self, data, **kwargs): ) # 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 ->" f" Add a 'keep' command to the query."