Wazuh Indexer API - Querying and Managing Data

Wazuh Indexer is built on OpenSearch and provides a REST API for searching, analyzing, and managing security data. Through the API you can execute DSL queries against alerts, build aggregations for analytical reports, use PPL and SQL for interactive analysis, and manage index templates and field mappings. Understanding the Indexer API is essential for building custom dashboards, automation workflows, and integrating Wazuh with external systems.

Wazuh Index Patterns

Wazuh Indexer stores data across several index types, each serving a distinct purpose with its own rotation schedule.

Core indices

IndexPurposeRotation
wazuh-alerts-*Alerts generated when detection rules fireDaily
wazuh-archives-*All events, including those that did not trigger alertsDaily
wazuh-monitoring-*Agent connection status historyWeekly
wazuh-statistics-*Wazuh server performance metricsWeekly

State indices

Starting with Wazuh 4.14, inventory and vulnerability data is stored in dedicated state indices:

IndexContents
wazuh-states-vulnerabilities-*Detected vulnerabilities and severity ratings
wazuh-states-inventory-packages-*Installed software packages
wazuh-states-inventory-processes-*Running processes
wazuh-states-inventory-ports-*Open network ports
wazuh-states-inventory-hardware-*CPU, memory, hardware components
wazuh-states-inventory-system-*OS, architecture, hostname
wazuh-states-inventory-networks-*IPv4/IPv6 network addresses

Listing indices

List all Wazuh indices:

curl -sk -u admin:$PASSWORD \
  "https://localhost:9200/_cat/indices/wazuh-*?v&s=index"

Check the size of a specific index:

curl -sk -u admin:$PASSWORD \
  "https://localhost:9200/_cat/indices/wazuh-alerts-*?v&h=index,docs.count,store.size&s=index"

Searching Alerts with DSL Queries

OpenSearch Query DSL (Domain Specific Language) is the primary query language for searching data in Wazuh Indexer.

Basic search (match)

Search alerts by rule description:

GET wazuh-alerts-*/_search
{
  "size": 10,
  "query": {
    "match": {
      "rule.description": "authentication failure"
    }
  },
  "sort": [
    { "timestamp": { "order": "desc" } }
  ]
}

Exact match (term)

Search alerts of a specific level:

GET wazuh-alerts-*/_search
{
  "query": {
    "term": {
      "rule.level": 12
    }
  }
}

Compound query (bool)

Combine conditions using must, should, must_not, and filter:

GET wazuh-alerts-*/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "rule.groups": "authentication_failed" } }
      ],
      "filter": [
        { "range": { "timestamp": { "gte": "now-24h" } } },
        { "term": { "agent.name": "web-server-01" } }
      ],
      "must_not": [
        { "term": { "rule.level": 3 } }
      ]
    }
  },
  "sort": [{ "timestamp": "desc" }],
  "size": 50
}

Range query

Search alerts within a specific time period:

GET wazuh-alerts-*/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "range": {
            "timestamp": {
              "gte": "2025-01-01T00:00:00",
              "lte": "2025-01-31T23:59:59",
              "format": "yyyy-MM-dd'T'HH:mm:ss"
            }
          }
        },
        {
          "range": {
            "rule.level": { "gte": 10 }
          }
        }
      ]
    }
  }
}

Searching nested fields

Wazuh stores MITRE ATT&CK data in structured fields:

GET wazuh-alerts-*/_search
{
  "query": {
    "bool": {
      "must": [
        { "term": { "rule.mitre.id": "T1110" } },
        { "term": { "rule.mitre.tactic": "Credential Access" } }
      ]
    }
  },
  "_source": ["timestamp", "rule.description", "rule.level", "agent.name"]
}

Wildcard and regex

Search using wildcard patterns:

GET wazuh-alerts-*/_search
{
  "query": {
    "wildcard": {
      "data.srcip": "192.168.1.*"
    }
  }
}

Search using regular expressions:

GET wazuh-alerts-*/_search
{
  "query": {
    "regexp": {
      "data.url": ".*\\.(php|asp|jsp)\\?.*id=.*"
    }
  }
}

Aggregations

Aggregations provide statistics and analytical reports over Wazuh data without retrieving individual documents.

Terms - top values

Top 10 most frequently triggered rules:

GET wazuh-alerts-*/_search
{
  "size": 0,
  "query": {
    "range": { "timestamp": { "gte": "now-7d" } }
  },
  "aggs": {
    "top_rules": {
      "terms": {
        "field": "rule.id",
        "size": 10,
        "order": { "_count": "desc" }
      },
      "aggs": {
        "rule_description": {
          "terms": { "field": "rule.description.keyword", "size": 1 }
        }
      }
    }
  }
}

