Skip to content

Commit 5942aeb

Browse files
Cleanup Esql Error Types
1 parent db963ff commit 5942aeb

File tree

8 files changed

+117
-214
lines changed

8 files changed

+117
-214
lines changed

detection_rules/esql_errors.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,21 @@ class EsqlSchemaError(EsqlKibanaBaseError):
4343
"""Error in ESQL schema. Validated via Kibana until AST is available."""
4444

4545

46-
class EsqlSemanticError(EsqlKibanaBaseError):
47-
"""Error with ESQL semantics. Validated via Kibana until AST is available."""
48-
49-
5046
class EsqlSyntaxError(EsqlKibanaBaseError):
5147
"""Error with ESQL syntax. Validated via Kibana until AST is available."""
5248

5349

5450
class EsqlTypeMismatchError(Exception):
5551
"""Error when validating types in ESQL."""
5652

53+
def __init__(self, message: str, elastic_client: Elasticsearch | None = None) -> None:
54+
if elastic_client:
55+
cleanup_empty_indices(elastic_client)
56+
super().__init__(message)
57+
58+
59+
class EsqlSemanticError(Exception):
60+
"""Error with ESQL semantics. Validated via Kibana until AST is available."""
61+
5762
def __init__(self, message: str) -> None:
5863
super().__init__(message)

detection_rules/index_mappings.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@
1717
from . import ecs, integrations, misc, utils
1818
from .config import load_current_package_version
1919
from .esql import EventDataset
20-
from .esql_errors import EsqlSchemaError, EsqlSemanticError, EsqlSyntaxError, cleanup_empty_indices
20+
from .esql_errors import (
21+
EsqlKibanaBaseError,
22+
EsqlSchemaError,
23+
EsqlSyntaxError,
24+
EsqlTypeMismatchError,
25+
cleanup_empty_indices,
26+
)
2127
from .integrations import (
2228
load_integrations_manifests,
2329
load_integrations_schemas,
@@ -251,12 +257,15 @@ def execute_query_against_indices(
251257
error_msg = str(e)
252258
if "parsing_exception" in error_msg:
253259
raise EsqlSyntaxError(str(e), elastic_client) from e
254-
raise EsqlSemanticError(str(e), elastic_client) from e
255-
finally:
256-
if delete_indices or misc.getdefault("skip_empty_index_cleanup")():
257-
for index_str in test_index_str.split(","):
258-
response = elastic_client.indices.delete(index=index_str.strip())
259-
log(f"Test index `{index_str}` deleted: {response}")
260+
if "Unknown column" in error_msg:
261+
raise EsqlSchemaError(str(e), elastic_client) from e
262+
if "verification_exception" in error_msg:
263+
raise EsqlTypeMismatchError(str(e), elastic_client) from e
264+
raise EsqlKibanaBaseError(str(e), elastic_client) from e
265+
if delete_indices or misc.getdefault("skip_empty_index_cleanup")():
266+
for index_str in test_index_str.split(","):
267+
response = elastic_client.indices.delete(index=index_str.strip())
268+
log(f"Test index `{index_str}` deleted: {response}")
260269

261270
query_column_names = [c["name"] for c in query_columns]
262271
log(f"Got query columns: {', '.join(query_column_names)}")
@@ -367,14 +376,16 @@ def prepare_mappings( # noqa: PLR0913
367376
non_ecs_mapping.update(non_ecs.get(index, {}))
368377
non_ecs_mapping = ecs.flatten(non_ecs_mapping)
369378
non_ecs_mapping = utils.convert_to_nested_schema(non_ecs_mapping)
370-
if not combined_mappings and not non_ecs_mapping:
371-
raise ValueError("No mappings found")
372-
index_lookup.update({"rule-non-ecs-index": non_ecs_mapping})
373379

374380
# Load ECS in an index mapping format (nested schema)
375381
current_version = Version.parse(load_current_package_version(), optional_minor_and_patch=True)
376382
ecs_schema = get_ecs_schema_mappings(current_version)
377383

378384
index_lookup.update({"rule-ecs-index": ecs_schema})
385+
utils.combine_dicts(combined_mappings, ecs_schema)
386+
387+
if not combined_mappings and not non_ecs_mapping and not ecs_schema:
388+
raise ValueError("No mappings found")
389+
index_lookup.update({"rule-non-ecs-index": non_ecs_mapping})
379390

380391
return existing_mappings, index_lookup, combined_mappings

detection_rules/rule.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from . import beats, ecs, endgame, utils
3131
from .config import load_current_package_version, parse_rules_config
3232
from .esql import get_esql_query_event_dataset_integrations
33+
from .esql_errors import EsqlSemanticError
3334
from .integrations import (
3435
find_least_compatible_version,
3536
get_integration_schema_fields,
@@ -963,7 +964,7 @@ class ESQLRuleData(QueryRuleData):
963964
def validates_esql_data(self, data: dict[str, Any], **_: Any) -> None:
964965
"""Custom validation for query rule type and subclasses."""
965966
if data.get("index"):
966-
raise ValidationError("Index is not a valid field for ES|QL rule type.")
967+
raise EsqlSemanticError("Index is not a valid field for ES|QL rule type.")
967968

968969
# Convert the query string to lowercase to handle case insensitivity
969970
query_lower = data["query"].lower()
@@ -981,7 +982,7 @@ def validates_esql_data(self, data: dict[str, Any], **_: Any) -> None:
981982

982983
# Ensure that non-aggregate queries have metadata
983984
if not combined_pattern.search(query_lower):
984-
raise ValidationError(
985+
raise EsqlSemanticError(
985986
f"Rule: {data['name']} contains a non-aggregate query without"
986987
f" metadata fields '_id', '_version', and '_index' ->"
987988
f" Add 'metadata _id, _version, _index' to the from command or add an aggregate function."
@@ -991,7 +992,7 @@ def validates_esql_data(self, data: dict[str, Any], **_: Any) -> None:
991992
# Match | followed by optional whitespace/newlines and then 'keep'
992993
keep_pattern = re.compile(r"\|\s*keep\b", re.IGNORECASE | re.DOTALL)
993994
if not keep_pattern.search(query_lower):
994-
raise ValidationError(
995+
raise EsqlSemanticError(
995996
f"Rule: {data['name']} does not contain a 'keep' command -> Add a 'keep' command to the query."
996997
)
997998

detection_rules/rule_validators.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -789,12 +789,13 @@ def validate_columns_index_mapping(
789789
# Check if the column exists in combined_mappings or a valid field generated from a function or operator
790790
keys = column_name.split(".")
791791
schema_type = utils.get_column_from_index_mapping_schema(keys, combined_mappings)
792+
schema_type = kql.parser.elasticsearch_type_family(schema_type) if schema_type else None
792793

793794
# Validate the type
794795
if not schema_type or column_type != schema_type:
795796
mismatched_columns.append(
796797
f"Dynamic field `{column_name}` is not correctly mapped. "
797-
f"If not dynamic: expected `{schema_type}`, got `{column_type}`."
798+
f"If not dynamic: expected from schema: `{schema_type}`, got from Kibana: `{column_type}`."
798799
)
799800

800801
if mismatched_columns:

tests/data/collection_cloudtrail_logging_created.toml

Lines changed: 0 additions & 63 deletions
This file was deleted.

tests/data/collection_cloudtrail_logging_created_correct.toml

Lines changed: 0 additions & 64 deletions
This file was deleted.

0 commit comments

Comments
 (0)