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, Wazuh Active Response).
- 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:
| Subproject | Description |
|---|---|
notifications/core-spi | Service Provider Interface. Defines destination models (SlackDestination, SmtpDestination, ChimeDestination, etc.) and the NotificationCore contract. |
notifications/core | Core implementation. Contains HTTP/SMTP/SES/SNS clients, transport providers, and all configurable settings (PluginSettings). |
notifications/notifications | Main 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
└── ActiveResponseDestination
ActiveResponseDestination has no transport: active_response channels are handled in the notifications module (see Notification flow).
Transport layer (core)
DestinationTransport (interface)
├── WebhookDestinationTransport (Slack, Chime, Teams, Webhooks)
├── SmtpDestinationTransport (SMTP Email)
├── SesDestinationTransport (AWS SES Email)
└── SnsDestinationTransport (AWS SNS)
REST handlers (notifications)
| Handler | Method | URI |
|---|---|---|
NotificationConfigRestHandler | POST | /_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 | |
NotificationFeaturesRestHandler | GET | /_plugins/_notifications/features |
NotificationChannelListRestHandler | GET | /_plugins/_notifications/channels |
SendTestMessageRestHandler | POST | /_plugins/_notifications/feature/test/{config_id} |
Setup environment
Requirements
- JDK: 21 or later. The build compiles to Java 21 (
sourceCompatibility,targetCompatibilityand KotlinjvmTargetinnotifications/build.gradle). CI builds with JDK 21 and 25, and the Wazuh Indexer bundles JDK 25. - Gradle: Use the included
./gradlewwrapper (no separate install needed). - IDE: IntelliJ IDEA with Kotlin plugin is recommended.
Clone and build
The build resolves wazuh-indexer-common-utils from the local Maven repository, so publish it first by running ./gradlew publishToMavenLocal in a checkout of that repository. The Gradle wrapper lives in the notifications/ directory:
git clone <notifications-repo-url>
cd wazuh-indexer-notifications/notifications
./gradlew build
The distribution zips are generated at (paths relative to the repository root):
notifications/core/build/distributions/wazuh-indexer-notifications-core-<version>.zip
notifications/notifications/build/distributions/wazuh-indexer-notifications-<version>.zip
Build packages
To create distribution packages:
# Full build (compile + test + assemble)
./gradlew build
# Assemble only (skip tests)
./gradlew assemble
The output zips can be installed on a Wazuh Indexer node with opensearch-plugin. Install the core plugin first, since the Notifications plugin extends it:
bin/opensearch-plugin install file:///path/to/wazuh-indexer-notifications-core-<version>.zip
bin/opensearch-plugin install file:///path/to/wazuh-indexer-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, from the notifications/ directory:
./gradlew :notifications:integTest
Key integration test classes:
| Test Class | Description |
|---|---|
SlackNotificationConfigCrudIT | Full CRUD lifecycle for Slack channels. |
ChimeNotificationConfigCrudIT | Full CRUD lifecycle for Chime channels. |
EmailNotificationConfigCrudIT | Full CRUD lifecycle for Email channels (SMTP/SES). |
MicrosoftTeamsNotificationConfigCrudIT | Full CRUD lifecycle for Microsoft Teams channels. |
WebhookNotificationConfigCrudIT | Full CRUD lifecycle for custom webhooks. |
SnsNotificationConfigCrudIT | Full CRUD lifecycle for SNS channels. |
CreateNotificationConfigIT | Config creation edge cases and validation. |
DeleteNotificationConfigIT | Config deletion including bulk delete. |
QueryNotificationConfigIT | Filtering, sorting, and pagination queries. |
GetPluginFeaturesIT | Feature discovery endpoint tests. |
GetNotificationChannelListIT | Channel list endpoint tests. |
SendTestMessageRestHandlerIT | Test message delivery flow. |
SendTestMessageWithMockServerIT | Test message with mock destination. |
SecurityNotificationIT | RBAC and access control tests. |
MaxHTTPResponseSizeIT | HTTP response size limit enforcement. |
NotificationsBackwardsCompatibilityIT | Backwards compatibility between versions. |
Notification flow
The data flow when sending a notification follows this sequence:
sequenceDiagram
participant C as Alerting / Reporting / ISM
participant N as SendNotificationAction
participant CI as .opensearch-notifications-config
participant T as DestinationTransportProvider
participant EXT as External service
participant B as ActiveResponseBulkIndexer
participant AR as wazuh-active-responses
C->>N: sendNotification (channel IDs, message)
N->>N: UserAccessManager.validateUser
N->>CI: Get channel configs (and email account/group configs)
CI-->>N: Configs
loop Each channel, in parallel (Dispatchers.IO)
alt Missing, access denied or muted
N->>N: Error EventStatus, nothing sent
else Slack, Chime, Teams, webhook, email, SNS
N->>T: NotificationCore.sendMessage(destination)
T->>EXT: Webhook / SMTP / SES / SNS transport
EXT-->>T: Response
T-->>N: DestinationMessageResponse
else active_response
N->>N: Get the triggering document, build the request
N->>B: add(IndexRequest)
B->>AR: Bulk on flush interval or max actions
end
end
N-->>C: SendNotificationResponse (EventStatus per channel)
- An internal plugin (Alerting, Reporting, ISM) or a user invokes the Notification plugin via Transport or REST API.
SendNotificationActiondelegates toSendMessageActionHelper.executeRequest. UserAccessManager.validateUserrejects a user without backend roles whenfilter_by_backend_rolesis enabled.- The channel configs, and the
smtp_account/ses_account/email_groupconfigs that email channels reference, are read from the.opensearch-notifications-configindex (NotificationConfigIndex). A missing, inaccessible or disabled (muted) channel gets an errorEventStatusand is skipped. - For the remaining channels, in parallel,
sendMessageThroughSpicallsNotificationCore.sendMessage, and theDestinationTransportProviderresolves the correct transport based on the destination type. Webhook URLs are checked againsthost_deny_listbefore sending. - The transport client delivers the message to the external service. There is no plugin-level retry: only the default retry strategy of the Apache HTTP client (
DefaultHttpRequestRetryStrategy) applies, to webhook deliveries. A failed delivery is returned as a non-200DeliveryStatus. active_responsechannels never reachNotificationCore.sendActiveResponseMessageparses<doc_id>|<index>from the message, reads that document, copies itswazuhobject (refusing with400whenlocationislocaland the document has nowazuh.agent.id), addswazuh.active_responsewith the channel parameters, and adds anIndexRequest(op typecreate) for thewazuh-active-responsesdata stream to theActiveResponseBulkIndexer. The status is200as soon as the request is queued; theBulkProcessorflushes everyopensearch.notifications.active_response.bulk_flush_interval_msor afterbulk_max_actionsrequests, and bulk failures are only logged and counted in a metric.- The
EventStatuslist is returned to the caller in aSendNotificationResponse. If any channel’s status is not200, the action fails with anOpenSearchStatusExceptionthat carries the wholeevent_status_list. Nothing is persisted about the delivery itself.
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:
- Add a new
ChannelDefinitionentry to theDEFAULT_CHANNELSlist inDefaultChannelInitializer.kt. - Choose a unique, stable
idprefixed withdefault_(e.g.,default_teams_channel). - Set
isEnabled = falseand use a placeholder URL with clear instructions in thedescription. - 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:
-
Define the destination model in
core-spi:- Create a new class extending
BaseDestinationinnotifications/core-spi/src/main/kotlin/.../destination/.
- Create a new class extending
-
Implement the transport in
core:- Create a new class implementing
DestinationTransportinnotifications/core/src/main/kotlin/.../transport/. - Register it in
DestinationTransportProvider.
- Create a new class implementing
-
Add the config type to the
DEFAULT_ALLOWED_CONFIG_TYPESlist incore/setting/PluginSettings.kt. -
Write tests: Add integration tests in
notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/.