Date histogram - timeline

Alert count per hour over the last 24 hours:

GET wazuh-alerts-*/_search
{
  "size": 0,
  "query": {
    "range": { "timestamp": { "gte": "now-24h" } }
  },
  "aggs": {
    "alerts_over_time": {
      "date_histogram": {
        "field": "timestamp",
        "fixed_interval": "1h"
      },
      "aggs": {
        "by_level": {
          "range": {
            "field": "rule.level",
            "ranges": [
              { "key": "low", "from": 0, "to": 7 },
              { "key": "medium", "from": 7, "to": 12 },
              { "key": "critical", "from": 12, "to": 16 }
            ]
          }
        }
      }
    }
  }
}

Cardinality - unique values

Count of unique agents, IP addresses, and rules:

GET wazuh-alerts-*/_search
{
  "size": 0,
  "query": {
    "range": { "timestamp": { "gte": "now-24h" } }
  },
  "aggs": {
    "unique_agents": {
      "cardinality": { "field": "agent.id" }
    },
    "unique_source_ips": {
      "cardinality": { "field": "data.srcip" }
    },
    "unique_rules": {
      "cardinality": { "field": "rule.id" }
    }
  }
}

Nested aggregations

Top 5 agents broken down by MITRE ATT&CK tactics:

GET wazuh-alerts-*/_search
{
  "size": 0,
  "aggs": {
    "by_agent": {
      "terms": { "field": "agent.name", "size": 5 },
      "aggs": {
        "by_tactic": {
          "terms": { "field": "rule.mitre.tactic", "size": 5 }
        },
        "max_level": {
          "max": { "field": "rule.level" }
        }
      }
    }
  }
}

Index Templates and Field Mappings

Viewing the current template

curl -sk -u admin:$PASSWORD \
  "https://localhost:9200/_template/wazuh?pretty"

Key Wazuh alert fields

FieldTypeDescription
timestampdateEvent timestamp
rule.idkeywordRule identifier
rule.levelintegerSeverity level (0-15)
rule.descriptiontext/keywordRule description
rule.groupskeywordRule groups
rule.mitre.idkeywordMITRE ATT&CK technique ID
rule.mitre.tactickeywordMITRE ATT&CK tactic
agent.idkeywordAgent identifier
agent.namekeywordAgent name
agent.ipipAgent IP address
data.srcipipSource IP address
data.dstipipDestination IP address
data.srcportintegerSource port
data.dstportintegerDestination port
locationkeywordLog source
full_logtextFull log message

Updating the template

To add a custom index pattern:

# Download the current template
curl -so template.json \
  "https://raw.githubusercontent.com/wazuh/wazuh/v4.14.4/extensions/elasticsearch/7.x/wazuh-template.json"

# Add a custom pattern to index_patterns
# Upload the updated template
curl -sk -u admin:$PASSWORD \
  -XPUT "https://localhost:9200/_template/wazuh-custom" \
  -H "Content-Type: application/json" \
  -d @template.json

Mapping conflicts

When a field has different types across indices, a mapping conflict occurs. To diagnose:

curl -sk -u admin:$PASSWORD \
  "https://localhost:9200/wazuh-alerts-*/_mapping/field/data.srcip?pretty"

Resolution: the field is defined with different types in different indices. Update the template and reindex the data, or use the keyword suffix:

{ "term": { "rule.description.keyword": "Authentication failure" } }

Dev Tools Console in Dashboard

Wazuh Dashboard includes the Dev Tools console (inherited from OpenSearch Dashboards) that allows executing API requests without curl.

Accessing Dev Tools

  1. Open Wazuh Dashboard
  2. Navigate to OpenSearch Dashboards - Dev Tools
  3. Or use the URL: https://<dashboard>:443/app/dev_tools#/console

Usage examples

In the Dev Tools console, you do not need to specify the URL or authentication:

# Check cluster health
GET _cluster/health

# Search for recent critical alerts
GET wazuh-alerts-*/_search
{
  "size": 5,
  "query": {
    "bool": {
      "filter": [
        { "range": { "rule.level": { "gte": 12 } } },
        { "range": { "timestamp": { "gte": "now-1h" } } }
      ]
    }
  },
  "sort": [{ "timestamp": "desc" }]
}

# Agent statistics
GET wazuh-alerts-*/_search
{
  "size": 0,
  "aggs": {
    "agents": {
      "terms": { "field": "agent.name", "size": 20 }
    }
  }
}

