@@ -159,6 +159,29 @@ def get_simulated_index_template_mappings(elastic_client: Elasticsearch, name: s
159159 return template ["template" ]["mappings" ]["properties" ]
160160
161161
162+ def prune_mappings_of_unsupported_types (
163+ integration : str , stream : str , stream_mappings : dict [str , Any ], log : Callable [[str ], None ]
164+ ) -> dict [str , Any ]:
165+ """Prune fields with unsupported types (ES|QL) from the provided mappings."""
166+ nested_multifields = find_nested_multifields (stream_mappings )
167+ for field in nested_multifields :
168+ field_name = str (field ).split (".fields." )[0 ].replace ("." , ".properties." ) + ".fields"
169+ log (
170+ f"Warning: Nested multi-field `{ field } ` found in `{ integration } -{ stream } `. "
171+ f"Removing parent field from schema for ES|QL validation."
172+ )
173+ delete_nested_key_from_dict (stream_mappings , field_name )
174+ nested_flattened_fields = find_flattened_fields_with_subfields (stream_mappings )
175+ for field in nested_flattened_fields :
176+ field_name = str (field ).split (".fields." )[0 ].replace ("." , ".properties." ) + ".fields"
177+ log (
178+ f"Warning: flattened field `{ field } ` found in `{ integration } -{ stream } ` with sub fields. "
179+ f"Removing parent field from schema for ES|QL validation."
180+ )
181+ delete_nested_key_from_dict (stream_mappings , field_name )
182+ return stream_mappings
183+
184+
162185def prepare_integration_mappings ( # noqa: PLR0913
163186 rule_integrations : list [str ],
164187 event_dataset_integrations : list [EventDataset ],
@@ -199,22 +222,7 @@ def prepare_integration_mappings( # noqa: PLR0913
199222 for stream in package_schema :
200223 flat_schema = package_schema [stream ]
201224 stream_mappings = flat_schema_to_index_mapping (flat_schema )
202- nested_multifields = find_nested_multifields (stream_mappings )
203- for field in nested_multifields :
204- field_name = str (field ).split (".fields." )[0 ].replace ("." , ".properties." ) + ".fields"
205- log (
206- f"Warning: Nested multi-field `{ field } ` found in `{ integration } -{ stream } `. "
207- f"Removing parent field from schema for ES|QL validation."
208- )
209- delete_nested_key_from_dict (stream_mappings , field_name )
210- nested_flattened_fields = find_flattened_fields_with_subfields (stream_mappings )
211- for field in nested_flattened_fields :
212- field_name = str (field ).split (".fields." )[0 ].replace ("." , ".properties." ) + ".fields"
213- log (
214- f"Warning: flattened field `{ field } ` found in `{ integration } -{ stream } ` with sub fields. "
215- f"Removing parent field from schema for ES|QL validation."
216- )
217- delete_nested_key_from_dict (stream_mappings , field_name )
225+ stream_mappings = prune_mappings_of_unsupported_types (integration , stream , stream_mappings , log )
218226 utils .combine_dicts (integration_mappings , deepcopy (stream_mappings ))
219227 index_lookup [f"{ integration } -{ stream } " ] = stream_mappings
220228
@@ -285,14 +293,19 @@ def get_filtered_index_schema(
285293 filtered_index_lookup = {
286294 key .replace ("logs-endpoint." , "logs-endpoint.events." ): value for key , value in filtered_index_lookup .items ()
287295 }
288- filtered_index_lookup .update (non_ecs_mapping )
289- filtered_index_lookup .update (custom_mapping )
290296
291297 # Reduce the combined mappings to only the matched indices (local schema validation source of truth)
298+ # Custom and non-ecs mappings are filtered before being sent to this function in prepare mappings
292299 combined_mappings : dict [str , Any ] = {}
293300 utils .combine_dicts (combined_mappings , deepcopy (ecs_schema ))
294301 for match in matches :
295- utils .combine_dicts (combined_mappings , deepcopy (filtered_index_lookup .get (match , {})))
302+ base = filtered_index_lookup .get (match , {})
303+ # Update filtered index with non-ecs and custom mappings
304+ # Need to use a merge here to not overwrite existing fields
305+ utils .combine_dicts (base , deepcopy (non_ecs_mapping .get (match , {})))
306+ utils .combine_dicts (base , deepcopy (custom_mapping .get (match , {})))
307+ filtered_index_lookup [match ] = base
308+ utils .combine_dicts (combined_mappings , deepcopy (base ))
296309
297310 # Reduce the index lookup to only the matched indices (remote/Kibana schema validation source of truth)
298311 filtered_index_mapping : dict [str , Any ] = {}
@@ -458,20 +471,34 @@ def prepare_mappings( # noqa: PLR0913
458471 index_lookup .update (integration_index_lookup )
459472
460473 # Load non-ecs schema and convert to index mapping format (nested schema)
474+ # For non_ecs we need both a mapping and a schema as custom schemas can override non-ecs fields
475+ # In these cases we need to accept the overwrite keep the original non-ecs field in the schema
476+ non_ecs_schema : dict [str , Any ] = {}
461477 non_ecs_mapping : dict [str , Any ] = {}
462478 non_ecs = ecs .get_non_ecs_schema ()
463479 for index in indices :
464- non_ecs_mapping .update (non_ecs .get (index , {}))
465- non_ecs_mapping = ecs .flatten (non_ecs_mapping )
466- non_ecs_mapping = utils .convert_to_nested_schema (non_ecs_mapping )
480+ index_mapping = non_ecs .get (index , {})
481+ non_ecs_schema .update (index_mapping )
482+ index_mapping = ecs .flatten (index_mapping )
483+ index_mapping = utils .convert_to_nested_schema (index_mapping )
484+ non_ecs_mapping .update ({index : index_mapping })
485+
486+ # These need to be handled separately as we need to be able to validate non-ecs fields as a whole
487+ # and also at a per index level as custom schemas can override non-ecs fields and/or indices
488+ non_ecs_schema = ecs .flatten (non_ecs_schema )
489+ non_ecs_schema = utils .convert_to_nested_schema (non_ecs_schema )
490+ non_ecs_schema = prune_mappings_of_unsupported_types ("non-ecs" , "non-ecs" , non_ecs_schema , log )
491+ non_ecs_mapping = prune_mappings_of_unsupported_types ("non-ecs" , "non-ecs" , non_ecs_mapping , log )
467492
468493 # Load custom schema and convert to index mapping format (nested schema)
469494 custom_mapping : dict [str , Any ] = {}
470495 custom_indices = ecs .get_custom_schemas ()
471496 for index in indices :
472- custom_mapping .update (custom_indices .get (index , {}))
473- custom_mapping = ecs .flatten (custom_mapping )
474- custom_mapping = utils .convert_to_nested_schema (custom_mapping )
497+ index_mapping = custom_indices .get (index , {})
498+ index_mapping = ecs .flatten (index_mapping )
499+ index_mapping = utils .convert_to_nested_schema (index_mapping )
500+ custom_mapping .update ({index : index_mapping })
501+ custom_mapping = prune_mappings_of_unsupported_types ("custom" , "custom" , custom_mapping , log )
475502
476503 # Load ECS in an index mapping format (nested schema)
477504 current_version = Version .parse (load_current_package_version (), optional_minor_and_patch = True )
@@ -484,8 +511,9 @@ def prepare_mappings( # noqa: PLR0913
484511
485512 index_lookup .update ({"rule-ecs-index" : ecs_schema })
486513
487- if (not integration_mappings or existing_mappings ) and not non_ecs_mapping and not ecs_schema :
514+ if (not integration_mappings or existing_mappings ) and not non_ecs_schema and not ecs_schema :
488515 raise ValueError ("No mappings found" )
489- index_lookup .update ({"rule-non-ecs-index" : non_ecs_mapping })
516+ index_lookup .update ({"rule-non-ecs-index" : non_ecs_schema })
517+ utils .combine_dicts (combined_mappings , deepcopy (non_ecs_schema ))
490518
491519 return existing_mappings , index_lookup , combined_mappings
0 commit comments