# Wazuh Indexer API - Querying and Managing Data

> Working with Wazuh Indexer API built on OpenSearch - index patterns, DSL queries, aggregations, PPL, SQL, index templates, and troubleshooting

Source: https://opennix.org/en/docs/wazuh/infrastructure/wazuh-indexer-api/


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

| Index | Purpose | Rotation |
|---|---|---|
| `wazuh-alerts-*` | Alerts generated when detection rules fire | Daily |
| `wazuh-archives-*` | All events, including those that did not trigger alerts | Daily |
| `wazuh-monitoring-*` | Agent connection status history | Weekly |
| `wazuh-statistics-*` | Wazuh server performance metrics | Weekly |

### State indices

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

| Index | Contents |
|---|---|
| `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:

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

Check the size of a specific index:

```bash
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:

```json
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:

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

### Compound query (bool)

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

```json
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:

```json
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:

```json
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:

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

Search using regular expressions:

```json
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:

```json
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:

```json
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:

```json
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:

```json
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

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

### Key Wazuh alert fields

| Field | Type | Description |
|---|---|---|
| `timestamp` | date | Event timestamp |
| `rule.id` | keyword | Rule identifier |
| `rule.level` | integer | Severity level (0-15) |
| `rule.description` | text/keyword | Rule description |
| `rule.groups` | keyword | Rule groups |
| `rule.mitre.id` | keyword | MITRE ATT&CK technique ID |
| `rule.mitre.tactic` | keyword | MITRE ATT&CK tactic |
| `agent.id` | keyword | Agent identifier |
| `agent.name` | keyword | Agent name |
| `agent.ip` | ip | Agent IP address |
| `data.srcip` | ip | Source IP address |
| `data.dstip` | ip | Destination IP address |
| `data.srcport` | integer | Source port |
| `data.dstport` | integer | Destination port |
| `location` | keyword | Log source |
| `full_log` | text | Full log message |

### Updating the template

To add a custom index pattern:

```bash
# 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:

```bash
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:

```json
{ "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

```bash
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

```sql
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

```sql
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

```bash
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:

```bash
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:

```bash
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:

```bash
# 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:

```bash
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:

| Feature | Wazuh Indexer (OpenSearch) | Elasticsearch |
|---|---|---|
| Core DSL | Identical syntax | Identical syntax |
| SQL Plugin | `_plugins/_sql` | `_sql` (X-Pack) |
| PPL | `_plugins/_ppl` | Not available |
| Alerting | `_plugins/_alerting` | X-Pack Watcher |
| Security | Security Plugin (built-in) | X-Pack Security (paid) |

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

### Splunk SPL

| Task | Wazuh PPL | Splunk SPL |
|---|---|---|
| Search | `source = index \| where field = 'value'` | `index=main field=value` |
| Aggregation | `stats count() by field` | `stats count by field` |
| Sorting | `sort - field` | `sort - field` |
| Limit | `head N` | `head N` |
| Time filter | `where timestamp > ...` | `earliest=-24h` |

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

### QRadar AQL

| Task | Wazuh SQL | QRadar AQL |
|---|---|---|
| Event search | `SELECT * FROM wazuh-alerts-*` | `SELECT * FROM events` |
| Filtering | `WHERE rule.level >= 10` | `WHERE severity >= 7` |
| Aggregation | `GROUP BY rule.id` | `GROUP BY qid` |
| Time filter | `WHERE timestamp > NOW() - INTERVAL 1 HOUR` | `WHERE 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:

```bash
# 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.

```bash
# 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:

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

### Index not found (index_not_found_exception)

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

Diagnostics:

```bash
# 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

```json
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

- [Wazuh Indexer Installation](/docs/wazuh/installation/wazuh-indexer-installation/) - deployment and configuration
- [Wazuh Dashboard Configuration](/docs/wazuh/infrastructure/wazuh-dashboard-configuration/) - data visualization and analysis
- [Wazuh Architecture](/docs/wazuh/getting-started/wazuh-architecture/) - platform component overview
- [Log Data Collection](/docs/wazuh/capabilities/wazuh-log-data-collection/) - configuring data sources

