Wazuh fix note · indexer

mapper_parsing_exception: the alert is in alerts.json but never reaches the dashboard

The indexer refused the document because one field arrived in a shape its mapping cannot hold: an object where the template says keyword, or a string where the mapping expects an object. The manager wrote the alert correctly. The loss happens after it, inside the indexer.

Dong Nguyen, ATK New Technology · 28 September 2026 · measured on Wazuh 4.14.7 unless stated otherwise


How to check in one minute

Take one alert that is in /var/ossec/logs/alerts/alerts.json but missing from the dashboard, and note the field paths it carries under data. Then ask the indexer how it mapped them. For Microsoft Graph sign-ins the two fields are:

curl -sk -u '<user>:<password>' \
  "https://<indexer>:9200/wazuh-alerts-*,wazuh-archives-*/_mapping/field/data.ms-graph.status,data.ms-graph.appliedConditionalAccessPolicies?pretty"

If a field comes back "type" : "keyword" and the missing alert has an object, or a list of objects, at that path, the indexer rejects it. For any other field, put its path in place of the two above.

If your Filebeat writes its own log, this counts which fields fail:

grep -o 'failed to parse field \[[^]]*\]' /var/log/filebeat/filebeat* | sort | uniq -c | sort -rn

We have not run a real Filebeat in our tests (see Limits), so we have not seen that log line ourselves. The mapping query does not depend on it.

Why it happens

The manager writes the alert to alerts.json. Filebeat then sends it through an ingest pipeline into an index whose mapping comes from the Wazuh index template. The mapping allows one type per field path. When a document carries a different shape at that path, the indexer answers with mapper_parsing_exception and does not store the document. Because the alert is already in alerts.json, everything on the manager side looks normal.

Two things make a clash likely:

Case 1: Microsoft Graph sign-ins, data.ms-graph.status

The template maps data.ms-graph.status and data.ms-graph.appliedConditionalAccessPolicies as keyword. We checked the template at release tags v4.8.0, v4.10.0, v4.12.0, v4.14.0, v4.14.3, v4.14.7 and v4.14.8: the same in all seven. Alerts and incidents send status as a string such as "resolved". Sign-ins send it as an object, and send the policies as a list of objects. On a test indexer every sign-in was rejected, in both arrival orders:

failed to parse field [data.ms-graph.status] of type [keyword] ... Can't get text on a START_OBJECT

The error names only the first field it trips on. A sign-in with a Conditional Access policy fails on appliedConditionalAccessPolicies; one with an empty policy list fails on status. When we fixed status alone, sign-ins that had a policy applied were still rejected.

A related detail: the stock 4.14.7 ruleset (0995-microsoft-graph_rules.xml, 99 rules) has no rule for relationship: signIns. A sign-in only matches 99500 at level 0, so it reaches the indexer through the archives, or through alerts once you write your own sign-in rule.

Case 2: a field that is sometimes an object, sometimes a string, data.data

Nextcloud events can carry data as an object or as an array. Our three test lines differed only in that, and all three matched rule 88207. With the stock template, where data.data is keyword, the object shape fails with failed to parse field [data.data] of type [keyword]. A separate template that makes data.data an object accepts those, and rejects the string shape instead. Changing the template alone only moves which shape fails.

Fix

Case 1: move the object shape to a new field, in the pipeline

Keep the string status of alerts and incidents where it is, because dashboards and saved searches expect it, and move only the sign-in shapes. One script processor, placed right after the json processor, in both the alerts and the archives pipeline:

{"script": {
  "lang": "painless",
  "ignore_failure": true,
  "description": "ms-graph signIns fix",
  "source": "def d = ctx.get('data'); if (!(d instanceof Map)) { return; } def g = d.get('ms-graph'); if (!(g instanceof Map)) { return; } if (g.get('status') instanceof Map) { g.put('signInStatus', g.remove('status')); } def p = g.get('appliedConditionalAccessPolicies'); if (p instanceof List && !p.isEmpty() && p.get(0) instanceof Map) { g.put('conditionalAccessPolicies', g.remove('appliedConditionalAccessPolicies')); }"
}}

What we measured: every sign-in and the alert indexed, in both orders, in a new index and in an index that already existed, so the fix works on today's index. data.ms-graph.status stays keyword and still aggregates. The complete script, with verify, apply and rollback, is in our write-up on dev.to.

Case 2: a separate template plus a rename by type

Two changes that only work together. First, a separate legacy template with a higher order that keeps the field an object:

PUT _template/fixpack-data-data
{"order": 1,
 "index_patterns": ["wazuh-alerts-4.x-*"],
 "mappings": {"properties": {"data": {"properties": {"data": {"type": "object"}}}}}}

Second, one rename processor right after the json processor, which moves the field only when it arrives as a string, so the new field only ever receives strings:

{"rename": {"field": "data.data", "target_field": "data.data_raw",
            "ignore_missing": true, "if": "ctx.data?.data instanceof String"}}

What we measured: every order of object, "[]" and "[{..}]" indexed cleanly, 4 of 4 in two separate orders. The template alone, the rename alone, and a rename by location each still failed. A template only applies when an index is created, so this fix takes effect on the next daily index. Note the side effect: the rename moves every string data.data, from every source, into data.data_raw, so searches and dashboards that read data.data as a string need to follow it.

Limits of what we measured

All of this was measured on a throwaway wazuh-indexer 4.14.7 with the stock template and the stock pipelines taken from the wazuh-manager 4.14.7 image. We sent events through the pipeline in the form Filebeat sends them, but not through a real Filebeat and not from a real manager. The sign-ins were built from the Graph v1.0 schema, not taken from a live tenant, and we have not checked fields that only the Graph beta API sends. We did not measure whether a patched pipeline survives a restart or an upgrade, or how any of this behaves on a multi-node cluster. The decoder behaviour was measured on two integrations. For the 21 scalar fields under data, we can say a clash is possible, not that it will happen. The fix template above covers wazuh-alerts-4.x-* only: we measured the alerts index, not the archives index, which the stock template also covers.

Free check

Check your whole manager for this and other silent rules: Rule Doctor Lite (free, read-only, one Python file).

Rule Doctor Lite reads rules and alerts on the manager; it does not read index mappings. For this error the mapping query above is the check. Lite shows you what else on the manager is silent.

curl -sLO https://github.com/xuxu298/rule-doctor-lite/releases/latest/download/rule-doctor-lite.py
sudo python3 rule-doctor-lite.py

Or have it fixed for you.