Autocompletion

Dev Tools supports autocompletion for index names, fields, and DSL keywords. Use Ctrl+Space to trigger suggestions.

PPL - Piped Processing Language

PPL provides a Unix-pipe-like syntax for querying data. It is convenient for analysts familiar with Splunk SPL.

Basic syntax

source = wazuh-alerts-* | where rule.level >= 10 | sort - timestamp | head 20

Filtering and aggregation

source = wazuh-alerts-*
| where timestamp > '2025-01-01 00:00:00'
| where rule.level >= 7
| stats count() as alert_count by rule.id, rule.description
| sort - alert_count
| head 10

Executing PPL via API

curl -sk -u admin:$PASSWORD \
  -XPOST "https://localhost:9200/_plugins/_ppl" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "source = wazuh-alerts-* | where rule.level >= 12 | stats count() by agent.name | sort - count()"
  }'

Time filters in PPL

source = wazuh-alerts-*
| where timestamp > DATE_SUB(NOW(), INTERVAL 24 HOUR)
| stats count() as alerts by span(timestamp, 1h) as hour
| sort hour

Grouping with multiple metrics

source = wazuh-alerts-*
| where timestamp > DATE_SUB(NOW(), INTERVAL 7 DAY)
| stats count() as total, max(rule.level) as max_level, dc(agent.id) as agents
  by rule.mitre.tactic
| sort - total

SQL Plugin

The OpenSearch SQL plugin allows using familiar SQL syntax to query Wazuh data.

Basic queries

SELECT timestamp, rule.id, rule.level, rule.description, agent.name
FROM wazuh-alerts-*
WHERE rule.level >= 10
  AND timestamp > NOW() - INTERVAL 24 HOUR
ORDER BY timestamp DESC
LIMIT 50

SQL aggregations

SELECT rule.id, rule.description, COUNT(*) as count
FROM wazuh-alerts-*
WHERE timestamp > NOW() - INTERVAL 7 DAY
GROUP BY rule.id, rule.description
ORDER BY count DESC
LIMIT 10

Executing SQL via API

curl -sk -u admin:$PASSWORD \
  -XPOST "https://localhost:9200/_plugins/_sql" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "SELECT agent.name, COUNT(*) as alerts FROM wazuh-alerts-* WHERE rule.level >= 7 GROUP BY agent.name ORDER BY alerts DESC"
  }'

Translating SQL to DSL

Useful for debugging - convert SQL to the equivalent DSL query:

curl -sk -u admin:$PASSWORD \
  -XPOST "https://localhost:9200/_plugins/_sql/_explain" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "SELECT * FROM wazuh-alerts-* WHERE rule.level >= 12 LIMIT 10"
  }'

Bulk Operations

Bulk operations are useful for data management and automation workflows.

Bulk search (msearch)

Execute multiple queries in a single call:

curl -sk -u admin:$PASSWORD \
  -XPOST "https://localhost:9200/_msearch" \
  -H "Content-Type: application/x-ndjson" \
  -d '
{"index":"wazuh-alerts-*"}
{"size":0,"query":{"range":{"rule.level":{"gte":12}}},"aggs":{"count":{"value_count":{"field":"_id"}}}}
{"index":"wazuh-alerts-*"}
{"size":0,"query":{"range":{"timestamp":{"gte":"now-1h"}}},"aggs":{"count":{"value_count":{"field":"_id"}}}}
'

Scroll API for large result sets

When you need to retrieve more than 10,000 documents, use the Scroll API:

# Initialize scroll
curl -sk -u admin:$PASSWORD \
  -XPOST "https://localhost:9200/wazuh-alerts-*/_search?scroll=5m" \
  -H "Content-Type: application/json" \
  -d '{
    "size": 1000,
    "query": { "range": { "timestamp": { "gte": "now-30d" } } },
    "sort": [{ "timestamp": "asc" }]
  }'

# Continue scrolling (use _scroll_id from the response)
curl -sk -u admin:$PASSWORD \
  -XPOST "https://localhost:9200/_search/scroll" \
  -H "Content-Type: application/json" \
  -d '{
    "scroll": "5m",
    "scroll_id": "<SCROLL_ID>"
  }'

Delete by query

Remove old low-level alerts:

curl -sk -u admin:$PASSWORD \
  -XPOST "https://localhost:9200/wazuh-alerts-*/_delete_by_query" \
  -H "Content-Type: application/json" \
  -d '{
    "query": {
      "bool": {
        "filter": [
          { "range": { "timestamp": { "lte": "now-90d" } } },
          { "range": { "rule.level": { "lte": 3 } } }
        ]
      }
    }
  }'

