This note comes from a question on the OpenSearch forum: a Windows rule for failed logons (EventID|all: '4625') with a threshold per destination host would not save. We created the same rule on stock OpenSearch 3.8.0 and answered three questions in order: is the rule wrong, does a threshold rule fire, and what to use if it does not do what you mean.
How to check yours
All three are read-only. Set these first:
IDX=http://localhost:9200 # your cluster. Ours had the security plugin off: plain HTTP, no login.
CURL="curl -s" # with security on: CURL="curl -sk -u admin:YOUR_PASSWORD" and IDX=https://...
1. Does the rule validate against your index?
$CURL -XPOST "$IDX/_plugins/_security_analytics/rules/validate" -H 'Content-Type: application/json' \
-d '{"index_name":"YOUR-INDEX","rules":["RULE_ID"]}'
In our test this returned {}. The same request without "index_name" returned the error from the forum, word for word.
2. Are your fields mapped for Security Analytics?
$CURL "$IDX/_plugins/_security_analytics/mappings/view?index_name=YOUR-INDEX&rule_topic=windows" | python3 -m json.tool | head -40
Look at unmapped_index_fields. Without a mapping for the rule's fields, our detector created no monitor and produced no finding at all (details below).
3. Is the same finding coming back every run?
$CURL -XGET "$IDX/_plugins/_security_analytics/findings/_search?detector_id=DETECTOR_ID&size=100" | python3 -c '
import json,sys
for f in json.load(sys.stdin).get("findings",[]):
print(f["timestamp"], len(f.get("related_doc_ids",[])), sorted(f.get("related_doc_ids",[]))[:3])'
If you see a new finding each run with the same document ids, you are looking at the time-window problem below, not at new attacks.
1. Is the rule wrong? No.
We created the rule through POST /_plugins/_security_analytics/rules?category=windows with this body, and it was accepted:
title: Repeated failed logons to one host
id: 5f0e3a3e-1111-4c2b-9b1a-000000028362
status: test
description: lab reproduction of forum t/28362
author: lab
date: 2026/10/28
logsource:
product: windows
detection:
selection:
EventID|all:
- '4625'
condition: selection | count(*) by dst_endpoint.hostname > 5
level: informational
- We first created it with a plain
EventID: 4625selection, then changed it toEventID|all: - '4625'as in the forum post, withPUT /_plugins/_security_analytics/rules/RULE_ID?category=windows. Both were accepted. - Once a detector used the rule,
PUTwas refused withRule with id … is actively used by detectors. Update can be forced by setting forced flag to true. Adding&forced=trueworked.
So changing dst_endpoint.hostname to other fields was changing the wrong thing.
Where “index_name is missing” comes from
We sent POST /_plugins/_security_analytics/rules/validate with only the rule id:
{"rules":["RULE_ID"]}
→ 400 {"type":"action_request_validation_exception","reason":"Validation Failed: 1: index_name is missing;"}
{"index_name":"winlog-28362","rules":["RULE_ID"]}
→ {}
That is the exact text from the forum. In the plugin, the string index_name appears only in this validate request and in the field-mapping APIs. We did not measure which API the Dashboards rule editor calls, because our test had no Dashboards. What we can say is that the API takes the rule, and that the error is what the validate API returns when it is not told which index to check against. If the editor gives you no way to pick an index, creating the rule through the API, as above, is a way round it.
2. Does a threshold rule fire? Yes, but read this first.
It needs field mappings first
Our first detector on a plain index created no monitor and produced no finding. Security Analytics reads the Windows rule's EventID as winlog.event_id, so the index needs an alias. We created the index with EventID (integer), dst_endpoint.hostname (keyword) and @timestamp, then:
$CURL -XPOST "$IDX/_plugins/_security_analytics/mappings" -H 'Content-Type: application/json' -d '{
"index_name": "winlog-28362",
"rule_topic": "windows",
"partial": true,
"alias_mappings": { "properties": { "winlog.event_id": { "type": "alias", "path": "EventID" } } }
}'
Then the detector, with the custom rule, a 1-minute schedule and no triggers. If your index already has a real winlog.event_id field, the alias fails: on an earlier index of ours the mapping call returned mapper [winlog.event_id] cannot be changed from type [integer] to [FieldAliasMapper].
The threshold fired on the right host
At 05:09 UTC we wrote 6 events with EventID 4625 for host dc01 and 3 for ws07. The first finding came at 05:10:20: 6 documents, all dc01. ws07, with 3, produced no finding. So the count(*) by … > 5 part works.
But the count has no time window
We wrote no more events. The detector kept producing a new finding for the same 6 documents on every run, once a minute:
| finding time (UTC) | documents | host |
|---|---|---|
| 05:10:20 | 6 | dc01 |
| 05:11:20 | 6 | dc01 |
| 05:12:20 | 6 | dc01 |
| … one every minute … | 6 | dc01 |
| total at 06:16:51 | 67 findings, none from new events | |
The reason is in the monitor Security Analytics generated for the rule. Its search, read back from the Alerting API, has the event-id query and the per-host aggregation, and no @timestamp range:
"query": { "query_string": { "query": "winlog.event_id: \"4625\"" } },
"aggregations": { "result_agg": { "terms": { "field": "dst_endpoint.hostname", "size": 10 } } }
trigger: params._cnt > 5.0
- Every run counts the whole index. Six failed logons from last week count the same as six in the last minute, and once a host passes the threshold it stays past it.
- Sigma
timeframedid not help. We addedtimeframe: 5mto the rule and created a new detector: the generated search had the same query and aggregation, still with no time range (only the quoting of4625differed). - Only 10 hosts per run. The terms aggregation has
"size": 10. We read that from the configuration; we did not test more than 10 hosts.
Our detector had no triggers, so we counted findings, not alerts or notifications. We did not test what a detector trigger sends when the same finding comes back every minute.
3. What to use for “more than 5 in 5 minutes” (tested)
An Alerting bucket-level monitor, where you write the time range yourself: event id 4625, @timestamp from 5 minutes before the run, grouped by dst_endpoint.hostname, trigger _count > 5, run every minute.
This is the body we sent, with only the index name replaced. It has no actions: we read the result from the alert list, not from a notification. Add your own action, and set its execution policy as described in the notes linked below.
$CURL -XPOST "$IDX/_plugins/_alerting/monitors" -H 'Content-Type: application/json' -d '{
"type": "monitor",
"monitor_type": "bucket_level_monitor",
"name": "bf-4625-by-host-5m",
"enabled": true,
"schedule": {
"period": {
"interval": 1,
"unit": "MINUTES"
}
},
"inputs": [
{
"search": {
"indices": [
"YOUR-INDEX"
],
"query": {
"size": 0,
"query": {
"bool": {
"filter": [
{
"term": {
"EventID": 4625
}
},
{
"range": {
"@timestamp": {
"gte": "{{period_end}}||-5m",
"lte": "{{period_end}}",
"format": "epoch_millis"
}
}
}
]
}
},
"aggregations": {
"by_host": {
"terms": {
"field": "dst_endpoint.hostname",
"size": 100
}
}
}
}
}
}
],
"triggers": [
{
"bucket_level_trigger": {
"name": "gt-5",
"severity": "3",
"condition": {
"buckets_path": {
"_count": "_count"
},
"parent_bucket_path": "by_host",
"script": {
"source": "params._count > 5",
"lang": "painless"
}
},
"actions": []
}
}
]
}'
| check | events in the index | result |
|---|---|---|
| 06:18:26 UTC | only the 6 dc01 events from 05:09, outside the 5-minute window | 0 alerts |
| 06:19:43 UTC | plus 6 new dc01 and 3 new ws07 written at 06:18:27 | 1 alert, ACTIVE, bucket dc01, 6 documents; nothing for ws07 |
Old events stopped counting once they were outside the window, which is the behaviour the Sigma rule did not give us. To keep one notification per burst instead of one per run, see repeated notifications for one burst; for one notification per matching line, see one notification per matching document.
Limits of what we measured
Stock OpenSearch 3.8.0 (opensearchproject/opensearch:3.8.0, Security Analytics 3.8.0.0), one node, security plugin off, plain HTTP, in our test container, 28 October 2026. No OpenSearch Dashboards: everything went through the REST API, so we cannot say what the Dashboards rule editor sends. One rule shape (one event id, one group-by field), synthetic events with three fields, 9 events and then 9 more. The detector had no triggers; we counted findings, not alerts or notifications. We did not test more than 10 hosts, a cluster, the security plugin's permissions, prebuilt Windows log integrations, or Sigma correlation rules. The Security Analytics plugin bundled with the Wazuh 5.0.0-rc1 indexer did not accept POST /_plugins/_security_analytics/rules at all (no handler found), so none of this was measured there.