Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Wazuh Indexer Technical Documentation

This folder contains the technical documentation for the Wazuh Indexer. The documentation is organized into the following guides:

  • Development Guide: Instructions for building, testing, and packaging the Indexer.
  • Reference Manual: Detailed information on the Indexer’s architecture, configuration, and usage.

Requirements

To work with this documentation, you need mdBook installed.

ToolRequired Version
mdbook0.5.2
mdbook-mermaid0.17.0
  • Get the latest cargo (hit enter when prompted for a default install)

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    
  • Install mdbook and mdbook-mermaid

    cargo install mdbook --version 0.5.2 --locked
    cargo install mdbook-mermaid --version 0.17.0 --locked
    

Usage

  • To build the documentation, run:

    ./build.sh
    

    The output will be generated in the book directory.

  • To serve the documentation locally for preview, run:

    ./server.sh
    

    The documentation will be available at http://127.0.0.1:3000.

Development documentation

Under this section, you will find the development documentation of Wazuh Indexer. This documentation contains instructions to compile, run, test and package the source code. Moreover, you will find instructions to set up a development environment in order to get started at developing the Wazuh Indexer.

This documentation assumes basic knowledge of certain tools and technologies, such as Docker, Bash (Linux) or Git.

Before you start coding, read the sections below: they cover how to open good pull requests and how our GitHub Actions behave when you do. Getting this right up front saves CI minutes and review cycles for everyone.

Pull Requests

These are the standard procedures for creating, updating, and reviewing Pull Requests across the Wazuh Indexer repositories.

Lifecycle

┌──────────┐    ┌──────────────┐    ┌─────────────────┐    ┌───────┐
│  Draft   │───▶│ Local build  │───▶│ Ready for       │───▶│ Merge │
│  PR      │    │ & test       │    │ review (CI runs)│    │       │
└──────────┘    └──────────────┘    └─────────────────┘    └───────┘

Every Pull Request must start in Draft status. Workflows do not run on Draft PRs — this is enforced across all repositories to avoid wasting GitHub Actions minutes on work in progress — so use Draft status freely while iterating on your changes.

Before marking the PR as ready, build the project successfully and run the tests locally to verify they pass. This prevents avoidable CI failures that waste runner time and delay reviews. Once everything is complete and locally validated, click “Ready for review” and move the linked issue to Pending review. This is the moment workflows are triggered for the first time.

To address review feedback, push new commits on top of the branch and re-request review once you have resolved all comments. Avoid amending or rebasing published commits during review, and if CI fails after pushing, investigate and fix it before requesting re-review. When the PR is approved and CI passes, it can be merged. Use squash merge for single-purpose PRs to keep a clean history.

Body template

Every repository provides the template below as its default .github/pull_request_template.md; it is pre-filled automatically when you open a PR. Every PR must be linked to an existing issue, and PRs should not be linked directly to GitHub Projects — track project status at the issue level instead.

## Description

<!--
Provide a brief description of the problem this pull request addresses. Include relevant context to help reviewers understand the purpose and scope of the changes.

If this pull request resolves an existing issue, reference it here. For example:
Closes #<issue_number>
-->

## Proposed Changes

<!--
Summarize the changes made in this pull request. Include:
- Features added
- Bugs fixed
- Any relevant technical details
-->

### Results and Evidence

<!--
Provide evidence of the changes made, such as:
- Logs
- Screenshots
- Before/after comparisons
-->

### Artifacts Affected

<!--
List the artifacts impacted by this pull request, such as:
- Executables (specify platforms if applicable)
- Default configuration files
- Packages
-->

### Configuration Changes

<!--
If applicable, list any configuration changes introduced by this pull request, including:
- New configuration parameters
- Changes to default values
- Backward compatibility notes
-->

### Documentation Updates

<!--
If applicable, list the sections of documentation that have been updated as part of this pull request.
-->

### Tests Introduced

<!--
If applicable, describe any new unit or integration tests added as part of this pull request. Include:
- Scope of the tests
- Any relevant details about test coverage
-->

## Review Checklist

<!--
List any manual tests completed to verify the functionality of the changes. Include any manual tests that are still required for final approval.
-->

- [ ] Code changes reviewed
- [ ] Relevant evidence provided
- [ ] Tests cover the new functionality
- [ ] Configuration changes documented
- [ ] Developer documentation reflects the changes
- [ ] Meets requirements and/or definition of done
- [ ] No unresolved dependencies with other issues
- [ ] PR is linked to the relevant issue(s)
- [ ] Correct labels applied (e.g., `no-changelog`)
- [ ] ...

Always link the related issue with Resolves #<number> so it auto-closes on merge, and describe why rather than just what — the diff already shows what changed, so the description should explain the motivation.

Reviewing a PR

Start from the linked issue to understand the context and acceptance criteria, then read the description and checklist before reading the code. Focus your feedback on correctness, clarity, and maintainability, and use GitHub’s suggestion feature for small fixes to speed up the process. Approve only when you are confident the changes are correct and complete.

Changelog

Every PR is expected to include a changelog entry, classified as Added, Changed, Removed, or Fixed. The 5_codequality_changelog.yml workflow enforces this. Apply the no-changelog label to bypass the check when the linked issue belongs to a private repository, or when the PR genuinely does not require a changelog update.

Changelog entries must always reference the issue, not the pull request, so the entry stays meaningful independently of how the change was implemented. An issue only belongs in the changelog if it affects the product and represents a change from a previously released version — internal/CI changes are never included, and neither are fixes for problems introduced in an unpublished version.

Best practices

  • Keep PRs small and focused. One issue per PR whenever possible.
  • Write descriptive commit messages. They should explain why, not just what.
  • Do not trigger CI unnecessarily. Keep PRs in Draft until ready, and validate locally first.

Workflows and Actions

This section defines the naming conventions and operational rules for the GitHub Actions and Workflows used across the Wazuh Indexer repositories.

Naming convention

Both Actions and Workflows follow the same pattern:

<major>_<prefix>_<target>
ComponentDescription
MajorProduct major version (e.g. 4, 5).
PrefixCategory prefix from the use cases below.
TargetThe action target: a component, module, subsystem, tool, language, etc.

The prefix is drawn from the following set of use cases:

Use casePrefixTargetExample
Code analysis (static/dynamic)codeanalysisCode analysis tool4_codeanalysis_coverity
Linter / auto-docscodelinterLinter5_codelinter_clangformat
Code quality (groups codeanalysis + codelinter)codequalityRepository5_codequality_changelog
Unit teststestunitModule5_testunit_engine
Component teststestcomponentComponent/module5_testcomponent_indexerconnector
Integration teststestintegrationModule4_testintegration_cluster
Package builderbuilderpackageSubsystem4_builderpackage_server
Precompiled object builderbuilderprecompiledSubsystem5_builderprecompiled_agent
Version bumpingbumperRepository5_bumper_repository

For workflows triggered on PR or push events, append _onpush to the name to distinguish them from their workflow_dispatch counterparts:

5_builderpackage_indexer.yml          ← workflow_dispatch (manual)
5_builderpackage_indexer_onpush.yml   ← PR / push trigger (automatic)

When composing jobs from Actions, a single job step cannot mix Actions with different prefixes, and steps must use matrices whenever possible.

Runners

Two types of runners are available:

RunnerTypeUsage
DefaultGitHub-hostedAll workflows unless there is a justified reason to use the dedicated runner.
DedicatedSelf-hostedReserved for resource-intensive workflows only. Currently used exclusively by 5_builderpackage_indexer (the full package builder).

Always prefer the default runner. The dedicated runner is a shared, limited resource — use it only when the workflow genuinely requires the extra capacity (e.g. the full product builder).

Draft PR enforcement

All PR workflows must be configured to skip Draft PRs, so that no CI minutes are consumed on work-in-progress PRs. This is enforced by adding the following condition to every PR-triggered workflow:

on:
  pull_request:
    types: [opened, synchronize, ready_for_review]

jobs:
  <job_name>:
    if: ${{ !github.event.pull_request.draft }}

Set up the Development Environment

1. Git

Install and configure Git (SSH keys, commits and tags signing, user and email).

  1. Set your username.
  2. Set your email address.
  3. Generate an SSH key.
  4. Add the public key to your GitHub account for authentication and signing.
  5. Configure Git to sign commits with your SSH key.

2. Repositories

Clone the Wazuh Indexer repositories (use SSH). Before you start, you need to properly configure your working repositories to have origin and upstream remotes.

mkdir -p ~/wazuh && cd ~/wazuh

# Plugins (no upstream fork)
git clone git@github.com:wazuh/wazuh-indexer-plugins.git

# Indexer core (forked from OpenSearch)
git clone git@github.com:wazuh/wazuh-indexer.git
cd wazuh-indexer
git remote add upstream git@github.com:opensearch-project/opensearch.git
cd ..

# Reporting plugin (forked from OpenSearch)
git clone git@github.com:wazuh/wazuh-indexer-reporting.git
cd wazuh-indexer-reporting
git remote add upstream git@github.com:opensearch-project/reporting.git
cd ..

# Security Analytics (forked from OpenSearch)
git clone git@github.com:wazuh/wazuh-indexer-security-analytics.git
cd wazuh-indexer-security-analytics
git remote add upstream git@github.com:opensearch-project/security-analytics.git
cd ..

# Notifications plugin (forked from OpenSearch)
git clone git@github.com:wazuh/wazuh-indexer-notifications.git
cd wazuh-indexer-notifications
git remote add upstream git@github.com:opensearch-project/notifications.git
cd ..

# Common Utils plugin (forked from OpenSearch)
git clone git@github.com:wazuh/wazuh-indexer-common-utils.git
cd wazuh-indexer-common-utils
git remote add upstream git@github.com:opensearch-project/common-utils.git
cd ..

# Alerting plugin (forked from OpenSearch)
git clone git@github.com:wazuh/wazuh-indexer-alerting.git
cd wazuh-indexer-alerting
git remote add upstream git@github.com:opensearch-project/alerting.git
cd ..

3. Vagrant

Install Vagrant with the Libvirt provider following the guide.

Then install the Vagrant SCP plugin:

vagrant plugin install vagrant-scp

4. IntelliJ IDEA

Prepare your IDE:

  1. Install IDEA Community Edition as per the official documentation.
  2. Set a global SDK to Eclipse Temurin following this guide.

You can find the JDK version to use under the wazuh-indexer/gradle/libs.versions.toml file. IntelliJ IDEA includes some JDKs by default. If you need to change it, or if you want to use a different distribution, follow the instructions in the next section.

5. Set up Java

When you open a Java project for the first time, IntelliJ will ask you to install the appropriate JDK for the project.

Using IDEA, install a JDK following this guide. The version to install must match the JDK version used by the Indexer (check wazuh-indexer/gradle/libs.versions.toml).

Once the JDK is installed, configure it as the default system-wide Java installation using update-alternatives:

sudo update-alternatives --install /usr/bin/java java /home/$USER/.jdks/temurin-21.0.9/bin/java 0

Check Java is correctly configured:

java --version

If you need to install or switch JDK versions, use sudo update-alternatives --config java to select the JDK of your preference.

Set the JAVA_HOME and PATH environment variables by adding these lines to your shell RC file (.bashrc, .zshrc, etc.):

export JAVA_HOME=/usr/lib/jvm/temurin-24-jdk-amd64
export PATH=$PATH:/usr/lib/jvm/temurin-24-jdk-amd64/bin

After that, restart your shell or run source ~/.zshrc (or similar) to apply the changes. Verify with java --version.

Tip: SDKMAN is a convenient tool for managing multiple JDK versions:

sdk install java 24-tem
sdk use java 24-tem

6. Docker (Optional)

Docker is useful for running integration tests and local test environments. Install Docker Engine following the official instructions.

Verify the installation:

docker --version
docker run hello-world

7. Test Cluster (Optional)

The repository includes a Vagrant-based test cluster at tools/test-cluster/ for end-to-end testing against a real Wazuh Indexer instance.

Prerequisites:

Refer to the tools/test-cluster/README.md for provisioning and usage instructions.

8. Verify the Setup

After completing the setup, verify everything works:

cd wazuh-indexer-plugins
./gradlew :wazuh-indexer-content-manager:compileJava

For the Notifications plugin (Kotlin-based, separate repository):

cd wazuh-indexer-notifications
./gradlew build

For the Common Utils plugin (Shared Library):

cd wazuh-indexer-common-utils
./gradlew clean build publishToMavenLocal

For the Alerting plugin:

cd wazuh-indexer-alerting
./gradlew build

If compilation succeeds, your environment is ready. See Build from Sources for more build commands.

How to generate a package

This guide includes instructions to generate distribution packages locally using Docker.

Wazuh Indexer supports any of these combinations:

  • distributions: ['tar', 'deb', 'rpm']
  • architectures: ['x64', 'arm64']

Windows is currently not supported.

For more information navigate to the compatibility section.

Before you get started, make sure to clean your environment by running ./gradlew clean on the root level of the wazuh-indexer repository.

Pre-requisites

The process to build packages requires Docker and Docker Compose.

Your workstation must meet the minimum hardware requirements (the more resources the better ☺):

  • 8 GB of RAM (minimum)
  • 4 cores

The tools and source code to generate a package of Wazuh Indexer are hosted in the wazuh-indexer repository, so clone it if you haven’t done already.

A wazuh-engine tarball is required to build the Wazuh Indexer package. Follow the Engine build instructions in the wazuh/wazuh repository to produce it. The resulting .tar.gz is passed to builder.sh via the -e flag.

Building wazuh-indexer packages

The Docker environment under wazuh-indexer/build-scripts/builder automates the build and assemble process for the Wazuh Indexer and its plugins, making it easy to create packages on any system.

Use the builder.sh script to build a package.

./builder.sh -h
Usage: ./builder.sh [args]

Arguments:
-p INDEXER_PLUGINS_BRANCH       [Optional] wazuh-indexer-plugins repo branch, default is 'main'.
-r INDEXER_REPORTING_BRANCH     [Optional] wazuh-indexer-reporting repo branch, default is 'main'.
-s SECURITY_ANALYTICS_BRANCH    [Optional] wazuh-indexer-security-analytics repo branch, default is 'main'.
-n NOTIFICATIONS_BRANCH         [Optional] wazuh-indexer-notifications repo branch, default is 'main'.
-t INDEXER_ALERTING_BRANCH      [Optional] wazuh-indexer-alerting repo branch, default is 'main'.
-c COMMON_UTILS_BRANCH          [Optional] wazuh-indexer-common-utils repo branch, default is 'main'.
-e ENGINE_TARBALL               [Required] Path to wazuh-engine tarball (.tar.gz) on the host.
-R REVISION     [Optional] Package revision, default is '0'.
-S STAGE        [Optional] Staging build, default is 'false'.
-d DISTRIBUTION [Optional] Distribution, default is 'rpm'.
-a ARCHITECTURE [Optional] Architecture, default is 'x64'.
-D      Destroy the docker environment
-h      Print help

The example below it will generate a wazuh-indexer package for Debian based systems, for the x64 architecture, using 1 as revision number and using the production naming convention.

# Within wazuh-indexer/build-scripts/builder
bash builder.sh -d deb -a x64 -R 0 -S true -e ./wazuh-engine-5.0.0-linux-amd64.tar.gz

The resulting package will be stored at wazuh-indexer/artifacts/dist.

The STAGE option defines the naming of the package. When set to false, the package will be unequivocally named with the commits’ SHA of the wazuh-indexer, wazuh-indexer-plugins, wazuh-indexer-reporting, wazuh-indexer-security-analytics, wazuh-indexer-notifications and wazuh-indexer-alerting repositories, in that order. For example: wazuh-indexer_5.0.0-0_x86_64_aff30960363-846f143-494d125-9c0c1fe-3b2a8d1-7e5f094.rpm.

Installing the generated package

The package produced under wazuh-indexer/artifacts/dist is the input to the standard Wazuh Indexer installation procedure. Follow the Installation guide for certificate creation, node configuration, and cluster initialization steps.

How to generate a container image

This guide includes instructions to generate distribution packages locally using Docker.

Wazuh Indexer supports any of these combinations:

  • distributions: ['tar', 'deb', 'rpm']
  • architectures: ['x64', 'arm64']

Windows is currently not supported.

For more information navigate to the compatibility section.

Before you get started, make sure to clean your environment by running ./gradlew clean on the root level of the wazuh-indexer repository.

Pre-requisites

The process to build packages requires Docker and Docker Compose.

Your workstation must meet the minimum hardware requirements (the more resources the better ☺):

  • 8 GB of RAM (minimum)
  • 4 cores

The tools and source code to generate a package of Wazuh Indexer are hosted in the wazuh-indexer repository, so clone it if you haven’t done already.

Building wazuh-indexer Docker images

The wazuh-indexer/build-scripts/docker folder contains the code to build Docker images. Below there is an example of the command needed to build the image. Set the build arguments and the image tag accordingly.

The Docker image is built from a wazuh-indexer tarball (tar.gz), which must be present in the same folder as the Dockerfile in wazuh-indexer/build-scripts/docker.

docker build \
   --build-arg="VERSION=<version>" \
   --build-arg="INDEXER_TAR_NAME=wazuh-indexer_<version>-<revision>_linux-x64.tar.gz" \
   --tag=wazuh-indexer:<version>-<revision> \
   --progress=plain \
   --no-cache .

Then, start a container with:

docker run -p 9200:9200 -it --rm wazuh-indexer:<version>-<revision>

The build-and-push-docker-image.sh script automates the process to build and push Wazuh Indexer Docker images to our repository in quay.io. The script takes several parameters. Use the -h option to display them.

To push images, credentials must be set at environment level:

  • QUAY_USERNAME
  • QUAY_TOKEN
Usage: build-scripts/build-and-push-docker-image.sh [args]

Arguments:
-n NAME         [required] Tarball name.
-r REVISION     [Optional] Revision qualifier, default is 0.
-h help

The script will stop if the credentials are not set, or if any of the required parameters are not provided.

This script is used in the 5_builderpackage_docker.yml GitHub Workflow, which is used to automate the process even more. When possible, prefer this method.

How to Build from Sources

The Wazuh Indexer Plugins repository uses Gradle as its build system. The root project contains multiple subprojects, one per plugin.

Building the Entire Project

To build all plugins (compile, test, and package):

./gradlew build

When completed, distribution artifacts for each plugin are located in their respective build/distributions/ directories.

Building a Specific Plugin

To build only the Content Manager plugin:

./gradlew :wazuh-indexer-content-manager:build

Other plugin targets follow the same pattern. To see all available projects:

./gradlew projects

Compile Only (No Tests)

For a faster feedback loop during development, compile without running tests:

./gradlew :wazuh-indexer-content-manager:compileJava

This is useful for checking that your code changes compile correctly before running the full test suite.

Output Locations

ArtifactLocation
Plugin ZIP distributionplugins/<plugin-name>/build/distributions/
Compiled classesplugins/<plugin-name>/build/classes/
Test reportsplugins/<plugin-name>/build/reports/tests/
Generated JARsplugins/<plugin-name>/build/libs/

Common Build Issues

JDK Version Mismatch

The project requires a specific JDK version (currently JDK 24, Eclipse Temurin). If you see compilation errors related to Java version, check:

java --version

Ensure JAVA_HOME points to the correct JDK. See Setup for details.

Dependency Resolution Failures

If Gradle cannot resolve dependencies:

  1. Check your network connection (dependencies are downloaded from Maven Central and repositories).
  2. Try clearing the Gradle cache: rm -rf ~/.gradle/caches/
  3. Re-run with --refresh-dependencies: ./gradlew build --refresh-dependencies

Out of Memory

For large builds, increase Gradle’s heap size in gradle.properties:

org.gradle.jvmargs=-Xmx4g

Linting and Formatting Errors

The build includes code quality checks (Spotless, etc.). If formatting checks fail:

./gradlew spotlessApply

Then rebuild.

Useful Gradle Flags

FlagDescription
--infoVerbose output
--debugDebug-level output
--stacktracePrint stack traces on failure
--parallelRun tasks in parallel (faster on multi-core)
-x testSkip tests: ./gradlew build -x test
--continuousWatch mode — rebuilds on file changes

How to run from sources

Every Wazuh Indexer repository includes one or more Gradle projects with predefined tasks to run and build the source code.

In this case, to run a Gradle project from source code, run the ./gradlew run command.

For Wazuh Indexer, additional plugins may be installed by passing the -PinstalledPlugins flag:

./gradlew run -PinstalledPlugins="['plugin1', 'plugin2']"

The ./gradlew run command will build and start the project, writing its log above Gradle’s status message. A lot of stuff is logged on startup, specifically these lines tell you that OpenSearch is ready.

[2020-05-29T14:50:35,167][INFO ][o.e.h.AbstractHttpServerTransport] [runTask-0] publish_address {127.0.0.1:9200}, bound_addresses {[::1]:9200}, {127.0.0.1:9200}
[2020-05-29T14:50:35,169][INFO ][o.e.n.Node               ] [runTask-0] started

It’s typically easier to wait until the console stops scrolling, and then run curl in another window to check if OpenSearch instance is running.

curl localhost:9200

{
  "name" : "runTask-0",
  "cluster_name" : "runTask",
  "cluster_uuid" : "oX_S6cxGSgOr_mNnUxO6yQ",
  "version" : {
    "number" : "1.0.0-SNAPSHOT",
    "build_type" : "tar",
    "build_hash" : "0ba0e7cc26060f964fcbf6ee45bae53b3a9941d0",
    "build_date" : "2021-04-16T19:45:44.248303Z",
    "build_snapshot" : true,
    "lucene_version" : "8.7.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  }
}

Use -Dtests.opensearch. to pass additional settings to the running instance. For example, to enable OpenSearch to listen on an external IP address, pass -Dtests.opensearch.http.host. Make sure your firewall or security policy allows external connections for this to work.

./gradlew run -Dtests.opensearch.http.host=0.0.0.0

How to Run the Tests

This section explains how to run the Wazuh Indexer Plugins tests at various levels.

Full Suite

To execute all tests and code quality checks (linting, documentation, formatting):

./gradlew check

This runs unit tests, integration tests, and static analysis tasks.

Unit Tests

Run all unit tests across the entire project:

./gradlew test

Run unit tests for a specific plugin:

./gradlew :wazuh-indexer-content-manager:test

Integration Tests

Run integration tests for a specific plugin:

./gradlew :wazuh-indexer-content-manager:integTest

YAML REST Tests

Plugins can define REST API tests using YAML test specs. To run them:

./gradlew :wazuh-indexer-content-manager:yamlRestTest

Reproducible Test Runs

Tests use randomized seeds. When a test fails, the output includes the seed that was used. To reproduce the exact same run:

./gradlew :wazuh-indexer-content-manager:test -Dtests.seed=DEADBEEF

Replace DEADBEEF with the actual seed from the failure output.

Viewing Test Reports

After running tests, HTML reports are generated at:

plugins/<plugin-name>/build/reports/tests/test/index.html

Open this file in a browser to see detailed results with pass/fail status, stack traces, and timing.

For integration tests:

plugins/<plugin-name>/build/reports/tests/integTest/index.html

Running a Single Test Class

To run a specific test class:

./gradlew :wazuh-indexer-content-manager:test --tests "com.wazuh.contentmanager.rest.service.RestPostRuleActionTests"

Test Cluster (Vagrant)

For end-to-end testing on a real Wazuh Indexer service, the repository includes a Vagrant-based test cluster at tools/test-cluster/. This provisions a virtual machine with Wazuh Indexer installed and configured.

Refer to its README.md for setup and usage instructions.

Package Testing

Smoke tests on built packages are run via GitHub Actions Workflows. These install packages on supported operating systems:

  • DEB packages — installed on the Ubuntu 24.04 GitHub Actions runner.
  • RPM packages — installed in a Red Hat 9 Docker container.

Useful Test Flags

FlagDescription
-Dtests.seed=<seed>Reproduce a specific randomized test run
-Dtests.verbose=truePrint test output to stdout
--tests "ClassName"Run a single test class
--tests "ClassName.methodName"Run a single test method
-x testSkip unit tests in a build

Wazuh Indexer Setup Plugin — Development Guide

This document describes how to extend the Wazuh Indexer setup plugin to create new index templates and index management policies (ISM) for OpenSearch.


📦 Creating a New Index

1. Add a New Index Template

Create a new JSON file in the directory: /plugins/setup/src/main/resources

Follow the existing structure and naming convention. Example:

{
  "index_patterns": ["<pattern>"],
  "mappings": {
    "date_detection": false,
    "dynamic": "strict",
    "properties": {
      <custom mappings and fields>
    }
  },
  "order": 1,
  "settings": {
    "index": {
      "number_of_shards": 1,
      "number_of_replicas": 1
    }
  }
}

2. Register the Index in the Code

Edit the constructor of the SetupPlugin class located at: /plugins/setup/src/main/java/com/wazuh/setup/SetupPlugin.java

Add the template and index entry to the indices map. There are two kind of indices:

  • Stream index. Stream indices contain time-based events of any kind (alerts, statistics, logs…).
  • Stateful index. Stateful indices represent the most recent information of a subject (active vulnerabilities, installed packages, open ports, …). These indices are different of Stream indices as they do not contain timestamps. The information is not based on time, as they always represent the most recent state.
/**
* Main class of the Indexer Setup plugin. This plugin is responsible for the creation of the index
* templates and indices required by Wazuh to work properly.
*/
public class SetupPlugin extends Plugin implements ClusterPlugin {

  // ...

  // Stream indices
  this.indices.add(new StreamIndex("my-stream-index-000001", "my-index-template-1", "my-alias"));
  // State indices
  this.indices.add(new StateIndex("my-state-index", "my-index-template-2"));

  //...
}

✅ Verifying Template and Index Creation After building the plugin and deploying the Wazuh Indexer with it, you can verify the index templates and indices using the following commands:

curl -X GET <indexer-IP>:9200/_index_template/
curl -X GET <indexer-IP>:9200/_cat/indices?v

Alternatively, use the Developer Tools console from the Wazuh Dashboard, or your browser.

🔁 Creating a New ISM (Index State Management) Policy

1. Add Rollover Alias to the Index Template

Edit the existing index template JSON file and add the following setting:

"plugins.index_state_management.rollover_alias": "<index-name>"

2. Define the ISM Policy

Refer to the OpenSearch ISM Policies documentation for more details.

Here is an example ISM policy:

{
  "policy": {
    "policy_id": "<index-name>-policy",
    "description": "<policy-description>",
    "last_updated_time": <unix-timestamp-in-milliseconds>,
    "schema_version": 1,
    "default_state": "hot",
    "states": [
      {
        "name": "hot",
        "actions": [
          {
            "retry": {
              "count": 3,
              "backoff": "exponential",
              "delay": "1m"
            },
            "rollover": {
              "min_doc_count": 200000000,
              "min_primary_shard_size": "20gb"
            }
          }
        ],
        "transitions": [
          {
            "state_name": "delete",
            "conditions": {
              "min_index_age": "<retention-time>"
            }
          }
        ]
      },
      {
        "name": "delete",
        "actions": [
          {
            "retry": {
              "count": 3,
              "backoff": "exponential",
              "delay": "1m"
            },
            "delete": {}
          }
        ],
        "transitions": []
      }
    ],
    "ism_template": [
      {
        "index_patterns": [
          "wazuh-<pattern>-*"
        ],
        "priority": <priority-int>
      }
    ]
  }
}

3. Register the ISM Policy in the Plugin Code

Edit the IndexStateManagement class located at: /plugins/setup/src/main/java/com/wazuh/setup/index/IndexStateManagement.java

Register the new policy constant and add it in the constructor:

// ISM policy name constant (filename without .json extension)
static final String MY_POLICY = "my-policy-filename";

...

/**
 * Constructor
 *
 * @param index    Index name
 * @param template Index template name
 */
public IndexStateManagement(String index, String template) {
    super(index, template);
    this.policies = new ArrayList<>();

    // Register the ISM policy to be created
    this.policies.add(MY_POLICY);
}

📌 Additional Notes

Always follow existing naming conventions to maintain consistency.

Use epoch timestamps (in milliseconds) for last_updated_time fields.

ISM policies and templates must be properly deployed before the indices are created.


🚀 Event Stream Templates

Overview

All event data streams share a single base template: templates/streams/events.json. At deployment time, the plugin generates one index template per event category by dynamically setting the index_patterns and rollover_alias fields from the base template. This means:

  • Source of truth: Only events.json exists in the repository.
  • At runtime: One index template is created for each category (e.g., wazuh-events-v5-cloud-services-template, wazuh-events-v5-security-template, etc.).

The StreamIndex class handles this: when constructed with only an index name (no explicit template path), it defaults to templates/streams/events and rewrites the index_patterns and rollover_alias to match the specific index.

How it works

// Single-arg constructor defaults to the shared events template
new StreamIndex("wazuh-events-v5-cloud-services")
// Equivalent to:
new StreamIndex("wazuh-events-v5-cloud-services", "templates/streams/events")

During createTemplate(), the plugin:

  1. Reads events.json from the classpath
  2. Overrides index_patterns to ["wazuh-events-v5-cloud-services*"]
  3. Overrides rollover_alias to "wazuh-events-v5-cloud-services"
  4. Creates the composable index template in OpenSearch

Verifying deployed templates

To list all event templates in a running cluster:

GET /_index_template/wazuh-events-*

Specialized stream templates

Some data streams use their own dedicated templates instead of the shared events.json:

Data StreamTemplateNotes
wazuh-events-raw-v5templates/streams/raw.jsonStores original unprocessed events
wazuh-events-v5-unclassifiedtemplates/streams/unclassified.jsonStores uncategorized events for investigation
wazuh-active-responsestemplates/streams/active-responses.jsonActive Response execution requests

These are registered with the two-arg constructor:

new StreamIndex("wazuh-events-raw-v5", "templates/streams/raw")
new StreamIndex("wazuh-events-v5-unclassified", "templates/streams/unclassified")
new StreamIndex("wazuh-active-responses", "templates/streams/active-responses")

🚀 Events Data Stream ISM Policy (stream-events-policy)

Overview

The stream-events-policy manages all wazuh-events-v5-* data streams. It combines rollover (based on shard size or document count) with a short retention period to ensure timely cleanup of processed event data.

Policy Details

  • Policy Name: stream-events-policy
  • Location: plugins/setup/src/main/resources/policies/stream-events-policy.json
  • Index Pattern: wazuh-events-v5-*
  • Retention Period: 1 hour
  • Rollover Conditions: 25 GB primary shard size or 200,000,000 documents
  • Priority: 50

Policy States

  1. Hot State

    • Actions: Rollover when primary shard reaches 25 GB or 200M documents
    • Transition Condition: Transitions to delete after 1 hour
  2. Delete State

    • Actions: Deletes the index
    • Retry Policy: 3 attempts with exponential backoff (1-minute initial delay)

🚀 Findings Data Stream ISM Policy (stream-findings-policy)

Overview

The stream-findings-policy manages all wazuh-findings-v5-* data streams. It combines rollover with a 90-day retention period to maintain detection findings for compliance and investigation purposes.

Policy Details

  • Policy Name: stream-findings-policy
  • Location: plugins/setup/src/main/resources/policies/stream-findings-policy.json
  • Index Pattern: wazuh-findings-v5-*
  • Retention Period: 90 days
  • Rollover Conditions: 25 GB primary shard size or 200,000,000 documents
  • Priority: 50

Policy States

  1. Hot State

    • Actions: Rollover when primary shard reaches 25 GB or 200M documents
    • Transition Condition: Transitions to delete after 90 days
  2. Delete State

    • Actions: Deletes the index
    • Retry Policy: 3 attempts with exponential backoff (1-minute initial delay)

🚀 Raw Events Data Stream ISM Policy (stream-raw-events-policy)

Overview

The stream-raw-events-policy manages the wazuh-events-raw-v5 data stream with an aggressive 10-minute retention for temporary raw event storage.

Policy Details

  • Policy Name: stream-raw-events-policy
  • Location: plugins/setup/src/main/resources/policies/stream-raw-events-policy.json
  • Index Pattern: wazuh-events-raw-v5*
  • Retention Period: 10 minutes
  • Rollover Conditions: 25 GB primary shard size or 200,000,000 documents
  • Priority: 100

Policy States

  1. Hot State

    • Actions: Rollover when primary shard reaches 25 GB or 200M documents
    • Transition Condition: Transitions to delete after 10 minutes
  2. Delete State

    • Actions: Deletes the index
    • Retry Policy: 3 attempts with exponential backoff (1-minute initial delay)

🚀 Active Responses Data Stream (wazuh-active-responses)

Overview

The wazuh-active-responses data stream stores Active Response execution requests generated when monitor triggers match their conditions. This is part of the Active Response 5.0 integration with Wazuh XDR, using the Indexer Alerting and Notifications plugins as the foundation.

Purpose

  • Active Response Pipeline: Structured and auditable execution pipeline for Active Response actions
  • Manager Retrieval: The Wazuh manager retrieves documents from this index to distribute and execute Active Responses on agents
  • Event Correlation: Each document references the source event (document ID and index) that triggered the response

Data Stream Configuration

Index Template

  • Location: plugins/setup/src/main/resources/templates/streams/active-responses.json
  • Index Pattern: wazuh-active-responses*
  • Rollover Alias: wazuh-active-responses
  • Priority: 1

Fields Included (WCS-compatible)

  • @timestamp: When the document was inserted into the wazuh-active-responses index (indexing time)
  • event.doc_id: Document ID of the matched alert that triggered the active response
  • event.index: Source index of the matched alert
  • wazuh.active_response.name: Name of the active response configured in the channel
  • wazuh.active_response.executable: Executable configured in the active response channel
  • wazuh.active_response.extra_arguments: Arguments configured in the channel
  • wazuh.active_response.location: Where to execute (local, defined-agent, all)
  • wazuh.active_response.agent_id: Agent configured in the channel
  • wazuh.active_response.type: Response type (stateless, stateful)
  • wazuh.active_response.stateful_timeout: Seconds configured in the channel (for stateful)
  • wazuh.agent.*: Agent metadata
  • wazuh.cluster.*: Cluster information
  • wazuh.space.name: Wazuh space/tenant information

ISM Policy

Policy Details

  • Policy Name: stream-active-responses-policy
  • Location: plugins/setup/src/main/resources/policies/stream-active-responses-policy.json
  • Retention Period: 3 days
  • Rollover Conditions: 25 GB primary shard size or 200,000,000 documents
  • Priority: 100

Configuration

The data stream is created automatically during plugin initialization. Ensure:

  1. The template file active-responses.json exists in templates/streams/
  2. The ISM policy file stream-active-responses-policy.json exists in policies/
  3. Both are registered in SetupPlugin.java and IndexStateManagement.java

Testing

Integration tests for the active responses data stream are located at: plugins/setup/src/test/java/com/wazuh/setup/ActiveResponsesIT.java

Defining default users and roles for Wazuh Indexer

The Wazuh Indexer packages include a set of default users and roles specially crafted for Wazuh’s use cases. This guide provides instructions to extend or modify these users and roles so they end up being included in the Wazuh Indexer package by default.

Note that the access control and permissions management are handled by the OpenSearch’s security plugin. As a result, we provide configuration files for it. The data is applied during the cluster’s initialization, as a result of running the indexer-security-init.sh script.

Considerations and conventions

As these configuration files are included in the Wazuh Indexer package, they are hosted in the wazuh-indexer repository. Be aware of that when reading this guide.

Any security related resource (roles, action groups, users, …) created by us must be reserved (reserved: true). This ensures they cannot be modified by the users, in order to guarantee the correct operation of Wazuh Central Components. Also, they should be visible (hidden: false) unless explicitly defined otherwise.

1. Adding a new user

Add the new user to the internal_users.wazuh.yml file located at: wazuh-indexer/distribution/src/config/security/.

new-user:
  # Generate the hash using the tool at `plugins/opensearch-security/tools/hash.sh -p <new-password>`
  hash: "<HASHED-PASSWORD>"
  reserved: true
  hidden: false
  backend_roles: []
  description: "New user description"

OpenSearch’s reference:

2. Adding a new role

Add the new role to the roles.wazuh.yml file located at: wazuh-indexer/distribution/src/config/security/.

  • Under index_permissions.index_patterns, list the index patterns the role will have effect on.
  • Under index_permissions.allowed_actions, list the allowed action groups or indiviual permissions granted to this role.

The default action groups for cluster_permissions and index_permissions are listed in the Default action groups documentation

role-read:
   reserved: true
   hidden: false
   cluster_permissions: []
   index_permissions:
     - index_patterns:
         - "wazuh-*"
       dls: ""
       fls: []
       masked_fields: []
       allowed_actions:
         - "read"
   tenant_permissions: []
   static: true
role-write:
   reserved: true
   hidden: false
   cluster_permissions: []
   index_permissions:
     - index_patterns:
         - "wazuh-*"
       dls: ""
       fls: []
       masked_fields: []
       allowed_actions:
         - "index"
   tenant_permissions: []
   static: true

OpenSearch’s reference:

3. Adding a new role mapping

Add the new role mapping to roles_mapping.wazuh.yml file located at: wazuh-indexer/distribution/src/config/security/. Note that the mapping name must match the role name.

  • Under users, list the users the role will be mapped to.
role-read:
   reserved: true
   hidden: false
   backend_roles: [ ]
   hosts: [ ]
   users:
     - "new-user"
   and_backend_roles: [ ]
role-write:
   reserved: true
   hidden: false
   backend_roles: [ ]
   hosts: [ ]
   users:
     - "new-user"
   and_backend_roles: [ ]

OpenSearch’s reference:

Testing the configuration

The validation of the new configuration needs to be tested on a running deployment of Wazuh Indexer containing the security plugin.

You can follow any of these paths:

A. Generating a new Wazuh Indexer package

  1. Apply your changes to the configuration files in wazuh-indexer/distribution/src/config/security/.
  2. Generate a new package (see Build Packages).
  3. Follow the official installation and configuration steps.
  4. Check the new changes are applied (you can use the UI or the API).

B. Applying the new configuration to an existing Wazuh Indexer deployment (using the UI or API)

  1. Use the Wazuh Indexer API or the Wazuh Dashboard to create a new security resource. Follow the steps in Defining users and roles.

C. Applying the new configuration to an existing Wazuh Indexer deployment (using configuration files)

  1. Add the new configuration to the affected file within /etc/wazuh-indexer/opensearch-security/.
  2. Run the /usr/share/wazuh-indexer/bin/indexer-security-init.sh script to load the new configuration.

The indexer-security-init.sh will overwrite your security configuration, including passwords. Use it under your own risk.

Alternatively, apply the new configuration using fine-grained options. See Applying changes to configuration files

Wazuh Indexer Reporting Plugin — Development Guide

This document describes how to build a Wazuh Reporting plugin development environment to create and test new features.

Working from a minimal environment

In order to deploy a minimal environment for developing the reporting plugin just for testing purposes, you must have at least a Wazuh Indexer and a Wazuh Dashboard environment running. Then, you can create your own SMPT server to test the email notifications from the following Mailpit configuration. To verify everything is working correctly, try generating reports following the user’s guide.

Working from real scenario packages

Preparing packages

  • Wazuh Indexer package (debian package based on OpenSearch 3.1.0). Compiled locally using the Docker builder: bash builder.sh -d deb -a x64.
  • Wazuh Dashboard package (debian package based on OpenSearch 3.1.0). Downloaded from wazuh-dashboard actions.

Note: To test using RPM packages, update the Vagrant configuration and provisioning scripts accordingly (for example, change generic/ubuntu2204 to generic/centos7 in the Vagrantfile and replace Debian-specific installation commands with RPM equivalents).

Preparing a development environment

Prepare a multi-VM Vagrant environment with the following components:

  • Server
    • Wazuh Indexer (including the reporting plugin).
    • Wazuh Dashboard (including the reporting plugin).
  • Mailpit
    • Mailpit SMTP server.

File location should be:

working-dir/
├── Vagrantfile
├── data/
│   ├── wazuh-indexer_*.deb
│   ├── wazuh-dashboard_*.deb
│   ├── gencerts.sh
│   ├── mailpit.sh
│   └── server.sh

Vagrantfile

Details

class VagrantPlugins::ProviderVirtualBox::Action::Network
  def dhcp_server_matches_config?(dhcp_server, config)
    true
  end
end

Vagrant.configure("2") do |config|

    config.vm.define "server" do |server|
      server.vm.box = "generic/ubuntu2204"
      server.vm.provider "virtualbox" do |vb|
        vb.memory = "8192"
      end
      # For Hyper-V provider
      #server.vm.provider "hyperv" do |hv|
      #  hv.memory = 8192
      #end
      server.vm.network "private_network", type: "dhcp"
      server.vm.hostname = "rhel-server"
      config.vm.provision "file", source: "data", destination: "/tmp/vagrant_data"

      server.vm.provision "shell", privileged: true, path: "data/server.sh"
    end

    config.vm.define "mailpit" do |mailpit|
      mailpit.vm.box = "generic/ubuntu2204"
      mailpit.vm.provider "virtualbox" do |vb|
        vb.memory = "1024"
      end
      # For Hyper-V provider
      #client.vm.provider "hyperv" do |hv|
      #  hv.memory = 8192
      #end
      mailpit.vm.network "private_network", type: "dhcp"
      mailpit.vm.hostname = "mailpit"

      config.vm.provision "file", source: "data", destination: "/tmp/vagrant_data"

      mailpit.vm.provision "shell", privileged: true, path: "data/mailpit.sh"
    end

end

server.sh

Details

#!/bin/bash

# Install
dpkg -i /tmp/vagrant_data/wazuh-indexer*.deb
dpkg -i /tmp/vagrant_data/wazuh-dashboard*.deb

# Setup

## Create certs
mkdir certs
cd certs || exit 1
bash /tmp/vagrant_data/gencerts.sh .

mkdir -p /etc/wazuh-indexer/certs
cp admin.pem  /etc/wazuh-indexer/certs/admin.pem
cp admin.key /etc/wazuh-indexer/certs/admin-key.pem
cp indexer.pem  /etc/wazuh-indexer/certs/indexer.pem
cp indexer-key.pem /etc/wazuh-indexer/certs/indexer-key.pem
cp ca.pem /etc/wazuh-indexer/certs/root-ca.pem
chown -R wazuh-indexer.wazuh-indexer /etc/wazuh-indexer/certs/

mkdir -p /etc/wazuh-dashboard/certs
cp dashboard.pem  /etc/wazuh-dashboard/certs/dashboard.pem
cp dashboard-key.pem /etc/wazuh-dashboard/certs/dashboard-key.pem
cp ca.pem /etc/wazuh-dashboard/certs/root-ca.pem
chown -R wazuh-dashboard.wazuh-dashboard /etc/wazuh-dashboard/certs/

systemctl daemon-reload

## set up Indexer
systemctl enable wazuh-indexer
systemctl start wazuh-indexer
/usr/share/wazuh-indexer/bin/indexer-security-init.sh

## set up Dashboard
systemctl enable wazuh-dashboard
systemctl start wazuh-dashboard

## enable IPv6
modprobe ipv6
sysctl -w net.ipv6.conf.all.disable_ipv6=0

## turn off firewalld
sudo ufw disable

mailpit.sh

Details

#!/bin/bash

# Install
curl -sOL https://raw.githubusercontent.com/axllent/mailpit/develop/install.sh && INSTALL_PATH=/usr/bin sudo bash ./install.sh

# Setup
## set up Mailpit
useradd -r -s /bin/false mailpit
groupadd -r mailpit
### Create directories
mkdir -p /var/lib/mailpit
chown -R mailpit.mailpit /var/lib/mailpit

### Create password file
mkdir -p /etc/mailpit
echo "admin:$(openssl passwd -apr1 admin)" > /etc/mailpit/passwords
chown -R mailpit.mailpit /var/lib/mailpit

## Create certs
mkdir certs
cd certs || exit 1
bash /tmp/vagrant_data/gencerts.sh .

mkdir -p /etc/mailpit/certs
cp admin.pem  /etc/mailpit/certs/admin.pem
cp admin.key /etc/mailpit/certs/admin-key.pem
cp mailpit.pem  /etc/mailpit/certs/mailpit.pem
cp mailpit-key.pem /etc/mailpit/certs/mailpit-key.pem
cp ca.pem /etc/mailpit/certs/root-ca.pem
chown -R mailpit.mailpit /etc/mailpit/certs/

## enable IPv6
modprobe ipv6
sysctl -w net.ipv6.conf.all.disable_ipv6=0

## turn off firewalld
sudo ufw disable

echo "======================================================"
echo "Start Mailpit with the following command:"
echo ""
echo "mailpit --listen 0.0.0.0:8025 --smtp 0.0.0.0:1025 --database /var/lib/mailpit.db --ui-auth-file /etc/mailpit/passwords --ui-tls-cert /etc/mailpit/certs/admin.pem --ui-tls-key /etc/mailpit/certs/admin-key.pem --smtp-tls-cert /etc/mailpit/certs/mailpit.pem --smtp-tls-key /etc/mailpit/certs/mailpit-key.pem"
echo "======================================================"

# Adding HTTPS: https://mailpit.axllent.org/docs/configuration/http/
# mailpit --ui-tls-cert /path/to/cert.pem --ui-tls-key /path/to/key.pem

# Adding basic authentication: https://mailpit.axllent.org/docs/configuration/passwords/
# mailpit --ui-auth-file /path/to/password-file

gencerts.sh

Details

#!/bin/bash

if [[ $# -ne 1 ]]; then
fs=$(mktemp -d)
else
fs=$1
shift
fi

echo Working directory $fs
cd $fs

if [[ ! -e $fs/cfssl ]]; then
curl -s -L -o $fs/cfssl https://pkg.cfssl.org/R1.2/cfssl_linux-amd64
curl -s -L -o $fs/cfssljson https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64
chmod 755 $fs/cfssl*
fi

cfssl=$fs/cfssl
cfssljson=$fs/cfssljson

if [[ ! -e $fs/ca.pem ]]; then

cat << EOF | $cfssl gencert -initca - | $cfssljson -bare ca -
{
  "CN": "Wazuh",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
  {
    "C": "US",
    "L": "San Francisco",
    "O": "Wazuh",
    "OU": "Wazuh Root CA"
  }
 ]
}
EOF

fi

if [[ ! -e $fs/ca-config.json ]]; then
$cfssl print-defaults config > ca-config.json
fi

gencert_rsa() {
        name=$1
        profile=$2
cat << EOF | $cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=$profile -hostname="$name,127.0.0.1,localhost" - | $cfssljson -bare $name -
{
  "CN": "$i",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
  {
    "C": "US",
    "L": "California",
    "O": "Wazuh",
    "OU": "Wazuh"
  }
  ],
  "hosts": [
    "$i",
    "localhost"
  ]
}
EOF
openssl pkcs8 -topk8 -inform pem -in $name-key.pem -outform pem -nocrypt -out $name.key
}

gencert_ec() {
    openssl ecparam -name secp256k1 -genkey -noout -out jwt-private.pem
    openssl ec -in jwt-private.pem -pubout -out jwt-public.pem
}

hosts=(indexer dashboard mailpit)
for i in "${hosts[@]}"; do
        gencert_rsa $i www
done

users=(admin)
for i in "${users[@]}"; do
        gencert_rsa $i client
done

gencert_ec

  1. Bring up the environment with vagrant up. Use the command provided in the console to start mailpit from within its VM. mailpit is configured to use TLS and access credentials (admin:admin). Use ip addr to check for the public IP address given to the VM and use that IP to access mailpit UI (e.g: https://172.28.128.136:8025/).
  2. Add the username and password for mailpit to the Wazuh Indexer keystore.
    echo "admin" | /usr/share/wazuh-indexer/bin/opensearch-keystore add opensearch.notifications.core.email.mailpit.username
    echo "admin" | /usr/share/wazuh-indexer/bin/opensearch-keystore add opensearch.notifications.core.email.mailpit.password
    chown wazuh-indexer:wazuh-indexer /etc/wazuh-indexer/opensearch.keystore
    
  3. Ensure mailpit is accessible within the server VM (e.g curl https://172.28.128.136:8025 -k -u admin:admin should return HTML code). If not, add it to the list of known hosts in /etc/hosts (e.g echo "172.28.128.136 mailpit mailpit" >> /etc/hosts).

Wazuh Indexer Content Manager Plugin — Development Guide

This document describes the architecture, components, and extension points of the Content Manager plugin, which manages security content synchronization from the Wazuh CTI API and provides REST endpoints for user-generated content management.


Overview

The Content Manager plugin handles:

  • Wazuh Cloud Credentials: Stores the CTI access token in .wazuh-internal-state and caches it in PluginSettings.accessToken for REST handler use.
  • Pre-registration with Wazuh Cloud: Supports pre-registration of the Wazuh instance with Wazuh Cloud, via environment variable.
  • Job Scheduling: Periodically checks for updates using the OpenSearch Job Scheduler.
  • Update Check Service: Sends a daily heartbeat to CTI so Wazuh can notify users when a newer version is available.
  • Content Synchronization: Keeps local indices in sync with the Wazuh CTI Catalog via snapshots and incremental JSON Patch updates.
  • Security Analytics Integration: Pushes rules, integrations, and detectors to the Security Analytics Plugin (SAP).
  • User-Generated Content: Full CUD for rules, decoders, integrations, KVDBs, and policies in the Draft space.
  • Engine Communication: Validates and promotes content via Unix Domain Socket to the Wazuh Engine.
  • Space Management: Manages content lifecycle through Draft → Test → Custom promotion.

Tuning the development environment

The build.gradlefile defines the development environment for the plugin. There, you can configure and modify the plugin’s behavior by setting custom values for any of the settings exposed by the plugin, or by setting up environment variables, as follows:

testClusters.integTest {
  // JVM tweaks.
  jvmArgs '-Xms2g', '-Xmx2g'

  // Environment variables.
  systemProperty "wazuh.version", "${wazuh_version}-beta3"
  
  // Plugin settings.
  setting 'plugins.content_manager.catalog.update_on_start', 'true'
}

Pre-registration with Wazuh Cloud

The Content Manager supports pre-registration of the Wazuh instance with Wazuh Cloud using the DEPLOY_KEY environment variable. If this variable is set at startup, the Content Manager automatically registers the token as if it were sent through the REST API, enabling immediate synchronization with the CTI API without manual intervention. Snapshots bundled with the package are removed in favor of fetching the latest content directly from the CTI API using the provided token. This streamlines the setup process for new deployments and ensures that they start with the most up-to-date detection content from their subscription plan.

State diagram

---
title: XDR pre-deploy on Cloud
---
stateDiagram-v2
    state if_state <<choice>>
    env_var_exists: Does env var exist?
    no_state: Unresgistered mode
    yes_state: Registered mode
    inititalization: Initialization


    [*] --> env_var_exists
    env_var_exists --> if_state
    if_state --> no_state: No
    if_state --> yes_state : yes

    no_state --> inititalization : Init from local snapshots
    yes_state --> inititalization : Init from active plan
    inititalization --> [*]

Sequence diagram

---
title: XDR pre-deploy on Cloud
---
sequenceDiagram
    onNodeStarted->>onNodeStarted: deployKeyExists()
    alt DEPLOY_KEY env var exists
        onNodeStarted->>SubscriptionService: register(deployKey)
        onNodeStarted->>SnapshotService: deleteSnapshots(snapshotsDir)
    end
    onNodeStarted->>CatalogSyncJob: trigger()


System Indices

The plugin manages the following indices. All 8 content indices use an alias-backed blue/green storage scheme (see Index Alias Convention below).

Alias (public name)PurposeHiddenCreated by
.wazuh-cti-consumersSync state (status, offsets)yesContent Manager
wazuh-threatintel-policiesPolicy documentsnoContent Manager
wazuh-threatintel-integrationsIntegration definitionsnoContent Manager
wazuh-threatintel-rulesDetection rulesnoContent Manager
wazuh-threatintel-decodersDecoder definitionsnoContent Manager
wazuh-threatintel-kvdbsKey-value databasesnoContent Manager
wazuh-threatintel-filtersEngine filter rulesnoContent Manager
wazuh-threatintel-enrichmentsIndicators of CompromisenoContent Manager
.wazuh-threatintel-vulnerabilitiesCVE vulnerability datayesContent Manager
.wazuh-content-manager-jobsJob scheduler metadatayesContent Manager

Index Alias Convention

Each content index uses an alias-backed blue/green storage scheme to enable zero-downtime content replacement during subscription plan changes.

Naming

  • Alias (public name): The stable name used by all readers, REST handlers, and dashboards. Example: wazuh-threatintel-rules.
  • Physical index: The actual index storing data, suffixed with -a or -b. Example: wazuh-threatintel-rules-a.

Only one physical index is live at a time. The alias points to it with is_write_index: true. The other suffix is reserved as the shadow (staging) slot for the next plan-change swap.

Key classes

ClassLocationResponsibility
ContentIndexcti/catalog/index/ContentIndex.javaCreates alias-backed physical indices. Has a 4-arg constructor for targeting shadow physical names directly. createIndex() creates the physical index and assigns the alias. createShadowIndex() creates a hidden physical index without an alias.
IndexSwapHelpercti/catalog/index/IndexSwapHelper.javaStateless utility class for swap operations: resolveShadowName(), resolveLivePhysicalName(), createShadowIndices(), reindexUserContent(), atomicSwap(), deleteIndices().
AbstractConsumerServicecti/catalog/service/AbstractConsumerService.javaDetects plan changes and delegates to performShadowSwap() instead of the old resetConsumer() wipe-and-reload path.

Shadow swap flow (plan change)

When AbstractConsumerService.syncConsumerServices() detects a plan change (the plan-provided resource URL differs from the persisted one), it runs the shadow swap path:

1. Resolve shadow physical names (the -a/-b suffix not currently live)
2. Create hidden shadow physical indices (index.hidden=true, no alias)
3. Download snapshot into shadow indices (reuse SnapshotServiceImpl)
4. Reindex user content (space.name != "standard") from live → shadow
   (only for consumer types with hasUserContent()=true, i.e., ruleset)
5. Unhide non-CVE shadow indices (set index.hidden=false)
6. Atomic alias swap (single IndicesAliasesRequest for all 8 aliases)
7. Rewrite consumer document in .wazuh-cti-consumers
8. Run post-sync cascade (onSyncComplete: SAP sync, engine promote, etc.)
9. Delete old physical indices

Error handling:

  • Failure before step 6: shadow indices are deleted, alias and consumer doc unchanged. Next sync retries.
  • Failure between steps 6–7: alias is swapped but consumer doc still says old resource. Next sync re-detects the plan change and re-runs the shadow path (at most one wasted rebuild, no user-visible corruption).

Concurrency: The CatalogSyncJob semaphore spans the entire synchronize() call, which includes the shadow swap. No additional locking is needed.

Normal incremental syncs

Regular incremental updates (no plan change) write through the alias to the live physical index. They are completely unaware of the -a/-b scheme.


Plugin Architecture

Entry Point

ContentManagerPlugin is the main class. It implements Plugin, ClusterPlugin, JobSchedulerExtension, and SystemIndexPlugin (which extends ActionPlugin). On startup it:

  1. Initializes PluginSettings, ConsumersIndex, CredentialsIndex, CtiConsole, CatalogSyncJob, EngineServiceImpl, and SpaceService.
  2. Registers all REST handlers via getRestHandlers().
  3. Creates the .wazuh-cti-consumers and .wazuh-internal-state indices on cluster manager nodes.
  4. Schedules the periodic CatalogSyncJob via the OpenSearch Job Scheduler.
  5. Optionally triggers an immediate sync on start.
  6. Registers/schedules TelemetryPingJob (wazuh-telemetry-ping-job) when plugins.content_manager.telemetry.enabled is true.
  7. Registers a dynamic settings consumer to enable/disable telemetry at runtime.

Update Check Service internals

The update check flow is split into two classes:

  • TelemetryPingJob (jobscheduler/jobs/TelemetryPingJob.java)

    • Runs through Job Scheduler every 1 day.
    • Reads cluster UUID from ClusterService metadata.
    • Reads Wazuh version through ContentManagerPlugin.getVersion().
    • Prevents overlap using a Semaphore (tryAcquire() guard).
    • Exposes a trigger() method for immediate invocation, used by ContentManagerPlugin to fire the first ping as soon as the job document is indexed.
  • TelemetryClient (cti/console/client/TelemetryClient.java)

    • Sends an asynchronous GET request to CTI /ping.
    • Headers sent:
      • wazuh-uid: cluster UUID
      • wazuh-tag: v<version>
    • Fire-and-forget behavior: callback logs success/failure without blocking scheduler threads.

CTI HTTP Client User-Agent

All HTTP clients that communicate with CTI services include a custom User-Agent header set as a default header on the HTTP client builder:

User-Agent: Wazuh Indexer <version>

The version is read from VERSION.json at plugin startup and stored in PluginSettings. The user-agent string is built by PluginSettings.getUserAgent() using the Constants.USER_AGENT_PREFIX constant. If the version is unavailable, the fallback value unknown is used.

Affected clients:

  • Console ApiClient (cti/console/client/ApiClient.java) — async HTTP client for CTI Console authentication and plans.
  • Catalog ApiClient (cti/catalog/client/ApiClient.java) — async HTTP client for CTI Catalog consumer and changes.
  • SnapshotClient (cti/catalog/client/SnapshotClient.java) — sync HTTP client for downloading CTI snapshots.
  • TelemetryClient (cti/console/client/TelemetryClient.java) — inherits from Console ApiClient.

Runtime toggle behavior:

  • plugins.content_manager.telemetry.enabled is a dynamic setting.
  • Enabling it schedules the job; the immediate first ping is fired from within scheduleTelemetryPingJob() only after the job document has been successfully indexed, guaranteeing the ping only runs when the scheduled job is correctly registered.
  • Disabling it removes the telemetry job document from .wazuh-content-manager-jobs.

REST Handlers

The plugin registers 26 REST handlers, grouped by domain:

DomainHandlerMethodURI
SubscriptionRestPostSubscriptionActionPOST/_plugins/_content_manager/subscription
RestGetSubscriptionActionGET/_plugins/_content_manager/subscription
RestDeleteSubscriptionActionDELETE/_plugins/_content_manager/subscription
UpdateRestPostUpdateActionPOST/_plugins/_content_manager/update
LogtestRestPostLogtestActionPOST/_plugins/_content_manager/logtest
PolicyRestPutPolicyActionPUT/_plugins/_content_manager/policy/{space}
RulesRestPostRuleActionPOST/_plugins/_content_manager/rules
RestPutRuleActionPUT/_plugins/_content_manager/rules/{id}
RestDeleteRuleActionDELETE/_plugins/_content_manager/rules/{id}
DecodersRestPostDecoderActionPOST/_plugins/_content_manager/decoders
RestPutDecoderActionPUT/_plugins/_content_manager/decoders/{id}
RestDeleteDecoderActionDELETE/_plugins/_content_manager/decoders/{id}
IntegrationsRestPostIntegrationActionPOST/_plugins/_content_manager/integrations
RestPutIntegrationActionPUT/_plugins/_content_manager/integrations/{id}
RestDeleteIntegrationActionDELETE/_plugins/_content_manager/integrations/{id}
KVDBsRestPostKvdbActionPOST/_plugins/_content_manager/kvdbs
RestPutKvdbActionPUT/_plugins/_content_manager/kvdbs/{id}
RestDeleteKvdbActionDELETE/_plugins/_content_manager/kvdbs/{id}
FiltersRestPostFilterActionPOST/_plugins/_content_manager/filters
RestPutFilterActionPUT/_plugins/_content_manager/filters/{id}
RestDeleteFilterActionDELETE/_plugins/_content_manager/filters/{id}
PromoteRestPostPromoteActionPOST/_plugins/_content_manager/promote
RestGetPromoteActionGET/_plugins/_content_manager/promote
SpacesRestDeleteSpaceActionDELETE/_plugins/_content_manager/space/{space}

Class Hierarchy

The REST handlers follow a Template Method pattern through a three-level abstract class hierarchy. There are two parallel branches — one where the target space is always draft (AbstractCreateAction / AbstractUpdateAction / AbstractDeleteAction) and one where the target space is supplied at runtime from the request body (AbstractCreateActionSpaces / AbstractUpdateActionSpaces / AbstractDeleteActionSpaces). The latter is used for resources like Filters that can live in either draft or standard space.

BaseRestHandler
├── AbstractContentAction
│   ├── AbstractCreateAction               # Target space always: draft
│   │   ├── RestPostRuleAction
│   │   ├── RestPostDecoderAction
│   │   ├── RestPostIntegrationAction
│   │   └── RestPostKvdbAction
│   ├── AbstractUpdateAction               # Target space always: draft
│   │   ├── RestPutRuleAction
│   │   ├── RestPutDecoderAction
│   │   ├── RestPutIntegrationAction
│   │   └── RestPutKvdbAction
│   ├── AbstractDeleteAction               # Target space always: draft
│   │   ├── RestDeleteRuleAction
│   │   ├── RestDeleteDecoderAction
│   │   ├── RestDeleteIntegrationAction
│   │   └── RestDeleteKvdbAction
│   ├── AbstractCreateActionSpaces         # Target space from request body (draft|standard)
│   │   └── RestPostFilterAction
│   ├── AbstractUpdateActionSpaces         # Target space from request body (draft|standard)
│   │   └── RestPutFilterAction
│   └── AbstractDeleteActionSpaces         # Target space from request body (draft|standard)
│       └── RestDeleteFilterAction
├── RestPutPolicyAction
├── RestDeleteSpaceAction
├── RestPostSubscriptionAction
├── RestPostUpdateAction
├── RestPostLogtestAction
├── RestPostPromoteAction
└── RestGetPromoteAction

AbstractContentAction

Base class for all content CUD actions. It:

  • Overrides prepareRequest() from BaseRestHandler.
  • Initializes shared services: SpaceService, SecurityAnalyticsService, IntegrationService.
  • Validates that a Draft policy exists before executing any content action.
  • Delegates to the abstract executeRequest() method for concrete logic.

AbstractCreateAction / AbstractCreateActionSpaces

Handles POST requests to create new resources. AbstractCreateAction hard-codes the target space to draft. AbstractCreateActionSpaces reads the space from the request body instead, allowing draft or standard as the target.

The executeRequest() workflow:

  1. Validate request body — ensures the request has content and valid JSON.
  2. Validate payload structure — checks for required resource key and optional integration key.
  3. Resource-specific validation — delegates to validatePayload() (abstract). Concrete handlers check required fields, duplicate titles, and parent integration existence.
  4. Generate ID and metadata — creates a UUID, sets date and modified timestamps, defaults enabled to true.
  5. External sync — delegates to syncExternalServices() (abstract). Typically upserts the resource in SAP or validates via the Engine.
  6. Index — wraps the resource in the CTI document structure and indexes it in the Draft space.
  7. Link to parent — delegates to linkToParent() (abstract). Usually adds the new resource ID to a parent integration’s resource list.
  8. Update hash — recalculates the Draft space policy hash via SpaceService.

Returns 201 Created with the new resource UUID on success.

AbstractUpdateAction / AbstractUpdateActionSpaces

Handles PUT requests to update existing resources. AbstractUpdateAction restricts updates to the draft space. AbstractUpdateActionSpaces accepts a space value (draft or standard) from the request body.

The executeRequest() workflow:

  1. Validate ID — checks the path parameter is present and correctly formatted.
  2. Check existence and space — verifies the resource exists and belongs to the Draft space.
  3. Parse and validate payload — same structural checks as create.
  4. Resource-specific validation — delegates to validatePayload() (abstract).
  5. Update timestamps — sets modified timestamp. Preserves immutable fields (creation date, author) from the existing document.
  6. External sync — delegates to syncExternalServices() (abstract).
  7. Re-index — overwrites the document in the index.
  8. Update hash — recalculates the Draft space hash.

Returns 200 OK with the resource UUID on success.

AbstractDeleteAction / AbstractDeleteActionSpaces

Handles DELETE requests. AbstractDeleteAction restricts deletions to the draft space. AbstractDeleteActionSpaces resolves the target space from the stored document (allowing deletion from both draft and standard).

The executeRequest() workflow:

  1. Validate ID — checks format and presence.
  2. Check existence and space — resource must exist in Draft space.
  3. Pre-delete validation — delegates to validateDelete() (optional override). Can prevent deletion if dependent resources exist.
  4. External sync — delegates to deleteExternalServices() (abstract). Removes from SAP. Handles 404 gracefully.
  5. Unlink from parent — delegates to unlinkFromParent() (abstract). Removes the resource ID from the parent integration’s list.
  6. Delete from index — removes the document.
  7. Update hash — recalculates the Draft space hash.

Returns 200 OK with the resource UUID on success.


YAML Content-Type Support

Decoders, KVDBs, and Filters accept Content-Type: application/yaml requests in addition to JSON. This is implemented through an opt-in pattern in the abstract handler hierarchy.

Architecture

The YAML support is built on three mechanisms in AbstractContentAction:

  1. isYamlRequest(RestRequest) — Detects YAML content type via XContentType.YAML.equals(request.getMediaType()). Returns false on any exception (e.g., test mocks that don’t stub getMediaType()).

  2. supportsYamlField() — Returns false by default. Overridden to true in concrete handlers that support YAML field storage: RestPostDecoderAction, RestPutDecoderAction, RestPostKvdbAction, RestPutKvdbAction, RestPostFilterAction, RestPutFilterAction.

  3. YAML/JSON branching in executeRequest() — Both AbstractCreateAction and AbstractUpdateAction (and their *Spaces variants) branch on isYamlRequest() and supportsYamlField():

    • YAML path: Parses the body via YamlUtils.fromYaml(), then validates the envelope structure with the same validateResourcePayload() call as the JSON path. The rawYaml for the yaml field is generated from the resource subtree via YamlUtils.toYaml().
    • JSON path: Unchanged — parses via Jackson ObjectMapper.readTree().

Both paths converge after parsing: resource-specific validation, ID generation, external sync, and indexing are identical regardless of content type.

Envelope structure

YAML requests use the same envelope as JSON. The integration (or space for filters) and resource keys appear at the top level of the YAML document:

---
integration: <uuid>
resource:
  metadata:
    title: "My Resource"
  content: { ... }

This is parsed into a JsonNode tree identical to what the JSON path produces.

YAML field storage

When supportsYamlField() returns true, the handler populates a yaml field on the CTI wrapper before indexing:

  • YAML requests: rawYaml is generated from the parsed resource subtree (not the raw request body, which includes the envelope).
  • JSON requests: YamlUtils.toYaml(resourceNode) auto-generates the YAML representation.

The yaml field is stored as text in the index mappings (see cti-decoders-mappings.json, cti-kvdbs-mappings.json, engine-filters-mappings.json).

Type fidelity

YamlUtils is configured with DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS to preserve floating-point precision. A post-parse fixDecimalScale() step ensures values like 5.0 retain scale 1 in their BigDecimal representation, preventing coercion to integer 5 during serialization.

The ContentIndex.create() method skips processPayload() when it receives a fully-formed CTI wrapper (with document, space, and hash keys), avoiding a lossy valueToTree() round-trip that would strip BigDecimal scale.

Key classes

ClassRole
YamlUtilsYAML - JSON conversion with USE_BIG_DECIMAL_FOR_FLOATS, fixDecimalScale()
DecoderModel with yaml field, fromPayload() generates YAML from document
KvdbModel with yaml field, same pattern as Decoder
FilterModel with yaml field, same pattern as Decoder
AbstractContentActionisYamlRequest(), supportsYamlField() base methods
ContentIndexcreate() skips processPayload() for pre-built wrappers

Engine Communication

The plugin communicates with the Wazuh Engine via a Unix Domain Socket for validation and promotion of content.

EngineSocketClient

Located at: engine/client/EngineSocketClient.java

  • Connects to the socket at /usr/share/wazuh-indexer/engine/sockets/engine-api.sock.
  • Sends HTTP-over-UDS requests: builds a standard HTTP/1.1 request string (method, headers, JSON body) and writes it to the socket channel.
  • Each request opens a new SocketChannel (using StandardProtocolFamily.UNIX) that is closed after the response is read.
  • Parses the HTTP response, extracting the status code and JSON body.

EngineService Interface

Defines the Engine operations:

MethodDescription
logtest(JsonNode log)Forwards a log test payload to the Engine
validate(JsonNode resource)Validates a resource payload
promote(JsonNode policy)Validates a full policy for promotion
validateResource(String type, JsonNode resource)Wraps a resource with its type and delegates to validate()

EngineServiceImpl

Implementation using EngineSocketClient. Maps methods to Engine API endpoints:

MethodEngine EndpointHTTP Method
logtest()/logtestPOST
validate()/content/validate/resourcePOST
promote()/content/validate/policyPOST

Space Model

Resources live in spaces that represent their lifecycle stage. The Space enum defines four spaces:

SpaceDescription
STANDARDProduction-ready CTI resources from the upstream catalog
CUSTOMUser-created resources that have been promoted to production
DRAFTResources under development — all user edits happen here
TESTIntermediate space for validation before production

Promotion Flow

Spaces promote in a fixed chain:

DRAFT → TEST → CUSTOM

The Space.promote() method returns the next space in the chain. STANDARD and CUSTOM spaces cannot be promoted further.

SpaceService

Located at: cti/catalog/service/SpaceService.java

Manages space-related operations:

  • getSpaceResources(spaceName) — Fetches all resources (document IDs and hashes) from all managed indices for a given space.
  • promoteSpace(indexName, resources, targetSpace) — Copies documents from one space to another via bulk indexing, updating the space.name field.
  • calculateAndUpdate(targetSpaces) — Recalculates the aggregate SHA-256 hash for each policy in the given spaces. The hash is computed by concatenating hashes of the policy and all its linked resources (integrations, decoders, KVDBs, rules).
  • buildEnginePayload(...) — Assembles the full policy payload (policy + all resources from target space with modifications applied) for Engine validation during promotion.
  • deleteResources(indexName, ids, targetSpace) — Bulk-deletes resources from a target space.

Document Structure

Every resource document follows this envelope structure:

{
  "document": {
    "id": "<uuid>",
    "title": "...",
    "date": "2026-01-01T00:00:00Z",
    "modified": "2026-01-15T00:00:00Z",
    "enabled": true
  },
  "hash": {
    "sha256": "abc123..."
  },
  "space": {
    "name": "draft",
    "hash": {
      "sha256": "xyz789..."
    }
  }
}

Content Synchronization Pipeline

Overview

sequenceDiagram
    participant Scheduler as JobScheduler/RestAction
    participant SyncJob as CatalogSyncJob
    participant Synchronizer as ConsumerRulesetService
    participant ConsumerSvc as ConsumerService
    participant CTI as External CTI API
    participant Snapshot as SnapshotService
    participant Update as UpdateService
    participant Indices as Content Indices
    participant SAP as SecurityAnalyticsServiceImpl

    Scheduler->>SyncJob: Trigger Execution
    activate SyncJob

    SyncJob->>Synchronizer: synchronize()

    Synchronizer->>ConsumerSvc: getLocalConsumer() / getRemoteConsumer()
    ConsumerSvc->>CTI: Fetch Metadata
    ConsumerSvc-->>Synchronizer: Offsets & Metadata

    alt Local Offset == 0 (Initialization)
        Synchronizer->>Snapshot: initialize(remoteConsumer)
        Snapshot->>CTI: Download Snapshot ZIP
        Snapshot->>Indices: Bulk Index Content (Rules/Integrations/etc.)
        Snapshot-->>Synchronizer: Done
    else Local Offset < Remote Offset (Update)
        Synchronizer->>Update: update(localOffset, remoteOffset)
        Update->>CTI: Fetch Changes
        Update->>Indices: Apply JSON Patches
        Update-->>Synchronizer: Done
    end

    opt Changes Applied (onSyncComplete)
        Synchronizer->>Indices: Refresh Indices

        Synchronizer->>SAP: upsertIntegration(doc)
        loop For each Integration
            SAP->>SAP: WIndexIntegrationAction
        end

        Synchronizer->>SAP: upsertRule(doc)
        loop For each Rule
            SAP->>SAP: WIndexRuleAction
        end

        Synchronizer->>SAP: upsertDetector(doc)
        loop For each Integration
            SAP->>SAP: WIndexDetectorAction
        end

        Synchronizer->>Synchronizer: calculatePolicyHash()
    end

    deactivate SyncJob

Initialization Phase

When local_offset = 0:

  1. Downloads a ZIP snapshot from the CTI API.
  2. Extracts and parses JSON files for each content type.
  3. Bulk-indexes content into respective indices.
  4. Registers all content with the Security Analytics Plugin via SecurityAnalyticsServiceImpl.

Update Phase

When local_offset > 0 and local_offset < remote_offset:

  1. Fetches the changes in batches from the CTI API.
  2. Applies JSON Patch operations (add, update, delete).
  3. Pushes the changes to the Security Analytics Plugin via SecurityAnalyticsServiceImpl.
  4. Updates the local offset.

Post-Synchronization Phase

  1. Refreshes all content indices.
  2. Upserts integrations, rules, and detectors into the Security Analytics Plugin via SecurityAnalyticsServiceImpl.
  3. Recalculates SHA-256 hashes for policy integrity verification.
  4. Sets consumer status to idle in .wazuh-cti-consumers.

Error Handling

If a critical error or data corruption is detected, the system resets local_offset to 0, triggering a full snapshot re-initialization on the next run.


Configuration Settings

To register a new setting, follow the existing pattern in PluginSettings.java. That will make it available in opensearch.yml.

For existing settings, check Settings Reference

When registering a new setting, document it in the section linked above.

REST API URIs

All endpoints are under /_plugins/_content_manager. The URI constants are defined in PluginSettings:

ConstantValue
PLUGINS_BASE_URI/_plugins/_content_manager
SUBSCRIPTION_URI/_plugins/_content_manager/subscription
UPDATE_URI/_plugins/_content_manager/update
LOGTEST_URI/_plugins/_content_manager/logtest
RULES_URI/_plugins/_content_manager/rules
DECODERS_URI/_plugins/_content_manager/decoders
INTEGRATIONS_URI/_plugins/_content_manager/integrations
KVDBS_URI/_plugins/_content_manager/kvdbs
FILTERS_URI/_plugins/_content_manager/filters
PROMOTE_URI/_plugins/_content_manager/promote
POLICY_URI/_plugins/_content_manager/policy
SPACE_URI/_plugins/_content_manager/space

REST API Reference

The full API is defined in openapi.yml.

Logtest

The Indexer acts as a proxy between the UI and the Engine. POST /logtest accepts the payload and forwards it to the Engine via UDS. No validation is performed. If the Engine responds, its response is returned directly. If the Engine is unreachable, a 500 error is returned.

A testing policy must be loaded in the Engine for logtest to work. Load a policy via the policy promotion endpoint.

---
title: Logtest execution
---
sequenceDiagram
    actor User
    participant UI
    participant Indexer
    participant Engine

    User->>UI: run logtest
    UI->>Indexer: POST /logtest
    Indexer->>Engine: POST /logtest (via UDS)
    Engine-->>Indexer: response
    Indexer-->>UI: response

Content CUD (Rules, Decoders, Integrations, KVDBs)

All four resource types follow the same patterns via the abstract class hierarchy:

Create (POST):

sequenceDiagram
    actor User
    participant Indexer
    participant Engine/SAP as Engine or SAP
    participant ContentIndex
    participant IntegrationIndex

    User->>Indexer: POST /_plugins/_content_manager/{resource_type}
    Indexer->>Indexer: Validate payload, generate UUID, timestamps
    Indexer->>Engine/SAP: Sync (validate/upsert)
    Engine/SAP-->>Indexer: OK
    Indexer->>ContentIndex: Index in Draft space
    Indexer->>IntegrationIndex: Link to parent integration
    Indexer-->>User: 201 Created + UUID

Update (PUT):

sequenceDiagram
    actor User
    participant Indexer
    participant ContentIndex
    participant Engine/SAP as Engine or SAP

    User->>Indexer: PUT /_plugins/_content_manager/{resource_type}/{id}
    Indexer->>ContentIndex: Check exists + is in Draft space
    Indexer->>Indexer: Validate, preserve metadata, update timestamps
    Indexer->>Engine/SAP: Sync (validate/upsert)
    Indexer->>ContentIndex: Re-index document
    Indexer-->>User: 200 OK + UUID

Delete (DELETE):

sequenceDiagram
    actor User
    participant Indexer
    participant ContentIndex
    participant Engine/SAP as Engine or SAP
    participant IntegrationIndex

    User->>Indexer: DELETE /_plugins/_content_manager/{resource_type}/{id}
    Indexer->>ContentIndex: Check exists + is in Draft space
    Indexer->>Engine/SAP: Delete from external service
    Indexer->>IntegrationIndex: Unlink from parent
    Indexer->>ContentIndex: Delete document
    Indexer-->>User: 200 OK + UUID

Policy Update

The policy endpoint now accepts a {space} path parameter (draft or standard), allowing the same handler to serve both spaces with different validation rules.

  • Draft space — all policy fields are accepted. The integrations and filters arrays allow reordering but not adding or removing entries. author, description, documentation, and references are required in addition to the boolean fields.
  • Standard space — only enrichments, filters, enabled, index_unclassified_events, and index_discarded_events can be modified. All other fields are preserved from the existing standard policy document. After a successful update, if the standard space hash changed, the updated policy is automatically loaded into the Engine.
flowchart TD
    UI[UI] -->|"PUT /policy/{space}"| Indexer
    Indexer -->|Validate space| SpaceCheck{is a valid space?}
    SpaceCheck -->|No| Error400[400 Bad Request]
    SpaceCheck -->|Yes| Parse[Parse & validate fields]
    Parse --> SpaceBranch{Space?}
    SpaceBranch -->|draft| StoreDraft[Update draft policy in wazuh-threatintel-policies]
    SpaceBranch -->|standard| StoreStd[Merge allowed fields into standard policy]
    StoreDraft --> Hash[Recalculate space hash]
    StoreStd --> Hash
    Hash --> EngineCheck{Standard hash changed?}
    EngineCheck -->|Yes| Engine[Load standard space into Engine]
    EngineCheck -->|No| OK[200 OK]
    Engine --> OK

Policy Schema

The wazuh-threatintel-policies index stores policy configurations. See the Policy document structure above for the envelope format.

Policy document fields:

FieldTypeDescriptionEditable in standard space
idkeywordUnique identifierNo
titlekeywordHuman-readable nameNo
datedateCreation timestampNo
modifieddateLast modification timestampNo
root_decoderkeywordRoot decoder for event processingNo
integrationskeyword[]Active integration IDsNo
authorkeywordPolicy authorNo
descriptiontextBrief descriptionNo
documentationkeywordDocumentation linkNo
referenceskeyword[]External reference URLsNo
filterskeyword[]Filter UUIDs (reordering allowed, no add/remove)Yes
enrichmentskeyword[]Enrichment types (file, domain-name, ip, url, geo)Yes
enabledbooleanWhether the policy is activeYes
index_unclassified_eventsbooleanIndex events that match no ruleYes
index_discarded_eventsbooleanIndex events explicitly discarded by rulesYes

Filters CUD (Engine Filters)

Filters follow the same CUD pattern as other resource types but use the AbstractCreateActionSpaces / AbstractUpdateActionSpaces / AbstractDeleteActionSpaces hierarchy. The key difference is that the target space is supplied in the request body rather than being fixed to draft. Both draft and standard are accepted.

Filters are linked directly to their space’s policy document (the filters array) rather than to a parent integration.

Create (POST):

sequenceDiagram
    actor User
    participant Indexer
    participant Engine
    participant FilterIndex as wazuh-threatintel-filters
    participant PoliciesIndex as wazuh-threatintel-policies

    User->>Indexer: POST /_plugins/_content_manager/filters
    Indexer->>Indexer: Validate payload + space (draft|standard)
    Indexer->>Engine: validateResource("filter", resource)
    Engine-->>Indexer: OK
    Indexer->>FilterIndex: Index in target space
    Indexer->>PoliciesIndex: Add filter ID to space policy filters[]
    Indexer-->>User: 201 Created + UUID

Update (PUT):

sequenceDiagram
    actor User
    participant Indexer
    participant Engine
    participant FilterIndex as wazuh-threatintel-filters

    User->>Indexer: PUT /_plugins/_content_manager/filters/{id}
    Indexer->>FilterIndex: Check exists + validate space (draft|standard)
    Indexer->>Indexer: Validate payload
    Indexer->>Engine: validateResource("filter", resource)
    Engine-->>Indexer: OK
    Indexer->>FilterIndex: Re-index document
    Indexer-->>User: 200 OK + UUID

Delete (DELETE):

sequenceDiagram
    actor User
    participant Indexer
    participant FilterIndex as wazuh-threatintel-filters
    participant PoliciesIndex as wazuh-threatintel-policies

    User->>Indexer: DELETE /_plugins/_content_manager/filters/{id}
    Indexer->>FilterIndex: Check exists + resolve space
    Indexer->>PoliciesIndex: Remove filter ID from space policy filters[]
    Indexer->>FilterIndex: Delete document
    Indexer-->>User: 200 OK + UUID

Space Reset

flowchart TD
    UI[UI] -->|"DELETE /space/{space}"| Indexer
    Indexer -->|Validate space| Check{space == draft?}
    Check -->|No| Error400[400 Bad Request]
    Check -->|Yes| DeleteSAP[Delete draft resources from SAP]
    DeleteSAP --> DeleteCTI[Delete all draft documents from wazuh-threatintel-* indices]
    DeleteCTI --> RegenPolicy[Re-generate default draft policy]
    RegenPolicy --> OK[200 OK]

Only the draft space can be reset. Attempting to reset any other space returns 400 Bad Request. Failures in SAP cleanup are logged but do not block the reset — the primary goal is clearing the content indices and regenerating the policy.


Debugging

Check Consumer Status

GET /.wazuh-cti-consumers/_search
{
  "query": { "match_all": {} }
}

The status field indicates the sync lifecycle state:

  • ready — sync complete; content is safe to read.
  • running — sync in progress; content may be partially written.
  • failed — the previous sync cycle was interrupted by an unexpected exception.

To find consumers that are currently syncing or that failed mid-sync:

GET /.wazuh-cti-consumers/_search
{
  "query": { "terms": { "status": ["running", "failed"] } }
}

Check Content by Space

GET /wazuh-threatintel-rules/_search
{
  "query": { "term": { "space.name": "draft" } },
  "size": 10
}

Monitor Plugin Logs

tail -f var/log/wazuh-indexer/wazuh-cluster.log | grep -E "ContentManager|CatalogSyncJob|SnapshotServiceImpl|UpdateServiceImpl|AbstractContentAction"

Important Notes

  • The plugin only runs on cluster manager nodes.
  • CTI API must be accessible for content synchronization.
  • All user content CUD operations require a Draft policy to exist.
  • The Engine socket must be available at the configured path for logtest, validation, and promotion.
  • Offset-based synchronization ensures no content is missed.

🧪 Testing

The plugin includes integration tests defined in the tests/content-manager directory. These tests cover various scenarios for managing integrations, decoders, rules, and KVDBs through the REST API.

01 - Integrations: Create Integration (9 scenarios)

#Scenario
1Successfully create an integration
2Create an integration with the same title as an existing integration
3Create an integration with missing title
4Create an integration with missing author
5Create an integration with missing category
6Create an integration with an explicit id in the resource
7Create an integration with missing resource object
8Create an integration with empty body
9Create an integration without authentication

01 - Integrations: Update Integration (8 scenarios)

#Scenario
1Successfully update an integration
2Update an integration changing its title to a title that already exists in draft space
3Update an integration with missing required fields
4Update an integration that does not exist
5Update an integration with an invalid UUID
6Update an integration with an id in the request body
7Update an integration attempting to add/remove dependency lists
8Update an integration without authentication

01 - Integrations: Delete Integration (7 scenarios)

#Scenario
1Successfully delete an integration with no attached resources
2Delete an integration that has attached resources
3Delete an integration that does not exist
4Delete an integration with an invalid UUID
5Delete an integration without providing an ID
6Delete an integration not in draft space
7Delete an integration without authentication

02 - Decoders: Create Decoder (7 scenarios)

#Scenario
1Successfully create a decoder
2Create a decoder without an integration reference
3Create a decoder with an explicit id in the resource
4Create a decoder with an integration not in draft space
5Create a decoder with missing resource object
6Create a decoder with empty body
7Create a decoder without authentication

02 - Decoders: Update Decoder (7 scenarios)

#Scenario
1Successfully update a decoder
2Update a decoder that does not exist
3Update a decoder with an invalid UUID
4Update a decoder not in draft space
5Update a decoder with missing resource object
6Update a decoder with empty body
7Update a decoder without authentication

02 - Decoders: Delete Decoder (7 scenarios)

#Scenario
1Successfully delete a decoder
2Delete a decoder that does not exist
3Delete a decoder with an invalid UUID
4Delete a decoder not in draft space
5Delete a decoder without providing an ID
6Delete a decoder without authentication
7Verify decoder is removed from index after deletion

03 - Rules: Create Rule (7 scenarios)

#Scenario
1Successfully create a rule
2Create a rule with missing title
3Create a rule without an integration reference
4Create a rule with an explicit id in the resource
5Create a rule with an integration not in draft space
6Create a rule with empty body
7Create a rule without authentication

03 - Rules: Update Rule (7 scenarios)

#Scenario
1Successfully update a rule
2Update a rule with missing title
3Update a rule that does not exist
4Update a rule with an invalid UUID
5Update a rule not in draft space
6Update a rule with empty body
7Update a rule without authentication

03 - Rules: Delete Rule (7 scenarios)

#Scenario
1Successfully delete a rule
2Delete a rule that does not exist
3Delete a rule with an invalid UUID
4Delete a rule not in draft space
5Delete a rule without providing an ID
6Delete a rule without authentication
7Verify rule is removed from index after deletion

04 - KVDBs: Create KVDB (9 scenarios)

#Scenario
1Successfully create a KVDB
2Create a KVDB with missing title
3Create a KVDB with missing author
4Create a KVDB with missing content
5Create a KVDB without an integration reference
6Create a KVDB with an explicit id in the resource
7Create a KVDB with an integration not in draft space
8Create a KVDB with empty body
9Create a KVDB without authentication

04 - KVDBs: Update KVDB (7 scenarios)

#Scenario
1Successfully update a KVDB
2Update a KVDB with missing required fields
3Update a KVDB that does not exist
4Update a KVDB with an invalid UUID
5Update a KVDB not in draft space
6Update a KVDB with empty body
7Update a KVDB without authentication

04 - KVDBs: Delete KVDB (7 scenarios)

#Scenario
1Successfully delete a KVDB
2Delete a KVDB that does not exist
3Delete a KVDB with an invalid UUID
4Delete a KVDB not in draft space
5Delete a KVDB without providing an ID
6Delete a KVDB without authentication
7Verify KVDB is removed from index after deletion

05 - Policy: Policy Initialization (6 scenarios)

#Scenario
1The “wazuh-threatintel-policies” index exists
2Exactly four policy documents exist (one per space)
3Standard policy has a different document ID than draft/test/custom
4Draft, test, and custom policies start with empty integrations and root_decoder
5Each policy document contains the expected structure
6Each policy has a valid SHA-256 hash

05 - Policy: Update Draft Policy (12 scenarios)

#Scenario
1Successfully update the draft policy
2Update policy with missing type field
3Update policy with wrong type value
4Update policy with missing resource object
5Update policy with missing required fields in resource
6Update policy attempting to add an integration to the list
7Update policy attempting to remove an integration from the list
8Update policy with reordered integrations list (allowed)
9Update policy with empty body
10Update policy without authentication
11Verify policy changes are NOT reflected in test space until promotion
12Verify policy changes are reflected in test space after promotion

06 - Log Test (4 scenarios)

#Scenario
1Successfully test a log event
2Send log test with empty body
3Send log test with invalid JSON
4Send log test without authentication

07 - Promote: Preview Promotion (7 scenarios)

#Scenario
1Preview promotion from draft to test
2Preview promotion from test to custom
3Preview promotion with missing space parameter
4Preview promotion with empty space parameter
5Preview promotion with invalid space value
6Preview promotion from custom (not allowed)
7Preview promotion without authentication

07 - Promote: Execute Promotion (18 scenarios)

#Scenario
1Successfully promote from draft to test
2Verify resources exist in test space after draft to test promotion
3Verify promoted resources exist in both draft and test spaces
4Verify test space hash is regenerated after draft to test promotion
5Verify promoted resource hashes match between draft and test spaces
6Verify deleting a decoder in draft does not affect promoted test space
7Successfully promote from test to custom
8Verify resources exist in custom space after test to custom promotion
9Verify promoted resources exist in both test and custom spaces
10Verify custom space hash is regenerated after test to custom promotion
11Verify promoted resource hashes match between test and custom spaces
12Promote from custom (not allowed)
13Promote with invalid space
14Promote with missing changes object
15Promote with incomplete changes (missing required resource arrays)
16Promote with non-update operation on policy
17Promote with empty body
18Promote without authentication

Tutorial: Adding a REST Endpoint to the Content Manager Plugin

This tutorial walks through adding a new REST endpoint to the Content Manager plugin, using a concrete example: a GET endpoint to retrieve a single rule by ID.

By the end, you will have a working GET /_plugins/_content_manager/rules/{id} endpoint that fetches a rule document from the wazuh-threatintel-rules index.


Prerequisites

  • Development environment set up (see Setup)
  • The project compiles: ./gradlew :wazuh-indexer-content-manager:compileJava

Step 1: Add the URI Constant

If your endpoint uses a new base URI, add it to PluginSettings. In this case, rules already have RULES_URI, and our GET endpoint uses the same base path with an {id} parameter, so no changes are needed.

The existing constant in PluginSettings.java:

public static final String RULES_URI = PLUGINS_BASE_URI + "/rules";

Our endpoint will match /_plugins/_content_manager/rules/{id} using the same base URI.


Step 2: Create the Handler Class

Create a new file at:

plugins/content-manager/src/main/java/com/wazuh/contentmanager/rest/service/RestGetRuleAction.java
package com.wazuh.contentmanager.rest.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.opensearch.core.rest.RestStatus;
import org.opensearch.rest.BaseRestHandler;
import org.opensearch.rest.BytesRestResponse;
import org.opensearch.rest.RestHandler.Route;
import org.opensearch.rest.RestRequest;
import org.opensearch.transport.client.node.NodeClient;

import java.util.List;

import com.wazuh.contentmanager.cti.catalog.index.ContentIndex;
import com.wazuh.contentmanager.settings.PluginSettings;
import com.wazuh.contentmanager.utils.Constants;

/**
 * GET /_plugins/_content_manager/rules/{id}
 *
 * Retrieves a single rule document by its ID from the wazuh-threatintel-rules index.
 */
public class RestGetRuleAction extends BaseRestHandler {

    private static final Logger log = LogManager.getLogger(RestGetRuleAction.class);
    private static final ObjectMapper MAPPER = new ObjectMapper();

    // A short identifier for log output and debugging.
    private static final String ENDPOINT_NAME = "content_manager_rule_get";

    @Override
    public String getName() {
        return ENDPOINT_NAME;
    }

    /**
     * Define the route. The {id} path parameter is automatically extracted
     * by OpenSearch and available via request.param("id").
     */
    @Override
    public List<Route> routes() {
        return List.of(
                new Route(RestRequest.Method.GET, PluginSettings.RULES_URI + "/{id}"));
    }

    /**
     * Prepare and execute the request. This method is called by the
     * OpenSearch REST framework for each incoming request.
     *
     * @param request the incoming REST request
     * @param client  the node client for index operations
     * @return a RestChannelConsumer that writes the response
     */
    @Override
    protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) {
        // Extract the {id} path parameter.
        String id = request.param(Constants.KEY_ID);

        return channel -> {
            try {
                // Validate the ID parameter is present.
                if (id == null || id.isBlank()) {
                    channel.sendResponse(new BytesRestResponse(
                            RestStatus.BAD_REQUEST,
                            "application/json",
                            "{\"error\": \"Missing required parameter: id\"}"));
                    return;
                }

                // Use ContentIndex to retrieve the document.
                ContentIndex index = new ContentIndex(client, Constants.INDEX_RULES, null);
                JsonNode document = index.getDocument(id);

                if (document == null) {
                    channel.sendResponse(new BytesRestResponse(
                            RestStatus.NOT_FOUND,
                            "application/json",
                            "{\"error\": \"Rule not found: " + id + "\"}"));
                    return;
                }

                // Return the document as JSON.
                String responseBody = MAPPER.writeValueAsString(document);
                channel.sendResponse(new BytesRestResponse(
                        RestStatus.OK,
                        "application/json",
                        responseBody));

            } catch (Exception e) {
                log.error("Failed to retrieve rule [{}]: {}", id, e.getMessage(), e);
                channel.sendResponse(new BytesRestResponse(
                        RestStatus.INTERNAL_SERVER_ERROR,
                        "application/json",
                        "{\"error\": \"Internal server error: " + e.getMessage() + "\"}"));
            }
        };
    }
}

Key Concepts

  • getName() — Returns a short identifier used in logs and debugging.
  • routes() — Defines the HTTP method and URI pattern. Uses Route to register the endpoint with OpenSearch’s REST framework.
  • prepareRequest() — The core method. Returns a RestChannelConsumer lambda that executes asynchronously and writes the response to the channel.
  • Path parameters{id} in the route path is automatically parsed. Access it with request.param("id").

Step 3: Register the Handler

Open ContentManagerPlugin.java and add the new handler to getRestHandlers():

@Override
public List<RestHandler> getRestHandlers(
        Settings settings,
        RestController restController,
        ClusterSettings clusterSettings,
        IndexScopedSettings indexScopedSettings,
        SettingsFilter settingsFilter,
        IndexNameExpressionResolver indexNameExpressionResolver,
        Supplier<DiscoveryNodes> nodesInCluster) {
    return List.of(
            // ... existing handlers ...

            // Rule endpoints
            new RestPostRuleAction(),
            new RestPutRuleAction(),
            new RestDeleteRuleAction(),
            new RestGetRuleAction(),  // <-- Add the new handler

            // ... remaining handlers ...
    );
}

Make sure to add the import at the top of the file:

import com.wazuh.contentmanager.rest.service.RestGetRuleAction;

Step 4: Build and Verify

Compile the plugin to check for errors:

./gradlew :wazuh-indexer-content-manager:compileJava

If compilation succeeds, run the full build (including tests):

./gradlew :wazuh-indexer-content-manager:build

Step 5: Test the Endpoint

Manual Testing

Start a local cluster (see tools/test-cluster) and test:

# Create a rule first (so there's something to fetch)
curl -X POST "https://localhost:9200/_plugins/_content_manager/rules" \
  -H "Content-Type: application/json" \
  -u admin:admin --insecure \
  -d '{
    "integration": "<integration-id>",
    "resource": {
      "title": "Test Rule"
    }
  }'

# The response returns the UUID. Use it to fetch:
curl -X GET "https://localhost:9200/_plugins/_content_manager/rules/<uuid>" \
  -u admin:admin --insecure

Writing a Unit Test

Create a test file at:

plugins/content-manager/src/test/java/com/wazuh/contentmanager/rest/service/RestGetRuleActionTests.java

At minimum, test that getName() and routes() return expected values:

package com.wazuh.contentmanager.rest.service;

import org.opensearch.rest.RestRequest;
import org.opensearch.test.OpenSearchTestCase;

public class RestGetRuleActionTests extends OpenSearchTestCase {

    public void testGetName() {
        RestGetRuleAction action = new RestGetRuleAction();
        assertEquals("content_manager_rule_get", action.getName());
    }

    public void testRoutes() {
        RestGetRuleAction action = new RestGetRuleAction();
        assertEquals(1, action.routes().size());
        assertEquals(RestRequest.Method.GET, action.routes().get(0).getMethod());
        assertTrue(action.routes().get(0).getPath().contains("/rules/{id}"));
    }
}

Run:

./gradlew :wazuh-indexer-content-manager:test

Summary

To add a new REST endpoint to the Content Manager plugin:

  1. Create the handler class — Extend BaseRestHandler (for simple endpoints) or one of the abstract classes (AbstractCreateAction, AbstractUpdateAction, AbstractDeleteAction) for standard CUD.
  2. Define routes — Use Route to declare the HTTP method and URI pattern.
  3. Implement logic — Override prepareRequest() (or executeRequest() if extending the abstract hierarchy).
  4. Register — Add the instance to ContentManagerPlugin.getRestHandlers().
  5. Build and test./gradlew :wazuh-indexer-content-manager:compileJava then ./gradlew :wazuh-indexer-content-manager:test.

For content CUD endpoints that need Draft space validation, Engine sync, and hash updates, extend AbstractContentAction or one of its children instead of BaseRestHandler directly.

Logtest Architecture and Developer Guide

Component Overview

The logtest flow involves three layers:

RestPostLogtestAction               →  LogtestService  →  EngineService + SecurityAnalyticsService
RestPostLogtestNormalizationAction  →       ↑                    ↑
RestPostLogtestDetectionAction      →       ↑                    ↑
       (REST handlers)                (Orchestration)       (External services)

RestPostLogtestAction (combined)

Path: rest/service/RestPostLogtestAction.java

The REST handler for POST /_plugins/_content_manager/logtest. Responsibilities:

  1. Validates the request has content and is valid JSON.
  2. Validates the required field space.
  3. Validates that space is "test" or "standard".
  4. Extracts the optional integration field (if present) and strips it from the Engine payload.
  5. Delegates to LogtestService.executeLogtest(integrationId, space, enginePayload). If integrationId is null, only engine normalization is performed.

The handler does not interact with indices or external services directly, all business logic is in the service.

RestPostLogtestNormalizationAction

Path: rest/service/RestPostLogtestNormalizationAction.java

The REST handler for POST /_plugins/_content_manager/logtest/normalization. Responsibilities:

  1. Validates the request has content and is valid JSON.
  2. Validates the required field space.
  3. Validates that space is "test" or "standard".
  4. Strips the integration field if present (not used for normalization).
  5. Delegates to LogtestService.executeNormalization(enginePayload).

RestPostLogtestDetectionAction

Path: rest/service/RestPostLogtestDetectionAction.java

The REST handler for POST /_plugins/_content_manager/logtest/detection. Responsibilities:

  1. Validates the request has content and is valid JSON.
  2. Validates the required fields space, integration, and input.
  3. Validates that space is "test" or "standard".
  4. Validates that input is a JSON object (not a string or array).
  5. Delegates to LogtestService.executeDetection(integrationId, space, inputEvent).

LogtestService

Path: cti/catalog/service/LogtestService.java

The orchestrator. Provides three public entry points:

  • executeLogtest() — Full combined flow (normalization + detection)
  • executeNormalization() — Engine-only: forwards payload to EngineService.logtest() and returns the response directly with parseMessageAsJson()
  • executeDetection() — SAP-only: looks up integration, fetches rule IDs/bodies, evaluates via SecurityAnalyticsService.evaluateRules(), and returns the SAP result

The full logtest flow:

  1. No-integration shortcut — If integrationId is null, delegates to executeEngineOnly(): runs the Engine normalization and returns the result with detection.status: "skipped" and reason: "No integration provided". Steps 2–5 below are skipped.
  2. Integration lookup — Queries wazuh-threatintel-integrations for a document matching document.id == integrationId and space.name == space. Returns 400 if not found.
  3. Engine processing — Sends the event payload to the Wazuh Engine via EngineService.logtest(). Extracts the normalized event from the output field. The engine result fields (output, asset_traces, validation) are included directly in the response (no wrapper).
  4. Rule fetching — Extracts rule IDs from the integration’s document.rules array, then fetches rule bodies from wazuh-threatintel-rules by document.id, filtered by the same space.
  5. SAP evaluation — Passes the normalized event JSON and rule bodies to SecurityAnalyticsService.evaluateRules().
  6. Response building — Combines engine and SAP results into a single JSON response under the keys normalization and detection.

Error handling:

  • If the Engine fails (HTTP error or exception), SAP evaluation is skipped and the response includes status: "skipped" with the reason.
  • If no integration is provided, detection is skipped (normalization-only mode).
  • If the integration has no rules, SAP returns rules_evaluated: 0, rules_matched: 0 with success status.
  • If SAP evaluation returns unparseable JSON, the SAP result is status: "error".

SecurityAnalyticsService / EventMatcher

The SAP evaluation happens in the security-analytics:

  • SecurityAnalyticsServiceImpl.evaluateRules() — Parses Sigma rule YAML strings into SigmaRule objects, then delegates to EventMatcher.
  • EventMatcher.evaluate() — Flattens the normalized event JSON into dot-notation keys, then evaluates each rule’s detection conditions against the flat map. Returns a JSON result string.

The EventMatcher handles:

  • Field-equals-value conditions (exact match, case-insensitive)
  • Keyword (value-only) conditions (searches all event fields)
  • Wildcards (* for multi-char, ? for single-char) via cached compiled regex patterns
  • String modifiers: contains, startswith, endswith
  • Explicit regex (re modifier)
  • CIDR subnet matching (IPv4 and IPv6)
  • Boolean, numeric (gt, gte, lt, lte), null, and string comparisons
  • Composite conditions: AND, OR, NOT
  • List values (any element matching counts as a match)

Match results use a nested rule object per match entry:

{
  "rule": { "id": "...", "title": "...", "level": "...", "tags": [...] },
  "matched_conditions": [...]
}

Data Flow

Client request
    │
    ▼
RestPostLogtestAction (combined)
    │  validates request
    │  strips "integration" field
    ▼
LogtestService.executeLogtest(integrationId, space, payload)
    │
    ├──► [if integrationId == null]
    │       → executeEngineOnly(payload)
    │       → returns normalization + detection: { status: "skipped" }
    │
    ├──► client.prepareSearch("wazuh-threatintel-integrations")
    │       → finds integration in given space (test or standard)
    │       → extracts rule IDs from document.rules
    │
    ├──► engineService.logtest(payload)
    │       → sends to Wazuh Engine socket
    │       → receives normalized event
    │       → extracts "output" node as normalized event JSON
    │
    ├──► client.prepareSearch("wazuh-threatintel-rules")
    │       → fetches rule bodies by document.id + space filter
    │
    ├──► securityAnalytics.evaluateRules(normalizedEventJson, ruleBodies)
    │       → parses YAML → SigmaRule objects
    │       → EventMatcher flattens event + evaluates conditions
    │       → returns JSON result
    │
    └──► builds combined response
            { normalization: {...}, detection: {...} }

Split Endpoints

In addition to the combined flow, there are two dedicated endpoints that execute normalization and detection independently:

RestPostLogtestNormalizationAction           RestPostLogtestDetectionAction
    │  validates: space                          │  validates: space, integration, input
    │  strips integration field                  │
    ▼                                            ▼
LogtestService.executeNormalization(payload)  LogtestService.executeDetection(id, space, input)
    │                                            │
    └──► engineService.logtest(payload)          ├──► client.prepareSearch(".cti-integrations")
         → returns engine response directly      │       → finds integration
                                                 ├──► extractRuleIds() + fetchRuleBodies()
                                                 │       → fetches rule content from .cti-rules
                                                 └──► securityAnalytics.evaluateRules(inputJson, ruleBodies)
                                                         → returns SAP result directly

Key differences from the combined endpoint:

  • Normalization returns the raw Engine response (no detection wrapper). The integration field is stripped if present but has no effect on behavior.
  • Detection accepts a pre-normalized event as the input JSON object. It does not call the Engine — it goes straight to integration lookup → rule fetch → SAP evaluation.

Index Dependencies

IndexUsageQuery
wazuh-threatintel-integrationsLook up integration by ID in the given spacedocument.id == X AND space.name == {space}
wazuh-threatintel-rulesFetch rule bodies by document IDs in the given spacedocument.id IN [...] AND space.name == {space}

Both indices must exist and have document.id mapped as keyword for term queries to work.

Testing

Unit Tests

Test classCovers
RestPostLogtestActionTestsRequest validation for combined endpoint (empty body, invalid JSON, missing fields, wrong space, delegation to service)
RestPostLogtestNormalizationActionTestsRequest validation for normalization endpoint (empty body, invalid JSON, missing space, invalid space, delegation, integration stripping)
RestPostLogtestDetectionActionTestsRequest validation for detection endpoint (empty body, invalid JSON, missing fields, invalid space, non-object input, delegation)
LogtestServiceTestsOrchestration logic (integration lookup, engine errors, rule fetching, SAP evaluation, response structure)
EventMatcherTestsSigma rule evaluation (field matching, wildcards, numerics, booleans, nulls, AND/OR/NOT conditions)

Integration Tests

Test classCovers
LogtestITEnd-to-end REST workflow against a live test cluster (request validation, integration lookup, promote + logtest, response structure)

Integration tests extend ContentManagerRestTestCase and run against a real OpenSearch cluster. Since the Wazuh Engine is not available in the test environment, engine-dependent tests validate graceful error handling (engine error → SAP skipped).

Adding New Logtest Features

Supporting a new validation field

  1. Add the field constant to Constants.java.
  2. Add validation logic in the relevant handler(s): RestPostLogtestAction, RestPostLogtestNormalizationAction, and/or RestPostLogtestDetectionAction.
  3. Add unit tests in the corresponding test classes.
  4. Add integration test in LogtestIT.

Supporting a new Engine response field

  1. Update LogtestService.executeEngine() to extract the field.
  2. Include it in the normalization map within buildCombinedResponse().
  3. Add unit test scenarios in LogtestServiceTests.
  4. Update the API docs (api.md) response fields table.

Extending SAP evaluation

  1. Modify EventMatcher.matchValue() to handle new SigmaType subclasses.
  2. Add test cases in EventMatcherTests.
  3. Update the Sigma rules doc (Sigma Rules) if new detection modifiers are supported.

Wazuh Indexer Notifications Plugin — Development Guide

This document describes the architecture, components, and extension points of the Notifications plugin, which provides multi-channel notification capabilities to the Wazuh Indexer.


Overview

The Notifications plugin handles:

  • Channel Management: CRUD operations for notification channels (Slack, Email, Chime, Microsoft Teams, Webhooks, SNS, SES).
  • Message Delivery: Abstracts different communication protocols (SMTP, HTTP, AWS SES/SNS) into a unified transport layer.
  • Test Notifications: Allows sending test messages to validate channel configuration.
  • Plugin Features: Exposes dynamic feature discovery so other plugins can query supported notification types.
  • Security Integration: Integrates with the Wazuh Indexer Security plugin for RBAC-based access control.

Project Structure

The plugin is organized into three Gradle subprojects:

SubprojectDescription
notifications/core-spiService Provider Interface. Defines destination models (SlackDestination, SmtpDestination, ChimeDestination, etc.) and the NotificationCore contract.
notifications/coreCore implementation. Contains HTTP/SMTP/SES/SNS clients, transport providers, and all configurable settings (PluginSettings).
notifications/notificationsMain plugin module. Registers REST handlers, transport actions, index operations, metrics, and security access management.

Class Hierarchy

Destination Models (core-spi)

BaseDestination
├── SlackDestination
├── ChimeDestination
├── MicrosoftTeamsDestination
├── CustomWebhookDestination
├── WebhookDestination
├── SmtpDestination
├── SesDestination
└── SnsDestination

Transport Layer (core)

DestinationTransport (interface)
├── WebhookDestinationTransport      (Slack, Chime, Teams, Webhooks)
├── SmtpDestinationTransport         (SMTP Email)
├── SesDestinationTransport          (AWS SES Email)
└── SnsDestinationTransport          (AWS SNS)

REST Handlers (notifications)

HandlerMethodURI
NotificationConfigRestHandlerPOST/_plugins/_notifications/configs
PUT/_plugins/_notifications/configs/{config_id}
GET/_plugins/_notifications/configs/{config_id}
GET/_plugins/_notifications/configs
DELETE/_plugins/_notifications/configs/{config_id}
DELETE/_plugins/_notifications/configs
NotificationFeaturesRestHandlerGET/_plugins/_notifications/features
NotificationChannelListRestHandlerGET/_plugins/_notifications/channels
SendTestMessageRestHandlerPOST/_plugins/_notifications/feature/test/{config_id}
NotificationStatsRestHandlerGET/_plugins/_notifications/_local/stats

Setup Environment

Requirements

  • JDK: version 11 or 17 (depending on the target Wazuh Indexer version).
  • Gradle: Use the included ./gradlew wrapper (no separate install needed).
  • IDE: IntelliJ IDEA with Kotlin plugin is recommended.

Clone and Build

git clone <notifications-repo-url>
cd wazuh-indexer-notifications
./gradlew build

The distribution zip will be generated at:

notifications/notifications/build/distributions/

Build Packages

To create distribution packages:

# Full build (compile + test + assemble)
./gradlew build

# Assemble only (skip tests)
./gradlew assemble

The output zip can be installed on a running Wazuh Indexer using:

bin/opensearch-plugin install file:///path/to/notifications-<version>.zip

Run Tests

Unit Tests

./gradlew test

Integration Tests

The integration test suite is located at:

notifications/notifications/src/test/kotlin/org/opensearch/integtest/

To execute the full integration test suite:

./gradlew :notifications:notifications:integTest

Key integration test classes:

Test ClassDescription
SlackNotificationConfigCrudITFull CRUD lifecycle for Slack channels.
ChimeNotificationConfigCrudITFull CRUD lifecycle for Chime channels.
EmailNotificationConfigCrudITFull CRUD lifecycle for Email channels (SMTP/SES).
MicrosoftTeamsNotificationConfigCrudITFull CRUD lifecycle for Microsoft Teams channels.
WebhookNotificationConfigCrudITFull CRUD lifecycle for custom webhooks.
SnsNotificationConfigCrudITFull CRUD lifecycle for SNS channels.
CreateNotificationConfigITConfig creation edge cases and validation.
DeleteNotificationConfigITConfig deletion including bulk delete.
QueryNotificationConfigITFiltering, sorting, and pagination queries.
GetPluginFeaturesITFeature discovery endpoint tests.
GetNotificationChannelListITChannel list endpoint tests.
SendTestMessageRestHandlerITTest message delivery flow.
SendTestMessageWithMockServerITTest message with mock destination.
SecurityNotificationITRBAC and access control tests.
MaxHTTPResponseSizeITHTTP response size limit enforcement.
NotificationsBackwardsCompatibilityITBackwards compatibility between versions.

Notification Flow

The data flow when sending a notification follows this sequence:

Monitor/Alerting Plugin
        │
        ▼
Notification Plugin Interface (REST / Transport)
        │
        ▼
Security Plugin (verify permissions)
        │
        ▼
.notifications index (persist notification, status = pending)
        │
        ▼
Transport Action (resolve destination type)
        │
        ├──► WebhookDestinationTransport ──► Slack / Chime / Teams / Custom Webhook
        ├──► SmtpDestinationTransport    ──► External SMTP Server
        ├──► SesDestinationTransport     ──► AWS SES
        └──► SnsDestinationTransport     ──► AWS SNS
                                                  │
                                                  ▼
                                             Recipient
  1. An internal plugin (Alerting, Reporting, ISM) or a user invokes the Notification plugin via Transport or REST API.
  2. The Security plugin verifies the caller’s permissions.
  3. The notification is persisted in the .notifications index with pending status.
  4. The DestinationTransportProvider resolves the correct transport based on the channel type.
  5. The transport client delivers the message to the external service.
  6. On failure, retries are attempted up to the configured limit.
  7. The notification status is updated to sent or failed.

Default Channel Initialization

The plugin creates a set of default notification channels on startup so that users have pre-configured templates for common integrations (Slack, Jira, PagerDuty, Shuffle). These channels are created disabled with placeholder URLs.

Implementation

The feature is implemented in DefaultChannelInitializer (notifications/notifications/src/main/kotlin/.../index/DefaultChannelInitializer.kt).

Adding or Modifying Default Channels

To add a new default channel:

  1. Add a new ChannelDefinition entry to the DEFAULT_CHANNELS list in DefaultChannelInitializer.kt.
  2. Choose a unique, stable id prefixed with default_ (e.g., default_teams_channel).
  3. Set isEnabled = false and use a placeholder URL with clear instructions in the description.
  4. Add a corresponding test case in DefaultChannelInitializerTests.kt.

ClusterPlugin Interface

The NotificationPlugin class implements ClusterPlugin to gain access to the onNodeStarted(DiscoveryNode) lifecycle hook.

Testing

Unit tests for the default channel initialization are in:

notifications/notifications/src/test/kotlin/.../index/DefaultChannelInitializerTests.kt

The tests verify:

  • All default channel definitions have valid configurations.
  • Channel IDs are unique and follow the naming convention.
  • The initializer correctly identifies missing channels and skips existing ones.

Extending with a New Destination

To add a new notification destination:

  1. Define the destination model in core-spi:

    • Create a new class extending BaseDestination in notifications/core-spi/src/main/kotlin/.../destination/.
  2. Implement the transport in core:

    • Create a new class implementing DestinationTransport in notifications/core/src/main/kotlin/.../transport/.
    • Register it in DestinationTransportProvider.
  3. Add the config type to the DEFAULT_ALLOWED_CONFIG_TYPES list in core/setting/PluginSettings.kt.

  4. Write tests: Add integration tests in notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/.

Security Analytics

The Security Analytics Plugin (SAP) is a fork of the OpenSearch Security Analytics plugin adapted for Wazuh. This page documents Wazuh-specific implementation details and extensions.

Case Management

Case management adds triage capabilities to Security Analytics findings, allowing analysts to track status, comments, tags, and user attribution on individual findings.

Case fields

WCS fieldOpenSearch typeDescription
wazuh.case.statuskeywordWorkflow status: ACTIVE, ACKNOWLEDGED, COMPLETED, ERROR, DELETED, AUDIT
wazuh.case.commentmatch_only_textFree-form analyst comment
wazuh.case.tagskeyword (array)Organizational tags
wazuh.case.created_atdateCase creation timestamp
wazuh.case.updated_atdateLast update timestamp
wazuh.case.user.namekeywordUser who performed the update

These fields are present in the index template but not populated at finding creation time, they are written exclusively through the update endpoint.

REST endpoint

RestUpdateFindingsAction

File: src/main/java/org/opensearch/securityanalytics/resthandler/RestUpdateFindingsAction.java

Route: PUT /_plugins/_security_analytics/findings/_update

Design decisions

  1. Bulk-based: the endpoint allows up to 50 finding updates per call.

  2. Partial doc update: uses UpdateRequest.doc() which merges the provided fields into the existing document. Only wazuh.case is touched, other finding fields are never modified.

Request validation

The handler performs eager validation before building the bulk request:

CheckHTTP statusMessage
Invalid/missing JSON body400Invalid JSON body: ...
Missing findings array400Request body must contain a "findings" array
Empty findings array400Findings array is empty
More than 50 items400Cannot update more than 50 findings at once
Element not a JSON object400Element at index N is not a JSON object
Missing _id400Element at index N is missing _id
Missing _index400Element at index N is missing _index
Missing/invalid case400Element at index N is missing or invalid case object

Validation errors short-circuit, the first error aborts the entire request.

Response format

{
  "took": 12,
  "errors": false,
  "items": [
    {
      "_id": "...",
      "_index": "...",
      "status": 200,
      "result": "updated"
    }
  ]
}
  • On full success: HTTP 200
  • On partial failure (some docs not found): HTTP 207 MULTI_STATUS
  • On total bulk failure: HTTP 500

Registration

The handler is registered in SecurityAnalyticsPlugin.getRestHandlers():

new RestUpdateFindingsAction()

Testing

Integration tests live in src/test/java/org/opensearch/securityanalytics/resthandler/UpdateFindingsIT.java.

The test class extends SecurityAnalyticsRestTestCase and covers:

  • Happy path: single update with all fields, partial updates, bulk updates, overwrite scenarios
  • Validation: empty array, missing fields (_id, _index, case), invalid JSON, exceeding max bulk items
  • Error handling: non-existent document (expects 207), response structure verification
  • Helpers: creates a temporary index with the wazuh.case mapping and indexes minimal finding documents for testing

Tests use the REST test client (makeRequest) and don’t require a full detector/monitor setup since the endpoint operates directly on documents by _id and _index.

Sequence diagram

sequenceDiagram
    participant UI as Wazuh Dashboard
    participant SAP as Security Analytics Plugin
    participant OS as OpenSearch (Bulk API)
    participant FI as Findings Index

    UI->>SAP: PUT /findings/_update { findings: [...] }
    SAP->>SAP: Validate request (JSON, required fields, limits)
    alt Validation fails
        SAP-->>UI: 400 Bad Request
    else Validation passes
        SAP->>OS: BulkRequest (UpdateRequest per finding)
        OS->>FI: Update doc (merge wazuh.case)
        FI-->>OS: Update result
        OS-->>SAP: BulkResponse
        alt All succeeded
            SAP-->>UI: 200 OK { took, errors: false, items }
        else Partial failure
            SAP-->>UI: 207 Multi-Status { took, errors: true, items }
        end
    end

Description

The Wazuh Indexer is a highly scalable, full-text search and analytics engine built on top of OpenSearch 3. In Wazuh 5.0 it becomes the core component of the Wazuh platform: in addition to indexing and storing security data, it now embeds the Wazuh Engine and hosts the threat detection, alerting, notification, reporting, and content management logic that previously ran on the Wazuh Manager.

The Wazuh Indexer can be deployed as a single-node instance for development and small environments, or as a multi-node cluster for production workloads requiring high availability and horizontal scalability.

What’s new in 5.0

Wazuh 5.0 consolidates most of the platform’s data plane and detection logic inside the Indexer:

  • The Wazuh Engine is bundled into the Wazuh Indexer packages and Docker images (x86_64 and aarch64). Plugins communicate with the Engine over a local Unix socket.
  • Threat detection has been migrated from the Wazuh Manager to the Indexer through the Security Analytics plugin (a Wazuh fork of the OpenSearch Security Analytics plugin), with extended Sigma rules syntax and per-space rules, log types and detectors.
  • Active Response has been migrated to the Indexer, driven by a dedicated Alerting monitor and persisted in the wazuh-active-responses data stream.
  • Filebeat is no longer used to forward events between the Wazuh Manager and the Indexer. Events now reach the Indexer through a built-in indexer connector.
  • Time-series data (events, findings, metrics, raw events, active responses) is stored in data streams with ISM policies for automatic rollover and retention.
  • A new Content Manager plugin owns the lifecycle of detection content (ruleset, vulnerabilities feed, IoC feed) and exposes a REST API for user-defined content with a draft → test → custom promotion workflow.
  • The Wazuh Common Schema (WCS) has been reworked and now lives in the wazuh-indexer-plugins repository, bumped to ECS 9.1.0, with per-category event and finding data streams.

See the release notes for the full list of changes and breaking changes.

Core concepts

The Wazuh Indexer stores data as JSON documents. Each document contains a set of fields (keys) mapped to values — strings, numbers, booleans, dates, arrays, nested objects, and more.

An index is a collection of related documents. For time-series data such as events, findings, metrics and active responses, the Wazuh Indexer uses data streams backed by rolling indices, managed by Index State Management (ISM) policies that handle rollover and retention based on age and size.

Documents are distributed across shards spread across cluster nodes. This distribution provides redundancy against hardware failures and allows query throughput to scale as nodes are added.

Detection content (rules, decoders, integrations, KVDBs, filters, policies, IoCs and the vulnerabilities feed) is organized into spaces:

  • standard — read-only content sourced from the Wazuh CTI API.
  • draft — user-editable workspace for new or modified resources.
  • test — staging space used to validate draft content against the Wazuh Engine.
  • custom — promoted user-defined content active in the Engine.

The Content Manager enforces the draft → test → custom promotion workflow and keeps the Engine synchronized with the active content.

Bundled plugins

The Wazuh Indexer ships with a curated set of plugins. Some are Wazuh-developed; others are Wazuh forks of upstream OpenSearch plugins tailored for the platform.

Setup plugin

The Setup plugin initializes the indexer environment on cluster startup. It creates all required index templates, ISM policies, data streams, and internal state indices, ensuring the correct schema and lifecycle rules are in place before any data is ingested. It also defines the Wazuh Common Schema — the standardized field mappings used across all Wazuh indices — and exposes a Settings API used to manage Wazuh-level settings that now reside in the Indexer (including Engine settings).

Content Manager plugin

The Content Manager keeps the Wazuh detection content up to date. It synchronizes the ruleset, vulnerabilities feed, and IoC feed from the Wazuh CTI API, and provides a REST API for user-defined threat intelligence resources — rules, decoders, integrations, KVDBs, filters and policies — supporting drafting, testing, promotion, manual or scheduled updates, subscription management and version checks.

A daily ping to the Wazuh CTI API surfaces content updates and deployment telemetry. The plugin communicates with the bundled Wazuh Engine through a Unix socket to validate user content and execute the logtest feature, which is split into a normalization phase (decoders) and a detection phase (rules).

A snapshot of the ruleset, vulnerabilities feed and IoC feed is bundled with the Wazuh Indexer packages, so a freshly installed cluster has content available offline.

See Content Manager for details.

Security Analytics plugin

A Wazuh fork of the OpenSearch Security Analytics plugin. It is the home of threat detection in 5.0:

  • Per-space log types, rules and threat detectors.
  • Extended Sigma rules syntax, including case-insensitive operators, the exists modifier, IPv6 support and dynamic event field referencing in findings.
  • Enriched findings written to wazuh-findings-v5-{category} data streams, embedding the full triggering event source and rule metadata (id, title, tags, level, status, MITRE, compliance).

Alerting plugin

A Wazuh fork of the OpenSearch Alerting plugin. It provides real-time alerting based on predefined monitors. Monitors are the core component used by the Security Analytics plugin for threat detection, and a dedicated monitor drives Active Response.

Notifications plugin

A Wazuh fork of the OpenSearch Notifications plugin. It supports multiple delivery channels — Slack, Microsoft Teams, Amazon Chime, Email (SMTP/SES), AWS SNS, and custom webhooks — and ships default webhooks for Slack, Jira, PagerDuty and Shuffle. It also provides a dedicated channel type that powers Active Response, batching execution requests through a bulk processor.

Reporting plugin

A Wazuh fork of the OpenSearch Reporting plugin, bundled by default in Wazuh Indexer packages. It generates PDF and CSV reports from dashboards and saved searches, on demand or on a schedule, with optional email delivery.

Common Utils library

A Wazuh fork of the OpenSearch Common Utils library. It provides the shared models and transport actions used across the Wazuh forks of Alerting, Notifications, Security Analytics and the Content Manager — including the Active Response channel definition.

Security plugin

The Security plugin provides role-based access control (RBAC), user authentication, and TLS encryption for both the REST API and inter-node transport layers. Wazuh 5.0 ships with a new set of reserved users and roles aligned with the new plugins (Content Manager, Alerting, Notifications, Reporting, Security Analytics). See Access Control for details.

Bundled Wazuh Engine

The Wazuh Engine is shipped inside the Wazuh Indexer packages and Docker images. It is responsible for:

  • Decoding and normalizing events before they are indexed (only in Wazuh Manager).
  • Validating user-defined threat intel content submitted through the Content Manager.
  • Enrichment: IoC content management, GeoIP enrichment, and engine filters for event pre-processing.
  • Executing logtest requests issued by the Content Manager.

The Engine listens on a local Unix socket with restricted permissions (750) and is reachable only by the Indexer plugins running on the same node.

Data storage

The Wazuh Indexer organizes data into purpose-specific indices and data streams. Time-series streams are categorized per event type (access management, applications, cloud services, network activity, security, system activity, unclassified).

Index patternDescription
wazuh-events-v5-{cat}Decoded and normalized security events from monitored endpoints, per category.
wazuh-events-raw-v5Raw incoming events, retained briefly under an aggressive ISM purge policy (gated by an Engine setting).
wazuh-findings-v5-{cat}Enriched findings produced by Security Analytics, embedding the triggering event and rule metadata.
wazuh-states-v5-*Stateful inventory data (vulnerabilities, packages, ports, FIM, services, browser extensions, SCA, etc.).
wazuh-active-responsesActive Response execution requests, driven by a dedicated Alerting monitor.
wazuh-metrics-*Agent and communications telemetry metrics.
wazuh-threatintel-*Content Manager system indices for CTI content (rules, decoders, integrations, KVDBs, filters, policies, IoCs).
.wazuh-cti-consumersInternal index tracking consumer state for CTI synchronization.

Agent and rule metadata is now relocated under the wazuh.* namespace, and inventory coverage has been extended to Linux systemd units and macOS launchd daemons/agents alongside Windows services. For a complete list of indices and their schemas, see the Setup Plugin documentation.

Integration with the Wazuh platform

In 5.0 the Wazuh Indexer is the central processing and storage tier of the platform:

  • Wazuh Agents collect endpoint data and send it to the Wazuh Manager.
  • Wazuh Manager acts as the ingestion gateway. It no longer runs analysis, threat detection, content management or active response — these have moved into the Indexer. Events are normalized and forwarded to the Indexer through the built-in indexer connector (Filebeat is no longer required).
  • Wazuh Indexer, through the bundled Wazuh Engine and its plugins, analyzes events, runs threat detection, manages detection content, dispatches notifications and active responses, and stores all resulting data.
  • Wazuh Dashboard (an OpenSearch Dashboards fork) provides the web UI for searching, visualizing and managing Wazuh data, and interacts with the Setup, Content Manager, Security Analytics, Alerting, Notifications and Reporting plugin APIs.

The Indexer exposes a standard REST API compatible with the OpenSearch API, so existing OpenSearch tools, clients and integrations work with the Wazuh Indexer out of the box.

Architecture

The Wazuh Indexer is built on top of OpenSearch and extends it with a set of purpose-built plugins that provide security event indexing, content management, access control, and reporting capabilities.

Component Overview

┌─────────────────────────────────────────────────────────────────────┐
│                        Wazuh Indexer                                │
│                                                                     │
│  ┌──────────────┐  ┌──────────────────┐  ┌──────────┐  ┌─────────┐  │
│  │ Setup Plugin │  │ Content Manager  │  │ Security │  │Reporting│  │
│  │              │  │     Plugin       │  │  Plugin  │  │ Plugin  │  │
│  └──────┬───────┘  └────────┬─────────┘  └────┬─────┘  └───┬─────┘  │
│         │                   │                 │            │        │
│  ┌──────┴────────┐   ┌──────┴───────────┐  ┌──┴───────┐    │        │
│  │Index Templates│   │  CTI API Client  │  │  RBAC &  │    │        │
│  │ISM Policies   │   │  Engine Client   │  │  Access  │    │        │
│  │Stream Indices │   │  Job Scheduler   │  │  Control │    │        │
│  │State Indices  │   │  Space Service   │  └──────────┘    │        │
│  └───────────────┘   └───────┬──────────┘                  │        │
│                              │                             │        │
│                    ┌─────────┴───────────────────────┐     │        │
│                    │  System Indices                 │     │        │
│                    │  .wazuh-cti-consumers           │     │        │
│                    │  wazuh-threatintel-rules        │     │        │
│                    │  wazuh-threatintel-decoders     │     │        │
│                    │  wazuh-threatintel-integrations │     │        │
│                    │  wazuh-threatintel-kvdbs        │     │        │
│                    │  wazuh-threatintel-policies     │     │        │
│                    │  wazuh-threatintel-enrichments  │     │        │
│                    └─────────────────────────────────┘     │        │
└─────────────────────────────────┬──────────────────────────┼────────┘
                                  │ Unix Socket              │
                          ┌───────┴────────┐          ┌──────┴───────┐
                          │  Wazuh Engine  │          │  Wazuh       │
                          │  (Analysis &   │          │  Dashboard   │
                          │   Detection)   │          │  (UI)        │
                          └────────────────┘          └──────────────┘

Plugins

Setup Plugin

The Setup plugin initializes the Wazuh Indexer environment when the cluster starts. It is responsible for:

  • Index templates: Defines the mappings and settings for all Wazuh indices (alerts, events, statistics, vulnerabilities, etc.).
  • ISM (Index State Management) policies: Configures lifecycle policies for automatic rollover, deletion, and retention of time-series indices.
  • Data streams: Creates the initial data stream indices that receive incoming event data.
  • State indices: Sets up internal indices used by other Wazuh components to track operational state.

The Setup plugin runs once during cluster initialization and ensures the required infrastructure is in place before other plugins begin operating.

Content Manager Plugin

The Content Manager is the most feature-rich plugin. It handles:

  • CTI synchronization: Periodically fetches threat intelligence content (rules, decoders, integrations, KVDBs, IoCs) from the Wazuh CTI API. On first run, it downloads a full snapshot; subsequent runs apply incremental patches.
  • User-generated content: Provides a REST API for creating, updating, and deleting custom decoders, rules, integrations, and KVDBs in a draft space.
  • Promotion workflow: Changes made in the draft space can be previewed and promoted to the Wazuh Engine for activation.
  • Engine communication: Communicates with the Wazuh Engine via a Unix socket for logtest execution, content validation, and configuration reload.
  • Policy management: Manages the Engine routing policy that controls how events are processed.

See Content Manager for full details.

Security Plugin

The Security plugin extends OpenSearch’s security capabilities for Wazuh-specific needs:

  • Role-based access control (RBAC): Defines predefined roles and permissions for Wazuh operations.
  • User management: Provides APIs and configuration for managing users and their access levels.
  • TLS/SSL: Handles transport and REST layer encryption.

Reporting Plugin

The Reporting plugin enables on-demand and scheduled report generation from the Wazuh Dashboard, producing PDF or CSV exports of dashboards and saved searches.

Data Flow

  1. Wazuh Agents collect security events from monitored endpoints and forward them to the Wazuh Manager.
  2. The Wazuh Engine on the server analyzes events using rules and decoders, then forwards alerts and events to the Wazuh Indexer via the Indexer API.
  3. The Setup Plugin ensures the correct index templates, data streams, and lifecycle policies exist.
  4. The Content Manager Plugin keeps the Engine’s detection content up to date by synchronizing with the CTI API and managing user customizations.
  5. The Wazuh Dashboard queries the Indexer to visualize alerts, events, and security analytics.

Compatibility

Supported operating systems

We aim to support as many operating systems as OpenSearch does. Wazuh indexer should work on many Linux distributions, but we only test a handful. The following table lists the operating system versions that we currently support.

For 5.0.0 and above, we support the operating system versions and architectures included in the table below.

NameVersionArchitecture
Red Hat9, 10x86_64, aarch64
Ubuntu22.04, 24.04x86_64, aarch64
Amazon Linux2023x86_64, aarch64

OpenSearch

Currently, Wazuh indexer is using version 3.5.0 of OpenSearch.

Getting Started

Requirements

Hardware recommendations

The Wazuh indexer can be installed as a single-node or as a multi-node cluster.

Hardware recommendations for each node

Minimum Recommended
Component RAM (GB) CPU (cores) RAM (GB) CPU (cores)
Wazuh indexer 8 4 32 8

Disk space requirements

The amount of data depends on the generated events per second (EPS). This table details the estimated disk space needed per agent to store 90 days of events on a Wazuh indexer server, depending on the type of monitored endpoints.

Monitored endpointsEPSStorage in Wazuh indexer (GB/90 days)
Servers0.253.7
Workstations0.11.5
Network devices0.57.4

For example, for an environment with 80 workstations, 10 servers, and 10 network devices, the storage needed on the Wazuh indexer server for 90 days of events is 230 GB.

Installation

Note: This documentation assumes you are already provisioned with a wazuh-indexer package through any of the possible methods:

Installing the Wazuh indexer step by step

Install and configure the Wazuh indexer as a single-node or multi-node cluster, following step-by-step instructions. The installation process is divided into three stages.

  1. Certificates creation

  2. Nodes installation

  3. Cluster initialization

Note: You need root user privileges to run all the commands described below.

1. Certificates creation

Generating the SSL certificates

  1. Download the wazuh-certs-tool.sh script and the config.yml configuration file. This creates the certificates that encrypt communications between the Wazuh central components.

    curl -sO https://packages-dev.wazuh.com/5.0/wazuh-certs-tool.sh
    curl -sO https://packages-dev.wazuh.com/5.0/config.yml
    
  2. Edit ./config.yml and replace the node names and IP values with the corresponding names and IP addresses. You need to do this for all Wazuh Manager, Wazuh indexer, and Wazuh dashboard nodes. Add as many node fields as needed.

    nodes:
      # Wazuh indexer nodes
      indexer:
        - name: node-1
          ip: "<indexer-node-ip>"
        #- name: node-2
        #  ip: "<indexer-node-ip>"
        #- name: node-3
        #  ip: "<indexer-node-ip>"
    
      # Wazuh manager nodes
      # If there is more than one Wazuh manager
      # node, each one must have a node_type
      manager:
        - name: wazuh-1
          ip: "<wazuh-manager-ip>"
        #  node_type: master
        #- name: wazuh-2
        #  ip: "<wazuh-manager-ip>"
        #  node_type: worker
        #- name: wazuh-3
        #  ip: "<wazuh-manager-ip>"
        #  node_type: worker
    
      # Wazuh dashboard nodes
      dashboard:
        - name: dashboard
          ip: "<dashboard-node-ip>"
    

    To learn more about how to create and configure the certificates, see the Certificates deployment section.

  3. Run ./wazuh-certs-tool.sh to create the certificates. For a multi-node cluster, these certificates need to be later deployed to all Wazuh instances in your cluster.

    ./wazuh-certs-tool.sh -A
    
  4. Compress all the necessary files.

    tar -cvf ./wazuh-certificates.tar -C ./wazuh-certificates/ .
    rm -rf ./wazuh-certificates
    
  5. Copy the wazuh-certificates.tar file to all the nodes, including the Wazuh indexer, Wazuh Manager, and Wazuh dashboard nodes. This can be done by using the scp utility.

2. Nodes installation

Installing package dependencies

Install the following packages if missing:

yum
yum install coreutils
apt
apt-get install debconf adduser procps

Installing the Wazuh indexer package

rpm
rpm -ivh --replacepkgs wazuh-indexer-<VERSION>.rpm
dpkg
dpkg -i wazuh-indexer-<VERSION>.deb

Configuring the Wazuh indexer

Edit the /etc/wazuh-indexer/opensearch.yml configuration file and replace the following values:

a. network.host: Sets the address of this node for both HTTP and transport traffic. The node will bind to this address and use it as its publish address. Accepts an IP address or a hostname. Use the same node address set in config.yml to create the SSL certificates.

b. node.name: Name of the Wazuh indexer node as defined in the config.yml file. For example, node-1.

c. cluster.initial_cluster_manager_nodes: List of the names of the master-eligible nodes. These names are defined in the config.yml file. Uncomment the node-2 and config.yml and node-3 lines, change the names, or add more lines, according to your config.yml definitions.

cluster.initial_cluster_manager_nodes:
- "node-1"
- "node-2"
- "node-3"

d. discovery.seed_hosts: List of the addresses of the master-eligible nodes. Each element can be either an IP address or a hostname. You may leave this setting commented if you are configuring the Wazuh indexer as a single node. For multi-node configurations, uncomment this setting and set the IP addresses of each master-eligible node.

discovery.seed_hosts:
- "10.0.0.1"
- "10.0.0.2"
- "10.0.0.3"

e. plugins.security.nodes_dn: List of the Distinguished Names of the certificates of all the Wazuh indexer cluster nodes. Uncomment the lines for node-2 and node-3 and change the common names (CN) and values according to your settings and your config.yml definitions.

plugins.security.nodes_dn:
- "CN=node-1,OU=Wazuh,O=Wazuh,L=California,C=US"
- "CN=node-2,OU=Wazuh,O=Wazuh,L=California,C=US"
- "CN=node-3,OU=Wazuh,O=Wazuh,L=California,C=US"

Deploying certificates

Note: Make sure that a copy of the wazuh-certificates.tar file, created during the initial configuration step, is placed in your working directory.

Run the following commands, replacing <INDEXER_NODE_NAME> with the name of the Wazuh indexer node you are configuring as defined in config.yml. For example, node-1. This deploys the SSL certificates to encrypt communications between the Wazuh central components.

NODE_NAME=<INDEXER_NODE_NAME>
mkdir -p /etc/wazuh-indexer/certs
tar -xf ./wazuh-certificates.tar -C /etc/wazuh-indexer/certs/ ./$NODE_NAME.pem ./$NODE_NAME-key.pem ./admin.pem ./admin-key.pem ./root-ca.pem
mv -n /etc/wazuh-indexer/certs/$NODE_NAME.pem /etc/wazuh-indexer/certs/indexer.pem
mv -n /etc/wazuh-indexer/certs/$NODE_NAME-key.pem /etc/wazuh-indexer/certs/indexer-key.pem
chmod 500 /etc/wazuh-indexer/certs
chmod 400 /etc/wazuh-indexer/certs/*
chown -R wazuh-indexer:wazuh-indexer /etc/wazuh-indexer/certs

Set up Wazuh Indexer in your environment

Follow the instructions in the Configuration section to set up Wazuh Indexer in your environment.

On offline installations, disable every task that requires an internet connection to prevent failures.

# opensearch.yml
plugins.content_manager.catalog.update_on_start: false
plugins.content_manager.catalog.update_on_schedule: false
plugins.content_manager.telemetry.enabled: false

Starting the service

Enable and start the Wazuh indexer service.

Systemd
systemctl daemon-reload
systemctl enable wazuh-indexer
systemctl start wazuh-indexer

SysV

Choose one option according to the operating system used.

a. RPM-based operating system:

chkconfig --add wazuh-indexer
service wazuh-indexer start

b. Debian-based operating system:

update-rc.d wazuh-indexer defaults 95 10
service wazuh-indexer start

Repeat this stage of the installation process for every Wazuh indexer node in your cluster. Then proceed with initializing your single-node or multi-node cluster in the next stage.

3. Cluster initialization

Run the Wazuh indexer indexer-security-init.sh script on any Wazuh indexer node to load the new certificates information and start the single-node or multi-node cluster.

/usr/share/wazuh-indexer/bin/indexer-security-init.sh

Note: You only have to initialize the cluster once, there is no need to run this command on every node.

Testing the cluster installation

  1. Replace $WAZUH_INDEXER_IP_ADDRESS and run the following commands to confirm that the installation is successful.

    curl -k -u admin:admin https://$WAZUH_INDEXER_IP_ADDRESS:9200
    

    Output

    {
      "name" : "node-1",
      "cluster_name" : "wazuh-cluster",
      "cluster_uuid" : "095jEW-oRJSFKLz5wmo5PA",
      "version" : {
        "number" : "7.10.2",
        "build_type" : "rpm",
        "build_hash" : "db90a415ff2fd428b4f7b3f800a51dc229287cb4",
        "build_date" : "2023-06-03T06:24:25.112415503Z",
        "build_snapshot" : false,
        "lucene_version" : "9.6.0",
        "minimum_wire_compatibility_version" : "7.10.0",
        "minimum_index_compatibility_version" : "7.0.0"
      },
      "tagline" : "The OpenSearch Project: https://opensearch.org/"
    }
    
  2. Replace $WAZUH_INDEXER_IP_ADDRESS and run the following command to check if the single-node or multi-node cluster is working correctly.

    curl -k -u admin:admin https://$WAZUH_INDEXER_IP_ADDRESS:9200/_cat/nodes?v
    

Requirements

Requirements

Hardware recommendations

The Wazuh indexer can be installed as a single-node or as a multi-node cluster.

Hardware recommendations for each node

Minimum Recommended
Component RAM (GB) CPU (cores) RAM (GB) CPU (cores)
Wazuh indexer 8 4 32 8

Disk space requirements

The amount of data depends on the generated events per second (EPS). This table details the estimated disk space needed per agent to store 90 days of events on a Wazuh indexer server, depending on the type of monitored endpoints.

Monitored endpointsEPSStorage in Wazuh indexer (GB/90 days)
Servers0.253.7
Workstations0.11.5
Network devices0.57.4

For example, for an environment with 80 workstations, 10 servers, and 10 network devices, the storage needed on the Wazuh indexer server for 90 days of events is 230 GB.

Packages

Packages

Wazuh Indexer packages can be downloaded from the internal S3 buckets though the following links. Note these links are placeholders, and that you need to replace the RELEASE_SERIES, the VERSION and the REVISION with the appropriate values.

wazuh_indexer_aarch64_rpm: "https://packages-staging.xdrsiem.wazuh.info/pre-release/<RELEASE_SERIES>/yum/wazuh-indexer-<VERSION>-<REVISION>.aarch64.rpm"
wazuh_indexer_amd64_deb: "https://packages-staging.xdrsiem.wazuh.info/pre-release/<RELEASE_SERIES>/apt/pool/main/w/wazuh-indexer/wazuh-indexer_<VERSION>-<REVISION>_amd64.deb"
wazuh_indexer_arm64_deb: "https://packages-staging.xdrsiem.wazuh.info/pre-release/<RELEASE_SERIES>/apt/pool/main/w/wazuh-indexer/wazuh-indexer_<VERSION>-<REVISION>_arm64.deb"
wazuh_indexer_x86_64_rpm: "https://packages-staging.xdrsiem.wazuh.info/pre-release/<RELEASE_SERIES>/yum/wazuh-indexer-<VERSION>-<REVISION>.x86_64.rpm"

Examples

wazuh_indexer_aarch64_rpm: "https://packages-staging.xdrsiem.wazuh.info/pre-release/5.x/yum/wazuh-indexer-5.0.0-alpha99.aarch64.rpm"
wazuh_indexer_amd64_deb: "https://packages-staging.xdrsiem.wazuh.info/pre-release/5.x/apt/pool/main/w/wazuh-indexer/wazuh-indexer_5.0.0-alpha99_amd64.deb"
wazuh_indexer_arm64_deb: "https://packages-staging.xdrsiem.wazuh.info/pre-release/5.x/apt/pool/main/w/wazuh-indexer/wazuh-indexer_5.0.0-alpha99_arm64.deb"
wazuh_indexer_x86_64_rpm: "https://packages-staging.xdrsiem.wazuh.info/pre-release/5.x/yum/wazuh-indexer-5.0.0-alpha99.x86_64.rpm"

Compatibility

Please refer to this section for information pertaining to compatibility.

Installation

Installation

Note: This documentation assumes you are already provisioned with a wazuh-indexer package through any of the possible methods:

Installing the Wazuh indexer step by step

Install and configure the Wazuh indexer as a single-node or multi-node cluster, following step-by-step instructions. The installation process is divided into three stages.

  1. Certificates creation

  2. Nodes installation

  3. Cluster initialization

Note: You need root user privileges to run all the commands described below.

1. Certificates creation

Generating the SSL certificates

  1. Download the wazuh-certs-tool.sh script and the config.yml configuration file. This creates the certificates that encrypt communications between the Wazuh central components.

    curl -sO https://packages-dev.wazuh.com/5.0/wazuh-certs-tool.sh
    curl -sO https://packages-dev.wazuh.com/5.0/config.yml
    
  2. Edit ./config.yml and replace the node names and IP values with the corresponding names and IP addresses. You need to do this for all Wazuh Manager, Wazuh indexer, and Wazuh dashboard nodes. Add as many node fields as needed.

    nodes:
      # Wazuh indexer nodes
      indexer:
        - name: node-1
          ip: "<indexer-node-ip>"
        #- name: node-2
        #  ip: "<indexer-node-ip>"
        #- name: node-3
        #  ip: "<indexer-node-ip>"
    
      # Wazuh manager nodes
      # If there is more than one Wazuh manager
      # node, each one must have a node_type
      manager:
        - name: wazuh-1
          ip: "<wazuh-manager-ip>"
        #  node_type: master
        #- name: wazuh-2
        #  ip: "<wazuh-manager-ip>"
        #  node_type: worker
        #- name: wazuh-3
        #  ip: "<wazuh-manager-ip>"
        #  node_type: worker
    
      # Wazuh dashboard nodes
      dashboard:
        - name: dashboard
          ip: "<dashboard-node-ip>"
    

    To learn more about how to create and configure the certificates, see the Certificates deployment section.

  3. Run ./wazuh-certs-tool.sh to create the certificates. For a multi-node cluster, these certificates need to be later deployed to all Wazuh instances in your cluster.

    ./wazuh-certs-tool.sh -A
    
  4. Compress all the necessary files.

    tar -cvf ./wazuh-certificates.tar -C ./wazuh-certificates/ .
    rm -rf ./wazuh-certificates
    
  5. Copy the wazuh-certificates.tar file to all the nodes, including the Wazuh indexer, Wazuh Manager, and Wazuh dashboard nodes. This can be done by using the scp utility.

2. Nodes installation

Installing package dependencies

Install the following packages if missing:

yum
yum install coreutils
apt
apt-get install debconf adduser procps

Installing the Wazuh indexer package

rpm
rpm -ivh --replacepkgs wazuh-indexer-<VERSION>.rpm
dpkg
dpkg -i wazuh-indexer-<VERSION>.deb

Configuring the Wazuh indexer

Edit the /etc/wazuh-indexer/opensearch.yml configuration file and replace the following values:

a. network.host: Sets the address of this node for both HTTP and transport traffic. The node will bind to this address and use it as its publish address. Accepts an IP address or a hostname. Use the same node address set in config.yml to create the SSL certificates.

b. node.name: Name of the Wazuh indexer node as defined in the config.yml file. For example, node-1.

c. cluster.initial_cluster_manager_nodes: List of the names of the master-eligible nodes. These names are defined in the config.yml file. Uncomment the node-2 and config.yml and node-3 lines, change the names, or add more lines, according to your config.yml definitions.

cluster.initial_cluster_manager_nodes:
- "node-1"
- "node-2"
- "node-3"

d. discovery.seed_hosts: List of the addresses of the master-eligible nodes. Each element can be either an IP address or a hostname. You may leave this setting commented if you are configuring the Wazuh indexer as a single node. For multi-node configurations, uncomment this setting and set the IP addresses of each master-eligible node.

discovery.seed_hosts:
- "10.0.0.1"
- "10.0.0.2"
- "10.0.0.3"

e. plugins.security.nodes_dn: List of the Distinguished Names of the certificates of all the Wazuh indexer cluster nodes. Uncomment the lines for node-2 and node-3 and change the common names (CN) and values according to your settings and your config.yml definitions.

plugins.security.nodes_dn:
- "CN=node-1,OU=Wazuh,O=Wazuh,L=California,C=US"
- "CN=node-2,OU=Wazuh,O=Wazuh,L=California,C=US"
- "CN=node-3,OU=Wazuh,O=Wazuh,L=California,C=US"

Deploying certificates

Note: Make sure that a copy of the wazuh-certificates.tar file, created during the initial configuration step, is placed in your working directory.

Run the following commands, replacing <INDEXER_NODE_NAME> with the name of the Wazuh indexer node you are configuring as defined in config.yml. For example, node-1. This deploys the SSL certificates to encrypt communications between the Wazuh central components.

NODE_NAME=<INDEXER_NODE_NAME>
mkdir -p /etc/wazuh-indexer/certs
tar -xf ./wazuh-certificates.tar -C /etc/wazuh-indexer/certs/ ./$NODE_NAME.pem ./$NODE_NAME-key.pem ./admin.pem ./admin-key.pem ./root-ca.pem
mv -n /etc/wazuh-indexer/certs/$NODE_NAME.pem /etc/wazuh-indexer/certs/indexer.pem
mv -n /etc/wazuh-indexer/certs/$NODE_NAME-key.pem /etc/wazuh-indexer/certs/indexer-key.pem
chmod 500 /etc/wazuh-indexer/certs
chmod 400 /etc/wazuh-indexer/certs/*
chown -R wazuh-indexer:wazuh-indexer /etc/wazuh-indexer/certs

Set up Wazuh Indexer in your environment

Follow the instructions in the Configuration section to set up Wazuh Indexer in your environment.

On offline installations, disable every task that requires an internet connection to prevent failures.

# opensearch.yml
plugins.content_manager.catalog.update_on_start: false
plugins.content_manager.catalog.update_on_schedule: false
plugins.content_manager.telemetry.enabled: false

Starting the service

Enable and start the Wazuh indexer service.

Systemd
systemctl daemon-reload
systemctl enable wazuh-indexer
systemctl start wazuh-indexer

SysV

Choose one option according to the operating system used.

a. RPM-based operating system:

chkconfig --add wazuh-indexer
service wazuh-indexer start

b. Debian-based operating system:

update-rc.d wazuh-indexer defaults 95 10
service wazuh-indexer start

Repeat this stage of the installation process for every Wazuh indexer node in your cluster. Then proceed with initializing your single-node or multi-node cluster in the next stage.

3. Cluster initialization

Run the Wazuh indexer indexer-security-init.sh script on any Wazuh indexer node to load the new certificates information and start the single-node or multi-node cluster.

/usr/share/wazuh-indexer/bin/indexer-security-init.sh

Note: You only have to initialize the cluster once, there is no need to run this command on every node.

Testing the cluster installation

  1. Replace $WAZUH_INDEXER_IP_ADDRESS and run the following commands to confirm that the installation is successful.

    curl -k -u admin:admin https://$WAZUH_INDEXER_IP_ADDRESS:9200
    

    Output

    {
      "name" : "node-1",
      "cluster_name" : "wazuh-cluster",
      "cluster_uuid" : "095jEW-oRJSFKLz5wmo5PA",
      "version" : {
        "number" : "7.10.2",
        "build_type" : "rpm",
        "build_hash" : "db90a415ff2fd428b4f7b3f800a51dc229287cb4",
        "build_date" : "2023-06-03T06:24:25.112415503Z",
        "build_snapshot" : false,
        "lucene_version" : "9.6.0",
        "minimum_wire_compatibility_version" : "7.10.0",
        "minimum_index_compatibility_version" : "7.0.0"
      },
      "tagline" : "The OpenSearch Project: https://opensearch.org/"
    }
    
  2. Replace $WAZUH_INDEXER_IP_ADDRESS and run the following command to check if the single-node or multi-node cluster is working correctly.

    curl -k -u admin:admin https://$WAZUH_INDEXER_IP_ADDRESS:9200/_cat/nodes?v
    

Configuration

Wazuh Indexer shares the same configuration system as OpenSearch. Refer to the OpenSearch documentation for general information about configuration files and settings.

Configuration files

Wazuh Indexer bundles two main configuration files on each node:

  • /etc/wazuh-indexer/opensearch.yml - main configuration file for Wazuh Indexer. This file contains the settings for the Wazuh Indexer cluster, such as cluster name, node name, network settings, and more.
  • /etc/wazuh-indexer/jvm.options - configuration file for the Java Virtual Machine (JVM) that runs Wazuh Indexer. This file contains settings for the JVM, such as heap size, garbage collection, and more.

System configuration

For production workloads, tune the following operating system and JVM settings on every Wazuh Indexer node before starting the service. The package installations create the wazuh-indexer service user; the settings below apply to that user and the host it runs on.

Note: All the commands in this section require root privileges.

JVM heap size

Wazuh Indexer runs on the Java Virtual Machine (JVM). The heap size determines how much memory the indexer can use for its internal data structures, caches, and request processing. Set it in the /etc/wazuh-indexer/jvm.options file.

Follow these recommendations when sizing the heap:

  • Set the initial heap size (-Xms) and the maximum heap size (-Xmx) to the same value. This prevents performance degradation caused by the JVM resizing the heap at runtime.
  • Set the heap to no more than 50% of the available system RAM. The other half is left for the operating system file system cache, which Wazuh Indexer relies on heavily.
  • Do not set the heap above approximately 32 GB. Above this threshold the JVM can no longer use compressed ordinary object pointers, which wastes memory and reduces performance.

For example, on a node with 8 GB of RAM, set the heap to 4 GB:

-Xms4g
-Xmx4g

Where:

  • -Xms4g sets the initial heap size to 4 GB.
  • -Xmx4g sets the maximum heap size to 4 GB.

Restart the service after changing the heap size:

systemctl restart wazuh-indexer

Memory locking

Configure Wazuh Indexer to lock its process address space into RAM so that none of the JVM is ever swapped out.

  1. bootstrap.memory_lock: true is enabled by default in /etc/wazuh-indexer/opensearch.yml. No changes are needed for package installations.

  2. Grant the wazuh-indexer service user permission to lock unlimited memory. The RPM and Debian packages already configure this for both systemd-based and SysVinit-based systems — no additional configuration is required for package installations.

    • systemd: LimitMEMLOCK=infinity is set in the service file.
    • SysVinit: ulimit -l unlimited is applied by the init script before starting the process.
  3. Reload the service manager and restart Wazuh Indexer:

    systemctl daemon-reload
    systemctl restart wazuh-indexer
    
  4. Verify that memory locking is active by checking that the mlockall value is true:

    curl -k -u <INDEXER_USERNAME>:<INDEXER_PASSWORD> "https://<INDEXER_IP_ADDRESS>:9200/_nodes?filter_path=**.mlockall&pretty"
    
    {
      "nodes" : {
        "sRuGbIQRRfC54wzwIHjJWQ" : {
          "process" : {
            "mlockall" : true
          }
        }
      }
    }
    

    If the output is false, memory locking failed and the following line appears in /var/log/wazuh-indexer/wazuh-indexer.log:

    memory locking requested for wazuh-indexer process but memory is not locked
    

    This usually means the wazuh-indexer user lacks the memlock permission. For systemd-based systems, confirm that LimitMEMLOCK=infinity is present in the service file, reload with systemctl daemon-reload, and restart the service. For SysVinit-based systems, confirm that step 2 was applied correctly.

Note: Enabling bootstrap.memory_lock causes the JVM to reserve all the memory it needs at startup, including native memory beyond the configured heap. Make sure the node has enough physical RAM for the heap plus this overhead, otherwise the service may fail to start.

Virtual memory

Wazuh Indexer uses memory-mapped files (mmapfs) to store its indices. The default operating system limit on memory map areas is too low for production use, which can cause the node to fail to start or run out of memory.

Set vm.max_map_count to at least 262144. To check the current value:

sysctl vm.max_map_count

To increase it permanently, add the following line to /etc/sysctl.conf:

vm.max_map_count=262144

Apply the change without rebooting:

sysctl -p

Note: When running Wazuh Indexer in a container, set vm.max_map_count on the host machine, not inside the container.

File descriptors

Wazuh Indexer uses a large number of file descriptors. Running out of them can lead to data loss, so increase the limit for the wazuh-indexer user to 65535 or higher.

The RPM and Debian packages already set this limit to 65535 through the systemd service, so no additional configuration is required for package installations. To raise the limit manually, create or edit a systemd service override:

mkdir -p /etc/systemd/system/wazuh-indexer.service.d/
cat > /etc/systemd/system/wazuh-indexer.service.d/override.conf << EOF
[Service]
LimitNOFILE=65535
EOF

Reload and restart the service:

systemctl daemon-reload
systemctl restart wazuh-indexer

Verify the limit applied to the running node by checking max_file_descriptors:

curl -k -u <INDEXER_USERNAME>:<INDEXER_PASSWORD> "https://<INDEXER_IP_ADDRESS>:9200/_nodes/stats/process?filter_path=**.max_file_descriptors&pretty"

Plugin settings

Setup settings

The Setup plugin is configured through settings in opensearch.yml. All settings use the plugins.setup prefix.

SettingData typeDefault valueDescription
plugins.setup.timeoutInteger30Timeout in seconds for index and search operations.
plugins.setup.backoffInteger15Delay in seconds for the retry mechanism involving initialization tasks.

Content manager settings

The Content Manager plugin is configured through settings in opensearch.yml. All settings use the plugins.content_manager prefix.

SettingData typeDefault valueDescription
plugins.content_manager.cti.apiStringhttps://api.pre.cloud.wazuh.com/api/v1Base URL for the Wazuh CTI API
plugins.content_manager.catalog.sync_intervalInteger60Sync interval in minutes. Valid range: 1–1440
plugins.content_manager.max_items_per_bulkInteger999Maximum documents per bulk indexing request. Valid range: 10–999
plugins.content_manager.max_concurrent_bulksInteger5Maximum concurrent bulk operations. Valid range: 1–5
plugins.content_manager.client.timeoutLong10HTTP client timeout in seconds for CTI API requests. Valid range: 10–50
plugins.content_manager.catalog.update_on_startBooleantrueTrigger content sync when the plugin starts
plugins.content_manager.catalog.update_on_scheduleBooleantrueEnable the periodic sync job
plugins.content_manager.catalog.rulesetString""Full CTI consumer URL for ruleset content
plugins.content_manager.catalog.iocsString""Full CTI consumer URL for IoC content
plugins.content_manager.catalog.vulnerabilitiesString""Full CTI consumer URL for vulnerabilities content
plugins.content_manager.catalog.create_detectorsBooleantrueAutomatically create Security Analytics detectors from CTI content
plugins.content_manager.telemetry.enabledBooleantrueEnable or disable the daily Update check service ping. This setting is dynamic.
plugins.content_manager.catalog.update_on_demandBooleantrueWhen false, on-demand content updates (POST /update) return 403 Forbidden for every caller, regardless of role.
plugins.content_manager.catalog.policy_update.enabledBooleantrueWhen false, policy updates (PUT /policy/{space}) return 403 Forbidden for every caller, regardless of role.

Security Analytics settings

The Security Analytics plugin is configured through settings in opensearch.yml. All node-scope settings use the plugins.security_analytics prefix. Almost every setting is dynamic and can be changed at runtime via the Cluster Settings API.

SettingData typeDefault valueDescription
plugins.security_analytics.alert_finding_enabledBooleantrueEnable rollover and retention management for the finding history indices
plugins.security_analytics.alert_finding_max_docsLong1000Deprecated. Maximum document count for a finding history index before rollover. Minimum 0
plugins.security_analytics.alert_finding_rollover_periodTime12hHow often the finding history rollover job runs
plugins.security_analytics.alert_history_enabledBooleantrueEnable rollover and retention management for the alert history indices
plugins.security_analytics.alert_history_max_ageTime30dMaximum age of an alert history index before rollover
plugins.security_analytics.alert_history_max_docsLong1000Maximum document count for an alert history index before rollover. Minimum 0
plugins.security_analytics.alert_history_retention_periodTime60dRetention period after which alert history indices are deleted
plugins.security_analytics.alert_history_rollover_periodTime12hHow often the alert history rollover job runs
plugins.security_analytics.auto_correlations_enabledBooleanfalseAutomatically generate correlation rules from new findings
plugins.security_analytics.correlation.detector_cache_ttlTime5mTTL for the in-memory monitor-id to detector cache. Set to 0s to disable the cache
plugins.security_analytics.correlation.events_backpressure.enabledBooleantrueWrite-block the events indices when the correlation backlog fills, so ingestion pauses and the backlog drains instead of the node running out of memory
plugins.security_analytics.correlation.events_backpressure.high_watermark_percentInteger100Backlog level, as a percent of correlation.max_pending_findings, at or above which the events indices are write-blocked. Valid range: 1–100
plugins.security_analytics.correlation.events_backpressure.low_watermark_percentInteger60Backlog level, as a percent of correlation.max_pending_findings, at or below which the events-index write block is lifted. Valid range: 0–99
plugins.security_analytics.correlation.max_in_flight_findingsInteger50Maximum number of correlation pipelines running concurrently. Valid range: 1–1000
plugins.security_analytics.correlation.max_pending_findingsInteger10000Maximum findings waiting for a free correlation slot. When the backlog is full, new findings are shed (correlation and enrichment skipped) so the node does not run out of memory under overload. Valid range: 1–1000000
plugins.security_analytics.correlation.metadata_cache_ttlTime5mTTL for the in-memory caches of log-type list and correlation rules by detector type. Set to 0s to disable both caches
plugins.security_analytics.correlation_history_max_ageTime30dMaximum age of a correlation history index before rollover
plugins.security_analytics.correlation_history_max_docsLong1000Maximum document count for a correlation history index before rollover. Minimum 0
plugins.security_analytics.correlation_history_retention_periodTime60dRetention period after which correlation history indices are deleted
plugins.security_analytics.correlation_history_rollover_periodTime12hHow often the correlation history rollover job runs
plugins.security_analytics.correlation_time_windowTime5mTime window used to group findings into correlations
plugins.security_analytics.enable_detectors_with_dedicated_query_indicesBooleantrueCreate dedicated query indices for new detectors
plugins.security_analytics.enable_workflow_usageBooleantrueUse Alerting composite workflows when running detectors
plugins.security_analytics.enriched_findings_bulk_sizeInteger100Number of enriched findings buffered before a bulk index request is fired. Valid range: 10–1000
plugins.security_analytics.enriched_findings_enrich_batch_sizeInteger100Findings drained per in-flight permit; their source events are fetched in one combined MultiGet instead of one request per finding. Valid range: 1–1000
plugins.security_analytics.enriched_findings_flush_intervalInteger5Seconds between periodic flushes of any leftover buffered enriched findings. Valid range: 1–60
plugins.security_analytics.enriched_findings_index_enabledBooleantrueToggle the enriched findings pipeline (see Architecture)
plugins.security_analytics.enriched_findings_max_in_flightInteger5Maximum concurrent enrichment chains, to bound peak load on the transport layer. Valid range: 1–10
plugins.security_analytics.enriched_findings_rule_cache_max_sizeInteger10000Maximum rule-metadata entries cached in memory by the enrichment service. Minimum 0. Static; requires a node restart to change
plugins.security_analytics.filter_by_backend_rolesBooleanfalseRestrict access to detectors, rules, and findings based on the requester’s backend roles
plugins.security_analytics.finding_history_max_ageTime30dMaximum age of a finding history index before rollover
plugins.security_analytics.finding_history_retention_periodTime60dRetention period after which finding history indices are deleted
plugins.security_analytics.index_timeoutTime60sTimeout for Security Analytics index operations
plugins.security_analytics.max_detectorsInteger10Maximum number of user-created detectors (Content Manager detectors do not count). Minimum 0

Wazuh Indexer Initialization plugin

The wazuh-indexer-setup plugin is a module composing the Wazuh Indexer responsible for the initialization of the indices required by Wazuh.

The Wazuh Indexer Setup Plugin in responsible for:

  • Create the index templates, to define the mappings and settings for the indices.
  • Create the initial indices. We distinguish between stateful and stream indices. While stream indices contain immutable time-series data and are rolled over periodically, stateful indices store dynamic data that can change over time and reside in a single index.
  • Stream indices are created with a data stream configuration and an ISM rollover policy.

Indices

The following table lists the indices created by this plugin.

Stream indices

IndexDescription
wazuh-events-raw-v5Stores original unprocessed events.
wazuh-events-v5-unclassifiedStores uncategorized events for investigation.
wazuh-active-responsesStores active response execution requests.
wazuh‑events-v5-<category>Stores events received by the Wazuh Manager, categorized by their origin or type. Refer to Wazuh Common Schema for more information.
wazuh‑findings-v5-<category>Stores security findings generated by the Threat Detectors. These are created each time an event trips a detection rule.
wazuh-metrics-agentsStores statistics about the Wazuh Agents state.
wazuh-metrics-commsStores statistics about the Wazuh Manager usage and performance. The information includes the number of events decoded, bytes received, and TCP sessions.

Stateful indices

IndexDescription
wazuh‑states-scaSecurity Configuration Assessment (SCA) scan results.
wazuh-states-fim-filesFile Integrity Monitoring: information about monitored files.
wazuh-states-fim-registry-keysFile Integrity Monitoring: information about the Windows registry (keys).
wazuh-states-fim-registry-valuesFile Integrity Monitoring: information about the Windows registry (values).
wazuh-states-inventory-browser-extensionsStores browser extensions/add-ons detected on the endpoint (Chromium-based browsers — Chrome/Edge/Brave/Opera —, Firefox, and Safari).
wazuh-states-inventory-groupsStores existing groups on the endpoint.
wazuh-states-inventory-hardwareBasic information about the hardware components of the endpoint.
wazuh-states-inventory-hotfixesContains information about the updates installed on Windows endpoints. This information is used by the vulnerability detector module to discover what vulnerabilities have been patched on Windows endpoints.
wazuh-states-inventory-interfacesStores information (up and down interfaces) as well as packet transfer information about the interfaces on a monitored endpoint.
wazuh-states-inventory-monitoringStores the connection status history of Wazuh agents (active, disconnected, pending, or never connected). The index is used by the Wazuh Dashboard to display agent status and historical trends.
wazuh-states-inventory-networksStores the IPv4 and IPv6 addresses associated with each network interface, as referenced in the wazuh-states-inventory-interfaces index.
wazuh-states-inventory-packagesStores information about the currently installed software on the endpoint.
wazuh-states-inventory-portsBasic information about open network ports on the endpoint.
wazuh-states-inventory-processesStores the detected running processes on the endpoints.
wazuh-states-inventory-protocolsStores routing configuration details for each network interface, as referenced in the wazuh-states-inventory-interfaces index.
wazuh-states-inventory-servicesStores system services detected on the endpoint (Windows Services, Linux systemd units, and macOS launchd daemons/agents).
wazuh-states-inventory-systemOperating system information, hostname and architecture.
wazuh-states-inventory-usersStores existing users on the endpoint.
wazuh-states-vulnerabilitiesActive vulnerabilities on the endpoint and its details.

Install

The wazuh-indexer-setup plugin is part of the official Wazuh Indexer packages and is installed by default. However, to manually install the plugin, follow the next steps.

Note: You need to use the wazuh-indexer or root user to run these commands.

/usr/share/wazuh-indexer/bin/opensearch-plugin install file://[absolute-path-to-the-plugin-zip]

Once installed, restart the Wazuh Indexer service.

Uninstall

Note You need to use the wazuh-indexer or root user to run these commands.

To list the installed plugins, run: /usr/share/wazuh-indexer/bin/opensearch-plugin list

To remove a plugin, use its name as a parameter with the remove command: /usr/share/wazuh-indexer/bin/opensearch-plugin remove <plugin-name>

/usr/share/wazuh-indexer/bin/opensearch-plugin remove wazuh-indexer-setup

Architecture

Design

The plugin implements the ClusterPlugin interface in order to be able to hook into the node’s lifecycle overriding the onNodeStarted() method.

The SetupPlugin class holds the list of indices to create. The logic for the creation of the index templates and the indices is encapsulated in the Index abstract class. Each subclass can override this logic if necessary. The SetupPlugin::onNodeStarted() method invokes the Index::initialize() method, effectively creating every index in the list.

By design, the plugin will overwrite any existing index template under the same name.

Retry mechanism

The plugin features a retry mechanism to handle transient faults. In case of a temporal failure (timeouts or similar) during the initialization of the indices, the task is retried after a given amount of time (back-off). If two consecutive faults occur during the initialization of the same index, the initialization process is halted, and the node is shut down. Proper logging is in place to notify administrators before the shutdown occurs.

The back-off time is configurable. Head to Configuration Files for more information.

Readiness marker

The plugin persists its initialization state in a hidden, single-document index, .wazuh-setup-status, managed by the SetupStatusIndex class. Other plugins (currently the Content Manager) read this marker to defer their own startup work until Setup has finished creating its indices, avoiding races where a dependent index template doesn’t exist yet.

The marker transitions once per boot:

ValueMeaning
runningIndex initialization is in progress (set when SetupStatusIndex.initialize() runs at the start of index initialization, overwriting any marker left over from a previous boot).
readyAll index templates, indices and data streams have been created successfully.
failedIndex initialization could not complete (an unhandled exception was thrown while initializing one of the indices).

Writing the marker is best-effort: a failure to persist it is logged but never interrupts node startup.

Replica configuration

During the node initialization, the plugin checks for the presence of the cluster.default_number_of_replicas setting in the node configuration. If this setting is defined, the plugin automatically updates the cluster’s persistent settings with this value. This ensures that the default number of replicas is consistently applied across the cluster as defined in the configuration file.

Class diagram

---
title: Wazuh Indexer setup plugin
---
classDiagram
    %% Classes
    class IndexInitializer
    <<interface>> IndexInitializer
    class Index
    <<abstract>> Index
    class IndexStateManagement
    class WazuhIndex
    <<abstract>> WazuhIndex
    class StateIndex
    class StreamIndex

    %% Relations
    IndexInitializer <|-- Index : implements
    Index <|-- IndexStateManagement
    Index <|-- WazuhIndex
    WazuhIndex <|-- StateIndex
    WazuhIndex <|-- StreamIndex

    %% Schemas
    class IndexInitializer {
        +createIndex(String index) void
        +createTemplate(String template) void
    }
    class Index {
        Client client
        ClusterService clusterService
        IndexUtils utils
        String index
        String template
        +Index(String index, String template)
        +setClient(Client client) IndexInitializer
        +setClusterService(ClusterService clusterService) IndexInitializer
        +setIndexUtils(IndexUtils utils) IndexInitializer
        +indexExists(String indexName) bool
        +initialize() void
        +createIndex(String index) void
        +createTemplate(String template) void
        %% initialize() podría reemplazarse por createIndex() y createTemplate()
    }
    class IndexStateManagement {
        -List~String~ policies
        +initialize() void
        -createPolicies() void
        -indexPolicy(String policy) void
    }
    class WazuhIndex {
    }
    class StreamIndex {
        -String alias
        +StreamIndex(String index, String template, String alias)
        +createIndex(String index)
    }
    class StateIndex {
    }

Sequence diagram

Note Calls to Client are asynchronous.

sequenceDiagram
    actor Node
    participant SetupPlugin
    participant Index
    participant Client
    Node->>SetupPlugin: plugin.onNodeStarted()
    activate SetupPlugin
    Note over Node,SetupPlugin: Invoked on Node::start()

    activate Index
    loop i..n indices
        SetupPlugin->>Index: i.initialize()


        Index-)Client: createTemplate(i)
        Client--)Index: response

        Index-)Client: indexExists(i)
        Client--)Index: response
        alt index i does not exist
            Index-)Client: createIndex(i)
            Client--)Index: response
        end
    end

    deactivate Index
    deactivate SetupPlugin

Wazuh Common Schema

Refer to the docs for complete definitions of the indices. The indices inherit the settings and mappings defined in the index templates.

Event stream templates

All event categories share a single base template: templates/streams/events.json. The StreamIndex class dynamically generates one index template per category at deployment time by overriding index_patterns and rollover_alias from the base template. Specialized streams (raw, unclassified, active-responses) use their own dedicated template files.

The WCS field definitions are organized under wcs/stateless/events/:

wcs/stateless/events/
├── main/          # Shared fields for all event categories
├── raw/           # Fields for raw (unprocessed) events
└── unclassified/  # Fields for uncategorized events

JavaDoc

The plugin is documented using JavaDoc. You can compile the documentation using the Gradle task for that purpose. The generated JavaDoc is in the build/docs folder.

./gradlew javadoc

Configuration

Setup settings

The Setup plugin is configured through settings in opensearch.yml. All settings use the plugins.setup prefix.

SettingData typeDefault valueDescription
plugins.setup.timeoutInteger30Timeout in seconds for index and search operations.
plugins.setup.backoffInteger15Delay in seconds for the retry mechanism involving initialization tasks.

API Reference

The Setup plugin exposes a REST API under /_plugins/_setup/. All endpoints require authentication.


Settings

Update Settings

Persists configuration settings to the .wazuh-settings index. Currently supports the engine.index_raw_events boolean flag, which controls whether the Engine indexes raw events into the wazuh-events-raw-v5 data stream.

Request

  • Method: PUT
  • Path: /_plugins/_setup/settings

Request Body

FieldTypeRequiredDescription
engineObjectYesEngine settings object
engine.index_raw_eventsBooleanYesWhether the Engine indexes raw events into the wazuh-events-raw-v5 data stream

Example Request

curl -sk -u admin:admin -X PUT \
  "https://192.168.56.6:9200/_plugins/_setup/settings" \
  -H 'Content-Type: application/json' \
  -d '{
    "engine": {
      "index_raw_events": true
    }
  }'

Example Response (success)

{
  "message": "Settings updated successfully.",
  "status": 200
}

Example Response (missing field)

{
  "message": "Missing required field: 'engine.index_raw_events'.",
  "status": 400
}

Example Response (invalid type)

{
  "message": "Field 'engine.index_raw_events' must be of type boolean.",
  "status": 400
}

Status Codes

CodeDescription
200Settings updated successfully
400Invalid request body, missing required fields, or wrong field type
500Internal server error (e.g., failed to persist settings to the index)

Documentation Maintenance — modifications to the REST API must be reflected in both openapi.yml and this file.

Wazuh Common Schema

The Wazuh Common Schema (WCS) is a standardized structure for organizing and categorizing security event data collected by Wazuh. It is designed to facilitate data analysis, correlation, and reporting across different data sources and types.

Categorization

The Wazuh Common Schema categorizes events into several key areas to streamline data management and analysis.

All event categories share a single base index template (events.json). At deployment time, the setup plugin dynamically generates one index template per category from this shared base, setting the appropriate index_patterns and rollover_alias for each. This means only one template file exists in the repository, but each category gets its own index template in the cluster.

The index mappings and settings for subcategories take precedence over those from the main category. In OpenSearch, index templates are applied in order of their “priority” value: templates with a lower priority are applied first, and those with a higher priority are applied afterward, allowing them to override previous settings. This means the index template for the main category is applied first (priority=1), and then the subcategory template (priority=10) is applied on top of it, so subcategory-specific settings override the main category defaults.

To list all deployed event templates:

GET /_index_template/wazuh-events-*

Categories

The Key column is the canonical identifier used throughout the system — in data stream names, integrations, rules, decoders, and the Security Analytics plugin. Use it exactly as shown when creating or referencing any of these resources.

NameKeyExample log types
Access Managementaccess-managementad_ldap, apache_access, okta
Applicationsapplicationsgithub, gworkspace, m365
Cloud Servicescloud-servicesazure, cloudtrail, s3
Network Activitynetwork-activitydns, network, vpcflow
Securitysecuritywaf
System Activitysystem-activitylinux, windows, others_macos
Otherotherothers_application, others_apt, others_web
UnclassifiedunclassifiedEvents that could not be categorized

Note: unclassified is a catch-all for events that could not be assigned to any other category. It is managed automatically by the pipeline and should not be used as a target category when creating new integrations or rules.

Data Streams

Each category maps to a dedicated data stream following the pattern wazuh-events-v5-{key}:

Events

wazuh-events-v5-access-management
wazuh-events-v5-applications
wazuh-events-v5-cloud-services
wazuh-events-v5-network-activity
wazuh-events-v5-other
wazuh-events-v5-security
wazuh-events-v5-system-activity
wazuh-events-v5-unclassified

Findings

wazuh-findings-v5-access-management
wazuh-findings-v5-applications
wazuh-findings-v5-cloud-services
wazuh-findings-v5-network-activity
wazuh-findings-v5-other
wazuh-findings-v5-security
wazuh-findings-v5-system-activity
wazuh-findings-v5-unclassified

Check Stream indices for details.

Content Manager

The Content Manager is a Wazuh Indexer plugin responsible for managing detection content — rules, decoders, integrations, key-value databases (KVDBs), and Indicators of Compromise (IoCs). It synchronizes content from the Wazuh Cyber Threat Intelligence (CTI) API, provides a REST API for user-generated content, and communicates with the Wazuh Engine to activate changes.

It also includes the Update check system, which communicates with the CTI Update check API once per day to let Wazuh determine whether a newer Wazuh version is available for the deployment.

Update check components are:

  • Update check API (CTI)
  • Update check system (Wazuh Indexer)
  • Update check UI (Wazuh Dashboard)

Content synchronization

The Content Manager periodically synchronizes content from the Wazuh CTI API. Three catalog consumers are managed:

  • Catalog context: Contains detection rules, decoders, integrations, KVDBs, and the routing policy.
  • IoC context: Contains Indicators of Compromise for threat detection.
  • CVE context: Contains Common Vulnerabilities and Exposures data, stored in wazuh-threatintel-vulnerabilities. CVE documents do not have a space and are not subject to removals from CTI.

Each catalog type has an associated consumer state document in .wazuh-cti-consumers, keyed by consumer type (for example, cti:catalog:consumer:ruleset).

Snapshot Initialization

On first run (when the local offset is 0), the Content Manager performs snapshot initialization:

  1. If a custom catalog URL is configured, it first attempts remote snapshot initialization using that consumer.
  2. If remote initialization fails, it falls back to the local packaged snapshot when available.
  3. If no custom catalog URL is configured, it initializes from the local packaged snapshot.
  4. It indexes content into the appropriate system indices using bulk operations and updates .wazuh-cti-consumers offsets.

Incremental Updates

When the local offset is behind the remote offset, the Content Manager fetches changes in batches (up to 1000 per request) and applies creation, update, and removal operations to the content indices. The local offset is updated after each successful batch.

If the local offset is ahead of the remote offset (e.g., consumer was changed), or if the update fails, the Content Manager resets to the latest snapshot to realign with the CTI API.

Sync Schedule

By default, synchronization runs:

  • On plugin startup (plugins.content_manager.catalog.update_on_start: true)
  • Periodically every 60 minutes (plugins.content_manager.catalog.sync_interval: 60)

The periodic job is registered with the OpenSearch Job Scheduler and tracked in the .wazuh-content-manager-jobs index.

Update Check Service

When plugins.content_manager.telemetry.enabled is true (default), the Content Manager schedules a daily update check heartbeat job.

  • Frequency: every 24 hours (with an immediate first ping as soon as the job is registered)
  • Scheduler document ID: wazuh-telemetry-ping-job
  • Endpoint: CTI /ping
  • Data sent: cluster UUID and deployed Wazuh version (through headers)

This information is used to detect update availability and surface notifications through the Wazuh Dashboard.

User-Generated Content

The Content Manager provides a full CUD REST API for creating custom detection content:

  • Rules: Custom detection rules associated with an integration.
  • Decoders: Custom log decoders associated with an integration.
  • Integrations: Logical groupings of related rules, decoders, and KVDBs.
  • KVDBs: Key-value databases used by rules and decoders for lookups.

User-generated content is stored in the draft space and is separate from the CTI-managed standard space. This separation ensures that user customizations never conflict with upstream CTI content.

See the API Reference for endpoint details.

Content Spaces

The Content Manager organizes content into spaces:

SpaceDescription
StandardRead-only content synced from the CTI API. This is the baseline detection content.
DraftWritable space for user-generated content. CUD operations target this space.
TestUsed for logtest operations and content validation before final promotion.
CustomThe final space for user content. Content promoted to this space is used by the Wazuh Engine (via the manager package) to actively decode and process logs.

Content flows through spaces in a promotion chain: Draft → Test → Custom. The Standard space exists independently as the upstream CTI baseline. Each space maintains its own copies of rules, decoders, integrations, KVDBs, filters, and the routing policy within the system indices.

Policy Management

The routing policy defines how the Wazuh Engine processes incoming events — which integrations are active and in what order. The Content Manager provides an API to update the draft policy:

curl -sk -u admin:admin -X PUT \
  "https://192.168.56.6:9200/_plugins/_content_manager/policy" \
  -H 'Content-Type: application/json' \
  -d '{"resource": { ... }}'

Policy changes are applied to the draft space and take effect after promotion.

Promotion Workflow

The promotion workflow moves content through the space chain (Draft → Test → Custom):

  1. Preview changes: GET /_plugins/_content_manager/promote?space=draft returns a diff of what will change (additions, updates, deletions for each content type).
  2. Execute promotion: POST /_plugins/_content_manager/promote promotes the content from the source space to the next space in the chain.

The promotion chain works as follows:

  • Draft → Test: Content is promoted for validation and logtest operations.
  • Test → Custom: Once validated, content is promoted to the Custom space where it becomes active — the Wazuh Engine (via the manager package) uses this space to decode and process logs in production.

During promotion, the Content Manager:

  • Sends updated content to the Engine
  • Validates the configuration
  • Triggers a configuration reload
  • Updates the target space to reflect the promoted content

Engine Communication

The Content Manager communicates with the Wazuh Engine through a Unix domain socket located at:

/usr/share/wazuh-indexer/engine/sockets/engine-api.sock

This socket is used for:

  • Logtest: Sends a log event to the Engine for analysis and returns the decoded/matched result.
  • Content validation: Validates rules and decoders before promotion.
  • Configuration reload: Signals the Engine to reload its configuration after promotion.

System Indices

The Content Manager uses the following system indices:

IndexDescription
.wazuh-cti-consumersSynchronization state for each CTI consumer type (type, resource, is_public, offsets, status)
.wazuh-internal-statePersisted CTI access token (hidden, single document)
wazuh-threatintel-rulesDetection rules (both CTI-synced and user-generated, across all spaces)
wazuh-threatintel-decodersLog decoders
wazuh-threatintel-integrationsIntegration definitions
wazuh-threatintel-kvdbsKey-value databases
wazuh-threatintel-policiesRouting policies
wazuh-threatintel-enrichmentsIndicators of Compromise
wazuh-threatintel-vulnerabilitiesCommon Vulnerabilities and Exposures (CVE data from CTI, no spaces, offset-tracked)
wazuh-threatintel-filtersEngine filters (routing filters for event classification)
.wazuh-content-manager-jobsJob Scheduler metadata for periodic sync and update check jobs

Wazuh Cloud subscription

To synchronize content from the CTI API, the Wazuh Indexer requires a valid CTI access token. The token is registered via the REST API:

  1. Store credentials by sending the CTI access token via POST /_plugins/_content_manager/subscription. The token is persisted in the .wazuh-internal-state hidden index and loaded into memory.
  2. The Content Manager uses the in-memory token for all CTI API requests.
  3. Without a registered token, sync operations return a 404 Token not found error.

See Subscription Management in the API Reference.

Pre-registration with Wazuh Cloud

The Content Manager supports pre-registration of the Wazuh instance with Wazuh Cloud using the DEPLOY_KEY environment variable. If this variable is set at startup, the Content Manager automatically registers the token as if it were sent through the REST API, enabling immediate synchronization with the CTI API without manual intervention. Snapshots bundled with the package are removed in favor of fetching the latest content directly from the CTI API using the provided token. This streamlines the setup process for new deployments and ensures that they start with the most up-to-date detection content from their subscription plan.

Architecture

The Content Manager plugin operates within the Wazuh Indexer environment. It is composed of several components that handle REST API requests, background job scheduling, content synchronization, user-generated content management, and Engine communication.

Components

REST Layer

Exposes HTTP endpoints under /_plugins/_content_manager/ for:

  • Subscription management (store CTI access token)
  • Manual content sync trigger
  • CUD operations on rules, decoders, integrations, and KVDBs
  • Policy management
  • Promotion preview and execution
  • Logtest execution
  • Content validation and promotion

Credentials Store

Manages the CTI access token used for all CTI API requests. The token is submitted via POST /subscription, persisted in the .wazuh-internal-state hidden index, and cached in PluginSettings.accessToken (a volatile String field). On node startup, the token is loaded from the index into memory. Without a registered token, sync and update operations are rejected.

All HTTP clients that communicate with CTI services send a custom User-Agent header in the format Wazuh Indexer <version> (e.g., Wazuh Indexer 5.0.0). This applies to the Catalog API client, Snapshot client, and Telemetry client.

Job Scheduler (CatalogSyncJob)

Implements the OpenSearch JobSchedulerExtension interface. Registers a periodic job (wazuh-catalog-sync-job) that triggers content synchronization at a configurable interval (default: 60 minutes). The job metadata is stored in .wazuh-content-manager-jobs.

Update Check Service (TelemetryPingJob)

Implements a daily heartbeat job (wazuh-telemetry-ping-job) that calls the CTI Update check API endpoint (/ping).

  • Enabled by default through plugins.content_manager.telemetry.enabled.
  • Can be toggled at runtime because it is a dynamic setting.
  • Sends deployment metadata required for update checks (cluster UUID, deployed Wazuh version, and user-agent).
  • Job metadata is stored in .wazuh-content-manager-jobs.
  • The first ping is dispatched immediately after the job is registered in the scheduler; subsequent runs follow the 1-day interval.

Consumer Service

Orchestrates synchronization for each catalog consumer type (ruleset, iocs, vulnerabilities). Compares local offsets (from .wazuh-cti-consumers) with remote offsets from the CTI API, then delegates to either the Snapshot Service or Update Service. Tracks the sync lifecycle through the status field in .wazuh-cti-consumers: set to running at the start of synchronize(), then to ready once all post-sync work (hash recalculation, Security Analytics sync, Engine notification) is complete, or to failed if an unexpected exception interrupts the sync.

Snapshot Service

Handles initial content loading. Initializes from either a remote CTI snapshot (when a custom consumer URL is configured) or a local packaged snapshot, then extracts and bulk-indexes content into the appropriate system indices. Performs data enrichment (e.g., converting JSON payloads to YAML for decoders).

Update Service

Handles incremental updates. Fetches change batches from the CTI API based on offset differences and applies create, update, and delete operations to content indices.

Security Analytics Service

Interfaces with the OpenSearch Security Analytics plugin. Creates, updates, and deletes Security Analytics rules, integrations, and detectors to keep them in sync with CTI content.

Dynamic Configuration: Instead of using hardcoded defaults, the service extracts enabled, interval, and source (index patterns) directly from the CTI integration payload. This allows CTI to control detector behavior dynamically.

Document ID model: SAP documents use their own auto-generated UUIDs as primary IDs, independent of the CTI document UUIDs. Each SAP document stores:

  • document.id — the UUID of the original CTI document in the Content Manager.
  • source — the space the document belongs to (e.g., “draft”, “test”, “custom”, or “standard”).

This design allows the same CTI resource to exist across multiple spaces without ID collisions. Association and lookup between CTI and SAP documents is performed by querying document.id + source.

Note: SAP enforces a maximum of 100 rules per detector. If an integration has more than 100 enabled rules, the detector creation or update request will be rejected. See Security Analytics — Detector constraints for details.

Space Service

Manages the four content spaces (standard, draft, test, custom). Routes CUD operations to the correct space partitions within system indices. Handles promotion by computing diffs between spaces in the promotion chain (Draft → Test → Custom).

Engine Client

Communicates with the Wazuh Engine via Unix domain socket at /usr/share/wazuh-indexer/engine/sockets/engine-api.sock. Used for logtest execution, content validation, and configuration reload.

Data Flows

CTI Sync (Snapshot)

Job Scheduler triggers
  → Consumer Service checks .wazuh-cti-consumers (offset = 0)
  → If custom catalog URL is configured: try remote snapshot first
  → If remote init fails: fallback to local packaged snapshot
  → If no custom catalog URL: initialize from local packaged snapshot
  → Extracts and bulk-indexes into wazuh-threatintel-rules, wazuh-threatintel-decoders, etc.
  → Updates .wazuh-cti-consumers with new offset
  → Security Analytics Service creates detectors using dynamic CTI configuration (max 100 rules per detector)

CTI Sync (Incremental)

Job Scheduler triggers
  → Consumer Service checks .wazuh-cti-consumers (local_offset < remote_offset)
  → Update Service fetches change batches from CTI API
  → Applies CREATE/UPDATE/DELETE to content indices
  → Updates .wazuh-cti-consumers offset
  → Security Analytics Service syncs changes

Update Check Heartbeat

Registration (on node start or dynamic enable)
  → TelemetryPingJob document indexed in .wazuh-content-manager-jobs
  → Immediate first ping fired once the document is written

Job Scheduler triggers (every 24h thereafter)
  → TelemetryPingJob checks plugins.content_manager.telemetry.enabled
  → Reads cluster UUID and current Wazuh version
  → TelemetryClient sends GET /ping to CTI Update check API
  → Wazuh Dashboard can surface update availability to users

User-Generated Content (CUD)

REST request (POST/PUT/DELETE)
  → Space Service routes to draft space
  → Writes to wazuh-threatintel-rules / wazuh-threatintel-decoders / wazuh-threatintel-integrations / wazuh-threatintel-kvdbs
  → Returns created/updated/deleted resource

Standard Policy Engine Loading

The local Wazuh Engine must always reflect the latest version of the standard space policy. Whenever the standard space space.hash changes, the full policy — including all referenced integrations, decoders, kvdbs, filters, and rules — is built and sent to the Engine via EngineService.promote().

The space.hash is an aggregate SHA-256 computed from the individual hashes of the policy and every resource it references. Any change to the policy will trigger a reload. These changes include:

  • New or updated integrations, decoders, rules, kvdbs, or filters (via CTI sync)
  • Changes to policy settings (enabled, index_unclassified_events, index_discarded_events)
  • Changes to the enrichment types list
  • Reordering of the filters list

The engine load is best-effort: if the Engine is unreachable, the error is logged but the operation (sync or REST update) still succeeds.

Promotion

GET /promote?space=draft
  → Space Service computes diff (draft vs test, or test vs custom)
  → Returns changes preview (adds, updates, deletes per content type)

POST /promote
  → Capture pre-promotion snapshots of target-space resources
  → Engine validates configuration (draft → test only, and only when the
    changeset includes decoders, kvdbs, or filters — promotions limited to
    integrations, rules, or the policy skip the engine call)
  → Consolidate changes to CM indices (tracked for rollback)
      → Apply adds/updates: policy, integrations, kvdbs, decoders, filters, rules
      → Apply deletes: integrations, kvdbs, decoders, filters, rules
  → Sync integrations and rules to SAP:
      → ADDs use POST (new SAP document)
      → UPDATEs use PUT (existing SAP document)
  → Delete removed integrations/rules from SAP

Rollback on Failure

If any Content Manager index mutation fails during the consolidation phase, the promotion endpoint automatically performs a LIFO (Last-In, First-Out) rollback to restore the system to its pre-promotion state.

Pre-Promotion Snapshots

Before any writes, the system captures:

  • Old versions (captureOldVersions): For each resource being added or updated, the current target-space version is fetched and stored. If the resource does not exist in the target space, null is stored.
  • Delete snapshots (captureDeleteSnapshots): For each resource being deleted, the full document is fetched from the source space and stored.

CM Index Rollback

Each successful index mutation is recorded as a RollbackStep(kind, resourceType). On failure, steps are replayed in strict reverse (LIFO) order:

Forward operationOld versionRollback action
ADD (apply)nullDelete the newly created document
UPDATE (apply)non-nullRestore the previous version
DELETEsnapshotRe-index the snapshotted document

Individual rollback step failures are logged and skipped so remaining steps can proceed.

SAP Reconciliation

After CM rollback completes, a best-effort SAP reconciliation runs in dependency order:

  1. Revert applied rules — ADDs are deleted from SAP; UPDATEs are restored to old version.
  2. Revert applied integrations — Same as above.
  3. Restore deleted integrations — Re-created from pre-deletion snapshots via POST.
  4. Restore deleted rules — Same as above.

SAP reconciliation failures are logged as warnings but do not cause the overall rollback to fail, since SAP sync is considered best-effort.

Consolidation fails at step N
  → LIFO rollback: undo step N-1, N-2, ..., 1
      → APPLY + null old version → delete from target index
      → APPLY + old version → restore old version to target index
      → DELETE → re-index snapshot to target index
  → SAP reconciliation (best-effort):
      → Delete rules that were added to SAP
      → Restore rules that were updated in SAP
      → Restore integrations that were added/updated in SAP
      → Re-create integrations/rules that were deleted from SAP
  → Return 500 with error message

Plan Change Handling (Blue/Green Swap)

When a subscription plan changes (e.g., free → pro, or vice versa), all downloaded content must be replaced with the content matching the new plan. The Content Manager uses a blue/green index swap to perform this replacement without any user-visible downtime.

How it works

  1. Detection. During each sync cycle, the Content Manager compares the plan-provided catalog URL against the one stored locally. If they differ, a plan change is detected.
  2. Shadow download. New content is downloaded into hidden staging indices. These shadow indices are invisible to users, dashboards, and REST queries during the rebuild.
  3. User content preservation. Any user-created content (draft rules, test decoders, custom integrations, etc.) is copied from the live indices into the shadow indices.
  4. Atomic switch. Once the shadow indices are fully ready, all index aliases are swapped in a single atomic operation. Users see either the entire old content or the entire new content — never a mix or an empty state.
  5. Cleanup. The old indices are deleted, freeing the temporary disk space.

Failure behavior

If the new content cannot be downloaded or processed (network error, source unavailable, etc.), the swap is abandoned cleanly: the staging indices are discarded, users continue to see the old content, and the system retries on the next scheduled sync. A failed swap is invisible to the end user.

Index Structure

Each content index (e.g., wazuh-threatintel-rules) is backed by an alias. The public alias name is the stable identifier used by all queries, dashboards, and REST APIs. The actual data lives in a physical index suffixed with -a or -b:

Public alias (stable name)Physical index (actual storage)
wazuh-threatintel-ruleswazuh-threatintel-rules-a or wazuh-threatintel-rules-b

Only one physical index is live at a time. The other is reserved as the staging slot for the next plan-change swap. Administrators and users should always address indices by their alias name — the physical suffix is an internal implementation detail.

Each content index stores documents from all spaces. Documents are differentiated by internal metadata fields that indicate their space membership. The document _id is a UUID assigned at creation time.

Example document structure in wazuh-threatintel-rules:

{
  "_index": "wazuh-threatintel-rules-a",
  "_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "_source": {
    "title": "SSH brute force attempt",
    "integration": "openssh",
    "space.name": "draft",
    ...
  }
}

The .wazuh-cti-consumers index stores one document per consumer type:

{
  "_index": ".wazuh-cti-consumers",
  "_id": "cti:catalog:consumer:ruleset",
  "_source": {
    "name": "public-ruleset-5",
    "context": "beta-2-ruleset-5",
    "type": "cti:catalog:consumer:ruleset",
    "resource": "https://api.pre.cloud.wazuh.com/api/v1/catalog/contexts/beta-2-ruleset-5/consumers/public-ruleset-5",
    "is_public": true,
    "status": "ready",
    "local_offset": 3932,
    "remote_offset": 3932
  }
}

The status field reflects the consumer’s synchronization lifecycle:

ValueMeaning
readySync is complete; content indices are up-to-date and safe to read.
runningSync is in progress; content may be partially written or inconsistent.
failedThe previous sync cycle was interrupted by an unexpected exception.

The status is set to running at the very start of a sync cycle and transitions to ready after all post-sync work finishes — including hash recalculation, Security Analytics Plugin synchronization, and Engine IoC notification — or to failed if an unexpected exception interrupts the cycle. The Job Scheduler logs the failure and retries on the next scheduled run regardless of the consumer’s status.

Configuration

Content manager settings

The Content Manager plugin is configured through settings in opensearch.yml. All settings use the plugins.content_manager prefix.

SettingData typeDefault valueDescription
plugins.content_manager.cti.apiStringhttps://api.pre.cloud.wazuh.com/api/v1Base URL for the Wazuh CTI API
plugins.content_manager.catalog.sync_intervalInteger60Sync interval in minutes. Valid range: 1–1440
plugins.content_manager.max_items_per_bulkInteger999Maximum documents per bulk indexing request. Valid range: 10–999
plugins.content_manager.max_concurrent_bulksInteger5Maximum concurrent bulk operations. Valid range: 1–5
plugins.content_manager.client.timeoutLong10HTTP client timeout in seconds for CTI API requests. Valid range: 10–50
plugins.content_manager.catalog.update_on_startBooleantrueTrigger content sync when the plugin starts
plugins.content_manager.catalog.update_on_scheduleBooleantrueEnable the periodic sync job
plugins.content_manager.catalog.rulesetString""Full CTI consumer URL for ruleset content
plugins.content_manager.catalog.iocsString""Full CTI consumer URL for IoC content
plugins.content_manager.catalog.vulnerabilitiesString""Full CTI consumer URL for vulnerabilities content
plugins.content_manager.catalog.create_detectorsBooleantrueAutomatically create Security Analytics detectors from CTI content
plugins.content_manager.telemetry.enabledBooleantrueEnable or disable the daily Update check service ping. This setting is dynamic.
plugins.content_manager.catalog.update_on_demandBooleantrueWhen false, on-demand content updates (POST /update) return 403 Forbidden for every caller, regardless of role.
plugins.content_manager.catalog.policy_update.enabledBooleantrueWhen false, policy updates (PUT /policy/{space}) return 403 Forbidden for every caller, regardless of role.

Offline configuration / disabling automatic updates

On offline installations, disable every task that requires an internet connection to prevent failures.

# opensearch.yml
plugins.content_manager.catalog.update_on_start: false
plugins.content_manager.catalog.update_on_schedule: false
plugins.content_manager.telemetry.enabled: false

On online installations, manual synchronization can be performed on demand using the Content Manager API:

POST /_plugins/_content_manager/update"

Custom scheduled synchronization interval

The plugin checks for new content every 60 minutes by default, but this can be customized by changing the plugins.content_manager.catalog.sync_interval setting. The value is specified in minutes and must be between 10 and 1440 (24 hours).

# opensearch.yml
plugins.content_manager.catalog.sync_interval: 1440

Custom CTI API Endpoint

To point to a different CTI API (e.g., production):

# opensearch.yml
plugins.content_manager.cti.api: "https://cti.wazuh.com/api/v1"

Custom Catalog Consumer URLs

To override default consumers, provide full HTTP(S) consumer URLs:

# opensearch.yml
plugins.content_manager.catalog.ruleset: "https://api.pre.cloud.wazuh.com/api/v1/catalog/contexts/beta-2-ruleset-5/consumers/public-ruleset-5"
plugins.content_manager.catalog.iocs: "https://api.pre.cloud.wazuh.com/api/v1/catalog/contexts/t1-iocs-5/consumers/public-iocs-5"
plugins.content_manager.catalog.vulnerabilities: "https://api.pre.cloud.wazuh.com/api/v1/catalog/contexts/t1-vulnerabilities-5/consumers/public-vulnerabilities-5"

Behavior:

  • If a setting is non-empty, Content Manager attempts remote snapshot initialization first.
  • If remote initialization fails, it falls back to the local packaged snapshot when available.
  • If a setting is empty, initialization uses the local packaged snapshot directly.

Tune Bulk Operations

For environments with limited resources, reduce the bulk operation concurrency:

# opensearch.yml
plugins.content_manager.max_items_per_bulk: 10
plugins.content_manager.max_concurrent_bulks: 2
plugins.content_manager.client.timeout: 30

Disable Security Analytics Detector Creation

If you do not use the OpenSearch Security Analytics plugin:

# opensearch.yml
plugins.content_manager.catalog.create_detectors: false

CTI communication headers

All HTTP clients that communicate with Wazuh CTI services send a custom User-Agent header:

User-Agent: Wazuh Indexer <version>

For example: Wazuh Indexer 5.0.0. This applies to the Console API client, Catalog API client, Snapshot client, and Telemetry client. The version is read from VERSION.json at plugin startup.

Update check service behavior

The update check service is enabled by default and runs once per day, with an immediate first ping fired as soon as the job is registered in the scheduler.

  • It is implemented by a scheduled job (wazuh-telemetry-ping-job) in .wazuh-content-manager-jobs.
  • It sends a request to the CTI Update check API endpoint (/ping).
  • The request includes:
    • Deployment identifier (wazuh-uid: cluster UUID)
    • Running version (wazuh-tag: v<version>)
    • User agent (Wazuh Indexer <version>)

This data allows Wazuh to determine if a newer version is available and notify users in the update check UI.

The service only sends deployment identification/version metadata required for update checks. It does not send rules, events, or log payloads.

Enable/Disable Update check service dynamically

The update check service can be enabled or disabled at runtime without restarting the node using the Cluster Settings API:

curl -sk -u admin:admin -X PUT "https://192.168.56.6:9200/_cluster/settings" -H 'Content-Type: application/json' -d'
{
  "persistent": {
    "plugins.content_manager.telemetry.enabled": false
  }
}'

Protecting sensitive configuration

Some endpoints modify configuration with a high impact on the platform and are protected by two independent controls:

EndpointMethodPermission (cluster action)
/_plugins/_content_manager/policy/{space}PUTindices:data/write/content_manager/policy/update
/_plugins/_content_manager/updatePOSTcluster:admin/content_manager/update/trigger
/_plugins/_setup/settingsPUTcluster:admin/setup/settings/update
  1. RBAC — each endpoint is gated by a cluster permission (the action name above), enforced by the security plugin. Only the superuser admin (role all_access, cluster wildcard *) holds these permissions; the bundled wazuh-server and wazuh-dashboard users do not. To delegate any of these actions without granting full superuser, create a dedicated role for the permission(s) above. See the access control reference.

  2. Per-endpoint disable settings — each endpoint can be disabled independently with its own node setting; when disabled it returns 403 Forbidden for every caller, including admin / all_access. This is intended for externally managed (e.g. Wazuh Cloud) deployments.

    EndpointSetting (set to false to disable)
    POST /_plugins/_content_manager/updateplugins.content_manager.catalog.update_on_demand
    PUT /_plugins/_content_manager/policy/{space}plugins.content_manager.catalog.policy_update.enabled
    PUT /_plugins/_setup/settingsplugins.setup.settings_update.enabled
# opensearch.yml — disable sensitive configuration endpoints on a managed deployment
plugins.content_manager.catalog.update_on_demand: false
plugins.content_manager.catalog.policy_update.enabled: false
plugins.setup.settings_update.enabled: false

Notes

  • Changes to opensearch.yml require a restart of the Wazuh Indexer to take effect, except for dynamic settings (like plugins.content_manager.telemetry.enabled), which can be updated at runtime via the OpenSearch API.
  • The catalog URL settings (plugins.content_manager.catalog.ruleset, plugins.content_manager.catalog.iocs, and plugins.content_manager.catalog.vulnerabilities) should only be changed if instructed by Wazuh support or documentation, and must point to valid absolute HTTP(S) CTI consumer endpoints.
  • The sync interval is enforced by the OpenSearch Job Scheduler. The actual sync timing may vary slightly depending on cluster load.
  • The update check service runs with a fixed interval of 1 day when enabled. The first ping is sent immediately after the job is registered (on node start or when the setting is dynamically enabled); subsequent pings follow the 1-day interval.
  • Detector Configuration: The settings for Security Analytics detectors (interval, enabled status, and source indices) are managed directly via CTI integration files. If an integration’s detector object is missing in the CTI source, the system will use built-in safety defaults.

API Reference

The Content Manager plugin exposes a REST API under /_plugins/_content_manager/. All endpoints require authentication.


YAML Content-Type Support

The Decoders, KVDBs, and Filters endpoints accept requests with Content-Type: application/yaml in addition to the standard Content-Type: application/json. When using YAML, the request body uses the same envelope structure as JSON — the only difference is the serialization format.

Envelope structure

Both JSON and YAML requests use the same envelope:

JSON example:

{
  "integration": "<uuid>",
  "resource": {
    "metadata": { "title": "My Decoder", "author": "Wazuh" },
    "name": "decoder/my-decoder/0",
    "enabled": true
  }
}

Equivalent YAML example:

---
integration: <uuid>
resource:
  metadata:
    title: "My Decoder"
    author: "Wazuh"
  name: decoder/my-decoder/0
  enabled: true

For resource types that do not require an integration field (e.g., Filters, which use space instead), the corresponding field appears at the top level of the envelope in both formats.

YAML field in responses

When a Decoder, KVDB, or Filter is created or updated, a yaml field is stored alongside the document in the indexed record. This field contains a YAML representation of the resource content:

  • YAML requests: The yaml field is generated from the resource subtree of the parsed envelope.
  • JSON requests: The yaml field is auto-generated from the resource content.

Type fidelity

YAML parsing preserves numeric type fidelity. Floating-point values like 5.0 are stored as 5.0 in both the yaml field and the document field — they are not coerced to integers.

Supported endpoints

EndpointMethodsYAML supported
/_plugins/_content_manager/decodersPOST, PUT
/_plugins/_content_manager/kvdbsPOST, PUT
/_plugins/_content_manager/filtersPOST, PUT
/_plugins/_content_manager/integrationsPOST, PUT
/_plugins/_content_manager/rulesPOST, PUT
/_plugins/_content_manager/policy/{space}PUT

Subscription Management

Store CTI Credentials

Stores the provided CTI access token in the .wazuh-internal-state hidden index and loads it into memory. If the index does not exist it is recreated automatically before writing.

Request

  • Method: POST
  • Path: /_plugins/_content_manager/subscription

Request Body

FieldTypeRequiredDescription
access_tokenStringYesThe CTI access token used to authenticate against the CTI API

Example Request

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/subscription" \
  -H 'Content-Type: application/json' \
  -d '{
    "access_token": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS"
  }'

Example Response

{
  "message": "Credentials received",
  "status": 201
}

Status Codes

CodeDescription
201Credentials stored successfully
400Missing or empty access_token field
500Internal error

Get CTI Subscription Status

Returns the current subscription status and active plan. For registered instances the plan comes from the authenticated CTI endpoint; for unregistered instances, the public free plan is returned.

If the stored token is rejected by the CTI API (e.g. expired or revoked), the credentials document is deleted automatically, the in-memory token is cleared, and the response falls back to the public free plan as if the instance were unregistered.

Request

  • Method: GET
  • Path: /_plugins/_content_manager/subscription

Example Request

curl -sk -u admin:admin -X GET \
  "https://localhost:9200/_plugins/_content_manager/subscription"

Example Response (registered)

{
  "message": {
    "plan": {
      "name": "Premium Plan",
      "is_public": false
    },
    "is_registered": true
  },
  "status": 200
}

Example Response (unregistered)

{
  "message": {
    "plan": {
      "name": "Free",
      "is_public": true
    },
    "is_registered": false
  },
  "status": 200
}

Status Codes

CodeDescription
200Subscription status returned successfully
500Internal error

Delete CTI Credentials

Clears the stored CTI access token document from the credentials index and clears the in-memory token. The credentials index is preserved. After this operation the instance is unregistered. If the credentials index does not exist the operation succeeds without error.

Request

  • Method: DELETE
  • Path: /_plugins/_content_manager/subscription

Example Request

curl -sk -u admin:admin -X DELETE \
  "https://localhost:9200/_plugins/_content_manager/subscription"

Example Response

{
  "message": "Credentials removed",
  "status": 200
}

Status Codes

CodeDescription
200Credentials removed successfully
500Internal error

Content Updates

Trigger Manual Sync

Triggers an immediate content synchronization with the CTI API. Requires a valid subscription.

Request

  • Method: POST
  • Path: /_plugins/_content_manager/update

Example Request

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/update"

Example Response (accepted)

{
  "message": "The update request has been accepted for processing.",
  "status": 202
}

Example Response (no credentials)

{
  "message": "Token not found. Please create a subscription before attempting to update.",
  "status": 404
}

Example Response (update in progress)

{
  "message": "A content update is already in progress.",
  "status": 409
}

Status Codes

CodeDescription
202Update request accepted for processing
404No access token registered
409A content update is already in progress
500Internal error during sync

Logtest

Execute Logtest

Sends a log event to the Wazuh Engine for analysis. If an integration ID is provided, the integration’s Sigma rules are also evaluated against the normalized event via the Security Analytics Plugin (SAP). If integration is omitted, only the normalization step is performed and the detection section is returned with status: "skipped".

Note: A testing policy must be loaded in the Engine for logtest to execute successfully. Load a policy via the policy promotion endpoint. When an integration is specified, it must exist in the specified space.

Request

  • Method: POST
  • Path: /_plugins/_content_manager/logtest

Request Body

FieldTypeRequiredDescription
integrationStringNoID of the integration to test against. If omitted, only normalization is performed.
spaceStringYes"test", "standard" or "custom"
queueIntegerYesQueue number for logtest execution
locationStringYesLog file path or logical source location
eventStringYesRaw log event to test
metadataObjectNoOptional metadata passed to the Engine
trace_levelStringNoTrace verbosity: NONE, ASSET_ONLY, or ALL

Example Request

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/logtest" \
  -H 'Content-Type: application/json' \
  -d '{
    "integration": "a0b448c8-3d3c-47d4-b7b9-cbc3c175f509",
    "space": "test",
    "queue": 1,
    "location": "/var/log/cassandra/system.log",
    "event": "INFO  [main] 2026-03-31 10:00:00 StorageService.java:123 - Node is ready to serve",
    "trace_level": "NONE"
  }'

Example Response (success with rule match)

{
  "status": 200,
  "message": {
    "normalization": {
      "output": {
        "event": {
          "category": ["database"],
          "kind": "event",
          "original": "INFO  [main] 2026-03-31 10:00:00 StorageService.java:123 - Node is ready to serve"
        },
        "wazuh": {
          "integration": {
            "name": "test-integ",
            "category": "other",
            "decoders": ["decoder/cassandra-default/0"]
          }
        },
        "message": "Node is ready to serve"
      },
      "asset_traces": [],
      "validation": {
        "valid": true,
        "errors": []
      }
    },
    "detection": {
      "status": "success",
      "rules_evaluated": 2,
      "rules_matched": 1,
      "matches": [
        {
          "rule": {
            "id": "85bba177-a2e9-4468-9d59-26f4798906c9",
            "title": "Cassandra Database Event Detected",
            "level": "low",
            "tags": []
          },
          "matched_conditions": [
            "event.category matched 'database'",
            "event.kind matched 'event'"
          ]
        }
      ]
    }
  }
}

Example Response (Engine error, SAP skipped)

{
  "status": 200,
  "message": {
    "normalization": {
      "status": "error",
      "error": {
        "message": "Failed to parse protobuff json request: invalid value",
        "code": "ENGINE_ERROR"
      }
    },
    "detection": {
      "status": "skipped",
      "reason": "Engine processing failed"
    }
  }
}

Example Response (no rules in integration)

{
  "status": 200,
  "message": {
    "normalization": {
      "output": { "..." : "..." },
      "asset_traces": [],
      "validation": { "valid": true, "errors": [] }
    },
    "detection": {
      "status": "success",
      "rules_evaluated": 0,
      "rules_matched": 0,
      "matches": []
    }
  }
}

Example Request (normalization only, no integration)

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/logtest" \
  -H 'Content-Type: application/json' \
  -d '{
    "space": "test",
    "queue": 1,
    "location": "/var/log/syslog",
    "event": "Mar 31 10:00:00 myhost sshd[1234]: Accepted publickey for user from 192.168.1.1 port 22 ssh2",
    "trace_level": "NONE"
  }'

Example Response (normalization only)

{
  "status": 200,
  "message": {
    "normalization": {
      "output": {
        "event": {
          "original": "Mar 31 10:00:00 myhost sshd[1234]: Accepted publickey for user from 192.168.1.1 port 22 ssh2"
        }
      },
      "asset_traces": [],
      "validation": { "valid": true, "errors": [] }
    },
    "detection": {
      "status": "skipped",
      "reason": "No integration provided"
    }
  }
}

Response Fields

FieldTypeDescription
normalization.outputObjectEngine normalized event output
normalization.asset_tracesArrayList of decoders that processed the event
normalization.validationObjectValidation result (valid, errors)
normalization.statusStringPresent on error: "error"
normalization.errorObjectPresent on error: message and code
detection.statusString"success", "error", or "skipped"
detection.reasonStringPresent when status is "skipped"
detection.rules_evaluatedIntegerNumber of Sigma rules evaluated
detection.rules_matchedIntegerNumber of rules that matched
detection.matchesArrayList of matched rules with details
detection.matches[].ruleObjectRule metadata: id, title, level, tags
detection.matches[].matched_conditionsArrayHuman-readable descriptions of conditions that matched

Status Codes

CodeDescription
200Logtest executed (check inner status fields)
400Missing/invalid fields or integration not found
500Engine socket communication error or internal error

Normalization Only

Sends a log event to the Wazuh Engine for decoding and normalization without performing Sigma rule detection. Use this to validate that decoders correctly parse events before testing detection rules.

Note: A testing policy must be loaded in the Engine for normalization to execute successfully.

Request

  • Method: POST
  • Path: /_plugins/_content_manager/logtest/normalization

Request Body

FieldTypeRequiredDescription
spaceStringYes"test" or "standard"
queueIntegerNoQueue number for logtest execution
locationStringNoLog file path or logical source location
inputStringNoRaw log event to normalize
metadataObjectNoOptional metadata passed to the Engine
trace_levelStringNoTrace verbosity: NONE, ASSET_ONLY, or ALL

Example Request

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/logtest/normalization" \
  -H 'Content-Type: application/json' \
  -d '{
    "space": "test",
    "queue": 1,
    "location": "/var/log/cassandra/system.log",
    "metadata": {},
    "trace_level": "NONE",
    "input": "INFO  [CompactionExecutor-3] 2025-11-30 14:23:45 CassandraDaemon.java:250 - Some message - 7500 - 4"
  }'

Example Response

{
  "status": 200,
  "message": {
    "output": {
      "log": {
        "level": "INFO",
        "origin": {
          "file": {
            "name": "CassandraDaemon.java",
            "line": 250
          }
        }
      },
      "wazuh": {
        "space": { "name": "test" },
        "protocol": { "location": "/var/log/cassandra/system.log", "queue": 1 },
        "integration": {
          "decoders": ["decoder/cassandra-default/0"],
          "name": "my-integration",
          "category": "other"
        }
      },
      "message": "Some message",
      "event": {
        "duration": 7500,
        "category": ["database"],
        "kind": "event",
        "severity": 4
      },
      "source": { "ip": "10.42.3.15" },
      "process": {
        "thread": { "name": "CompactionExecutor-3" }
      }
    },
    "asset_traces": [],
    "validation": {
      "valid": true,
      "errors": []
    }
  }
}

Response Fields

FieldTypeDescription
message.outputObjectEngine normalized event output
message.asset_tracesArrayList of decoders that processed the event
message.validationObjectValidation result (valid, errors)

Status Codes

CodeDescription
200Normalization executed successfully
400Missing/invalid fields
500Engine socket communication error or internal error

Detection Only

Evaluates an already-normalized event against the Sigma rules of a given integration via the Security Analytics Plugin (SAP). This endpoint does not call the Wazuh Engine — the normalized event must be provided directly in the input field.

Use this after obtaining a normalized event from the /logtest/normalization endpoint, or when you already have a normalized event and want to test different integrations’ rules against it.

Note: The integration must exist in the specified space. The input field must be a JSON object (the normalized event), not a raw log string.

Request

  • Method: POST
  • Path: /_plugins/_content_manager/logtest/detection

Request Body

FieldTypeRequiredDescription
spaceStringYes"test" or "standard"
integrationStringYesUUID of the integration whose rules to evaluate
inputObjectYesNormalized event object to evaluate rules against

Example Request

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/logtest/detection" \
  -H 'Content-Type: application/json' \
  -d '{
    "space": "test",
    "integration": "d3f3b0b8-4e25-4273-83ef-56a62003bcf7",
    "input": {
      "event": {
        "duration": 7500,
        "category": ["database"],
        "kind": "event",
        "severity": 4,
        "type": ["info"]
      },
      "source": { "ip": "10.42.3.15" },
      "process": {
        "thread": { "name": "CompactionExecutor-3" },
        "command_line": "/query tables"
      },
      "log": {
        "origin": {
          "file": { "name": "CassandraDaemon.java", "line": 250 }
        }
      }
    }
  }'

Example Response (matches found)

{
  "status": 200,
  "message": {
    "status": "success",
    "rules_evaluated": 12,
    "rules_matched": 6,
    "matches": [
      {
        "rule": {
          "id": "4e52f215-bccc-4c0f-a37c-70606022be8e",
          "title": "TEST: Numeric gte+lt only",
          "level": "high",
          "tags": ["attack.execution", "attack.t1059"]
        },
        "matched_conditions": [
          "event.duration matched '>= 5000'",
          "event.severity matched '< 10'"
        ]
      },
      {
        "rule": {
          "id": "1d489ded-7523-4329-8cd0-ebb21865a318",
          "title": "TEST: Exact match event.kind=event",
          "level": "low",
          "tags": ["attack.execution", "attack.t1059"]
        },
        "matched_conditions": [
          "event.kind matched 'event'"
        ]
      }
    ]
  }
}

Example Response (no rules in integration)

{
  "status": 200,
  "message": {
    "status": "success",
    "rules_evaluated": 0,
    "rules_matched": 0,
    "matches": []
  }
}

Response Fields

FieldTypeDescription
message.statusString"success" or "error"
message.rules_evaluatedIntegerNumber of Sigma rules evaluated
message.rules_matchedIntegerNumber of rules that matched
message.matchesArrayList of matched rules with details
message.matches[].ruleObjectRule metadata: id, title, level, tags
message.matches[].matched_conditionsArrayHuman-readable descriptions of matched conditions

Status Codes

CodeDescription
200Detection executed (check message.status)
400Missing/invalid fields or integration not found
500Internal error

Policy

Update Policy

Updates the routing policy in the specified space. The policy defines which integrations are active, the root decoder, enrichment types, and how events are routed through the Engine.

Note: The integrations and filters arrays allow reordering but do not allow adding or removing entries — membership is managed via their respective CRUD endpoints.

Space-specific behavior

  • Draft space (/policy/draft): All policy fields are accepted. The metadata fields author, description, documentation, and references are required in addition to the boolean fields.
  • Standard space (/policy/standard): Only enrichments, filters, enabled, index_unclassified_events, and index_discarded_events can be modified. All other fields are preserved from the existing standard policy document. If the update changes the space hash, the full standard policy is automatically loaded to the local Engine.

Request

  • Method: PUT
  • Path: /_plugins/_content_manager/policy/{space}

Path Parameters

ParameterTypeRequiredDescription
spaceStringYesTarget space (draft or standard)

Request Body

FieldTypeRequiredDescription
resourceObjectYesThe policy resource object

Fields within resource:

FieldTypeRequiredDescription
metadataObjectYes (draft)Policy metadata (see below)
root_decoderStringNoIdentifier of the root decoder for event processing
integrationsArrayNoList of integration IDs (reorder only, no add/remove)
filtersArrayNoList of filter UUIDs (reorder only, no add/remove)
enrichmentsArrayNoEnrichment types (no duplicates; values depend on engine capabilities)
enabledBooleanYesWhether the policy is active and synchronized by the Engine
index_unclassified_eventsBooleanYesWhether uncategorized events are indexed
index_discarded_eventsBooleanYesWhether discarded events are indexed

Fields within resource.metadata:

FieldTypeRequiredDescription
titleStringNoHuman-readable policy name
authorStringYes (draft)Author of the policy
descriptionStringYes (draft)Brief description
documentationStringYes (draft)Documentation text or URL
referencesArrayYes (draft)External reference URLs

Example Request (draft space)

curl -sk -u admin:admin -X PUT \
  "https://192.168.56.6:9200/_plugins/_content_manager/policy/draft" \
  -H 'Content-Type: application/json' \
  -d '{
    "resource": {
      "metadata": {
        "title": "Draft policy",
        "author": "Wazuh Inc.",
        "description": "Custom policy",
        "documentation": "",
        "references": [
          "https://wazuh.com"
        ]
      },
      "root_decoder": "",
      "integrations": [
        "f16f33ec-a5ea-4dc4-bf33-616b1562323a"
      ],
      "filters": [],
      "enrichments": [],
      "enabled": true,
      "index_unclassified_events": false,
      "index_discarded_events": false
    }
  }'

Example Request (standard space)

curl -sk -u admin:admin -X PUT \
  "https://192.168.56.6:9200/_plugins/_content_manager/policy/standard" \
  -H 'Content-Type: application/json' \
  -d '{
    "resource": {
      "enrichments": ["connection"],
      "filters": [],
      "enabled": true,
      "index_unclassified_events": false,
      "index_discarded_events": false
    }
  }'

Example Response

{
  "message": "kQPmV5wBi_TgruUn97RT",
  "status": 200
}

The message field contains the OpenSearch document ID of the updated policy.

Status Codes

CodeDescription
200Policy updated
400Invalid space, missing resource field, missing required fields, invalid enrichments, or disallowed modification of integrations/filters
500Internal error

Rules

Rules follow the Sigma format with Wazuh extensions. See Sigma Rules for the full format reference, including the mitre, compliance, and metadata blocks.

Validation notes:

  • The logsource.product field must exactly match the metadata.title of the parent integration.
  • Detection fields are validated against the Wazuh Common Schema (WCS); rules referencing unknown fields are rejected.
  • IPv6 addresses are supported in detection conditions (standard, compressed, and CIDR notation).

Create Rule

Creates a new detection rule in the draft space. The rule is linked to the specified parent integration and validated by the Security Analytics Plugin.

The rule is also synchronized to the SAP, where a separate document is created with its own auto-generated UUID. The SAP document stores the CTI document UUID in a document.id field and the space in a source field (e.g., “Draft”) for cross-reference.

Request

  • Method: POST
  • Path: /_plugins/_content_manager/rules

Request Body

FieldTypeRequiredDescription
integrationStringYesUUID of the parent integration (must be in draft space)
resourceObjectYesThe rule definition

Fields within resource:

FieldTypeRequiredDescription
metadataObjectYesRule metadata (see below)
sigma_idStringNoSigma rule ID
enabledBooleanNoWhether the rule is enabled
statusStringYesRule status (e.g., experimental, stable)
levelStringYesAlert level (e.g., low, medium, high, critical)
logsourceObjectNoLog source definition (product, category)
detectionObjectYesSigma detection logic with condition and selection fields
mitreObjectNoMITRE ATT&CK mapping (see Sigma Rules)
complianceObjectNoCompliance framework mapping (see Sigma Rules)

Fields within resource.metadata:

FieldTypeRequiredDescription
titleStringYesRule title (must be unique within the draft space)
authorStringNoRule author
descriptionStringNoRule description
referencesArrayNoReference URLs
documentationStringNoDocumentation text or URL

Example Request

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/rules" \
  -H 'Content-Type: application/json' \
  -d '{
    "integration": "6b7b7645-00da-44d0-a74b-cffa7911e89c",
    "resource": {
      "metadata": {
        "title": "Test Rule",
        "description": "A Test rule",
        "author": "Tester",
        "references": [
          "https://wazuh.com"
        ]
      },
      "sigma_id": "19aefed0-ffd4-47dc-a7fc-f8b1425e84f9",
      "enabled": true,
      "status": "experimental",
      "logsource": {
        "product": "system",
        "category": "system"
      },
      "detection": {
        "condition": "selection",
        "selection": {
          "event.action": [
            "hash_test_event"
          ]
        }
      },
      "level": "low",
      "mitre": {
        "tactic": ["TA0001"],
        "technique": ["T1190"],
        "subtechnique": []
      },
      "compliance": {
        "pci_dss": ["6.5.1"]
      }
    }
  }'

Example Response

{
  "message": "6e1c43f1-f09b-4cec-bb59-00e3a52b7930",
  "status": 201
}

The message field contains the UUID of the created rule.

Status Codes

CodeDescription
201Rule created
400Missing fields, duplicate title, integration not in draft space, or validation failure
500Internal error or SAP unavailable

Update Rule

Updates an existing rule in the draft space.

Request

  • Method: PUT
  • Path: /_plugins/_content_manager/rules/{id}

Parameters

NameInTypeRequiredDescription
idPathString (UUID)YesRule document ID

Request Body

FieldTypeRequiredDescription
resourceObjectYesUpdated rule definition (same fields as create)

Note: On update, enabled, metadata.title, and metadata.author are required. The detection and logsource fields are also required.

Example Request

curl -sk -u admin:admin -X PUT \
  "https://192.168.56.6:9200/_plugins/_content_manager/rules/6e1c43f1-f09b-4cec-bb59-00e3a52b7930" \
  -H 'Content-Type: application/json' \
  -d '{
    "resource": {
      "metadata": {
        "title": "Test Hash Generation Rule",
        "description": "A rule to verify that SHA-256 hashes are calculated correctly upon creation.",
        "author": "Tester"
      },
      "enabled": true,
      "status": "experimental",
      "logsource": {
        "product": "system",
        "category": "system"
      },
      "detection": {
        "condition": "selection",
        "selection": {
          "event.action": [
            "hash_test_event"
          ]
        }
      },
      "level": "low"
    }
  }'

Example Response

{
  "message": "6e1c43f1-f09b-4cec-bb59-00e3a52b7930",
  "status": 200
}

Status Codes

CodeDescription
200Rule updated
400Invalid request, not in draft space, or validation failure
404Rule not found
500Internal error

Delete Rule

Deletes a rule from the draft space. The rule is also removed from any integrations that reference it.

Request

  • Method: DELETE
  • Path: /_plugins/_content_manager/rules/{id}

Parameters

NameInTypeRequiredDescription
idPathString (UUID)YesRule document ID

Example Request

curl -sk -u admin:admin -X DELETE \
  "https://192.168.56.6:9200/_plugins/_content_manager/rules/6e1c43f1-f09b-4cec-bb59-00e3a52b7930"

Example Response

{
  "message": "6e1c43f1-f09b-4cec-bb59-00e3a52b7930",
  "status": 200
}

Status Codes

CodeDescription
200Rule deleted
404Rule not found
500Internal error

Decoders

Create Decoder

Creates a new log decoder in the draft space. The decoder is validated against the Wazuh Engine before being stored, and automatically linked to the specified integration.

Note: A testing policy must be loaded in the Engine for decoder validation to succeed.

Request

  • Method: POST
  • Path: /_plugins/_content_manager/decoders

Request Body

FieldTypeRequiredDescription
integrationStringYesUUID of the parent integration (must be in draft space)
resourceObjectYesThe decoder definition

Fields within resource:

FieldTypeDescription
nameStringDecoder name identifier (e.g., decoder/core-wazuh-message/0)
enabledBooleanWhether the decoder is enabled
checkArrayDecoder check logic — array of condition objects
normalizeArrayNormalization rules — array of mapping objects
metadataObjectDecoder metadata (see below)

Fields within metadata:

FieldTypeDescription
titleStringHuman-readable decoder title
descriptionStringDecoder description
moduleStringModule name
compatibilityStringCompatibility description
authorObjectAuthor info (name, email, url)
referencesArrayReference URLs
versionsArraySupported versions

Example Request

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/decoders" \
  -H 'Content-Type: application/json' \
  -d '{
    "integration": "0aa4fc6f-1cfd-4a7c-b30b-643f32950f1f",
    "resource": {
      "enabled": true,
      "metadata": {
        "author": {
          "name": "Wazuh, Inc."
        },
        "compatibility": "All wazuh events.",
        "description": "Base decoder to process Wazuh message format.",
        "module": "wazuh",
        "references": [
          "https://documentation.wazuh.com/"
        ],
        "title": "Wazuh message decoder",
        "versions": [
          "Wazuh 5.*"
        ]
      },
      "name": "decoder/core-wazuh-message/0",
      "check": [
        {
          "tmp_json.event.action": "string_equal(\"netflow_flow\")"
        }
      ],
      "normalize": [
        {
          "map": [
            {
              "@timestamp": "get_date()"
            }
          ]
        }
      ]
    }
  }'

Example Response

{
  "message": "d_0a6aaebe-dd0b-44cc-a787-ffefd4aac175",
  "status": 201
}

The message field contains the UUID of the created decoder (prefixed with d_).

Example Request (YAML)

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/decoders" \
  -H 'Content-Type: application/yaml' \
  --data-binary '---
integration: 0aa4fc6f-1cfd-4a7c-b30b-643f32950f1f
resource:
  enabled: true
  metadata:
    author:
      name: "Wazuh, Inc."
    compatibility: "All wazuh events."
    description: "Base decoder to process Wazuh message format."
    module: wazuh
    references:
      - "https://documentation.wazuh.com/"
    title: "Wazuh message decoder"
    versions:
      - "Wazuh 5.*"
  name: decoder/core-wazuh-message/0
  check:
    - tmp_json.event.action: "string_equal(\"netflow_flow\")"
  normalize:
    - map:
        - "@timestamp": "get_date()"
'

Note: See YAML Content-Type Support for details on the YAML envelope format and type fidelity.

Status Codes

CodeDescription
201Decoder created
400Missing integration field, integration not in draft space, or Engine validation failure
500Engine unavailable or internal error

Update Decoder

Updates an existing decoder in the draft space. The decoder is re-validated against the Wazuh Engine.

Request

  • Method: PUT
  • Path: /_plugins/_content_manager/decoders/{id}

Parameters

NameInTypeRequiredDescription
idPathStringYesDecoder document ID

Request Body

FieldTypeRequiredDescription
resourceObjectYesUpdated decoder definition (same fields as create)

Example Request

curl -sk -u admin:admin -X PUT \
  "https://192.168.56.6:9200/_plugins/_content_manager/decoders/bb6d0245-8c1d-42d1-8edb-4e0907cf45e0" \
  -H 'Content-Type: application/json' \
  -d '{
    "resource": {
      "name": "decoder/test-decoder/0",
      "enabled": false,
      "metadata": {
        "title": "Test Decoder UPDATED",
        "description": "Updated description",
        "author": {
          "name": "Hello there"
        }
      },
      "check": [],
      "normalize": []
    }
  }'

Example Response

{
  "message": "bb6d0245-8c1d-42d1-8edb-4e0907cf45e0",
  "status": 200
}

Status Codes

CodeDescription
200Decoder updated
400Invalid request, not in draft space, or Engine validation failure
404Decoder not found
500Internal error

Delete Decoder

Deletes a decoder from the draft space. The decoder is also removed from any integrations that reference it. A decoder cannot be deleted if it is currently set as the root decoder in the draft policy.

Request

  • Method: DELETE
  • Path: /_plugins/_content_manager/decoders/{id}

Parameters

NameInTypeRequiredDescription
idPathString (UUID)YesDecoder document ID

Example Request

curl -sk -u admin:admin -X DELETE \
  "https://192.168.56.6:9200/_plugins/_content_manager/decoders/acbdba85-09c4-45a0-a487-61c8eeec58e6"

Example Response

{
  "message": "acbdba85-09c4-45a0-a487-61c8eeec58e6",
  "status": 200
}

Example Response (set as root decoder)

{
  "message": "Cannot remove decoder [acbdba85-09c4-45a0-a487-61c8eeec58e6] as it is set as root decoder.",
  "status": 400
}

Status Codes

CodeDescription
200Decoder deleted
400Decoder is set as root decoder
404Decoder not found
500Internal error

Filters

Create Filter

Creates a new filter in the draft or standard space. The filter is validated against the Wazuh Engine before being stored and automatically linked to the specified space’s policy.

Request

  • Method: POST
  • Path: /_plugins/_content_manager/filters

Request Body

FieldTypeRequiredDescription
spaceStringYesTarget space: draft or standard
resourceObjectYesThe filter definition

Fields within resource:

FieldTypeDescription
nameStringFilter name identifier (e.g., filter/prefilter/0)
enabledBooleanWhether the filter is enabled
checkStringFilter check expression
typeStringFilter type (e.g., pre-filter)
metadataObjectFilter metadata (see below)

Fields within metadata:

FieldTypeDescription
descriptionStringFilter description
authorObjectAuthor info (name, email, url)

Example Request

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/filters" \
  -H 'Content-Type: application/json' \
  -d '{
    "space": "draft",
    "resource": {
      "name": "filter/prefilter/0",
      "enabled": true,
      "metadata": {
        "description": "Default filter to allow all events (for default ruleset)",
        "author": {
          "email": "info@wazuh.com",
          "name": "Wazuh, Inc.",
          "url": "https://wazuh.com"
        }
      },
      "check": "$host.os.platform == '\''ubuntu'\''",
      "type": "pre-filter"
    }
  }'

Example Response

{
  "message": "f_a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
  "status": 201
}

The message field contains the UUID of the created filter (prefixed with f_).

Example Request (YAML)

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/filters" \
  -H 'Content-Type: application/yaml' \
  --data-binary '---
space: draft
resource:
  name: filter/prefilter/0
  enabled: true
  metadata:
    description: "Default filter to allow all events (for default ruleset)"
    author:
      email: info@wazuh.com
      name: "Wazuh, Inc."
      url: "https://wazuh.com"
  check: "$host.os.platform == '\''ubuntu'\''"
  type: pre-filter
'

Note: See YAML Content-Type Support for details on the YAML envelope format and type fidelity.

Status Codes

CodeDescription
201Filter created
400Missing space field, invalid space, or Engine validation failure
500Engine unavailable or internal error

Update Filter

Updates an existing filter in the draft or standard space. The filter is re-validated against the Wazuh Engine.

Request

  • Method: PUT
  • Path: /_plugins/_content_manager/filters/{id}

Parameters

NameInTypeRequiredDescription
idPathString (UUID)YesFilter document ID

Request Body

FieldTypeRequiredDescription
spaceStringYesTarget space: draft or standard
resourceObjectYesUpdated filter definition (same fields as create)

Example Request

curl -sk -u admin:admin -X PUT \
  "https://192.168.56.6:9200/_plugins/_content_manager/filters/a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6" \
  -H 'Content-Type: application/json' \
  -d '{
    "space": "draft",
    "resource": {
      "name": "filter/prefilter/0",
      "enabled": true,
      "metadata": {
        "description": "Updated filter description",
        "author": {
          "email": "info@wazuh.com",
          "name": "Wazuh, Inc.",
          "url": "https://wazuh.com"
        }
      },
      "check": "$host.os.platform == '\''ubuntu'\''",
      "type": "pre-filter"
    }
  }'

Example Response

{
  "message": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
  "status": 200
}

Status Codes

CodeDescription
200Filter updated
400Invalid request, invalid space, or Engine validation failure
404Filter not found
500Internal error

Delete Filter

Deletes a filter from the draft or standard space. The filter is also removed from the associated policy.

Request

  • Method: DELETE
  • Path: /_plugins/_content_manager/filters/{id}

Parameters

NameInTypeRequiredDescription
idPathString (UUID)YesFilter document ID

Example Request

curl -sk -u admin:admin -X DELETE \
  "https://192.168.56.6:9200/_plugins/_content_manager/filters/a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"

Example Response

{
  "message": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
  "status": 200
}

Status Codes

CodeDescription
200Filter deleted
404Filter not found
500Internal error

Integrations

Create Integration

Creates a new integration in the draft space. An integration is a logical grouping of related rules, decoders, and KVDBs. The integration is validated against the Engine and registered in the Security Analytics Plugin.

The integration is also synchronized to the SAP, where a separate document is created with its own auto-generated UUID. The SAP document stores the CTI document UUID in a document.id field and the space in a source field (e.g., “Draft”) for cross-reference.

Request

  • Method: POST
  • Path: /_plugins/_content_manager/integrations

Request Body

FieldTypeRequiredDescription
resourceObjectYesThe integration definition

Fields within resource:

FieldTypeRequiredDescription
metadataObjectYesIntegration metadata (see below)
categoryStringYesCategory (e.g., cloud-services, network-activity, security, system-activity)
enabledBooleanNoWhether the integration is enabled

Fields within resource.metadata:

FieldTypeRequiredDescription
titleStringYesIntegration title (must be unique in draft space)
authorStringYesAuthor of the integration
descriptionStringNoDescription
documentationStringNoDocumentation text or URL
referencesArrayNoReference URLs

Note: Do not include the id field — it is auto-generated by the Indexer.

Example Request

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/integrations" \
  -H 'Content-Type: application/json' \
  -d '{
    "resource": {
      "metadata": {
        "title": "azure-functions",
        "author": "Wazuh Inc.",
        "description": "This integration supports Azure Functions app logs.",
        "documentation": "https://docs.wazuh.com/integrations/azure-functions",
        "references": [
          "https://wazuh.com"
        ]
      },
      "category": "cloud-services",
      "enabled": true
    }
  }'

Example Response

{
  "message": "94e5a2af-505e-4164-ab62-576a71873308",
  "status": 201
}

The message field contains the UUID of the created integration.

Status Codes

CodeDescription
201Integration created
400Missing required fields (title, author, category), duplicate title, or validation failure
500Internal error or SAP/Engine unavailable

Update Integration

Updates an existing integration in the draft space. Only integrations in the draft space can be updated.

Request

  • Method: PUT
  • Path: /_plugins/_content_manager/integrations/{id}

Parameters

NameInTypeRequiredDescription
idPathString (UUID)YesIntegration document ID

Request Body

FieldTypeRequiredDescription
resourceObjectYesUpdated integration definition

Fields within resource (all required for update):

FieldTypeRequiredDescription
metadataObjectYesIntegration metadata (see below)
categoryStringYesCategory
enabledBooleanYesWhether the integration is enabled
rulesArrayYesOrdered list of rule IDs
decodersArrayYesOrdered list of decoder IDs
kvdbsArrayYesOrdered list of KVDB IDs

Fields within resource.metadata:

FieldTypeRequiredDescription
titleStringYesIntegration title
authorStringYesAuthor
descriptionStringYesDescription
documentationStringYesDocumentation text or URL
referencesArrayYesReference URLs

Note: The rules, decoders, and kvdbs arrays are mandatory on update to allow reordering. Pass empty arrays [] if the integration has none.

Example Request

curl -sk -u admin:admin -X PUT \
  "https://192.168.56.6:9200/_plugins/_content_manager/integrations/94e5a2af-505e-4164-ab62-576a71873308" \
  -H 'Content-Type: application/json' \
  -d '{
    "resource": {
      "metadata": {
        "title": "azure-functions-update",
        "author": "Wazuh Inc.",
        "description": "This integration supports Azure Functions app logs.",
        "documentation": "updated documentation",
        "references": []
      },
      "category": "cloud-services",
      "enabled": true,
      "rules": [],
      "decoders": [],
      "kvdbs": []
    }
  }'

Example Response

{
  "message": "94e5a2af-505e-4164-ab62-576a71873308",
  "status": 200
}

Status Codes

CodeDescription
200Integration updated
400Invalid request, missing required fields, not in draft space, or duplicate title
404Integration not found
500Internal error

Delete Integration

Deletes an integration from the draft space. The integration must have no attached decoders, rules, or KVDBs — delete those first.

Request

  • Method: DELETE
  • Path: /_plugins/_content_manager/integrations/{id}

Parameters

NameInTypeRequiredDescription
idPathString (UUID)YesIntegration document ID

Example Request

curl -sk -u admin:admin -X DELETE \
  "https://192.168.56.6:9200/_plugins/_content_manager/integrations/94e5a2af-505e-4164-ab62-576a71873308"

Example Response

{
  "message": "94e5a2af-505e-4164-ab62-576a71873308",
  "status": 200
}

Example Response (has dependencies)

{
  "message": "Cannot delete integration because it has decoders attached",
  "status": 400
}

Status Codes

CodeDescription
200Integration deleted
400Integration has dependent resources (decoders/rules/kvdbs)
404Integration not found
500Internal error

KVDBs

Create KVDB

Creates a new key-value database in the draft space, linked to the specified integration.

Request

  • Method: POST
  • Path: /_plugins/_content_manager/kvdbs

Request Body

FieldTypeRequiredDescription
integrationStringYesUUID of the parent integration (must be in draft space)
resourceObjectYesThe KVDB definition

Fields within resource:

FieldTypeRequiredDescription
metadataObjectYesKVDB metadata (see below)
contentObjectYesKey-value data (at least one entry required)
nameStringNoKVDB identifier name
enabledBooleanNoWhether the KVDB is enabled

Fields within resource.metadata:

FieldTypeRequiredDescription
titleStringYesKVDB title
authorStringYesAuthor
descriptionStringNoDescription
documentationStringNoDocumentation
referencesArrayNoReference URLs

Example Request

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/kvdbs" \
  -H 'Content-Type: application/json' \
  -d '{
    "integration": "f16f33ec-a5ea-4dc4-bf33-616b1562323a",
    "resource": {
      "metadata": {
        "title": "non_standard_timezones",
        "author": "Wazuh Inc.",
        "description": "",
        "documentation": "",
        "references": [
          "https://wazuh.com"
        ]
      },
      "name": "non_standard_timezones",
      "enabled": true,
      "content": {
        "non_standard_timezones": {
          "AEST": "Australia/Sydney",
          "CEST": "Europe/Berlin",
          "CST": "America/Chicago",
          "EDT": "America/New_York",
          "EST": "America/New_York",
          "IST": "Asia/Kolkata",
          "MST": "America/Denver",
          "PKT": "Asia/Karachi",
          "SST": "Asia/Singapore",
          "WEST": "Europe/London"
        }
      }
    }
  }'

Example Response

{
  "message": "9d4ec6d5-8e30-4ea3-be05-957968c02dae",
  "status": 201
}

The message field contains the UUID of the created KVDB.

Example Request (YAML)

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/kvdbs" \
  -H 'Content-Type: application/yaml' \
  --data-binary '---
integration: f16f33ec-a5ea-4dc4-bf33-616b1562323a
resource:
  metadata:
    title: non_standard_timezones
    author: "Wazuh Inc."
    description: ""
    documentation: ""
    references:
      - "https://wazuh.com"
  name: non_standard_timezones
  enabled: true
  content:
    non_standard_timezones:
      AEST: Australia/Sydney
      CEST: Europe/Berlin
      CST: America/Chicago
      EDT: America/New_York
      EST: America/New_York
      IST: Asia/Kolkata
      MST: America/Denver
      PKT: Asia/Karachi
      SST: Asia/Singapore
      WEST: Europe/London
'

Note: See YAML Content-Type Support for details on the YAML envelope format and type fidelity.

Status Codes

CodeDescription
201KVDB created
400Missing integration or required resource fields, integration not in draft space
500Internal error

Update KVDB

Updates an existing KVDB in the draft space.

Request

  • Method: PUT
  • Path: /_plugins/_content_manager/kvdbs/{id}

Parameters

NameInTypeRequiredDescription
idPathString (UUID)YesKVDB document ID

Request Body

FieldTypeRequiredDescription
resourceObjectYesUpdated KVDB definition

Fields within resource (all required for update):

FieldTypeRequiredDescription
metadataObjectYesKVDB metadata (see below)
contentObjectYesKey-value data
nameStringNoKVDB identifier name
enabledBooleanNoWhether the KVDB is enabled

Fields within resource.metadata:

FieldTypeRequiredDescription
titleStringYesKVDB title
authorStringYesAuthor
descriptionStringYesDescription
documentationStringYesDocumentation
referencesArrayYesReference URLs

Example Request

curl -sk -u admin:admin -X PUT \
  "https://192.168.56.6:9200/_plugins/_content_manager/kvdbs/9d4ec6d5-8e30-4ea3-be05-957968c02dae" \
  -H 'Content-Type: application/json' \
  -d '{
    "resource": {
      "metadata": {
        "title": "non_standard_timezones-2",
        "author": "Wazuh.",
        "description": "UPDATE",
        "documentation": "UPDATE.doc",
        "references": [
          "https://wazuh.com"
        ]
      },
      "name": "test-UPDATED",
      "enabled": true,
      "content": {
        "non_standard_timezones": {
          "AEST": "Australia/Sydney",
          "CEST": "Europe/Berlin",
          "CST": "America/Chicago",
          "EDT": "America/New_York",
          "EST": "America/New_York",
          "IST": "Asia/Kolkata",
          "MST": "America/Denver",
          "PKT": "Asia/Karachi",
          "SST": "Asia/Singapore",
          "WEST": "Europe/London"
        }
      }
    }
  }'

Example Response

{
  "message": "9d4ec6d5-8e30-4ea3-be05-957968c02dae",
  "status": 200
}

Status Codes

CodeDescription
200KVDB updated
400Invalid request, missing required fields, or not in draft space
404KVDB not found
500Internal error

Delete KVDB

Deletes a KVDB from the draft space. The KVDB is also removed from any integrations that reference it.

Request

  • Method: DELETE
  • Path: /_plugins/_content_manager/kvdbs/{id}

Parameters

NameInTypeRequiredDescription
idPathString (UUID)YesKVDB document ID

Example Request

curl -sk -u admin:admin -X DELETE \
  "https://192.168.56.6:9200/_plugins/_content_manager/kvdbs/9d4ec6d5-8e30-4ea3-be05-957968c02dae"

Example Response

{
  "message": "9d4ec6d5-8e30-4ea3-be05-957968c02dae",
  "status": 200
}

Status Codes

CodeDescription
200KVDB deleted
404KVDB not found
500Internal error

Promotion

Preview Promotion Changes

Returns a preview of changes that would be applied when promoting from the specified space. This is a dry-run operation that does not modify any content.

Request

  • Method: GET
  • Path: /_plugins/_content_manager/promote

Parameters

NameInTypeRequiredDescription
spaceQueryStringYesSource space to preview: draft or test

Example Request

curl -sk -u admin:admin \
  "https://192.168.56.6:9200/_plugins/_content_manager/promote?space=draft"

Example Response

{
  "changes": {
    "kvdbs": [
      {
        "operation": "add",
        "id": "4441d331-847a-43ed-acc6-4e09d8d6abb9"
      }
    ],
    "rules": [],
    "decoders": [],
    "filters": [],
    "integrations": [
      {
        "operation": "add",
        "id": "f16f33ec-a5ea-4dc4-bf33-616b1562323a"
      }
    ],
    "policy": [
      {
        "operation": "update",
        "id": "f75bda3d-1926-4a8d-9c75-66382109ab04"
      }
    ]
  }
}

The response lists changes grouped by content type. Each change includes:

  • operation: add, update, or remove
  • id: Document ID of the affected resource

Status Codes

CodeDescription
200Preview returned successfully
400Invalid or missing space parameter
500Internal error

Execute Promotion

Promotes content from the source space to the next space in the promotion chain (Draft → Test → Custom). The request body must include the source space and the changes to apply (typically obtained from the preview endpoint).

For Draft → Test promotions, the changeset is forwarded to the local Wazuh Engine for validation only when it includes decoders, kvdbs, or filters. Promotions limited to integrations, rules, or the policy skip the engine call entirely. Test → Custom promotions never invoke the engine.

In addition to copying documents across CTI indices, promotion also synchronizes integrations and rules with the Security Analytics Plugin (SAP). For each promoted resource, a new SAP document is created in the target space with:

  • A newly generated UUID as the SAP document primary ID.
  • A document.id field storing the original CTI document UUID for cross-reference.
  • A source field indicating the target space (e.g., “Test”, “Custom”).

New resources (ADD operations) use POST to create SAP documents; existing resources (UPDATE operations) use PUT to update them in-place.

This ensures that the same CTI resource can exist in multiple spaces with independent SAP documents.

Rollback on Failure

If any Content Manager index mutation fails during the consolidation phase, the endpoint automatically performs a LIFO rollback to restore the system to its pre-promotion state:

  1. Pre-promotion snapshots are captured before any writes — old versions for adds/updates, full documents for deletes.
  2. CM rollback: Each completed mutation is undone in reverse order. ADDs are deleted, UPDATEs are restored to their previous version, DELETEs are re-indexed from the snapshot.
  3. SAP reconciliation (best-effort): Rules and integrations synced to SAP during the forward pass are reverted — new SAP documents are deleted, updated ones are restored, and deleted ones are re-created from snapshots.

Individual rollback or SAP reconciliation step failures are logged but do not prevent remaining steps from executing. On rollback, the endpoint returns a 500 status.

Request

  • Method: POST
  • Path: /_plugins/_content_manager/promote

Request Body

FieldTypeRequiredDescription
spaceStringYesSource space: draft or test
changesObjectYesChanges to promote (from preview response)

The changes object contains arrays for each content type (policy, integrations, kvdbs, decoders, rules, filters), each with operation and id fields.

Example Request

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/promote" \
  -H 'Content-Type: application/json' \
  -d '{
    "space": "draft",
    "changes": {
      "kvdbs": [],
      "decoders": [
        {
          "operation": "add",
          "id": "f56f3865-2827-464b-8335-30561b0f381b"
        }
      ],
      "rules": [],
      "filters": [],
      "integrations": [
        {
          "operation": "add",
          "id": "0aa4fc6f-1cfd-4a7c-b30b-643f32950f1f"
        }
      ],
      "policy": [
        {
          "operation": "update",
          "id": "baf9b03f-5872-4409-ab02-507b7f93d0c8"
        }
      ]
    }
  }'

Example Response

{
  "message": "Promotion completed successfully",
  "status": 200
}

Status Codes

CodeDescription
200Promotion successful
400Invalid request body or missing space field
500Engine communication error or validation failure

Spaces

Reset Space

Resets a user space (draft) to its initial state.

When resetting the draft space, this operation will:

  • Remove all documents (integrations, rules, decoders, kvdbs) that belong to the given space.
  • Re-generate the default policy for the given space.

Note: Only draft space can be reset.

Request

  • Method: DELETE
  • Path: /_plugins/_content_manager/space/{space}

Parameters

NameInTypeRequiredDescription
spacePathStringYesThe name of the user space to reset (draft)

Example Request

curl -sk -u admin:admin -X DELETE \
  "https://192.168.56.6:9200/_plugins/_content_manager/space/draft"

Example Response

{
  "message": "Space reset successfully",
  "status": 200
}

Status Codes

CodeDescription
200Space reset successfully
400Invalid space identifier, or attempted to reset a space different from draft
500Internal error (e.g., Engine unavailable or deletion failure)

Version Check

Check Available Updates

Returns whether there are newer versions of Wazuh available for download. The endpoint reads the current installed version from VERSION.json and queries the CTI API for available updates. The response includes the latest available major, minor, and patch updates when available.

Request

  • Method: GET
  • Path: /_plugins/_content_manager/version/check

Example Request

curl -sk -u admin:admin \
  "https://192.168.56.6:9200/_plugins/_content_manager/version/check"

Example Response (updates available)

{
  "message": {
    "uuid": "bd7f0db0-d094-48ca-b883-7019484ce71f",
    "last_check_date": "2026-04-14T15:28:41.347387+00:00",
    "current_version": "v5.0.0",
    "last_available_major": {
      "tag": "v6.0.0",
      "title": "Wazuh v6.0.0",
      "description": "Major release with new features...",
      "published_date": "2026-03-01T10:00:00Z",
      "semver": { "major": 6, "minor": 0, "patch": 0 }
    },
    "last_available_minor": {
      "tag": "v5.1.0",
      "title": "Wazuh v5.1.0",
      "description": "Minor improvements and enhancements...",
      "published_date": "2026-02-15T10:00:00Z",
      "semver": { "major": 5, "minor": 1, "patch": 0 }
    },
    "last_available_patch": {
      "tag": "v5.0.1",
      "title": "Wazuh v5.0.1",
      "description": "Bug fixes and stability improvements...",
      "published_date": "2026-01-20T10:00:00Z",
      "semver": { "major": 5, "minor": 0, "patch": 1 }
    }
  },
  "status": 200
}

Example Response (no updates)

{
  "message": {
    "uuid": "bd7f0db0-d094-48ca-b883-7019484ce71f",
    "last_check_date": "2026-04-14T15:28:41.347387+00:00",
    "current_version": "v5.0.0",
    "last_available_major": {},
    "last_available_minor": {},
    "last_available_patch": {}
  },
  "status": 200
}

Example Response (version not found)

{
  "message": "Unable to determine current Wazuh version.",
  "status": 500
}

Status Codes

CodeDescription
200Version check completed (may include updates or empty)
500Unable to determine version or internal error
502CTI API returned an error

Note: Categories with no available updates are represented as empty objects {}.


Documentation Maintenance

To maintain technical consistency, any modification, addition or removal
of endpoints in the REST API source code must be reflected in the openapi.yml
specification and this api.md reference guide.

Rule Testing Workflow

This guide explains how to create, test, and promote custom detection rules using the Content Manager’s logtest feature. The logtest endpoint lets you validate that your rules and decoders correctly detect events before deploying them to production.

Overview

The rule testing workflow follows the Content Manager’s space promotion chain:

Draft → Test → Custom
  1. Draft: Create your integration, decoders, and rules.
  2. Test: Promote to the test space and validate with logtest.
  3. Custom: Once validated, promote to custom for production use.

Logtest sends a raw log event through the full detection pipeline — the Wazuh Engine normalizes the event, and the Security Analytics Plugin (SAP) evaluates your Sigma rules against the normalized output. The combined result shows exactly what was decoded and which rules matched.

Logtest supports the test, standard, and custom spaces. Use test for validating draft content, standard for testing against production rules, and custom for validating content promoted to production


Step 1: Create an Integration

An integration groups related decoders, rules, and KVDBs together. Start by creating one:

curl -sk -u admin:admin -X POST \
  "https://localhost:9200/_plugins/_content_manager/integrations" \
  -H 'Content-Type: application/json' \
  -d '{
    "resource": {
      "category": "endpoint-security",
      "enabled": true,
      "metadata": {
        "title": "SSH Brute Force Detection",
        "author": "Security Team",
        "description": "Detects SSH brute force attempts from auth logs.",
        "references": ["https://attack.mitre.org/techniques/T1110/"]
      }
    }
  }'

The response returns the integration ID:

{
  "message": "a0b448c8-3d3c-47d4-b7b9-cbc3c175f509",
  "status": 201
}

Save this ID — you’ll need it for creating rules and running logtest.

Step 2: Create a Decoder

Decoders tell the Engine how to parse and normalize raw log events. Link a decoder to your integration:

curl -sk -u admin:admin -X POST \
  "https://localhost:9200/_plugins/_content_manager/decoders" \
  -H 'Content-Type: application/json' \
  -d '{
    "integration": "a0b448c8-3d3c-47d4-b7b9-cbc3c175f509",
    "resource": {
      "enabled": true,
      "metadata": {
        "title": "SSH Auth Log Decoder",
        "author": "Security Team",
        "description": "Parses sshd authentication events from auth.log.",
        "module": "sshd",
        "references": ["https://wazuh.com"],
        "versions": ["Wazuh 5.*"]
      },
      "name": "decoder/sshd-auth/0",
      "check": [
        {"tmp_json.event.original": "regex_match(sshd\\\\[)"}
      ],
      "normalize": [
        {
          "map": [
            {"event.category": "[\"authentication\"]"},
            {"event.kind": "event"},
            {"@timestamp": "get_date()"}
          ]
        }
      ]
    }
  }'

Step 3: Create a Rule

Rules use the Sigma format to define detection logic. Link a rule to the same integration:

curl -sk -u admin:admin -X POST \
  "https://localhost:9200/_plugins/_content_manager/rules" \
  -H 'Content-Type: application/json' \
  -d '{
    "integration": "a0b448c8-3d3c-47d4-b7b9-cbc3c175f509",
    "resource": {
      "metadata": {
        "title": "SSH Failed Password Attempt",
        "description": "Detects failed SSH password authentication attempts.",
        "author": "Security Team",
        "references": ["https://attack.mitre.org/techniques/T1110/001/"]
      },
      "sigma_id": "ssh-failed-password",
      "enabled": true,
      "status": "experimental",
      "logsource": {
        "product": "linux",
        "category": "authentication"
      },
      "detection": {
        "condition": "selection",
        "selection": {
          "event.category": "authentication",
          "event.outcome": "failure"
        }
      },
      "level": "medium",
      "tags": ["attack.credential-access", "attack.t1110.001"],
      "mitre": {
        "tactic": ["TA0006"],
        "technique": ["T1110"],
        "subtechnique": ["T1110.001"]
      }
    }
  }'

Step 4: Promote to Test Space

Before running logtest, your content must be in the test space.

# 1. Preview what will be promoted
curl -sk -u admin:admin \
  "https://localhost:9200/_plugins/_content_manager/promote?space=draft"

# 2. Execute the promotion (use the changes from the preview response)
curl -sk -u admin:admin -X POST \
  "https://localhost:9200/_plugins/_content_manager/promote" \
  -H 'Content-Type: application/json' \
  -d '{
    "space": "draft",
    "changes": { ... }
  }'

Step 5: Run Logtest

Send a sample event to validate your detection pipeline:

curl -sk -u admin:admin -X POST \
  "https://localhost:9200/_plugins/_content_manager/logtest" \
  -H 'Content-Type: application/json' \
  -d '{
    "integration": "a0b448c8-3d3c-47d4-b7b9-cbc3c175f509",
    "space": "test",
    "queue": 1,
    "location": "/var/log/auth.log",
    "event": "Dec 19 12:00:00 host sshd[12345]: Failed password for root from 10.0.0.1 port 54321 ssh2",
    "trace_level": "ALL"
  }'

Understanding the Response

The response has two sections:

normalization — Shows how the Engine decoded and normalized the event:

{
  "normalization": {
    "output": {
      "event": {
        "category": ["authentication"],
        "kind": "event",
        "outcome": "failure",
        "original": "Dec 19 12:00:00 host sshd[12345]: Failed password for root from 10.0.0.1 port 54321 ssh2"
      },
      "source": { "ip": "10.0.0.1" },
      "user": { "name": "root" }
    },
    "asset_traces": ["decoder/sshd-auth/0"],
    "validation": { "valid": true, "errors": [] }
  }
}

detection — Shows which Sigma rules matched the normalized event:

{
  "detection": {
    "status": "success",
    "rules_evaluated": 1,
    "rules_matched": 1,
    "matches": [
      {
        "rule": {
          "id": "85bba177-a2e9-4468-9d59-26f4798906c9",
          "title": "SSH Failed Password Attempt",
          "level": "medium",
          "tags": ["attack.credential-access", "attack.t1110.001"]
        },
        "matched_conditions": [
          "event.category matched 'authentication'",
          "event.outcome matched 'failure'"
        ]
      }
    ]
  }
}

Trace Levels

The trace_level field controls how much detail the Engine returns:

LevelDescription
NONEOnly the final normalized output. Use for quick checks.
ASSET_ONLYOutput plus the list of decoders that matched (asset traces).
ALLFull trace including every decoder attempted. Use for debugging decoder issues.

Step 6: Iterate

If the results aren’t what you expect:

  1. Decoder not matching? Check asset_traces — if your decoder isn’t listed, review the check conditions. Use trace_level: ALL to see which decoders were attempted.
  2. Rule not matching? Compare the normalized event fields with your rule’s detection block. Field names and values must match exactly (case-insensitive for strings).
  3. Unexpected matches? Review matched_conditions to understand why a rule triggered.

After making changes:

  • Update the rule or decoder via PUT on the respective endpoint.
  • Re-promote draft → test.
  • Run logtest again.

Step 7: Promote to Custom

Once your rules are validated, promote from test to custom for production use:

# Preview
curl -sk -u admin:admin \
  "https://localhost:9200/_plugins/_content_manager/promote?space=test"

# Execute
curl -sk -u admin:admin -X POST \
  "https://localhost:9200/_plugins/_content_manager/promote" \
  -H 'Content-Type: application/json' \
  -d '{
    "space": "test",
    "changes": { ... }
  }'

Content in the custom space is picked up by the Wazuh Engine and actively used for log processing.


Best Practices

Rule Design

  • Start specific, broaden later. Begin with tight detection conditions and loosen them as you understand the log patterns. Overly broad rules generate noise.
  • Use meaningful field names. Align your decoder’s normalize output with the Wazuh Common Schema (WCS) — e.g., event.category, source.ip, user.name.
  • Set appropriate severity levels. Use informational for visibility rules, low/medium for suspicious activity, and high/critical only for confirmed threats or high-confidence detections.
  • Add context to rules. Include description, references, falsepositives, and MITRE mappings. This helps analysts triage alerts and understand why a rule exists.

Testing Strategy

  • Test with real log samples. Use actual log events from your environment, not fabricated examples. Real logs expose edge cases (encoding, missing fields, unexpected formats).
  • Test positive AND negative cases. Verify that your rule matches what it should, and verify it does NOT match what it shouldn’t. Send benign events that look similar to confirm no false positives.
  • Use trace_level: ALL when debugging. The full trace shows every decoder attempt, making it easy to spot why a particular decoder was or wasn’t selected.
  • Test one change at a time. When iterating on rules or decoders, change one thing per cycle. This makes it clear what fixed (or broke) the detection.

Promotion Workflow

  • Always preview before promoting. The promote preview shows exactly what will change. Review it to avoid promoting unintended modifications.
  • Keep draft as your working space. Make all edits in draft. Never try to modify content directly in test or custom.
  • Promote frequently in small batches. Smaller promotions are easier to validate and roll back. Avoid accumulating dozens of changes before testing.
  • Validate in test before promoting to custom. The test space exists specifically for this purpose. Don’t skip it.

Split Endpoints: Normalization and Detection

In addition to the combined logtest endpoint, you can run normalization and detection as separate steps. This is useful for:

  • Debugging decoders without noise from detection results.
  • Testing multiple integrations against the same normalized event without re-running the Engine each time.
  • Iterating on rules without waiting for normalization on each call.

Normalization Only

curl -sk -u admin:admin -X POST \
  "https://localhost:9200/_plugins/_content_manager/logtest/normalization" \
  -H 'Content-Type: application/json' \
  -d '{
    "space": "test",
    "queue": 1,
    "location": "/var/log/auth.log",
    "input": "Dec 19 12:00:00 host sshd[12345]: Failed password for root from 10.0.0.1 port 54321 ssh2",
    "trace_level": "ALL"
  }'

The response contains only the Engine’s normalized output (no detection section):

{
  "status": 200,
  "message": {
    "output": {
      "event": {
        "category": ["authentication"],
        "kind": "event",
        "outcome": "failure"
      },
      "source": { "ip": "10.0.0.1" },
      "user": { "name": "root" }
    },
    "asset_traces": ["decoder/sshd-auth/0"],
    "validation": { "valid": true, "errors": [] }
  }
}

Detection Only

Take the normalized event (the output object from normalization) and pass it as input along with the integration ID:

curl -sk -u admin:admin -X POST \
  "https://localhost:9200/_plugins/_content_manager/logtest/detection" \
  -H 'Content-Type: application/json' \
  -d '{
    "space": "test",
    "integration": "a0b448c8-3d3c-47d4-b7b9-cbc3c175f509",
    "input": {
      "event": {
        "category": ["authentication"],
        "kind": "event",
        "outcome": "failure"
      },
      "source": { "ip": "10.0.0.1" },
      "user": { "name": "root" }
    }
  }'

The response contains only the detection result:

{
  "status": 200,
  "message": {
    "status": "success",
    "rules_evaluated": 1,
    "rules_matched": 1,
    "matches": [
      {
        "rule": {
          "id": "85bba177-a2e9-4468-9d59-26f4798906c9",
          "title": "SSH Failed Password Attempt",
          "level": "medium",
          "tags": ["attack.credential-access", "attack.t1110.001"]
        },
        "matched_conditions": [
          "event.category matched 'authentication'",
          "event.outcome matched 'failure'"
        ]
      }
    ]
  }
}

Quick Reference

ActionEndpointMethod
Create integration/_plugins/_content_manager/integrationsPOST
Create decoder/_plugins/_content_manager/decodersPOST
Create rule/_plugins/_content_manager/rulesPOST
Update rule/_plugins/_content_manager/rules/{id}PUT
Preview promotion/_plugins/_content_manager/promote?space={space}GET
Execute promotion/_plugins/_content_manager/promotePOST
Run logtest (combined)/_plugins/_content_manager/logtestPOST
Normalization only/_plugins/_content_manager/logtest/normalizationPOST
Detection only/_plugins/_content_manager/logtest/detectionPOST

For full endpoint details, see the API Reference. For Sigma rule format details, see Sigma Rules.

Troubleshooting

Common issues and diagnostic procedures for the Content Manager plugin.

Common Errors

“Error communicating with Engine socket: Connection refused”

The Wazuh Engine is not running or the Unix socket is not accessible.

Resolution:

  1. Check the socket file exists:

    ls -la /usr/share/wazuh-indexer/engine/sockets/engine-api.sock
    
  2. Ensure the Wazuh Indexer process has permission to access the socket file.

“Token not found”

No CTI access token has been registered. The Content Manager cannot sync content without a valid token.

Resolution:

Register credentials by posting the CTI access token:

curl -sk -u admin:admin -X POST \
  "https://192.168.56.6:9200/_plugins/_content_manager/subscription" \
  -H 'Content-Type: application/json' \
  -d '{
    "access_token": "<your-cti-access-token>"
  }'

A successful registration returns {"message":"Credentials received","status":201}. The token is persisted in .wazuh-internal-state and loaded into memory immediately.

Sync Not Running

Content is not being updated despite having a valid subscription.

Diagnosis:

  1. Check consumer state and offsets:

    curl -sk -u admin:admin \
      "https://192.168.56.6:9200/.wazuh-cti-consumers/_search?pretty"
    

    If local_offset equals remote_offset, the content is already up-to-date.

  2. Check the sync job is registered and enabled:

    curl -sk -u admin:admin \
      "https://192.168.56.6:9200/.wazuh-content-manager-jobs/_search?pretty"
    

    Verify the job has "enabled": true and the schedule interval matches your configuration.

  3. Check if scheduled sync is enabled in opensearch.yml:

    plugins.content_manager.catalog.update_on_schedule: true
    
  4. Trigger a manual sync to test:

    curl -sk -u admin:admin -X POST \
      "https://192.168.56.6:9200/_plugins/_content_manager/update"
    

Socket File Not Found

The Unix socket used for Engine communication does not exist.

Expected path: /usr/share/wazuh-indexer/engine/sockets/engine-api.sock

Resolution:

  1. Verify the Wazuh Engine is installed and running.
  2. Check the Engine configuration for the socket path.
  3. Ensure the engine/sockets/ directory exists under the Wazuh Indexer installation path.

Diagnostic Commands

Check Consumer State

View synchronization state for all content contexts:

curl -sk -u admin:admin \
  "https://192.168.56.6:9200/.wazuh-cti-consumers/_search?pretty"

Example output:

{
  "hits": {
    "hits": [
      {
        "_id": "cti:catalog:consumer:ruleset",
        "_source": {
          "name": "public-ruleset-5",
          "context": "beta-2-ruleset-5",
          "type": "cti:catalog:consumer:ruleset",
          "resource": "https://api.pre.cloud.wazuh.com/api/v1/catalog/contexts/beta-2-ruleset-5/consumers/public-ruleset-5",
          "is_public": true,
          "status": "ready",
          "local_offset": 3932,
          "remote_offset": 3932
        }
      }
    ]
  }
}
  • status == ready: Sync is complete; content is safe to read.
  • status == running: Sync is in progress. If this persists after a sync should have finished, the node process may have been interrupted (e.g. killed) mid-cycle without a chance to record failed.
  • status == failed: The previous sync cycle was interrupted by an unexpected exception. Check the Content Manager logs around the time this consumer was last synced; the job retries automatically on its next scheduled run.
  • local_offset == remote_offset: Content is up-to-date.
  • local_offset < remote_offset: Content needs updating.
  • local_offset == 0: Content has never been synced (snapshot required).

Check Sync Job

View the periodic sync job configuration:

curl -sk -u admin:admin \
  "https://192.168.56.6:9200/.wazuh-content-manager-jobs/_search?pretty"

Count Content Documents

Check how many rules, decoders, etc. have been indexed:

# Rules
curl -sk -u admin:admin "https://192.168.56.6:9200/wazuh-threatintel-rules/_count?pretty"

# Decoders
curl -sk -u admin:admin "https://192.168.56.6:9200/wazuh-threatintel-decoders/_count?pretty"

# Integrations
curl -sk -u admin:admin "https://192.168.56.6:9200/wazuh-threatintel-integrations/_count?pretty"

# KVDBs
curl -sk -u admin:admin "https://192.168.56.6:9200/wazuh-threatintel-kvdbs/_count?pretty"

# IoCs
curl -sk -u admin:admin "https://192.168.56.6:9200/wazuh-threatintel-enrichments/_count?pretty"

Job Scheduling on Startup

During node startup, scheduleCatalogSyncJob and scheduleTelemetryPingJob both require the .wazuh-content-manager-jobs index to reach yellow status with at least one active shard before they can register their job documents. On a freshly initialized or resource-constrained cluster this can time out, producing entries like:

INFO   ... Failed to schedule Telemetry Ping Job: Index .wazuh-content-manager-jobs not ready
INFO   ... Retrying Telemetry Ping Job (attempt 1/3) in 15s.

The plugin automatically retries each registration up to 3 times with a linear backoff (15 s, 30 s, 45 s). Each attempt logs the failure reason and the scheduled retry delay at INFO — these are expected during startup and do not require action.

If all retries fail, the plugin logs ERROR ... Giving up scheduling <job> after 3 attempts. and the job will only be retried on the next node start. A persistent failure usually indicates the cluster cannot allocate shards — check cluster health with GET _cluster/health and verify index allocation settings.

Log Monitoring

Content Manager logs are part of the Wazuh Indexer logs. Use the following patterns to filter relevant entries:

# General Content Manager activity
grep -i "content.manager\|ContentManager\|CatalogSync" \
  /var/log/wazuh-indexer/wazuh-indexer.log

# Sync job execution
grep -i "CatalogSyncJob\|consumer-sync" \
  /var/log/wazuh-indexer/wazuh-indexer.log

# CTI API communication
grep -i "cti\|CTIClient" \
  /var/log/wazuh-indexer/wazuh-indexer.log

# Engine socket communication
grep -i "engine.*socket\|EngineClient" \
  /var/log/wazuh-indexer/wazuh-indexer.log

# Errors only
grep -i "ERROR.*content.manager" \
  /var/log/wazuh-indexer/wazuh-indexer.log

Resetting Content

To force a full re-sync from snapshot, delete the consumer state document and restart the indexer:

# Delete consumer state (forces snapshot on next sync)
curl -sk -u admin:admin -X DELETE \
  "https://192.168.56.6:9200/.wazuh-cti-consumers/_doc/*"

# Restart indexer to trigger sync
systemctl restart wazuh-indexer

Warning: This will re-download and re-index all content from scratch. Use only when troubleshooting persistent sync issues.

Reporting

Reporting plugin

The wazuh-indexer-reporting plugin provides functionality for generating customizable reports based on data stored in the Wazuh Indexer. Most of this data originates from the Wazuh Manager, which collects and analyzes security events from registered agents. The plugin supports both scheduled and on‑demand report generation. Reports can be delivered via email or downloaded on demand through the Wazuh Dashboard or the API. Users can create, read, update, and delete custom reports. Access to these actions is governed by the Wazuh Indexer’s role‑based access control (RBAC) permissions. This plugin is built on top of OpenSearch’s native Reporting and Notifications plugins.

Use case: email notifications

Configuring the email notifications channel

In Wazuh Dashboard, go to Notifications > Channels and click on Create channel:

  1. Fill in a name (e.g Email notifications).
  2. Select Email as Channel Type.
  3. Check SMTP sender as Sender Type.
  4. Click on Create SMTP sender.
    1. Fill in a name (e.g mailpit).
    2. Fill in an email address.
    3. In Host, type mailpit (adapt this to your SMTP server Domain Name).
    4. For port, type 1025 (adapt this to your SMTP server settings).
    5. Select None as Encryption method.
    6. Click on Create.
  5. Click on Create recipient group.
    1. Fill in a name (e.g email-notifications-recipient-group).
    2. On Emails, type any email.
    3. Click on Create.

The fields should now be filled in as follows: Create Channel

  1. Click on Send test message to validate the configuration, a green message should pop up.
  2. Finally, click on Create.

More information on how to configure the email notifications channel can be found in the OpenSearch documentation.

Creating a new report

For more information on how to create reports, please refer to the OpenSearch documentation. The reporting plugin also allows you to create notifications following the behaviour on OpenSearch’s notifications plugin.

Generate and download a report

To create a new report you must have predefined the report settings. Once the report is configured, you can generate it by clicking the “Generate Report” button. This is only available on “On demand” report definitions as scheduled reports will be generated automatically. The report will be processed and made available for download at the Reports section on Explore -> Report.

You can also create a csv or xlsx report without a report definition by saving a search on Explore -> Discover. Remember to have an available index pattern.

Generate a report definition

Before creating a report definition you must have generated and saved a Dashboard, a Visualization, a search or a Notebook. Then you can do so at the Explore -> Reporting section, choosing the intended configuration. This generates PDF/PNG reports or CSV/XLSX reports in case a saved search is selected.

Managing permissions on reporting via RBAC

The Reporting plugin uses the Wazuh Indexer RBAC (role-based access control) system to manage permissions. This means that users must have the appropriate roles assigned to them in order to create, read, update, or delete reports. The roles can be managed through the Wazuh Dashboard Index Management -> Security -> Roles section. The following permissions are available for the Reporting plugin:

1. cluster:admin/opendistro/reports/definition/create
2. cluster:admin/opendistro/reports/definition/update
3. cluster:admin/opendistro/reports/definition/on_demand
4. cluster:admin/opendistro/reports/definition/delete
5. cluster:admin/opendistro/reports/definition/get
6. cluster:admin/opendistro/reports/definition/list
7. cluster:admin/opendistro/reports/instance/list
8. cluster:admin/opendistro/reports/instance/get
9. cluster:admin/opendistro/reports/menu/download

There are already some predefined roles that can be used to manage permissions on reporting:

  • reports_read_access: permissions 5 to 9.
  • reports_instances_read_access: 7 to 9.
  • reports_full_access: permissions 1 to 9.

More information on how to modify and map roles on the Wazuh Indexer can be found in the Wazuh Indexer documentation.

Security Analytics

The Security Analytics Plugin (SAP) is a fork of the OpenSearch Security Analytics plugin adapted for Wazuh. It evaluates incoming events against Sigma detection rules, creates findings when rules match, and correlates related findings across detectors.

SAP runs inside the Wazuh Indexer and operates as an OpenSearch plugin, using the standard OpenSearch transport layer for all internal communication.

Detector rule space restriction

A detector can only reference rules from a single space type — either Standard (pre-packaged Sigma rules) or Custom (user-promoted rules) — never both simultaneously. This applies to both detector creation and update operations.

When the restriction is violated, the API returns 400 Bad Request.

Detector constraints

ConstraintValueDescription
Max rules per detector100Each detector input can reference at most 100 rules (custom or pre-packaged). Requests that exceed this limit are rejected with HTTP 400.

This limit is enforced at the transport layer (TransportIndexDetectorAction) and applies to all detector creation and update paths, including inter-plugin calls from the Content Manager.

Wazuh enriched findings

What is a finding?

A finding is a record that a monitored event matched a Sigma detection rule. SAP creates one finding per matching event and stores it in the .opensearch-sap-{category}-findings-* data stream. Each finding contains:

FieldDescription
idUnique finding identifier
detector_idThe detector that produced the finding
related_doc_idsIDs of the source documents that triggered the match
queriesThe Sigma rule(s) that matched
indexThe source index where the triggering event lives
timestampWhen the finding was created

Raw findings contain only identifiers — they do not embed the triggering event payload or rule metadata.

What is an enriched finding?

An enriched finding is an augmented version of a raw SAP finding. Because the Wazuh Dashboard needs the full event payload and rule context to render alert details, WazuhEnrichedFindingService enriches each finding with:

  • The full triggering event source (fetched from the source index by document ID)
  • Rule metadata under wazuh.rule: name, severity level, compliance mappings, MITRE ATT&CK tags

Rule metadata is merged into the event’s existing wazuh object, so wazuh.integration.* from the triggering event sits alongside wazuh.rule.* in the enriched document.

Enriched findings are written to wazuh-findings-v5-{category}*, where {category} is derived from the wazuh.integration.category field in the triggering event.

How findings are generated (high level)

The following steps happen for every event that matches a detection rule:

  1. A Wazuh Manager sends an event to the Wazuh Indexer. The event is indexed in the monitored data stream.
  2. SAP’s Alerting monitor evaluates the event against all active Sigma rules for the configured log category.
  3. On a match, SAP creates a raw finding and fires the SUBSCRIBE_FINDINGS_ACTION transport action.
  4. TransportCorrelateFindingAction receives the action, runs the correlation engine, and calls WazuhEnrichedFindingService.enrich(finding).
  5. The service asynchronously fetches the triggering event source and the matching rule’s metadata, assembles the enriched document, and bulk-indexes it into wazuh-findings-v5-{category}*.

The enrichment step is fire-and-forget: it never blocks the SAP write path and failures are logged at WARN level without propagating to the caller.

See Architecture for the low-level implementation details.

Architecture

Enrichment pipeline

When SAP produces a finding, WazuhEnrichedFindingService runs an asynchronous enrichment chain that fetches the triggering event and the matching rule’s metadata, assembles an enriched document, and bulk-indexes it into wazuh-findings-v5-{category}*.

The complete flow is shown in the sequence diagram below:

sequenceDiagram
    participant A as Wazuh Manager
    participant I as Wazuh Indexer
    participant SAP as Security Analytics Plugin
    participant TC as TransportCorrelateFindingAction
    participant WS as WazuhEnrichedFindingService
    participant SI as Source Index
    participant RI as Rules Index
    participant WF as wazuh-findings-v5-{category}*

    A->>I: Ingest event
    I->>SAP: Monitor evaluates event against Sigma rules
    SAP->>SAP: Rule matches → create raw finding
    SAP->>TC: SUBSCRIBE_FINDINGS_ACTION
    TC->>WS: enrich(finding)
    WS->>WS: Add to findingsQueue
    WS->>WS: Acquire semaphore permit (max 50 in-flight)
    WS->>SI: GetRequest (triggering event by doc ID)
    SI-->>WS: Event source map
    WS->>WS: resolveCategory(wazuh.integration.category)
    alt Rule metadata cache hit
        WS->>WS: Read from ruleMetadataCache
    else Cache miss
        WS->>RI: MultiGetRequest (pre-packaged + custom rules indices)
        RI-->>WS: Rule metadata
        WS->>WS: Store in ruleMetadataCache
    end
    WS->>WS: buildAndIndex (assemble enriched document)
    WS->>WS: Add to pendingRequests queue
    alt Batch full (100 items)
        WS->>WF: client.bulk (stashed thread context)
    else Periodic flush (every 5 s)
        WS->>WF: client.bulk (stashed thread context)
    end
    WS->>WS: Release semaphore permit

Detector Provisioning

Threat detectors for Wazuh integrations are created dynamically based on CTI content. The system has moved away from hardcoded configurations to a request-driven model via WIndexDetectorRequest.

Dynamic Detector Factory

The DetectorFactory class is responsible for assembling the Detector object. Instead of using fixed values, it now consumes parameters provided by the Content Manager:

  • Enabled Status: Controlled by CTI to activate or deactivate detectors globally.
  • Scan Interval: Customizable per integration (e.g., critical integrations can have shorter intervals).
  • Source Indices: Defines the target indices or index patterns the detector will monitor.

Fallback Logic

To ensure system stability, the DetectorFactory implements a fallback mechanism for source indices:

  • If the sources list is provided and not empty, it is used as the detector’s input.
  • If sources is null or empty, the factory defaults to the legacy pattern: wazuh-events-v5-{category}.

Implementation details

Fire-and-forget execution

WazuhEnrichedFindingService.enrich() returns immediately after adding the finding to the internal queue. All network I/O and document assembly happen on async transport threads. Failures are logged at WARN level and never surface to the SAP write path.

Bounded concurrency

A Semaphore with MAX_IN_FLIGHT permits limits how many enrichment chains run simultaneously. Findings that arrive while all permits are held are queued in a ConcurrentLinkedQueue and processed as permits become available. This prevents transport-layer overload on resource-constrained nodes.

Rule metadata cache

Rule metadata (severity level, compliance mappings, MITRE ATT&CK tags) is stored in an in-memory ConcurrentHashMap keyed by rule ID. On the first finding for a given rule, the service issues a MultiGetRequest against both the pre-packaged rules index (opensearch-pre-packaged-rules) and the custom rules index (opensearch-custom-rules). Subsequent findings from the same detector reuse the cached entry, eliminating repeated round-trips.

The cache is unbounded and lives for the lifetime of the node. It is cleared only on plugin reload or node restart.

Bulk indexing

Index requests are accumulated in a ConcurrentLinkedQueue<IndexRequest>. Two flush paths drain this queue:

  • Batch trigger: every time pendingCount reaches a multiple of BULK_BATCH_SIZE, the thread that incremented the counter calls drainAndFlush() immediately.
  • Periodic flush: a fixed-delay scheduler fires drainAndFlush() every FLUSH_INTERVAL to drain any remainder that has not yet reached the batch threshold.

drainAndFlush() polls all pending requests into a single BulkRequest and calls client.bulk(). The call is wrapped in threadPool.getThreadContext().stashContext() so the security plugin accepts the request regardless of which thread pool the flush runs on.

Category resolution

Before assembling an enriched document, the service reads wazuh.integration.category from the triggering event. If the field is absent or its value is not one of the recognized LOG_CATEGORY values, enrichment is skipped for that finding and a WARN log entry is emitted.

Document layout

buildAndIndex starts from a shallow copy of the triggering event source and overlays the following fields:

FieldSource
@timestamp@timestamp of the original triggering event
event.*Pre-existing event fields plus doc_id, index
wazuh.ruleSigma rule metadata (id, title, tags, sigma_id, and any of level, status, compliance, mitre present in the rule index entry)

Rule metadata is nested under wazuh.rule. Because the event’s wazuh map (which carries wazuh.integration.*) is shared with the shallow copy, the service defensively copies it before adding rule, so the original event source is never mutated.

Dynamic Configuration Injection

The WTransportIndexDetectorAction serves as the entry point for detector creation. It extracts the enabled, interval, and sources fields from the WIndexDetectorRequest and injects them into the factory method. This ensures that any change in the CTI catalog is reflected in the Security Analytics engine without requiring code changes or restarts.

Technical parameters

ParameterValueDescription
BULK_BATCH_SIZE100Pending index requests accumulated before a batch-trigger flush
MAX_IN_FLIGHT50Maximum concurrent async enrichment chains
FLUSH_INTERVAL5 sInterval between periodic flush runs
Target data streamwazuh-findings-v5-{category}*Data stream destination, resolved per finding
Rule metadata cacheUnbounded, in-memoryConcurrentHashMap, keyed by rule ID, cleared on restart
Index operation typeCREATEPrevents overwriting existing enriched findings

System indices

IndexDescription
.opensearch-sap-{category}-findings-*Raw SAP findings written by the Security Analytics Plugin
.opensearch-pre-packaged-rulesWazuh-provided Sigma rules; source for rule metadata
.opensearch-custom-rulesUser-created custom rules; fallback source for rule metadata
wazuh-findings-v5-{category}*Enriched findings written by WazuhEnrichedFindingService

Configuration

Security Analytics settings

The Security Analytics plugin is configured through settings in opensearch.yml. All node-scope settings use the plugins.security_analytics prefix. Almost every setting is dynamic and can be changed at runtime via the Cluster Settings API.

SettingData typeDefault valueDescription
plugins.security_analytics.alert_finding_enabledBooleantrueEnable rollover and retention management for the finding history indices
plugins.security_analytics.alert_finding_max_docsLong1000Deprecated. Maximum document count for a finding history index before rollover. Minimum 0
plugins.security_analytics.alert_finding_rollover_periodTime12hHow often the finding history rollover job runs
plugins.security_analytics.alert_history_enabledBooleantrueEnable rollover and retention management for the alert history indices
plugins.security_analytics.alert_history_max_ageTime30dMaximum age of an alert history index before rollover
plugins.security_analytics.alert_history_max_docsLong1000Maximum document count for an alert history index before rollover. Minimum 0
plugins.security_analytics.alert_history_retention_periodTime60dRetention period after which alert history indices are deleted
plugins.security_analytics.alert_history_rollover_periodTime12hHow often the alert history rollover job runs
plugins.security_analytics.auto_correlations_enabledBooleanfalseAutomatically generate correlation rules from new findings
plugins.security_analytics.correlation.detector_cache_ttlTime5mTTL for the in-memory monitor-id to detector cache. Set to 0s to disable the cache
plugins.security_analytics.correlation.events_backpressure.enabledBooleantrueWrite-block the events indices when the correlation backlog fills, so ingestion pauses and the backlog drains instead of the node running out of memory
plugins.security_analytics.correlation.events_backpressure.high_watermark_percentInteger100Backlog level, as a percent of correlation.max_pending_findings, at or above which the events indices are write-blocked. Valid range: 1–100
plugins.security_analytics.correlation.events_backpressure.low_watermark_percentInteger60Backlog level, as a percent of correlation.max_pending_findings, at or below which the events-index write block is lifted. Valid range: 0–99
plugins.security_analytics.correlation.max_in_flight_findingsInteger50Maximum number of correlation pipelines running concurrently. Valid range: 1–1000
plugins.security_analytics.correlation.max_pending_findingsInteger10000Maximum findings waiting for a free correlation slot. When the backlog is full, new findings are shed (correlation and enrichment skipped) so the node does not run out of memory under overload. Valid range: 1–1000000
plugins.security_analytics.correlation.metadata_cache_ttlTime5mTTL for the in-memory caches of log-type list and correlation rules by detector type. Set to 0s to disable both caches
plugins.security_analytics.correlation_history_max_ageTime30dMaximum age of a correlation history index before rollover
plugins.security_analytics.correlation_history_max_docsLong1000Maximum document count for a correlation history index before rollover. Minimum 0
plugins.security_analytics.correlation_history_retention_periodTime60dRetention period after which correlation history indices are deleted
plugins.security_analytics.correlation_history_rollover_periodTime12hHow often the correlation history rollover job runs
plugins.security_analytics.correlation_time_windowTime5mTime window used to group findings into correlations
plugins.security_analytics.enable_detectors_with_dedicated_query_indicesBooleantrueCreate dedicated query indices for new detectors
plugins.security_analytics.enable_workflow_usageBooleantrueUse Alerting composite workflows when running detectors
plugins.security_analytics.enriched_findings_bulk_sizeInteger100Number of enriched findings buffered before a bulk index request is fired. Valid range: 10–1000
plugins.security_analytics.enriched_findings_enrich_batch_sizeInteger100Findings drained per in-flight permit; their source events are fetched in one combined MultiGet instead of one request per finding. Valid range: 1–1000
plugins.security_analytics.enriched_findings_flush_intervalInteger5Seconds between periodic flushes of any leftover buffered enriched findings. Valid range: 1–60
plugins.security_analytics.enriched_findings_index_enabledBooleantrueToggle the enriched findings pipeline (see Architecture)
plugins.security_analytics.enriched_findings_max_in_flightInteger5Maximum concurrent enrichment chains, to bound peak load on the transport layer. Valid range: 1–10
plugins.security_analytics.enriched_findings_rule_cache_max_sizeInteger10000Maximum rule-metadata entries cached in memory by the enrichment service. Minimum 0. Static; requires a node restart to change
plugins.security_analytics.filter_by_backend_rolesBooleanfalseRestrict access to detectors, rules, and findings based on the requester’s backend roles
plugins.security_analytics.finding_history_max_ageTime30dMaximum age of a finding history index before rollover
plugins.security_analytics.finding_history_retention_periodTime60dRetention period after which finding history indices are deleted
plugins.security_analytics.index_timeoutTime60sTimeout for Security Analytics index operations
plugins.security_analytics.max_detectorsInteger10Maximum number of user-created detectors (Content Manager detectors do not count). Minimum 0

History indices

Each history group (alerts, findings, correlations) is managed by an independent rollover job with the same five knobs: an enable toggle, a rollover period, a max age, a max document count, and a retention period after which old indices are deleted.

To tune retention for the alert history indices:

# opensearch.yml
plugins.security_analytics.alert_history_enabled: true
plugins.security_analytics.alert_history_rollover_period: 12h
plugins.security_analytics.alert_history_max_age: 30d
plugins.security_analytics.alert_history_max_docs: 1000
plugins.security_analytics.alert_history_retention_period: 60d

The same pattern applies to finding history (plugins.security_analytics.alert_finding_*, plugins.security_analytics.finding_history_*), and correlation history (plugins.security_analytics.correlation_history_*).

plugins.security_analytics.alert_finding_max_docs is deprecated. Configure finding history rollover through the other finding_history_* settings.

Correlation tuning

The correlation engine runs after every matched finding and consults two in-memory caches plus a concurrency limiter:

# opensearch.yml
plugins.security_analytics.correlation_time_window: 5m
plugins.security_analytics.auto_correlations_enabled: false
plugins.security_analytics.correlation.detector_cache_ttl: 5m
plugins.security_analytics.correlation.metadata_cache_ttl: 5m
plugins.security_analytics.correlation.max_in_flight_findings: 50

Both detector_cache_ttl and metadata_cache_ttl accept 0s to disable the cache entirely, which forces a lookup against the corresponding system index on every finding. Lower correlation.max_in_flight_findings on resource-constrained nodes to bound peak demand on the search thread pool.

Detector behavior

# opensearch.yml
plugins.security_analytics.enable_detectors_with_dedicated_query_indices: true
plugins.security_analytics.enriched_findings_index_enabled: true
plugins.security_analytics.enable_workflow_usage: true
plugins.security_analytics.filter_by_backend_roles: false

Setting enriched_findings_index_enabled to false disables the Wazuh enriched findings pipeline described in Architecture; raw SAP findings continue to be written to .opensearch-sap-{category}-findings-*, but no wazuh-findings-v5-{category}* documents are produced.

Overload protection and enrichment throughput

Under sustained load, doc-level monitors publish findings faster than correlation and enrichment can process them. These settings bound that work so the node sheds or pauses load instead of running out of memory, and tune how efficiently the enrichment pipeline writes wazuh-findings-v5-*.

# opensearch.yml
# Bound the correlation backlog
plugins.security_analytics.correlation.max_pending_findings: 10000

# Pause ingestion when the backlog fills, resume when it drains
plugins.security_analytics.correlation.events_backpressure.enabled: true
plugins.security_analytics.correlation.events_backpressure.high_watermark_percent: 100
plugins.security_analytics.correlation.events_backpressure.low_watermark_percent: 60

# Enrichment pipeline throughput
plugins.security_analytics.enriched_findings_bulk_size: 100
plugins.security_analytics.enriched_findings_enrich_batch_size: 100
plugins.security_analytics.enriched_findings_max_in_flight: 5
plugins.security_analytics.enriched_findings_flush_interval: 5
plugins.security_analytics.enriched_findings_rule_cache_max_size: 10000

Two independent overload guards act on the correlation backlog:

  • correlation.max_pending_findings caps how many findings may wait for a free correlation slot (the slots themselves are limited by correlation.max_in_flight_findings). When the backlog is full and events_backpressure is disabled, extra findings are shed, so the node stays up.
  • With events_backpressure.enabled, instead of shedding findings the plugin write-blocks the events indices when the backlog reaches high_watermark_percent, so no new events are indexed and the backlog can drain; the block is lifted at low_watermark_percent.

The enrichment throughput settings shape the load the pipeline puts on the cluster: enrich_batch_size findings are drained per in-flight permit and their source events are fetched in one combined MultiGet; enriched documents are buffered and written in bulks of bulk_size, flushed at least every flush_interval seconds; max_in_flight bounds the concurrent enrichment chains; and rule_cache_max_size bounds the in-memory rule-metadata cache.

Updating a setting at runtime

Almost every Security Analytics setting is dynamic. To change one without restarting the node, use the Cluster Settings API:

curl -sk -u admin:admin -X PUT "https://192.168.56.6:9200/_cluster/settings" -H 'Content-Type: application/json' -d'
{
  "persistent": {
    "plugins.security_analytics.correlation.max_in_flight_findings": 100
  }
}'

Notes

  • Changes to opensearch.yml require a restart of the Wazuh Indexer to take effect. Dynamic settings can additionally be updated at runtime via the Cluster Settings API shown above.
  • index.correlation is an index-scope setting and must be applied to individual indices (for example, via an index template or the _settings API), not to the cluster as a whole.
  • Rollover jobs are enforced by the OpenSearch Job Scheduler. Actual rollover timing may vary slightly depending on cluster load.

Rules

Wazuh uses the Sigma rule format as the standard for Security Analytics detection rules. The Content Manager plugin accepts rules that follow the Sigma specification, extended with Wazuh-specific blocks for metadata, threat intelligence mapping, and compliance coverage.

This page describes the supported rule format, including field requirements, detection logic, supported modifiers, and Wazuh extensions.

For the full Sigma standard, see the Sigma Rules Specification.


Starting Example

The following example demonstrates a complete Sigma rule using all supported blocks:

metadata:
  title: Python SQL Exceptions
  author: Thomas Patzke
  description: Detects SQL exceptions in Python applications according to PEP 249.

sigma_id: 19aefed0-ffd4-47dc-a7fc-f8b1425e84f9
status: stable
level: medium
enabled: true

tags:
  - attack.initial-access
  - attack.t1190

logsource:
  category: application
  product: python

detection:
  keywords:
    - DataError
    - IntegrityError
    - ProgrammingError
    - OperationalError
  condition: keywords

falsepositives:
  - Application bugs

mitre:
  tactic:
    - TA0001
  technique:
    - T1190
  subtechnique: []

compliance:
  pci_dss:
    - "6.5.1"
  gdpr:
    - Art. 32

Components

A Wazuh Sigma rule is composed of the following blocks:

  • Detection
    The rule’s matching logic — selections, keywords, and conditions.
  • Log Source
    The type of log data the rule targets.
  • Metadata
    Authorship and lifecycle information (title, author, description, dates, references).
  • MITRE ATT&CK
    Threat intelligence mapping to MITRE tactics, techniques, and subtechniques.
  • Compliance
    Compliance framework mapping (GDPR, PCI DSS, NIST 800-53, etc.).

The sections below describe each component in detail.


Top-Level Fields

The following table lists all supported top-level fields in a Wazuh Sigma rule. Fields marked as Required must be present for the rule to pass validation.

FieldTypeRequiredDescription
idStringYesGlobally unique rule identifier (UUIDv4 recommended)
statusStringYesRule maturity status: experimental, test, or stable
levelStringYesAlert severity: informational, low, medium, high, or critical
sigma_idStringNoOriginal Sigma rule identifier (UUID), preserved when importing from upstream
enabledBooleanNoWhether the rule is active (default: true)
tagsArrayNoCategorization tags (e.g., attack.initial-access)
falsepositivesArrayNoKnown sources of false positives
detectionObjectYesDetection logic: selections, keywords, and conditions
logsourceObjectYesClassifies the type of log data the rule targets
mitreObjectNoMITRE ATT&CK threat intelligence mapping
complianceObjectNoCompliance framework mapping
metadataObjectYesOther information

Detection

Required

The detection section defines the rule’s matching logic. It consists of one or more named selections (or keywords) and a condition that combines them using boolean logic.

detection:
  condition: selection
  selection:
    event.action: account-locked
    event.category|contains: authentication

The detection section must always contain:

  • At least one named selection or a keywords list.
  • A condition field that references those selections.

Important

All fields referenced in the detection section are validated against the Wazuh Common Schema. Rules that reference unknown fields are rejected with a structured error response identifying the offending field names. This prevents silent mismatches where a rule appears active but never triggers because it queries a non-existent field.

Selections

A selection is a named object whose keys correspond to existing WCS fields and whose values define the matching criteria. A selection matches when any or all of its field conditions are satisfied, depending on the chosen syntax:

Field list (implicit OR)

detection:
  selection:
    event.action:
      - login_failed # or
      - authentication_error
  condition: selection

This rule matches when event.action is either "login_failed" or "authentication_error".

Field dictionary (implicit AND)

detection:
  selection:
    log.level: ERROR # and
    event.kind: event
  condition: selection

This rule matches when log.level is "ERROR" and event.kind is "event".

Keywords (implicit OR)

The detection by keywords performs value-only searches across all event fields, without specifying a target field name:

detection:
  keywords:
    - DataError # or
    - IntegrityError
    - OperationalError
  condition: keywords

Each item in the list is effectively separated by a logical “OR” operator, meaning that the rule will match if any of the specified keywords are found in any field of the event.

Conditions

The condition field is a string expression that combines named selections using boolean logic to define when the rule triggers. Each identifier in the condition must correspond to a named selection defined in the same detection object.

condition: (selection_one or selection_two) and not filter
OperatorDescriptionExample
andBoth operands must matchselection1 and selection2
orAt least one operand must matchsel_error or sel_warn
notNegates the following operandselection and not filter
( )Groups expressions for precedence(sel_a or sel_b) and not exclusion

Example: simple condition

detection:
  selection:
    log.level: ERROR
  condition: selection

Example: OR condition

detection:
  sel_error:
    log.level: ERROR
  sel_warn:
    log.level: WARN
  condition: sel_error or sel_warn

Example: AND with NOT (exclusion pattern)

detection:
  selection:
    event.kind: event
  filter:
    process.thread.name|startswith: Test
  condition: selection and not filter

Example: multi-selection AND

detection:
  sel_severity:
    event.severity|gte: 8
  sel_message:
    message|contains: fatal
  condition: sel_severity and sel_message

Reference: See Sigma Conditions for the full specification of condition syntax.

Modifiers

Modifiers transform how a field value is compared during detection. They are appended to the field name using the pipe (|) character:

field_name|modifier: value

Multiple modifiers can be chained: field|modifier1|modifier2: value.

contains

Matches when the field value contains the specified substring. Wildcards are inserted around the value.

message|contains: timeout

startswith

Matches when the field value begins with the specified string. A wildcard is inserted at the end of the value.

process.thread.name|startswith: Gossip

endswith

Matches when the field value ends with the specified string. A wildcard is inserted at the beginning of the value.

process.thread.name|endswith: "-5"

base64

Encodes the provided value as a Base64 string before comparison. Used to detect commands or parameters that an attacker has Base64-encoded to evade plain-text detection.

process.command_line|base64: "/bin/bash"

base64offset

Generates all three possible Base64 offsets of the value to account for the byte position where it might appear inside a larger Base64-encoded blob. Usually preferred over base64 when matching a substring inside an encoded stream, and typically chained with contains.

process.command_line|base64offset|contains: "/bin/bash"

wide

Transforms the value to a UTF-16 (wide-character) byte sequence before comparison. Must be chained with an encoding modifier such as base64 or base64offset — it cannot be the final modifier in the chain because the intermediate representation contains null bytes.

process.command_line|wide|base64offset|contains: "ping"

windash

Expands command-line flag prefixes to match all Windows dash variants: -, /, (en dash), (em dash), and (horizontal bar). Useful for detecting invocations where attackers swap dash characters to evade signatures.

process.command_line|windash|contains: " -enc "

re

Matches the field value against a regular expression.

process.thread.name|re: "^Repair"

Submodifiers can be chained with re|<flag>:

  • i — case-insensitive matching.
  • m — multi-line mode (^/$ match the start/end of each line).
  • s — single-line mode (. also matches newline characters).

cidr

Matches when the field value (an IPv4 or IPv6 address) falls within the specified CIDR subnet.

source.ip|cidr: 10.42.0.0/16

IPv6 addresses are supported in the following formats:

  • Standard: Full 8-group notation with leading zeros.

    E.g., 2001:0db8:85a3:0000:0000:8a2e:0370:7334.

  • Compressed: Zero-compression using :: to omit consecutive zero groups.

    E.g., 2001:db8:85a3::8a2e:370:7334.

  • CIDR: Subnet notation with a prefix length.

    E.g., 2001:db8::/32.

exists

Checks whether the field is present in the event. The value must be true (field must exist) or false (field must be absent).

source.ip|exists: true

all

By default, list values are combined with OR. The all modifier changes the logic to AND, requiring every value in the list to match. Cannot be applied to single-item lists.

event.category|contains|all:
  - authentication
  - failure

lt

Matches when the field value is less than the specified number.

event.severity|lt: 10

lte

Matches when the field value is less than or equal to the specified number.

event.severity|lte: 3

gt

Matches when the field value is greater than the specified number.

event.severity|gt: 7

gte

Matches when the field value is greater than or equal to the specified number.

event.duration|gte: 5000

Reference: See Sigma Modifiers for additional context on value transformation modifiers.


Log Source

Required

The logsource section classifies the type of log data the rule targets. It helps organize rules by their applicable data source but does not affect detection matching directly.

product

Required

The product or platform generating the log (e.g., linux, windows, python). Must hold the same value as metadata.title from the integration it belongs to.

logsource:
  product: linux

category

Optional

A broad classification of the log type within the product (e.g., authentication, process_creation, application, webserver, firewall). Useful for grouping related rules across products.

logsource:
  category: authentication

service

Optional

The specific service, daemon, or log channel within the product (e.g., sshd, security, syslog, kerberos). Use this when the log can be attributed to a particular subsystem or event channel.

logsource:
  service: sshd

definition

Optional

Free-form notes describing onboarding requirements or prerequisites for the log source — for example, audit policies that must be enabled, agent configuration needed, or specific event IDs to collect.

logsource:
  definition: Script Block Logging must be enabled

Reference: See Sigma Log Sources for general guidance on log source classification, including the standard combinations of product, category, and service.


Metadata

Required

The metadata block contains authorship and lifecycle fields. Only title is required; the others are optional.

title

Required

Human-readable rule title shown in alerts and the rule catalog.

metadata:
  title: Suspicious SSH Login from IPv6

Keep titles short and avoid prefixes like “Detects when …” or “This rule will …”.

author

Optional

The author of the rule. Free-form text; may include contact information such as an email address or handle.

metadata:
  author: Security Team

date

Optional

Creation date in ISO 8601 format (YYYY-MM-DD). Auto-managed when the rule is first registered.

metadata:
  date: "2026-01-15"

modified

Optional

Last modification date in ISO 8601 format (YYYY-MM-DD). Auto-managed when the rule’s content changes.

metadata:
  modified: "2026-03-02"

The modified date changes when the rule is updated.

description

Optional

Brief explanation of what the rule detects and the context in which it is useful. Used by other products and services as a short summary of the rule’s intent — avoid prefixes like “Detects when …” or “This rule will …”.

metadata:
  description: SSH login attempts from known malicious IPv6 ranges.

references

Optional

URLs or plain-text references (advisories, CVE IDs, blog posts, documentation) explaining the motivation for the rule or providing background for analysts.

metadata:
  references:
    - https://example.com/advisory/2025-001
    - CVE-2025-22222

documentation

Optional

Free-form documentation text or a documentation URL providing additional context for analysts triaging the alert.

metadata:
  documentation: https://docs.example.com/rules/ssh-ipv6

supports

Optional

List of supported platforms, products, or contexts in which the rule is intended to operate.

metadata:
  supports:
    - linux
    - macos

MITRE ATT&CK

Optional

The mitre block maps a rule to MITRE ATT&CK tactics, techniques, and subtechniques. Each field is an array of ID strings:

  • tactic
    MITRE tactic IDs (e.g., TA0002, TA0005).
  • technique
    MITRE technique IDs (e.g., T1059, T1562).
  • subtechnique
    MITRE subtechnique IDs (e.g., T1059.001).

Example

mitre:
  tactic:
    - TA0002
    - TA0005
  technique:
    - T1059
    - T1562
  subtechnique:
    - T1059.001

Compliance

Optional

The compliance block maps a rule to one or more compliance frameworks. Each key is a normalized framework identifier and its value is an array of requirement ID strings.

Supported frameworks

  • gdpr
    GDPR
  • pci_dss
    PCI DSS
  • cmmc
    CMMC
  • nist_800_53
    NIST 800-53
  • nist_800_171
    NIST 800-171
  • hipaa
    HIPAA
  • iso_27001
    ISO 27001
  • nis2
    NIS2
  • tsc
    TSC
  • fedramp
    FedRAMP

Example

compliance:
  gdpr:
    - Art. 32
    - Art. 25
  pci_dss:
    - "2.2.1"
    - "6.3.3"
  cmmc:
    - AC.1.001
  nist_800_53:
    - AC-3
    - AU-2
  hipaa:
    - 164.312(a)(1)

Dynamic Event Field Referencing

A Sigma rule’s metadata is normally static: the title, tags, mitre, and compliance blocks describe the rule itself and are attached unchanged to every finding it generates. Wazuh extends Sigma with dynamic event-field referencing, allowing those metadata fields to embed placeholders that resolve against the triggering event at enrichment time. Each finding written to the wazuh-findings-v5-{logtype}-* index then reflects the specific context of the event that matched — for example, the agent ID, hostname, or any other field present in the normalized event.

Syntax

A placeholder takes the form {{ field.path }}, where field.path is a dot-separated path into the triggering event’s _source document. Whitespace inside the delimiters is trimmed, so {{ wazuh.agent.id }} and {{wazuh.agent.id}} are equivalent. Placeholders may appear anywhere inside a supported field’s value and may be mixed with literal text.

Supported fields

Interpolation is applied only to the following fields of the enriched finding’s rule object:

  • title
  • tags
  • mitre.tactic, mitre.technique, mitre.subtechnique
  • compliance.* (every framework sub-array)

The detection block — both selection and condition — is never interpolated.

Example

id: ed85157d-711b-4edb-8390-492ec63c92ac
sigma_id: 12345678-90ab-cdef-1234-567890abcdef
logsource:
  product: apache-http
tags:
  - attack.impact
  - attack.t1499.004
  - "{{ wazuh.agent.host.name }}"
level: high
status: test
detection:
  condition: selection
  selection:
    message|contains:
      - exit signal Segmentation Fault
    wazuh.integration.name: apache-http
metadata:
  title: "Apache segmentation fault in agent {{ wazuh.agent.id }}"
  description: Segmentation faults raised by an Apache worker process.
mitre:
  tactic:
    - TA0040
  technique:
    - T1499
  subtechnique:
    - T1499.004
compliance:
  pci_dss:
    - "6.2"
    - "11.4"

When this rule matches an event where wazuh.agent.id = "001" and wazuh.agent.host.name = "web-prod-01", the resulting enriched finding contains:

{
  "title": "Apache segmentation fault in agent 001",
  "tags": ["attack.impact", "attack.t1499.004", "web-prod-01"],
  "mitre": {
    "tactic": ["TA0040"],
    "technique": ["T1499"],
    "subtechnique": ["T1499.004"]
  },
  "compliance": {
    "pci_dss": ["6.2", "11.4"]
  }
}

Resolution rules

  • Scalars (string, number, boolean) are coerced to their string representation and substituted in place of the placeholder.
  • Scalar arrays are expanded — each array element is coerced to a string and contributed as an additional element of the surrounding array. Supported for tags, mitre.*, and compliance.*.
  • Missing, null, or non-scalar (object) values resolve to the empty string. Finding generation never fails because of an unresolved placeholder.
  • A field whose value consists solely of a placeholder that resolves to the empty string is dropped from the surrounding array or map. For instance, a tag of "{{ missing.field }}" will not appear in rule.tags.

Scope

Interpolation runs after the matching rule is fetched and before the enriched finding is indexed. It affects only the document written to wazuh-findings-v5-{logtype}-*. The raw rule document stored in the rule index is unchanged.

Case Management

Case management allows analysts to track and manage the lifecycle of findings produced by Security Analytics detectors. Each finding can be annotated with case metadata enabling triage workflows directly on the indexed data.

Overview

When a detection rule matches an event, Security Analytics creates a finding. By default, findings contain only detection fields. Case management extends findings with a wazuh.case object that supports:

  • Status tracking - move findings through a workflow (e.g., ACTIVEACKNOWLEDGEDCOMPLETED)
  • Comments - attach free-form notes to findings
  • Tags - organize findings with keyword labels
  • User attribution - record which analyst updated the finding
  • Timestamps - track when the case was created and last updated

Case fields

The following fields are available under wazuh.case in the findings data stream:

FieldTypeDescription
wazuh.case.statuskeywordCurrent status. One of: ACTIVE, ACKNOWLEDGED, COMPLETED, ERROR, DELETED, AUDIT
wazuh.case.commentmatch_only_textFree-form comment attached to the finding
wazuh.case.tagskeyword[]Tags for organization and filtering
wazuh.case.created_atdateTimestamp when the case was first created. Managed by the UI
wazuh.case.updated_atdateTimestamp of the last update. Managed by the UI
wazuh.case.user.namekeywordName of the user who last updated the case. Managed by the UI

Updating findings

Use the Update Findings endpoint to set or modify case fields on one or more existing findings.

Request

PUT /_plugins/_security_analytics/findings/_update

Body

{
  "findings": [
    {
      "_id": "<finding-document-id>",
      "_index": "<finding-index-name>",
      "case": {
        "status": "ACKNOWLEDGED",
        "comment": "Reviewed by SOC analyst",
        "tags": ["critical", "reviewed"],
        "created_at": "2026-06-10T08:00:00.000Z",
        "updated_at": "2026-06-10T09:00:00.000Z",
        "user": {
          "name": "analyst1"
        }
      }
    }
  ]
}

Note: The fields created_at, updated_at, and user.name are automatically managed by the Wazuh Dashboard. They should not be set manually.

FieldRequiredDescription
findingsYesArray of finding updates (max 50 per request)
findings[]._idYesDocument ID of the finding
findings[]._indexYesIndex where the finding is stored
findings[].caseYesObject with the case fields to set or update

All fields inside case are optional, you can update only the fields you need (partial update).

Response

{
  "took": 12,
  "errors": false,
  "items": [
    {
      "_id": "abc123",
      "_index": "wazuh-findings-v5-threat-000001",
      "status": 200,
      "result": "updated"
    }
  ]
}

Error responses

StatusCondition
400Invalid JSON, missing required fields, empty array, or exceeding 50-item limit
207Partial failure, some items succeeded, some failed (e.g., document not found)

Example: triage workflow

Note: Case management is designed to be performed through the Wazuh Dashboard, which handles timestamps and user attribution automatically. The examples below use curl for illustration purposes.

# 1. Acknowledge a finding
curl -X PUT "https://localhost:9200/_plugins/_security_analytics/findings/_update" \
  -H "Content-Type: application/json" \
  -d '{
    "findings": [{
      "_id": "finding-001",
      "_index": "wazuh-findings-v5-threat-000001",
      "case": {
        "status": "ACKNOWLEDGED",
        "comment": "Under investigation"
      }
    }]
  }'

# 2. Close the finding after investigation
curl -X PUT "https://localhost:9200/_plugins/_security_analytics/findings/_update" \
  -H "Content-Type: application/json" \
  -d '{
    "findings": [{
      "_id": "finding-001",
      "_index": "wazuh-findings-v5-threat-000001",
      "case": {
        "status": "COMPLETED",
        "comment": "False positive - benign admin activity"
      }
    }]
  }'

Querying findings by case status

Since wazuh.case.status is a keyword field, you can filter findings by status using standard queries:

# Get all acknowledged findings
curl -XGET "https://127.0.0.1:9200/wazuh-findings-v5-*/_search" -H 'Content-Type: application/json' -d'
{
  "query": {
    "term": {
      "wazuh.case.status": {
        "value": "ACKNOWLEDGED"
      }
    }
  }
}'

Notifications

The Wazuh Indexer Notifications plugin is a specialized component designed to extend the Wazuh Indexer (based on OpenSearch) with multi-channel notification capabilities. It allows the system to send alerts, reports, and messages via Email (SMTP/SES), Slack, Microsoft Teams, Amazon Chime, Amazon SNS, and Custom Webhooks.

Key Capabilities

  • Multi-channel delivery: Send notifications to Slack, Microsoft Teams, Chime, Email (SMTP and AWS SES), AWS SNS, and custom HTTP webhooks.
  • Unified REST API: Create, update, delete, and query notification channel configurations through a single API surface at /_plugins/_notifications/.
  • Test notifications: Validate channel configuration by sending a test message before relying on it for production alerts.
  • Feature discovery: Other plugins can query supported notification features dynamically.
  • RBAC integration: Access to notification configurations is governed by the Wazuh Indexer Security plugin, with backend-role–based filtering.
  • Extensible architecture: The plugin uses a Service Provider Interface (SPI) pattern, making it straightforward to add new destination types.

Supported Channel Types

Channel TypeProtocolDescription
slackHTTPS (Webhook)Posts messages to a Slack channel via an Incoming Webhook URL.
chimeHTTPS (Webhook)Posts messages to an Amazon Chime room via a webhook URL.
microsoft_teamsHTTPS (Webhook)Posts messages to a Microsoft Teams channel via a connector webhook.
webhookHTTP/HTTPSSends a payload to an arbitrary HTTP endpoint with configurable method, headers, and URL.
emailSMTP / AWS SESSends email messages. Requires an smtp_account or ses_account configuration.
snsAWS SNS SDKPublishes a message to an Amazon SNS topic.
smtp_accountDefines SMTP server connection details (host, port, method, credentials).
ses_accountDefines AWS SES sending details (region, role ARN, from address).
email_groupDefines a group of email recipients for reuse across email-type channels.

Default Notification Channels

On first startup, the Notifications plugin automatically creates a set of default notification channels. These channels are pre-configured with placeholder URLs and are disabled by default, they serve as templates that users can customize with their own credentials and then enable.

The following default channels are created:

Channel NameTypeTarget ServiceDefault URL
Slack ChannelslackSlackhttps://hooks.slack.com/services/YOUR_WORKSPACE_ID/YOUR_CHANNEL_ID/YOUR_WEBHOOK_TOKEN
Jira ChannelwebhookJira Cloudhttps://your-domain.atlassian.net/rest/api/3/issue
PagerDuty ChannelwebhookPagerDuty Events API v2https://events.pagerduty.com/v2/enqueue
Shuffle ChannelwebhookShuffle SOARhttps://shuffler.io/api/v1/hooks/WEBHOOK_ID

Behavior

  • Default channels are created only on the cluster manager node during startup.
  • The initialization is idempotent: channels that already exist are not recreated or overwritten.
  • All default channels are created with an empty access list, making them visible to all users.
  • Each channel has a fixed ID (e.g., default_slack_channel), so they can be referenced predictably.

Configuring a Default Channel

To activate a default channel:

  1. Retrieve the channel configuration using the List Notification Configs API or through the Wazuh Dashboard.
  2. Update the channel with your real credentials (webhook URL, API keys, headers, etc.).
  3. Set is_enabled to true.

For example, to configure the Slack channel:

curl -sk -u admin:admin -X PUT \
  "https://localhost:9200/_plugins/_notifications/configs/default_slack_channel" \
  -H 'Content-Type: application/json' \
  -d '{
    "config": {
      "name": "Slack Channel",
      "description": "Production Slack notifications",
      "config_type": "slack",
      "is_enabled": true,
      "slack": {
        "url": "https://hooks.slack.com/services/T0123/B0456/xyzSecretToken"
      }
    }
  }'

Note: A sample alerting monitor is created alongside these channels. Review it under Alerting > Monitors in the Wazuh Dashboard before enabling alerts.

Dependencies

This plugin has a dependency on the wazuh-indexer-common-utils repository. It uses the Common Utils jar to provide shared utility functions and common components required for plugin functionality.

Version

The current plugin version is 5.0.0-alpha0 (see VERSION.json in the repository root).

Architecture

The Notifications plugin follows a layered architecture that separates destination definitions, transport logic, and plugin orchestration.

High-Level Architecture

The Notifications plugin runs inside the Wazuh Indexer and acts as a bridge between internal producers of alerts (such as Alerting, Reporting, and ISM) and external delivery services like SMTP servers, webhooks, and AWS services.

At a high level, the architecture is composed of three main parts:

  • Notification producers (inside the Indexer)
    Internal plugins such as Alerting, Reporting, ISM, and other Wazuh Indexer components generate alerts and events.
    When they need to send a notification (for example, a Slack message or an email), they call the Notifications plugin either through:

    • The REST API exposed by the Indexer, or
    • Internal transport actions.
  • Notifications plugin (inside the Indexer)
    The plugin itself is structured in several layers:

    • REST / Transport layer

      • Exposes the /_plugins/_notifications/... REST endpoints.
      • Receives requests to create, update, list, and delete notification channel configurations, send test notifications, and query features.
      • Validates requests and delegates the work to internal transport actions.
    • Security integration

      • Uses the Security plugin to validate permissions for each request.
      • When filter_by_backend_roles is enabled, it filters which notification configurations each user can see or use based on backend roles.
    • Core SPI layer

      • Defines common contracts and models such as NotificationCore, BaseDestination, and concrete destination types like SlackDestination, SmtpDestination, SesDestination, and SnsDestination.
      • Encapsulates message content (MessageContent) and delivery responses (DestinationMessageResponse).
    • Core implementation (transport logic)

      • Implements concrete transports:
        • WebhookDestinationTransport for Slack, Microsoft Teams, Chime, and generic webhooks (HTTP/HTTPS).
        • SmtpDestinationTransport for email via SMTP.
        • SesDestinationTransport for email via AWS SES.
        • SnsDestinationTransport for messages via AWS SNS.
      • Manages HTTP client pools, connection and socket timeouts, host deny lists, and HTTP response size limits.
      • Retrieves SMTP/SES/SNS credentials from the OpenSearch Keystore or other secure settings via a credential provider.
    • Persistence and configuration

      • Stores notification channel configurations in an internal index (for example, .notifications).
      • Uses NotificationConfigIndex and ConfigIndexingActions to create, read, update, and delete configurations.
      • Exposes internal metrics through the stats endpoint so operators can inspect request counts and error patterns.
  • External destination services (outside the Indexer)
    After the plugin resolves the destination type, the corresponding transport sends the message to:

    • SMTP servers (corporate mail, Gmail, etc.),
    • Webhook endpoints (Slack, Microsoft Teams, Amazon Chime, custom HTTP integrations),
    • AWS services such as SES and SNS.

    Once delivery is attempted, the plugin updates the notification status (for example, sent or failed) and returns the outcome to the caller (Alerting, Reporting, or the user calling the REST API).

Plugin Layers

1. Core SPI (core-spi)

The Service Provider Interface layer defines the contracts and models:

  • NotificationCore: Interface that the core implementation must satisfy. Defines sendMessage() and related operations.
  • BaseDestination: Abstract base class for all destination types. Subclasses include SlackDestination, ChimeDestination, MicrosoftTeamsDestination, CustomWebhookDestination, SmtpDestination, SesDestination, and SnsDestination.
  • MessageContent: Encapsulates the notification message (title, text body, HTML body, attachment).
  • DestinationMessageResponse: Standard response from any delivery attempt (status code, response body).

2. Core Implementation (core)

The Core layer provides the actual delivery logic:

  • Transport Providers:

    • WebhookDestinationTransport — handles Slack, Chime, Microsoft Teams, and custom webhook delivery via HTTP POST.
    • SmtpDestinationTransport — sends emails using SMTP protocol (supports STARTTLS/SSL).
    • SesDestinationTransport — sends emails via the AWS SES SDK.
    • SnsDestinationTransport — publishes messages to AWS SNS topics.
  • HTTP Client Pool: DestinationClientPool manages a pool of DestinationHttpClient instances with configurable connection limits, timeouts, and host deny lists.

  • Credential Management: The CredentialsProvider abstraction loads SMTP/SES/SNS credentials from the OpenSearch Keystore or from secure settings.

  • Plugin Settings (PluginSettings): All tunable parameters — email size limits, connection pools, timeouts, allowed config types, host deny lists — are centralized here and dynamically updatable via cluster settings.

3. Notification Plugin (notifications)

The Plugin module ties everything together:

  • REST Handlers: Map HTTP requests to internal transport actions (see API Reference).
  • Transport Actions: Asynchronous action classes (CreateNotificationConfigAction, DeleteNotificationConfigAction, GetNotificationConfigAction, UpdateNotificationConfigAction, SendNotificationAction, SendTestNotificationAction, GetPluginFeaturesAction, GetChannelListAction, PublishNotificationAction).
  • Index Operations: NotificationConfigIndex manages the .notifications index for storing channel configurations. ConfigIndexingActions handles create/read/update/delete operations on the index.
  • Metrics: The Metrics class tracks counters for all API operations (create, update, delete, info, features, channels, send test).
  • Security: UserAccessManager enforces RBAC based on backend roles when filter_by_backend_roles is enabled.

Send Notification Sequence

The following sequence describes the flow when an internal plugin (e.g., Alerting) sends a notification:

  1. The Alerting Monitor triggers an alert and calls the Notification plugin via the Transport Interface.
  2. The Security Plugin verifies the caller’s permissions.
  3. The notification is persisted in the notifications index with status pending/in-progress.
  4. The plugin resolves the destination type and delegates to the appropriate transport:
    • Email: SmtpDestinationTransport or SesDestinationTransport sends the email. On failure, retries up to the configured limit.
    • Webhook: WebhookDestinationTransport sends the HTTP request to Slack, Chime, Teams, or a custom endpoint.
    • SNS: SnsDestinationTransport publishes to the SNS topic.
  5. The delivery status is returned and the notification record is updated.
  6. The Alerting plugin acknowledges the result and updates the alert status.

Configuration Management Sequence

  1. A user (via Dashboard or REST API) creates or updates a notification channel configuration.
  2. The request is routed to NotificationConfigRestHandler.
  3. The configuration is validated and persisted in the .notifications index.
  4. On retrieval, configurations can be filtered by type, name, status, and other fields.

Configuration

Notifications settings

The Notifications plugin is configured through settings in opensearch.yml and cluster-level dynamic settings. The plugin also supports default values from a YAML configuration file bundled with the plugin.

Configuration Files

On startup, the plugin loads default settings from:

  • Core defaults: /etc/wazuh-indexer/wazuh-indexer-notifications-core/notifications-core.yml
  • Plugin defaults: /etc/wazuh-indexer/wazuh-indexer-notifications/notifications.yml

These files provide initial values that can be overridden by settings in opensearch.yml or through the cluster settings API.


Core Settings (opensearch.notifications.core.*)

These settings control the core notification delivery engine.

Email Settings

SettingTypeDefaultDescription
opensearch.notifications.core.email.size_limitInteger10000000 (10 MB)Maximum total size of an email message including attachments. Minimum: 10000 (10 KB).
opensearch.notifications.core.email.minimum_header_lengthInteger160Minimum header length for email messages. Used to calculate available body size.

HTTP Connection Settings

SettingTypeDefaultDescription
opensearch.notifications.core.http.max_connectionsInteger60Maximum number of simultaneous HTTP connections for webhooks.
opensearch.notifications.core.http.max_connection_per_routeInteger20Maximum HTTP connections per destination route.
opensearch.notifications.core.http.connection_timeoutInteger5000HTTP connection timeout in milliseconds.
opensearch.notifications.core.http.socket_timeoutInteger50000HTTP socket timeout in milliseconds.
opensearch.notifications.core.http.host_deny_listList<String>[]List of denied hosts. Webhook destinations targeting these hosts will be blocked. Inherits from legacy plugins.destination.host.deny_list if not set.

General Core Settings

SettingTypeDefaultDescription
opensearch.notifications.core.max_http_response_sizeIntegerSame as http.max_content_lengthMaximum allowed HTTP response size in bytes. Protects against oversized responses from webhook endpoints.
opensearch.notifications.core.allowed_config_typesList<String>["slack", "chime", "microsoft_teams", "webhook", "email", "sns", "ses_account", "smtp_account", "email_group"]List of channel types that users are allowed to create. Remove a type from this list to disable it cluster-wide.
opensearch.notifications.core.tooltip_supportBooleantrueEnable or disable tooltip support in the Dashboard UI.

Plugin Settings (opensearch.notifications.*)

These settings control the plugin’s general behavior.

SettingTypeDefaultDescription
opensearch.notifications.general.operation_timeout_msLong60000Timeout in milliseconds for internal operations (index reads/writes). Minimum: 100.
opensearch.notifications.general.default_items_query_countInteger100Default number of items returned per query when not specified. Minimum: 10.
opensearch.notifications.general.filter_by_backend_rolesBooleanfalseWhen true, users can only see notification configurations created by users who share the same backend role. Inherits from plugins.alerting.filter_by_backend_roles if not set.

Email Destination Secure Settings

SMTP and SES credentials are stored securely in the OpenSearch Keystore rather than in plain text configuration files.

SMTP Account Credentials

To configure SMTP credentials for an email account named my_smtp_account:

# Add SMTP username
bin/opensearch-keystore add opensearch.notifications.core.email.my_smtp_account.username

# Add SMTP password
bin/opensearch-keystore add opensearch.notifications.core.email.my_smtp_account.password

The secure setting key prefix is opensearch.notifications.core.email.<account_name>.username and opensearch.notifications.core.email.<account_name>.password.

Note: Legacy settings from Alerting (plugins.alerting.destination.email.<account_name>.*) are also supported as fallback.


Example Configuration

A minimal opensearch.yml configuration for the Notifications plugin:

# Notification core settings
opensearch.notifications.core.email.size_limit: 10000000
opensearch.notifications.core.http.max_connections: 60
opensearch.notifications.core.http.connection_timeout: 5000
opensearch.notifications.core.http.socket_timeout: 50000
opensearch.notifications.core.http.host_deny_list:
  - "10.0.0.0/8"
  - "172.16.0.0/12"

# Allowed channel types (remove a type to disable it)
opensearch.notifications.core.allowed_config_types:
  - slack
  - chime
  - microsoft_teams
  - webhook
  - email
  - sns
  - ses_account
  - smtp_account
  - email_group

# Plugin settings
opensearch.notifications.general.operation_timeout_ms: 60000
opensearch.notifications.general.default_items_query_count: 100
opensearch.notifications.general.filter_by_backend_roles: false

Dynamic Settings Update

All settings marked as Dynamic can be updated at runtime through the cluster settings API:

curl -X PUT "https://localhost:9200/_cluster/settings" \
  -H 'Content-Type: application/json' \
  -d '{
    "persistent": {
      "opensearch.notifications.core.http.max_connections": 100,
      "opensearch.notifications.general.filter_by_backend_roles": true
    }
  }'

API Reference

All Notification plugin endpoints use the base path /_plugins/_notifications.


Notification Configs

Create a Notification Config

Creates a new notification channel configuration.

MethodPOST
URI/_plugins/_notifications/configs

Request body:

{
  "config": {
    "name": "<config-name>",
    "description": "<config-description>",
    "config_type": "<channel-type>",
    "is_enabled": true,
    "<channel-type>": {
      // channel-specific fields
    }
  }
}

Slack example:

{
  "config": {
    "name": "my-slack-channel",
    "description": "Slack notifications for alerts",
    "config_type": "slack",
    "is_enabled": true,
    "slack": {
      "url": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXX"
    }
  }
}

Email example (with SMTP account):

{
  "config": {
    "name": "my-email-channel",
    "description": "Email alerts via SMTP",
    "config_type": "email",
    "is_enabled": true,
    "email": {
      "email_account_id": "<smtp-account-config-id>",
      "recipient_list": [
        { "recipient": "alerts@example.com" }
      ],
      "email_group_id_list": []
    }
  }
}

SMTP account example:

{
  "config": {
    "name": "my-smtp-account",
    "description": "Corporate SMTP server",
    "config_type": "smtp_account",
    "is_enabled": true,
    "smtp_account": {
      "host": "smtp.example.com",
      "port": 587,
      "method": "start_tls",
      "from_address": "noreply@example.com"
    }
  }
}

Webhook example:

{
  "config": {
    "name": "my-custom-webhook",
    "description": "Custom webhook for incident system",
    "config_type": "webhook",
    "is_enabled": true,
    "webhook": {
      "url": "https://incident.example.com/api/alert",
      "header_params": {
        "Content-Type": "application/json"
      },
      "method": "POST"
    }
  }
}

Microsoft Teams example:

{
  "config": {
    "name": "my-teams-channel",
    "description": "Teams notifications",
    "config_type": "microsoft_teams",
    "is_enabled": true,
    "microsoft_teams": {
      "url": "https://outlook.office.com/webhook/..."
    }
  }
}

SNS example:

{
  "config": {
    "name": "my-sns-topic",
    "description": "SNS notifications",
    "config_type": "sns",
    "is_enabled": true,
    "sns": {
      "topic_arn": "arn:aws:sns:us-east-1:123456789012:my-topic",
      "role_arn": "arn:aws:iam::123456789012:role/sns-publish-role"
    }
  }
}

Response:

{
  "config_id": "<generated-config-id>"
}

Update a Notification Config

Updates an existing notification channel configuration.

MethodPUT
URI/_plugins/_notifications/configs/{config_id}

Request body: Same structure as create. All fields in the config object are replaced.

{
  "config": {
    "name": "updated-slack-channel",
    "description": "Updated description",
    "config_type": "slack",
    "is_enabled": true,
    "slack": {
      "url": "https://hooks.slack.com/services/T00000000/B00000000/YYYYYYYY"
    }
  }
}

Response:

{
  "config_id": "<config-id>"
}

Get a Notification Config

Retrieves a specific notification configuration by ID.

MethodGET
URI/_plugins/_notifications/configs/{config_id}

Response:

{
  "config_list": [
    {
      "config_id": "<config-id>",
      "last_updated_time_ms": 1234567890,
      "created_time_ms": 1234567890,
      "config": {
        "name": "my-slack-channel",
        "description": "Slack notifications for alerts",
        "config_type": "slack",
        "is_enabled": true,
        "slack": {
          "url": "https://hooks.slack.com/services/..."
        }
      }
    }
  ],
  "total_hits": 1
}

List Notification Configs

Retrieves notification configurations with filtering, sorting, and pagination.

MethodGET
URI/_plugins/_notifications/configs

Query parameters:

ParameterTypeDescription
config_idStringFilter by a single config ID.
config_id_listStringComma-separated list of config IDs.
from_indexIntegerPagination offset (default: 0).
max_itemsIntegerMaximum items to return (default: 100).
sort_fieldStringField to sort by (e.g., config_type, name, last_updated_time_ms).
sort_orderStringSort order: asc or desc.
config_typeStringFilter by channel type (e.g., slack,email).
is_enabledBooleanFilter by enabled status.
nameStringFilter by name (text search).
descriptionStringFilter by description (text search).
last_updated_time_msStringRange filter (e.g., 1609459200000..1640995200000).
created_time_msStringRange filter.
slack.urlStringFilter by Slack webhook URL (text search).
chime.urlStringFilter by Chime webhook URL.
microsoft_teams.urlStringFilter by Teams webhook URL.
webhook.urlStringFilter by custom webhook URL.
smtp_account.hostStringFilter by SMTP host.
smtp_account.from_addressStringFilter by SMTP from address.
smtp_account.methodStringFilter by SMTP method (ssl, start_tls, none).
sns.topic_arnStringFilter by SNS topic ARN.
sns.role_arnStringFilter by SNS role ARN.
ses_account.regionStringFilter by SES region.
ses_account.role_arnStringFilter by SES role ARN.
ses_account.from_addressStringFilter by SES from address.
queryStringSearch across all keyword and text filter fields.
text_queryStringSearch across text filter fields only.

Example:

curl -sk -u admin:admin \
  "https://localhost:9200/_plugins/_notifications/configs?config_type=slack&max_items=10&sort_order=desc"

Delete a Notification Config

Deletes one or more notification configurations.

MethodDELETE
URI/_plugins/_notifications/configs/{config_id}

Or for bulk delete:

MethodDELETE
URI/_plugins/_notifications/configs?config_id_list=id1,id2,id3

Response:

{
  "delete_response_list": {
    "<config-id>": "OK"
  }
}

Channels

List Notification Channels

Returns a simplified list of all configured notification channels (ID, name, type, and enabled status).

MethodGET
URI/_plugins/_notifications/channels

Response:

{
  "channel_list": [
    {
      "config_id": "<id>",
      "name": "my-slack-channel",
      "config_type": "slack",
      "is_enabled": true
    }
  ],
  "total_hits": 1
}

Features

Get Plugin Features

Returns the notification features and allowed config types supported by the plugin.

MethodGET
URI/_plugins/_notifications/features

Response:

{
  "allowed_config_type_list": [
    "slack",
    "chime",
    "microsoft_teams",
    "webhook",
    "email",
    "sns",
    "ses_account",
    "smtp_account",
    "email_group"
  ],
  "plugin_features": {
    "tooltip_support": "true"
  }
}

Test Notifications

Send Test Notification

Sends a test notification to a configured channel to validate the configuration.

MethodPOST
URI/_plugins/_notifications/feature/test/{config_id}

Note: GET is also supported for backwards compatibility but is deprecated and will be removed in a future major version.

Example:

curl -sk -u admin:admin -X POST \
  "https://localhost:9200/_plugins/_notifications/feature/test/<config-id>"

Response:

{
  "status_list": [
    {
      "config_id": "<config-id>",
      "config_type": "slack",
      "config_name": "my-slack-channel",
      "delivery_status": {
        "status_code": "200",
        "status_text": "ok"
      }
    }
  ]
}

Stats

Get Plugin Stats

Returns internal plugin metrics and counters.

MethodGET
URI/_plugins/_notifications/_local/stats

Response: A JSON object with flattened metric counters including:

  • Request totals and interval counts for each API operation (create, update, delete, info, features, channels, send test).

Summary Table

EndpointMethodDescription
/_plugins/_notifications/configsPOSTCreate a new notification channel.
/_plugins/_notifications/configs/{id}PUTUpdate an existing notification channel.
/_plugins/_notifications/configs/{id}GETGet a specific notification channel.
/_plugins/_notifications/configsGETList/search notification channels with filters.
/_plugins/_notifications/configs/{id}DELETEDelete a notification channel.
/_plugins/_notifications/configsDELETEBulk delete (with config_id_list param).
/_plugins/_notifications/channelsGETList all channels (simplified view).
/_plugins/_notifications/featuresGETGet supported features and config types.
/_plugins/_notifications/feature/test/{id}POSTSend a test notification.
/_plugins/_notifications/_local/statsGETGet plugin metrics.

Troubleshooting

Common issues and solutions when working with the Notifications plugin.


Channel Configuration Issues

Slack notifications are not delivered

Symptoms: Creating a Slack config succeeds, but test notifications fail with a non-200 status.

Possible causes:

  1. Invalid webhook URL. Verify the Incoming Webhook URL is active in your Slack workspace settings.
  2. Host deny list. Check if the Slack domain is included in opensearch.notifications.core.http.host_deny_list.
  3. Network connectivity. The Wazuh Indexer node must have outbound HTTPS access to hooks.slack.com.

Resolution:

# Verify the config
curl -sk -u admin:admin \
  "https://localhost:9200/_plugins/_notifications/configs/<config-id>"

# Send a test notification
curl -sk -u admin:admin -X POST \
  "https://localhost:9200/_plugins/_notifications/feature/test/<config-id>"

Check the delivery_status in the response for the HTTP status code and error message.


Email delivery fails with timeout

Symptoms: Email notifications fail with connection timeout errors.

Possible causes:

  1. SMTP server unreachable. Verify the Wazuh Indexer node can reach the SMTP server on the configured port.
  2. Timeout too short. The default connection timeout is 5000 ms and socket timeout is 50000 ms. Increase if needed.
  3. TLS configuration mismatch. Ensure the SMTP method (none, ssl, start_tls) matches the server’s requirements.

Resolution:

# Increase timeouts via cluster settings
curl -X PUT "https://localhost:9200/_cluster/settings" \
  -H 'Content-Type: application/json' \
  -d '{
    "persistent": {
      "opensearch.notifications.core.http.connection_timeout": 10000,
      "opensearch.notifications.core.http.socket_timeout": 120000
    }
  }'

SMTP credentials not found

Symptoms: Email delivery fails with “Credential not found for account” error.

Resolution: SMTP credentials must be stored in the OpenSearch Keystore, not in opensearch.yml.

bin/opensearch-keystore add opensearch.notifications.core.email.<account_name>.username
bin/opensearch-keystore add opensearch.notifications.core.email.<account_name>.password

Restart the node after adding keystore entries.


Permission Issues

“User doesn’t have backend roles configured”

Symptoms: API calls return 403 Forbidden with the message “User doesn’t have backend roles configured.”

Cause: The setting opensearch.notifications.general.filter_by_backend_roles is true, but the current user has no backend roles assigned.

Resolution:

  • Assign backend roles to the user in the Security plugin, or
  • Disable RBAC filtering:
curl -X PUT "https://localhost:9200/_cluster/settings" \
  -H 'Content-Type: application/json' \
  -d '{
    "persistent": {
      "opensearch.notifications.general.filter_by_backend_roles": false
    }
  }'

User cannot see other users’ configurations

Cause: When filter_by_backend_roles is enabled, users can only see configurations created by users who share at least one backend role. Users with the all_access role can see all configurations.


HTTP Response Size Limit

“HTTP response too large” error

Symptoms: Webhook notifications to endpoints that return large responses fail.

Cause: The response from the webhook destination exceeds opensearch.notifications.core.max_http_response_size.

Resolution:

curl -X PUT "https://localhost:9200/_cluster/settings" \
  -H 'Content-Type: application/json' \
  -d '{
    "persistent": {
      "opensearch.notifications.core.max_http_response_size": 20971520
    }
  }'

Plugin Stats

To inspect the plugin’s internal metrics and check for anomalies:

curl -sk -u admin:admin \
  "https://localhost:9200/_plugins/_notifications/_local/stats"

This returns counters for all API operations, which can help identify whether requests are reaching the plugin.


Logs

Enable debug logging for the Notifications plugin:

curl -X PUT "https://localhost:9200/_cluster/settings" \
  -H 'Content-Type: application/json' \
  -d '{
    "persistent": {
      "logger.org.opensearch.notifications": "DEBUG",
      "logger.org.opensearch.notifications.core": "DEBUG"
    }
  }'

Check the Wazuh Indexer logs for entries prefixed with notifications:.

Alerting

The Wazuh Indexer Alerting plugin monitors data stored in the Wazuh Indexer, evaluates user-defined trigger conditions on a schedule, and executes actions when those conditions are met. Actions typically deliver notifications through the Notifications plugin (Slack, email, webhooks, etc.) but can also drive Wazuh-specific workflows such as Active Response.

The plugin is a fork of the OpenSearch Alerting plugin adapted for Wazuh.

Key Capabilities

  • Multiple monitor types: Query-level, bucket-level, document-level, and the Wazuh-specific Active Response monitor. See Architecture for details.
  • Flexible triggers: Define conditions using the full OpenSearch query DSL, aggregation results, or per-document matching with percolate queries.
  • Notification actions: When a trigger fires, send alerts through any channel configured in the Notifications plugin — Slack, Microsoft Teams, email, custom webhooks, PagerDuty, and more.
  • Workflows: Chain multiple monitors into composite workflows for complex detection scenarios.
  • Alert lifecycle management: Track alerts through Active, Acknowledged, Completed, and Error states. Add comments to alerts for collaboration.
  • RBAC integration: Access to monitors, alerts, and destinations is governed by the Security plugin with backend-role–based filtering.
  • Cross-cluster monitoring: Monitor indices on remote clusters connected via cross-cluster search.
  • REST API: Full programmatic control over monitors, workflows, alerts, findings, and comments. See API Reference.
  • Dashboard UI: Create, manage, and monitor alerts through the Wazuh Dashboard interface.

Limits

  • Maximum monitors: Users can create up to 10 custom monitors. This limit applies to all monitor types.

Wazuh Integration Points

Security Analytics

The Security Analytics plugin uses alerting monitors to evaluate incoming events against Sigma detection rules. When an event matches a rule, Security Analytics creates a finding and can trigger an alert. The alerting monitor drives the detection loop — periodically querying new events and running them through the configured detectors.

Notifications

Alerting actions route through the Notifications plugin for message delivery. When a trigger fires, the alerting plugin calls the Notifications plugin via its internal transport interface to send messages to configured channels. This means any destination supported by Notifications (Slack, Teams, email, webhooks, SNS) is available as an alerting action target.

Active Response

The Alerting plugin includes a Wazuh-specific Active Response monitor type that extends document-level monitoring for automated response workflows. This monitor type has specific constraints:

  • Indices: Must target indices matching the wazuh-findings-v5-* prefix.
  • Schedule: Maximum interval of 1 minute (60,000 ms).
  • Triggers: Only DocumentLevelTrigger is supported.

When an Active Response monitor triggers, it writes execution requests to the wazuh-active-responses data stream. The Wazuh Manager retrieves documents from this data stream to distribute and execute Active Response actions on agents. Each document references the source event that triggered the response.

Default Monitors

On first startup, a sample alerting monitor is created alongside the default notification channels. This monitor serves as a template that users can customize and enable. Review it under Alerting > Monitors in the Wazuh Dashboard before enabling alerts.

Dependencies

DependencyPurpose
Notifications pluginDelivers alert notifications to configured channels
Security pluginEnforces RBAC on monitors, alerts, and destinations
Job Scheduler pluginSchedules and executes monitors at configured intervals
wazuh-indexer-common-utilsShared utility functions and common components

Further Reading

For the full upstream API reference, advanced configuration, and Dashboard usage guides, see the OpenSearch Alerting documentation.

Architecture

The Alerting plugin runs inside the Wazuh Indexer as an OpenSearch plugin. It schedules monitors that query indices, evaluates trigger conditions against the results, and executes actions (typically sending notifications) when conditions are met.

Core Concepts

The alerting pipeline follows a linear flow:

  1. A Monitor runs on a schedule, executing a query against one or more indices.
  2. The query results are evaluated against one or more Triggers — boolean conditions that determine whether an alert should fire.
  3. When a trigger condition is met, the monitor executes its configured Actions — typically sending a notification through the Notifications plugin.
  4. An Alert record is created to track the triggered condition through its lifecycle.
  5. For document-level monitors, Findings record which specific documents matched the trigger.

Monitor Types

Monitor TypeDescriptionTrigger TypeInput Type
Query-level (per query)Executes an OpenSearch query and evaluates the aggregation results as a whole. Suitable for threshold-based alerts (e.g., error count > 100).QueryLevelTriggerSearchInput
Bucket-level (per bucket)Monitors aggregation bucket results individually. Each bucket that meets the trigger condition generates a separate alert.BucketLevelTriggerSearchInput (with aggregations)
Cluster metrics (per cluster metrics)Periodically calls OpenSearch cluster APIs (cluster health, stats, tasks, etc.) and evaluates the response. Suitable for monitoring cluster state rather than indexed data.QueryLevelTriggerClusterMetricsInput
Document-level (per document)Matches individual documents using percolate queries. Creates a finding for each matching document.DocumentLevelTriggerDocLevelMonitorInput
CompositeChains multiple monitors into a workflow and evaluates conditions across their alerts. See Workflows.ChainedAlertTriggerCompositeInput
Active ResponseWazuh-specific extension of document-level monitoring for automated response. See Active Response.DocumentLevelTriggerDocLevelMonitorInput

Active Response Monitor Constraints

The Active Response monitor type enforces stricter validation than standard document-level monitors:

  • Indices must match the wazuh-findings-v5-* prefix.
  • Schedule interval cannot exceed 60,000 milliseconds (1 minute).
  • Only DocumentLevelTrigger is accepted — other trigger types are rejected.

Triggers

Each monitor type uses a corresponding trigger type:

  • QueryLevelTrigger: Evaluates a script condition against the full query response. The script has access to the query results, aggregations, and monitor metadata.
  • BucketLevelTrigger: Evaluates a condition per aggregation bucket. Supports composite aggregations for paginating through large result sets.
  • DocumentLevelTrigger: Defines per-document matching conditions using query DSL. Documents that match the trigger’s queries generate findings.
  • ChainedAlertTrigger: Evaluates a condition over the alerts produced by the delegate monitors of a composite (workflow) monitor, allowing alerts to fire based on combinations of upstream monitor results.

Cluster metrics monitors reuse QueryLevelTrigger, evaluating a script condition against the cluster API response.

Actions

Actions define what happens when a trigger fires. Each action specifies:

  • A destination — a notification channel configured in the Notifications plugin (Slack, email, webhook, etc.).
  • A message template — a Mustache template that formats the alert details into the notification body.
  • An optional throttle — a minimum interval between repeated notifications for the same alert (up to plugins.alerting.action_throttle_max_value, default 24 hours).

When a trigger fires, the plugin calls the Notifications plugin via its internal transport interface to deliver the message.

Alert Lifecycle

Alerts transition through the following states:

StateDescription
ActiveThe trigger condition is currently met. The alert was just created or continues to fire.
AcknowledgedA user has acknowledged the alert through the Dashboard or API.
CompletedThe trigger condition is no longer met. The alert resolved naturally.
ErrorAn error occurred during monitor execution or action delivery.

Findings

Document-level monitors produce findings — records of individual documents that matched the monitor’s trigger conditions. Each finding contains:

  • The matching document IDs and source index.
  • The queries (rules) that matched.
  • A timestamp of when the match was detected.

Findings are stored in rolling indices (.opensearch-alerting-finding-history-*) with a default retention of 30 days.

These raw alerting findings are not the same as the findings surfaced in the Wazuh context. Detectors managed by the Security Analytics plugin run on document-level monitors internally, but produce their own enriched findings — augmented with the full event payload and rule metadata — which are written to wazuh-findings-v5-* indices. A plain document-level monitor only produces the raw findings described above; it does not perform this enrichment.

Workflows

Workflows chain multiple monitors into a composite execution unit. A workflow defines an ordered sequence of monitors (delegates) that run together. This enables multi-stage detection scenarios where the output of one monitor informs the next.

Workflows have their own CRUD API and can be executed, searched, and managed independently of individual monitors.

Alerting Indices

The plugin manages the following system indices:

IndexDescriptionRetention
.opendistro-alerting-alertsCurrent active alerts
.opendistro-alerting-alert-history-*Historical alert records30 days (daily rollover)
.opensearch-alerting-finding-history-*Document-level monitor findings30 days (12-hour rollover)
.opensearch-alerting-comments-history-*Alert comments and annotations30 days (12-hour rollover)
.opensearch-scheduled-jobsMonitor and workflow definitions

Rollover periods and retention are configurable through plugin settings.

Configuration

The Alerting plugin is configured through cluster settings under the plugins.alerting.* namespace. All settings can be updated dynamically via the cluster settings API.

Monitor Settings

SettingTypeDefaultDescription
plugins.alerting.monitor.max_monitorsInteger10Maximum number of monitors allowed per node.
plugins.alerting.monitor.max_triggersInteger10Maximum number of triggers per monitor (hard max: 50).
plugins.alerting.monitor.doc_level_monitor_shard_fetch_sizeInteger10000Number of documents fetched per shard for document-level monitors.
plugins.alerting.monitor.doc_level_monitor_fan_out_nodesInteger1000Maximum number of nodes to fan out document-level monitor queries to.
plugins.alerting.monitor.doc_level_monitor_fanout_max_durationTimeValue3mMaximum duration for fan-out operations in document-level monitors.
plugins.alerting.monitor.doc_level_monitor_execution_max_durationTimeValue4mMaximum total execution duration for document-level monitors.
plugins.alerting.monitor.percolate_query_max_num_docs_in_memoryInteger50000Maximum number of documents held in memory for percolate queries.
plugins.alerting.monitor.percolate_query_docs_size_memory_percentage_limitInteger10Maximum percentage of JVM heap used for percolate query documents.
plugins.alerting.monitor.doc_level_monitor_query_field_names_enabledBooleantrueEnable field name extraction for document-level monitor queries.

Timeout Settings

SettingTypeDefaultDescription
plugins.alerting.input_timeoutTimeValue30sTimeout for monitor input (query) execution.
plugins.alerting.index_timeoutTimeValue30sTimeout for index operations (writing alerts, findings).
plugins.alerting.bulk_timeoutTimeValue30sTimeout for bulk index operations.
plugins.alerting.request_timeoutTimeValue10sTimeout for internal transport requests.

Alert History Settings

SettingTypeDefaultDescription
plugins.alerting.alert_history_enabledBooleantrueEnable alert history storage.
plugins.alerting.alert_history_rollover_periodTimeValue1dHow often to roll over the alert history index.
plugins.alerting.alert_history_max_ageTimeValue30dMaximum age of alert history indices before deletion.
plugins.alerting.alert_history_max_docsLong1000000Maximum number of documents per alert history index.
plugins.alerting.alert_history_retention_periodTimeValue30dRetention period for alert history data.
plugins.alerting.alert_backoff_millisTimeValue50msBackoff interval between alert write retries.
plugins.alerting.alert_backoff_countInteger3Number of retry attempts for failed alert writes.
plugins.alerting.max_actionable_alert_countLong50Maximum number of alerts that can trigger actions in a single monitor execution.

Finding History Settings

SettingTypeDefaultDescription
plugins.alerting.alert_finding_enabledBooleantrueEnable finding history storage.
plugins.alerting.alert_finding_rollover_periodTimeValue12hHow often to roll over the finding history index.
plugins.alerting.finding_history_max_ageTimeValue30dMaximum age of finding history indices before deletion.
plugins.alerting.alert_findings_indexing_batch_sizeInteger1000Batch size for bulk-indexing findings.

Comment Settings

SettingTypeDefaultDescription
plugins.alerting.comments_enabledBooleantrueEnable the alert comments feature.
plugins.alerting.comments_history_max_docsLong1000Maximum number of documents per comments history index.
plugins.alerting.comments_history_max_ageTimeValue30dMaximum age of comments history indices before deletion.
plugins.alerting.comments_history_rollover_periodTimeValue12hHow often to roll over the comments history index.
plugins.alerting.max_comment_character_lengthInteger2000Maximum character length for a single comment.
plugins.alerting.max_comments_per_alertInteger500Maximum number of comments allowed per alert.
plugins.alerting.max_comments_per_notificationInteger3Maximum number of comments included in alert notification messages.

General Settings

SettingTypeDefaultDescription
plugins.alerting.filter_by_backend_rolesBooleantrueWhen enabled, users can only view monitors and alerts created by users who share the same backend role.
plugins.alerting.action_throttle_max_valueTimeValue24hMaximum throttle duration for alert actions.
plugins.alerting.cross_cluster_monitoring_enabledBooleantrueEnable monitoring of indices on remote clusters via cross-cluster search.

Updating Settings

All settings can be updated at runtime through the cluster settings API:

curl -sk -u admin:admin -X PUT \
  "https://localhost:9200/_cluster/settings" \
  -H 'Content-Type: application/json' \
  -d '{
    "persistent": {
      "plugins.alerting.monitor.max_monitors": 20,
      "plugins.alerting.alert_history_max_age": "60d"
    }
  }'

API Reference

The Alerting plugin exposes a REST API under the /_plugins/_alerting/ base path. This page summarizes the available endpoints. For full request/response schemas, see the OpenSearch Alerting API documentation.

Endpoint Summary

Monitors

MethodEndpointDescription
POST/_plugins/_alerting/monitorsCreate a monitor
PUT/_plugins/_alerting/monitors/{id}Update a monitor
GET/_plugins/_alerting/monitors/{id}Get a monitor by ID
DELETE/_plugins/_alerting/monitors/{id}Delete a monitor
GET/_plugins/_alerting/monitors/_searchSearch monitors
POST/_plugins/_alerting/monitors/{id}/_executeExecute a monitor immediately

Workflows

MethodEndpointDescription
POST/_plugins/_alerting/workflowsCreate a workflow
PUT/_plugins/_alerting/workflows/{id}Update a workflow
GET/_plugins/_alerting/workflows/{id}Get a workflow by ID
DELETE/_plugins/_alerting/workflows/{id}Delete a workflow
POST/_plugins/_alerting/workflows/{id}/_executeExecute a workflow immediately

Alerts

MethodEndpointDescription
GET/_plugins/_alerting/alertsList alerts across all monitors
GET/_plugins/_alerting/workflows/{id}/alertsList alerts for a specific workflow
POST/_plugins/_alerting/monitors/{id}/_acknowledge/alertsAcknowledge one or more alerts

Findings

MethodEndpointDescription
GET/_plugins/_alerting/findingsList findings from document-level monitors

Comments

MethodEndpointDescription
POST/_plugins/_alerting/comments/{alertId}Add a comment to an alert
PUT/_plugins/_alerting/comments/{commentId}Update a comment
DELETE/_plugins/_alerting/comments/{commentId}Delete a comment
GET/_plugins/_alerting/comments/_searchSearch comments

Destinations (Legacy)

MethodEndpointDescription
GET/_plugins/_alerting/destinations/{id}Get a destination by ID
GET/_plugins/_alerting/destinations/_searchSearch destinations

Note: Destination management has been migrated to the Notifications plugin. Use the Notifications API (/_plugins/_notifications/) for creating and managing notification channels.

Examples

Create a Query-Level Monitor

This example creates a monitor that checks every 5 minutes whether the number of error-level events in the last hour exceeds 100:

curl -sk -u admin:admin -X POST \
  "https://localhost:9200/_plugins/_alerting/monitors" \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "monitor",
    "name": "High error rate",
    "monitor_type": "query_level_monitor",
    "enabled": true,
    "schedule": {
      "period": {
        "interval": 5,
        "unit": "MINUTES"
      }
    },
    "inputs": [
      {
        "search": {
          "indices": ["wazuh-events-v5-*"],
          "query": {
            "size": 0,
            "query": {
              "bool": {
                "filter": [
                  { "range": { "@timestamp": { "gte": "now-1h" } } },
                  { "term": { "event.severity": "error" } }
                ]
              }
            },
            "aggs": {
              "error_count": {
                "value_count": { "field": "@timestamp" }
              }
            }
          }
        }
      }
    ],
    "triggers": [
      {
        "query_level_trigger": {
          "name": "Error threshold exceeded",
          "severity": "1",
          "condition": {
            "script": {
              "source": "ctx.results[0].aggregations.error_count.value > 100",
              "lang": "painless"
            }
          },
          "actions": []
        }
      }
    ]
  }'

Acknowledge Alerts

curl -sk -u admin:admin -X POST \
  "https://localhost:9200/_plugins/_alerting/monitors/{monitorId}/_acknowledge/alerts" \
  -H 'Content-Type: application/json' \
  -d '{
    "alerts": ["alert-id-1", "alert-id-2"]
  }'

Execute a Monitor On-Demand

curl -sk -u admin:admin -X POST \
  "https://localhost:9200/_plugins/_alerting/monitors/{monitorId}/_execute"

Upgrade

This section guides you through the upgrade process of the Wazuh indexer.

The Wazuh indexer cluster remains operational throughout the upgrade. The rolling upgrade process allows nodes to be updated one at a time, ensuring continuous service availability and minimizing disruptions. The steps detailed in the following sections apply to both single-node and multi-node Wazuh indexer clusters. For multi-node Wazuh indexer clusters, repeat the following steps on every node.

Note: This documentation assumes you are already provisioned with a wazuh-indexer package through any of the possible methods:

Preparing the upgrade

Perform the following steps on any of the Wazuh indexer nodes replacing $WAZUH_INDEXER_IP_ADDRESS, $USERNAME, and $PASSWORD.

  1. Disable shard replication to prevent shard replicas from being created while Wazuh indexer nodes are being taken offline for the upgrade.

    curl -X PUT "https://$WAZUH_INDEXER_IP_ADDRESS:9200/_cluster/settings" \
    -u $USERNAME:$PASSWORD -k -H "Content-Type: application/json" -d '
    {
        "persistent": {
            "cluster.routing.allocation.enable": "primaries"
        }
    }'
    

    Output

    {
      "acknowledged": true,
      "persistent": {
        "cluster": {
          "routing": {
            "allocation": {
              "enable": "primaries"
            }
          }
        }
      },
      "transient": {}
    }
    
  2. Perform a flush operation on the cluster to commit transaction log entries to the index.

    curl -X POST "https://$WAZUH_INDEXER_IP_ADDRESS:9200/_flush" -u $USERNAME:$PASSWORD -k
    

    Output

    {
    "_shards" : {
        "total" : 19,
        "successful" : 19,
        "failed" : 0
       }
    }
    

Upgrading the Wazuh indexer nodes

  1. Stop the Wazuh indexer service.

    Systemd

    systemctl stop wazuh-indexer
    

    SysV

    service wazuh-indexer stop
    
  2. Upgrade the Wazuh indexer to the latest version.

    rpm

    rpm -ivh --replacepkgs wazuh-indexer-<VERSION>.rpm
    

    dpkg

    dpkg -i wazuh-indexer-<VERSION>.deb
    
  3. Restart the Wazuh indexer service.

    Systemd

    systemctl daemon-reload
    systemctl enable wazuh-indexer
    systemctl start wazuh-indexer
    

    SysV

    Choose one option according to the operating system used.

    a. RPM-based operating system:

    chkconfig --add wazuh-indexer
    service wazuh-indexer start
    

    b. Debian-based operating system:

    update-rc.d wazuh-indexer defaults 95 10
    service wazuh-indexer start
    

Repeat steps 1 to 3 above on all Wazuh indexer nodes before proceeding to the post-upgrade actions.

Post-upgrade actions

Perform the following steps on any of the Wazuh indexer nodes replacing $WAZUH_INDEXER_IP_ADDRESS, $USERNAME, and $PASSWORD.

  1. Check that the newly upgraded Wazuh indexer nodes are in the cluster.

    curl -k -u $USERNAME:$PASSWORD https://$WAZUH_INDEXER_IP_ADDRESS:9200/_cat/nodes?v
    
  2. Re-enable shard allocation.

    curl -X PUT "https://$WAZUH_INDEXER_IP_ADDRESS:9200/_cluster/settings" \
    -u $USERNAME:$PASSWORD -k -H "Content-Type: application/json" -d '
    {
        "persistent": {
            "cluster.routing.allocation.enable": "all"
        }
    }
    '
    

    Output

    {
        "acknowledged" : true,
        "persistent" : {
            "cluster" : {
            "routing" : {
                "allocation" : {
                "enable" : "all"
                }
            }
            }
        },
        "transient" : {}
    }
    
  3. Check the status of the Wazuh indexer cluster again to see if the shard allocation has finished.

    curl -k -u $USERNAME:$PASSWORD https://$WAZUH_INDEXER_IP_ADDRESS:9200/_cat/nodes?v
    

    Output

    ip         heap.percent ram.percent cpu load_1m load_5m load_15m node.role node.roles                                        cluster_manager name
    172.18.0.3           34          86  32    6.67    5.30     2.53 dimr      cluster_manager,data,ingest,remote_cluster_client -               wazuh2.indexer
    172.18.0.4           21          86  32    6.67    5.30     2.53 dimr      cluster_manager,data,ingest,remote_cluster_client *               wazuh1.indexer
    172.18.0.2           16          86  32    6.67    5.30     2.53 dimr      cluster_manager,data,ingest,remote_cluster_client -               wazuh3.indexer
    

Uninstall

Note: You need root user privileges to run all the commands described below.

Yum

yum remove wazuh-indexer -y
rm -rf /var/lib/wazuh-indexer/
rm -rf /usr/share/wazuh-indexer/
rm -rf /etc/wazuh-indexer/

APT

apt-get remove wazuh-indexer -y
rm -rf /var/lib/wazuh-indexer/
rm -rf /usr/share/wazuh-indexer/
rm -rf /etc/wazuh-indexer/

Backup and restore

In this section you can find instructions on how to create and restore a backup of your Wazuh Indexer key files, preserving file permissions, ownership, and path. Later, you can move this folder contents back to the corresponding location to restore your certificates and configurations. Backing up these files is useful in cases such as moving your Wazuh installation to another system.

Note: This backup only restores the configuration files, not the data. To back up data stored in the indexer, use snapshots.

Creating a backup

To create a backup of the Wazuh indexer, follow these steps. Repeat them on every cluster node you want to back up.

Note: You need root user privileges to run all the commands described below.

Preparing the backup

  1. Backup the existing Wazuh indexer security configuration files.

    /usr/share/wazuh-indexer/bin/indexer-security-init.sh --options "-backup /etc/wazuh-indexer/opensearch-security -icl -nhnv"
    
  2. Create the destination folder to store the files. For version control, add the date and time of the backup to the name of the folder.

    backup_folder=~/wazuh_files_backup/$(date +%F_%H:%M)
    mkdir -p $backup_folder && echo $backup_folder
    
  3. Save the host information.

    cat /etc/*release* > $backup_folder/host-info.txt
    echo -e "\n$(hostname): $(hostname -I)" >> $backup_folder/host-info.txt
    

Backing up the Wazuh indexer

Back up the Wazuh indexer certificates and configuration

rsync -aREz \
/etc/wazuh-indexer/certs/ \
/etc/wazuh-indexer/jvm.options \
/etc/wazuh-indexer/jvm.options.d \
/etc/wazuh-indexer/log4j2.properties \
/etc/wazuh-indexer/opensearch.yml \
/etc/wazuh-indexer/opensearch.keystore \
/etc/wazuh-indexer/opensearch-security/ \
/etc/wazuh-indexer/wazuh-indexer-reports-scheduler/ \
/etc/wazuh-indexer/wazuh-indexer-notifications/ \
/etc/wazuh-indexer/wazuh-indexer-notifications-core/ \
/usr/lib/sysctl.d/wazuh-indexer.conf $backup_folder

Compress the files and transfer them to the new server:

tar -cvzf wazuh-indexer-backup.tar.gz $backup_folder

Restoring Wazuh indexer from backup

This guide explains how to restore a backup of your configuration files.

Note: This guide is designed specifically for restoration from a backup of the same version.


Note: For a multi-node setup, there should be a backup file for each node within the cluster. You need root user privileges to execute the commands below.

Preparing the data restoration

  1. In the new node, move the compressed backup file to the root / directory:

    mv wazuh-indexer-backup.tar.gz /
    cd /
    
  2. Decompress the backup files and change the current working directory to the directory based on the date and time of the backup files:

    tar -xzvf wazuh-indexer-backup.tar.gz
    cd $backup_folder
    

Restoring Wazuh indexer files

Perform the following steps to restore the Wazuh indexer files on the new server.

  1. Stop the Wazuh indexer to prevent any modifications to the Wazuh indexer files during the restoration process:

    systemctl stop wazuh-indexer
    
  2. Restore the Wazuh indexer configuration files and change the file permissions and ownership accordingly:

    cp etc/wazuh-indexer/jvm.options /etc/wazuh-indexer/jvm.options
    cp -r etc/wazuh-indexer/jvm.options.d/ /etc/wazuh-indexer/jvm.options.d/
    cp etc/wazuh-indexer/log4j2.properties /etc/wazuh-indexer/log4j2.properties
    cp etc/wazuh-indexer/opensearch.keystore /etc/wazuh-indexer/opensearch.keystore
    cp -r etc/wazuh-indexer/wazuh-indexer-reports-scheduler/ /etc/wazuh-indexer/wazuh-indexer-reports-scheduler/
    cp -r etc/wazuh-indexer/wazuh-indexer-notifications/ /etc/wazuh-indexer/wazuh-indexer-notifications/
    cp -r etc/wazuh-indexer/wazuh-indexer-notifications-core/ /etc/wazuh-indexer/wazuh-indexer-notifications-core/
    cp usr/lib/sysctl.d/wazuh-indexer.conf /usr/lib/sysctl.d/wazuh-indexer.conf
    
    chown wazuh-indexer:wazuh-indexer /etc/wazuh-indexer/jvm.options
    chown -R wazuh-indexer:wazuh-indexer /etc/wazuh-indexer/jvm.options.d
    chown wazuh-indexer:wazuh-indexer /etc/wazuh-indexer/log4j2.properties
    chown wazuh-indexer:wazuh-indexer /etc/wazuh-indexer/opensearch.keystore
    chown -R wazuh-indexer:wazuh-indexer /etc/wazuh-indexer/wazuh-indexer-reports-scheduler/
    chown -R wazuh-indexer:wazuh-indexer /etc/wazuh-indexer/wazuh-indexer-notifications/
    chown -R wazuh-indexer:wazuh-indexer /etc/wazuh-indexer/wazuh-indexer-notifications-core/
    chown wazuh-indexer:wazuh-indexer /usr/lib/sysctl.d/wazuh-indexer.conf
    
  3. Start the Wazuh indexer service:

    systemctl start wazuh-indexer
    
  4. Clear the backup files to free up space:

    rm -rf $backup_folder
    rm -rf /wazuh-indexer-backup.tar.gz
    

Security

Security

Wazuh Indexer uses the OpenSearch Security plugin to manage access control and security features.

Configuration files

The configuration files for the security plugin are located under the /etc/wazuh-indexer/opensearch-security/ directory by default.

Modifying these files directly is not recommended. Instead, use the Wazuh Dashboard Security plugin to create new security resources. See Define Users and Roles.

Among these files, Wazuh Indexer uses these particularly to add its own security resources:

  • internal_users.yml: Defines the internal users for the Wazuh Indexer. Each user has a hashed password, reserved status, backend roles, and a description.

  • roles.yml: Defines the roles and their permissions within the Wazuh Indexer. Each role specifies the cluster permissions, index permissions, and tenant permissions.

  • roles_mapping.yml: Maps users and backend roles to the defined roles. This file specifies which users or backend roles have access to each role.

The Access Control section contains information about the security resources added to the Wazuh Indexer by default.

Access Control

Wazuh Indexer uses the OpenSearch Security plugin to manage access control and security features. This allows you to define users, roles, and permissions for accessing indices and performing actions within the Wazuh Indexer.

You can find a more detailed overview of the OpenSearch Security plugin in the OpenSearch documentation.

Wazuh default Internal Users

Wazuh defines internal users and roles for the different Wazuh components to handle index management.

These default users and roles definitions are stored in the internal_users.yml, roles.yml, and roles_mapping.yml files on the /etc/wazuh-indexer/opensearch-security/ directory.

Find more info about the configurations files in the Configuration files section.

Users

UserDescriptionRoles
wazuh-serverUser for the Wazuh Manager with read/write access to stateful indices and write-only access to stateless indices.stateless-write, stateful-delete, stateful-write, stateful-read, cm_subscription_read
wazuh-dashboardUser for Wazuh Dashboard with read access to stateful and stateless indices, and management level permissionsfor the monitoring indices.sample-data-management, metrics-write, metrics-read, stateless-read, stateful-read, cm_subscription_write

Roles

Role NameAccess DescriptionIndex PatternsPermissions
stateful-readGrants read-only permissions to stateful indices.wazuh-states-*read
stateful-writeGrants write-only permissions to stateful indices.wazuh-states-*index
stateful-deleteGrants delete permissions to stateful indices.wazuh-states-*delete
stateless-readGrants read-only permissions to stateless indices.wazuh-alerts*, wazuh-archives*read
stateless-writeGrants write-only permissions to stateless indices.wazuh-alerts*, wazuh-archives*index
metrics-readGrants read permissions to metrics indices.wazuh-monitoring*, wazuh-statistics*read
metrics-writeGrants write permissions to metrics indices.wazuh-monitoring*, wazuh-statistics*index
sample-data-managementGrants full permissions to sample data indices.*-sample-*data_access, manage
cm_subscription_readGrants permissions to retrieve subscriptions for the server.N/Acluster:monitor/content_manager/subscription/get
cm_subscription_writeGrants permissions to create and delete subscriptions for the content manager.N/Acluster:admin/content_manager/subscription/create, cluster:admin/content_manager/subscription/delete

Sensitive configuration endpoints

A small set of endpoints modify configuration with a high impact on the platform. They are protected by two independent controls:

EndpointMethodPermission (cluster action)
/_plugins/_content_manager/policy/{space}PUTindices:data/write/content_manager/policy/update
/_plugins/_content_manager/updatePOSTcluster:admin/content_manager/update/trigger
/_plugins/_setup/settingsPUTcluster:admin/setup/settings/update
  1. RBAC — each endpoint is gated by the cluster permission above, enforced by the OpenSearch Security plugin. Only the superuser admin (role all_access, cluster wildcard *) holds these permissions; no bundled standard user does — wazuh-server and wazuh-dashboard are intentionally excluded from all three. To delegate any of these actions without granting full superuser, create a dedicated role granting only the permission(s) above and map it to the chosen user.
  2. Per-endpoint disable settings — each endpoint can be disabled independently by setting its node setting to false, after which it returns 403 Forbidden for every caller, regardless of role (intended for externally managed deployments such as Wazuh Cloud): plugins.content_manager.catalog.update_on_demand (content update trigger), plugins.content_manager.catalog.policy_update.enabled (policy updates), and plugins.setup.settings_update.enabled (setup settings). See Protecting sensitive configuration.

Defining Users and Roles

You can create and manage users and roles through the Wazuh Dashboard UI.

Default users and roles cannot be modified. Instead, duplicate them and modify the duplicates.

Creating a New User, Role, and Role Mapping via the Wazuh Dashboard

Prerequisites

  • You must be logged in as a user with administrative privileges (e.g., admin).

Follow these steps:

1. Create a Role

  1. In the Wazuh Dashboard, go to Index Management -> Security -> Roles.
  2. Click Create role.
  3. Enter a Role name (e.g., custom-read-write).
  4. Under Cluster permissions, select permissions if needed.
  5. Under Index permissions:
  6. Click Create to save the role.

2. Create a User

  1. In the Wazuh Dashboard, go to Index Management -> Security -> Internal users.
  2. Click Create internal user.
  3. Fill in the following:
    • Username (e.g., new-user)
    • Password (enter and confirm)
    • Description (optional)
  4. Click Create to create the user.

3. Verify Role Mapping

When you assign a role to a user during creation, the mapping is created automatically. To review or edit:

  1. In Security, go to Roles.
  2. Find and click your role (custom-read-write).
  3. Go to Mapped users
  4. Click Map users.
  5. Fill in the following:
    • Users (e.g., new-user).
    • Backend roles (optional).
  6. Click Map to save the mapping.

4. Test Access

After creating the user and role:

  1. Log out from the Dashboard.
  2. Log in with the new user’s credentials.
  3. Navigate to Index Management -> Dev Tools.
  4. Run a query to test access, such as:
    GET /wazuh-*/_search
    

Additional Resources

Release notes

Highlights

  • New “Setup” initialization plugin.
    • Creation of Wazuh indices, index templates and ISM policies on startup #425.
    • Data streams by default for time-series indices (events, findings, metrics, active responses, raw events).
    • Adds ISM policies for data streams automatic rollover and removal based on age and size #466.
    • Adds metrics data streams for agent and communications telemetry #34711.
    • Some Wazuh settings now reside in the Indexer, and can be managed using the Settings API in the Setup plugin #833.
  • New “Content Manager” plugin.
    • Official threat intel content management for Wazuh CTI (ruleset, vulnerabilities feed, IoC feed).
    • Custom content management for user-defined threat intel resources (rules, decoders, integrations, KVDBs, filters, policies).
    • Content organized into spaces: standard (read-only, sourced from CTI), draft, test, custom — with a draft → test → custom promotion workflow.
    • Scheduled automatic updates by default, with manual updates also supported.
    • Implements a log test feature split into normalization (decoders) and detection (rules) phases.
    • Implements a REST API for content management, log testing, manual updates, promotion, subscription management, and version checks.
    • Daily version-check ping to Wazuh CTI to surface content updates and deployment telemetry.
  • Fork of OpenSearch’s Security Analytics plugin. [1]
    • Threat Detection migrated from the Wazuh Manager to the Wazuh Indexer Security Analytics plugin.
    • Extended Sigma rules syntax #47.
    • Per-space support for Log Types and Rules #37.
    • Per-space threat detectors #117.
    • Rules parser improvements:
      • Case-insensitive Sigma operators #182.
      • Add exists Sigma modifiers #173.
      • Add support for IPv6 addresses in Sigma rules.
    • Dynamic event field referencing in findings #181.
    • Enriched findings written to wazuh-findings-v5-{category} data streams, embedding the full triggering event source and rule metadata (id, title, tags, level, status, MITRE, compliance).
  • Fork of OpenSearch’s Reporting plugin. [2]
    • Bundled by default in Wazuh Indexer packages — PDF/CSV reports from dashboards and saved searches, on-demand or on a schedule, with email delivery.
  • Fork of OpenSearch’s Notifications plugin. [3]
    • Webhooks for Slack, Jira, PagerDuty and Shuffle created by default.
    • Dedicated monitor for Active Response #8.
    • Multi-channel support: Slack, Microsoft Teams, Amazon Chime, Email (SMTP/SES), AWS SNS, and custom webhooks.
  • Fork of OpenSearch’s Alerting plugin. [4]
    • Dedicated monitor for Active Response #8.
  • Fork of OpenSearch’s Common Utils repository. [5]
    • Shared models and actions used across the Wazuh forks of Alerting, Notifications, Security Analytics and the Content Manager.
  • Built-in Wazuh Engine.
    • Bundled in Wazuh Indexer packages and Docker images (x86_64 and aarch64).
    • Communicates with the Content Manager over a local Unix socket.
    • Validation of user-defined threat intel content.
    • Engine enrichment: IoC content management, GeoIP enrichment, and engine filters for event pre-processing #33493.
  • Active Response has been migrated to the Wazuh Indexer.
    • Dedicated wazuh-active-responses data stream for execution requests, with its own ISM policy.
    • Driven by a dedicated Alerting monitor.
  • New mdBook documentation (#254).
  • Reworked Wazuh Indexer packages and build scripts.
    • Wazuh Indexer packages now work for Systemd, SysV and initd service managers #602.
    • Snapshots for ruleset, vulnerabilities feed and IoC feed are now included in Wazuh Indexer packages so a freshly installed cluster has content available offline.
  • New set of default users and roles #1538.
    • Reserved Wazuh roles aligned with the new plugins (Content Manager, Alerting, Notifications, Reporting, Security Analytics).
  • Reworked and extended Wazuh Common Schema.
    • Bump to ECS v9.1.0.
    • Per-category event and finding data streams (wazuh-events-v5-{category}, wazuh-findings-v5-{category}) covering access management, applications, cloud services, network activity, security, system activity, and unclassified events.
    • Raw events stream wazuh-events-raw-v5 with an aggressive purge ISM policy (gated by an Engine setting in the Setup plugin).
    • Agent and rule metadata relocated under the wazuh.* namespace.
    • New inventory coverage for Linux systemd units and macOS launchd daemons/agents alongside Windows services.

Breaking changes

  • Wazuh Indexer 4.x can not be upgraded to 5.x. A new installation of Wazuh Indexer 5.x is required.
  • Multi-tenancy disabled by default #1080.
  • Remove Performance Analyzer plugin from Wazuh Indexer packages #891.
  • Filebeat is no longer used to forward events from the Wazuh Manager to the Wazuh indexer — replaced by the built-in indexer connector.
  • Upgrade to OpenSearch 3.0 #874.
    • Replace and remove deprecated settings — configurations carried over from 4.x are no longer valid #475.
    • Update to JDK 25 #1341.
  • Migration of the Wazuh Common Schema from the wazuh-indexer repository to the wazuh-indexer-plugins repository. Folder renamed to wcs #879.
  • Supported operating systems updated for 5.0.0: Red Hat 9/10, Ubuntu 22.04/24.04, and Amazon Linux 2023 (x86_64 and aarch64). Earlier distributions supported in 4.x are no longer covered.

Migration Guide

This guide describes how to migrate an existing Wazuh indexer 4.x deployment to Wazuh indexer 5.x.

Important Only configuration is migrated, and it is migrated manually. There is no automatic upgrade tooling, and indexed data cannot be migrated (see Data cannot be migrated). The procedure requires a fresh 5.x installation and manual re-creation of configuration and security settings. If you need to retain access to historical 4.x data, you can optionally keep the 4.x environment running in parallel.

Scope

Wazuh indexer 5.x is a major release based on OpenSearch 3.x. It ships with new index schemas, a revised security model, and renamed or removed configuration settings. As a result, a 4.x cluster cannot be upgraded in place. The migration covers configuration only — base node settings, certificates, and OpenSearch Security authentication/authorization. It does not cover indexed data.

The procedure is:

  1. Stand up a new 5.x cluster on a fresh host.
  2. Re-create base configuration, certificates, and security settings against the 5.x layout.

The 4.x cluster is never modified by this procedure. Since indexed data is not migrated, you may optionally keep the 4.x environment running in parallel as a read-only legacy deployment if you still need to query historical data — see Data cannot be migrated.

Prerequisites

Before starting:

Configuration migration

Migration from 4.x to 5.x is a selective carry-over: re-create each setting against the 5.x configuration tree rather than copying 4.x files verbatim. Both versions install configuration under /etc/wazuh-indexer/, and the layout has changed only slightly, but several OpenSearch 3.x settings have been renamed or removed and must be reviewed before any 4.x value is reused.

Important Do not copy 4.x configuration files over the 5.x files. The defaults shipped with 5.x are tuned for the new base engine. Use the 4.x files as a reference and re-apply each setting into the corresponding 5.x file.

The canonical 5.x configuration layout is:

PathPurpose
/etc/wazuh-indexer/opensearch.ymlMain cluster and node configuration
/etc/wazuh-indexer/jvm.optionsJVM heap and GC settings
/etc/wazuh-indexer/log4j2.propertiesLogging configuration
/etc/wazuh-indexer/certs/Transport and HTTP TLS certificates
/etc/wazuh-indexer/opensearch-security/Security plugin configuration (see Security migration)

For a full description of each file, see Configuration.

Procedure

Perform these steps on the new 5.x host.

  1. Install the 5.x package on a fresh host following Installation. This creates the default 5.x configuration tree under /etc/wazuh-indexer/.

  2. Stop the new service before editing configuration:

    systemctl stop wazuh-indexer
    
  3. Copy the relevant 4.x configuration values into the corresponding 5.x files — do not overwrite the 5.x files. Review each setting against Settings changes below.

  4. Migrate certificates by placing the existing trust and node certificates under /etc/wazuh-indexer/certs/ and updating the plugins.security.ssl.* paths in opensearch.yml accordingly.

  5. Port jvm.options and log4j2.properties by copying only individual non-default lines into the 5.x files. Do not replace the 5.x files outright.

  6. Re-create the security configuration. See Security migration.

  7. Start the service:

    systemctl daemon-reload
    systemctl enable wazuh-indexer
    systemctl start wazuh-indexer
    
  8. Confirm the node joins the new 5.x cluster:

    curl -k -u $USERNAME:$PASSWORD https://$WAZUH_INDEXER_IP_ADDRESS:9200/_cat/nodes?v
    

Settings changes

The following 4.x settings have changed in 5.x and must be reviewed before reuse. This list is not exhaustive: validate every remaining setting against the upstream OpenSearch 3.x breaking changes and release notes before starting the service.

4.x setting5.x replacementNotes
opensearch_performance_analyzer.*RemovedThe opensearch-performance-analyzer plugin is no longer shipped. Remove any related entries.
plugins.anomaly_detection.*RemovedThe opensearch-anomaly-detection plugin is no longer shipped. Remove any related entries.
plugins.asynchronous_search.*RemovedThe opensearch-asynchronous-search plugin is no longer shipped. Remove any related entries.
plugins.ml_commons.*RemovedThe opensearch-ml plugin is no longer shipped. Remove any related entries.
plugins.query.datasources.*RemovedThe opensearch-sql plugin is no longer shipped. Remove any related entries.
plugins.neural_search.*RemovedThe opensearch-neural-search plugin is no longer shipped. Remove any related entries.
knn.*RemovedThe opensearch-knn plugin is no longer shipped. Remove any related entries.
compatibility.override_main_response_versionRemovedPresent in 4.x opensearch.yml for legacy Filebeat compatibility. Removed in OpenSearch 3.0; a node that still defines it will not boot. Delete the setting.
Multi-tenancy settingsDisabled by defaultDashboard multi-tenancy is off by default in 5.x.

Security migration

Authentication and authorization are managed by the OpenSearch Security plugin in both versions, but 5.x ships a new set of default internal users, roles, and role mappings tailored to the Wazuh stack — the 4.x defaults are not carried over. For the full, up-to-date list of 5.x default users, roles, and permissions, see Access Control.

In 5.x, all security plugin configuration lives under /etc/wazuh-indexer/opensearch-security/:

FilePurpose
config.ymlAuthentication and authorization backends (internal, LDAP, SAML, OIDC, JWT, etc.)
internal_users.ymlLocal user accounts and password hashes
roles.ymlRole definitions
roles_mapping.ymlMapping from authenticated identities to roles
action_groups.ymlReusable groups of permissions referenced by roles
tenants.ymlDashboard tenants
nodes_dn.ymlNode certificate distinguished names allowed into the cluster
allowlist.ymlREST API paths reachable while the cluster is in a restricted state (replaces 4.x whitelist.yml)
audit.ymlAudit-logging configuration

Procedure

Perform these steps on the new 5.x host.

  1. Export the live 4.x security configuration. The on-disk files under /etc/wazuh-indexer/opensearch-security/ may be stale, since the active configuration is stored in the security index. Use the backup procedure to write the live configuration to disk before reusing it. See Back up and Restore.

  2. On the new 5.x host, do not overwrite the shipped files. Edit them in place under /etc/wazuh-indexer/opensearch-security/. For each custom entry in the 4.x files, decide whether it should be re-created in 5.x:

    • Custom internal users → add to internal_users.yml (existing password hashes can be reused as-is).
    • Custom roles → add to roles.yml, keeping the 5.x index patterns and permission names.
    • Role mappings → add to roles_mapping.yml, referencing the new role names.
    • External authentication backends (LDAP, Active Directory, SAML, OIDC, JWT, Kerberos, client-certificate) → re-create the corresponding authc / authz blocks in config.yml against the 5.x schema.

    Tip — bulk copy alternative Reviewing every entry individually is the safest option, but it is tedious and risks silently dropping a custom user or role you set up long ago and no longer remember. As an alternative, copy all custom entries from the 4.x files into the corresponding 5.x files at once, then prune afterwards. This guarantees nothing is lost, at the cost of dragging along stale entries. Copied entries may reference 4.x index patterns or permission names that changed in 5.x, and may collide with the new 5.x default users and roles — so still validate the result against Access Control before applying.

  3. Apply the configuration with the /usr/share/wazuh-indexer/bin/indexer-security-init.sh script shipped with the package.

  4. Restart the service and verify authentication works for each backend before pointing production traffic at the new cluster.

The exact syntax for each external authentication backend is defined and maintained by the upstream OpenSearch Security plugin and may evolve between OpenSearch versions. Always cross-check the backend configuration against the upstream documentation before applying it:

Data cannot be migrated

Wazuh indexer 4.x indices cannot be migrated to a 5.x cluster. There is no in-place upgrade, no snapshot restore, and no _reindex path from 4.x data. To retain access to historical 4.x data, keep the 4.x environment running in parallel as a legacy, read-only deployment.

Why

Wazuh indexer 5.x introduces new index schemas (the v5 suffix) and new index templates. The schemas, field types, and routing of the 5.x indices differ from 4.x in ways that prevent the older shards from being opened or transformed by a 5.x cluster:

ConcernDescription
Index schema5.x uses new templates and mappings under wazuh-events-v5, wazuh-findings-v5, and wazuh-states-v5. These have no direct counterpart in 4.x.
Engine versionThe OpenSearch 3.x base in 5.x reads Lucene segments produced by its own and the immediately preceding major version only. Older 4.x shards fall outside the supported range.
Document shapeField names, types, and parent-child relationships in the v5 mappings differ from 4.x documents in ways that cannot be transformed losslessly by a reindex.

Optionally keeping a legacy 4.x environment

Whether to retain the old cluster is entirely your decision and depends on whether you still need historical 4.x data. If you do not, the 4.x environment can be decommissioned once the 5.x cluster is in service.

If you do need historical visibility, you can run the existing 4.x cluster alongside the new 5.x deployment:

  1. Leave the 4.x cluster in place after the migration; do not uninstall it.
  2. Switch the 4.x cluster to a read-only role: stop ingestion from the Wazuh server into 4.x, and optionally mark its indices read-only to prevent accidental writes.
  3. Keep the existing 4.x dashboard pointed at the 4.x cluster for users who need historical data; a 5.x dashboard cannot read 4.x indices.
  4. Plan a retention window after which the 4.x environment can be decommissioned according to your data-retention policy.