Comparison with Other SIEM Platforms

Elasticsearch Query DSL

Wazuh Indexer (OpenSearch) uses syntax fully compatible with Elasticsearch 7.x Query DSL. Key differences:

FeatureWazuh Indexer (OpenSearch)Elasticsearch
Core DSLIdentical syntaxIdentical syntax
SQL Plugin_plugins/_sql_sql (X-Pack)
PPL_plugins/_pplNot available
Alerting_plugins/_alertingX-Pack Watcher
SecuritySecurity Plugin (built-in)X-Pack Security (paid)

Migrating queries from Elasticsearch to Wazuh Indexer requires no changes to DSL syntax.

Splunk SPL

TaskWazuh PPLSplunk SPL
Searchsource = index | where field = 'value'index=main field=value
Aggregationstats count() by fieldstats count by field
Sortingsort - fieldsort - field
Limithead Nhead N
Time filterwhere timestamp > ...earliest=-24h

PPL in OpenSearch is closest to SPL in syntax, making migration easier for Splunk analysts.

QRadar AQL

TaskWazuh SQLQRadar AQL
Event searchSELECT * FROM wazuh-alerts-*SELECT * FROM events
FilteringWHERE rule.level >= 10WHERE severity >= 7
AggregationGROUP BY rule.idGROUP BY qid
Time filterWHERE timestamp > NOW() - INTERVAL 1 HOURWHERE starttime > NOW - 1 HOURS

SQL syntax in OpenSearch is close to standard SQL, making adaptation straightforward for QRadar users.

Troubleshooting

Slow queries

Symptom: queries take longer than 10 seconds to execute.

Diagnostics:

# Check cluster load
curl -sk -u admin:$PASSWORD "https://localhost:9200/_nodes/stats/os,jvm?pretty"

# Enable slow query logging
curl -sk -u admin:$PASSWORD \
  -XPUT "https://localhost:9200/wazuh-alerts-*/_settings" \
  -H "Content-Type: application/json" \
  -d '{
    "index.search.slowlog.threshold.query.warn": "5s",
    "index.search.slowlog.threshold.query.info": "2s"
  }'

# View slow log indices
curl -sk -u admin:$PASSWORD \
  "https://localhost:9200/_cat/indices/.opendistro-slow-log-*?v"

Solutions:

  • Use filter instead of must for conditions that do not require relevance scoring
  • Limit the time range of your query - avoid querying all historical data
  • Add "size": 0 when you only need aggregations
  • Avoid wildcard and regexp on fields without keyword mapping
  • Increase JVM heap if garbage collection runs too frequently

Field mapping conflicts

Symptom: illegal_argument_exception error when running a query.

# Check the mapping of a specific field
curl -sk -u admin:$PASSWORD \
  "https://localhost:9200/wazuh-alerts-*/_mapping/field/data.srcip?pretty"

Resolution: the field is defined with different types across indices. Update the template and reindex the data, or use the keyword suffix:

{ "term": { "rule.description.keyword": "Authentication failure" } }

Index not found (index_not_found_exception)

Symptom: no such index [wazuh-alerts-*] error.

Diagnostics:

# Check existing indices
curl -sk -u admin:$PASSWORD "https://localhost:9200/_cat/indices/wazuh-*?v"

# Check aliases
curl -sk -u admin:$PASSWORD "https://localhost:9200/_cat/aliases/wazuh-*?v"

# Verify that Filebeat is sending data
curl -sk -u admin:$PASSWORD \
  "https://localhost:9200/_cat/indices/wazuh-alerts-*?v&s=index:desc&h=index,docs.count,store.size"

Solutions:

  • Verify that Filebeat is running and connected to the Indexer
  • Check Filebeat configuration: filebeat.yml must contain the correct output.elasticsearch.hosts
  • Verify the index template: _template/wazuh
  • For archive data, ensure logall_json is enabled in ossec.conf

Result window too large

Symptom: Result window is too large error when querying with "from": 10000.

OpenSearch limits from + size to 10,000 by default. Solutions:

  • Use the Scroll API for full data export
  • Use search_after for pagination
  • For analytics, use aggregations instead of fetching individual documents
GET wazuh-alerts-*/_search
{
  "size": 100,
  "query": { "match_all": {} },
  "sort": [{ "timestamp": "desc" }, { "_id": "asc" }],
  "search_after": ["2025-01-15T10:30:00.000Z", "abc123"]
}

Additional Resources

Last updated on