Skip to content

Commit df7a657

Browse files
Support custom schema index overwrite
1 parent f6fc1c9 commit df7a657

File tree

1 file changed

+37
-21
lines changed

1 file changed

+37
-21
lines changed

detection_rules/index_mappings.py

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
162185
def 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,17 +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-
# This overwrites any conflicts with non-ecs preferring what is defined in custom mappings
289-
# This can be done safely as we have a specific non-ecs-index that will also be included with only non-ecs mappings
290-
filtered_index_lookup.update(non_ecs_mapping)
291-
filtered_index_lookup.update(custom_mapping)
292296

293297
# Reduce the combined mappings to only the matched indices (local schema validation source of truth)
294298
# Custom and non-ecs mappings are filtered before being sent to this function in prepare mappings
295299
combined_mappings: dict[str, Any] = {}
296300
utils.combine_dicts(combined_mappings, deepcopy(ecs_schema))
297301
for match in matches:
298-
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 user 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))
299309

300310
# Reduce the index lookup to only the matched indices (remote/Kibana schema validation source of truth)
301311
filtered_index_mapping: dict[str, Any] = {}
@@ -473,8 +483,12 @@ def prepare_mappings( # noqa: PLR0913
473483
index_mapping = utils.convert_to_nested_schema(index_mapping)
474484
non_ecs_mapping.update({index: index_mapping})
475485

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
476488
non_ecs_schema = ecs.flatten(non_ecs_schema)
477489
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)
478492

479493
# Load custom schema and convert to index mapping format (nested schema)
480494
custom_mapping: dict[str, Any] = {}
@@ -484,6 +498,7 @@ def prepare_mappings( # noqa: PLR0913
484498
index_mapping = ecs.flatten(index_mapping)
485499
index_mapping = utils.convert_to_nested_schema(index_mapping)
486500
custom_mapping.update({index: index_mapping})
501+
custom_mapping = prune_mappings_of_unsupported_types("custom", "custom", custom_mapping, log)
487502

488503
# Load ECS in an index mapping format (nested schema)
489504
current_version = Version.parse(load_current_package_version(), optional_minor_and_patch=True)
@@ -499,5 +514,6 @@ def prepare_mappings( # noqa: PLR0913
499514
if (not integration_mappings or existing_mappings) and not non_ecs_schema and not ecs_schema:
500515
raise ValueError("No mappings found")
501516
index_lookup.update({"rule-non-ecs-index": non_ecs_schema})
517+
utils.combine_dicts(combined_mappings, deepcopy(non_ecs_schema))
502518

503519
return existing_mappings, index_lookup, combined_mappings

0 commit comments

Comments
 (0)