Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ca1175d
feat(health-check): add notification integration on health-check proc…
gonzaarancibia Oct 20, 2025
ecca794
docs(health-check-doc): add documentation about notification task
gonzaarancibia Oct 21, 2025
33d2f87
feat(health-check-notification): improve channels descriptions
gonzaarancibia Oct 21, 2025
7d5afad
refactor(health-check-notification): refactor code
gonzaarancibia Oct 21, 2025
8233b92
docs(changelog): add entry
gonzaarancibia Oct 21, 2025
18c2125
refactor(health-check-notification): delete useless validation
gonzaarancibia Oct 22, 2025
8f47fbb
test(health-check-notification): add testing development
gonzaarancibia Oct 22, 2025
5435b3f
refactor: run prettier
gonzaarancibia Oct 22, 2025
39aa15d
refactor(health-check-notification): rename functions
gonzaarancibia Oct 23, 2025
7bac673
refactor(health-check-notification): delete useless code
gonzaarancibia Oct 23, 2025
5c13a56
refactor(health-check-notification): rename function and delete usele…
gonzaarancibia Oct 23, 2025
217f272
conditionally register notification health check task
guidomodarelli Oct 23, 2025
77d622e
Revert "conditionally register notification health check task"
guidomodarelli Oct 24, 2025
891a727
add TODO to validate notifications plugin before registering channel
guidomodarelli Oct 24, 2025
fbc5bda
add reference to related discussion in code comment
guidomodarelli Oct 24, 2025
edb65fc
conditionally register notification health check task
guidomodarelli Oct 23, 2025
e5b2416
fix prettier
guidomodarelli Oct 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable changes to the Wazuh app project will be documented in this file.
### Added

- Support for Wazuh 5.0.0
- Added default notification channels through health check [#7827](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7827)
- Added sample data generators for agents monitoring and server statistics [#7597](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7597)
- Added "form-data": "^4.0.4" to the resolutions section to ensure this specific version is installed [#7662](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7662)
- Added prompts to some views related to problems with server API and alerts index pattern [#7694](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7694)
Expand Down
1 change: 1 addition & 0 deletions docs/ref/modules/healthcheck.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ The checks represents the unit to check and some could do some write actions suc
| `index-pattern:states-fim-registry-values` | Validate (create is possible) the existence of a compatible index pattern for FIM registry values states |
| `index-pattern:states-sca` | Validate (create is possible) the existence of a compatible index pattern for Configuration Assessment states |
| `server-api:connection-compatibility` | Validate the connection and compatibility with the server API hosts |
| `notification-channel:verify-default-channels` | Validate the existence of the default notification channels or create them if they do not exist. |

## Execution results

Expand Down
3 changes: 2 additions & 1 deletion plugins/main/opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"security",
"securityDashboards",
"searchguard",
"telemetry"
"telemetry",
"notificationsDashboards"
],
"server": true,
"ui": true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
const OPENSEARCH_API_BASE_PATH = '/_plugins/_notifications';
export const OPENSEARCH_API = Object.freeze({
CONFIGS: `${OPENSEARCH_API_BASE_PATH}/configs`,
EVENTS: `${OPENSEARCH_API_BASE_PATH}/events`,
TEST_MESSAGE: `${OPENSEARCH_API_BASE_PATH}/feature/test`,
FEATURES: `${OPENSEARCH_API_BASE_PATH}/features`,
});

export type ChannelType = 'slack' | 'webhook';

export interface ChannelDefinition {
id: string;
name: string;
type: ChannelType;
}

interface ChannelConfig {
name: string;
description: string;
config_type: ChannelType;
is_enabled: boolean;
slack?: object;
webhook?: object;
}

export const defaultChannels: ChannelDefinition[] = [
{ id: 'default_slack_channel', name: 'Slack Channel', type: 'slack' },
{ id: 'default_jira_channel', name: 'Jira Channel', type: 'webhook' },
{
id: 'default_pagerduty_channel',
name: 'PagerDuty Channel',
type: 'webhook',
},
{ id: 'default_shuffle_channel', name: 'Shuffle Channel', type: 'webhook' },
];

export const defaultChannelConfigs: Record<string, ChannelConfig> = {
'Slack Channel': {
name: 'Slack Channel',
description:
'Default slack notification channel. To start receiving alerts, create a monitor, trigger and monitor action in the Alerting section, and edit this channel.',
config_type: 'slack',
is_enabled: false,
slack: {
url: 'https://hooks.slack.com/services/YOUR_WORKSPACE_ID/YOUR_CHANNEL_ID/YOUR_WEBHOOK_TOKEN',
},
},
'PagerDuty Channel': {
name: 'PagerDuty Channel',
description: `Default PagerDuty notification channel.

Create a PagerDuty integration and copy its Integration Key into the "X-Routing-Key" header below. Then, create a monitor and trigger in the Alerting section to start receiving incidents.

You can test this channel from a custom monitor action, e.g.:\n\n{\n "event_action": "trigger",\n "payload": {\n "summary": "testing pd",\n "source": "Alerting ES plugin",\n "severity": "critical"\n }\n}`,
config_type: 'webhook',
is_enabled: false,
webhook: {
url: 'https://events.pagerduty.com/v2/enqueue',
method: 'POST',
header_params: {
'Content-Type': 'application/json',
'X-Routing-Key': 'YOUR_PAGERDUTY_API_KEY',
},
},
},
'Jira Channel': {
name: 'Jira Channel',
description: `Default Jira notification channel.

Configure your Jira domain and authentication using a Base64-encoded "email:api_token" value in the Authorization header. Then, create a monitor and trigger in the Alerting section to start generating issues.

You can test this channel from a custom monitor action, e.g.:\n\n{\n "fields": {\n "project": { "key": "CRM" },\n "summary": "Wazuh Alert: Test",\n "description": {\n "type": "doc",\n "version": 1,\n "content": [\n {\n "type": "paragraph",\n "content": [\n { "type": "text", "text": "This is a test from Wazuh Alerting." }\n ]\n }\n ]\n },\n "issuetype": { "name": "Task" }\n }\n}`,
config_type: 'webhook',
is_enabled: false,
webhook: {
url: 'https://your-domain.atlassian.net/rest/api/3/issue',
method: 'POST',
header_params: {
'Content-Type': 'application/json',
Authorization: 'Basic base64(email:api_token)',
},
},
},
'Shuffle Channel': {
name: 'Shuffle Channel',
description:
'Default Shuffle notification channel. To start triggering workflows, create a monitor and trigger in the Alerting section, and edit this channel.',
config_type: 'webhook',
is_enabled: false,
webhook: {
url: 'https://shuffler.io/api/v1/hooks/WEBHOOK_ID',
method: 'POST',
header_params: {
'Content-Type': 'application/json',
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
CoreSetup,
ILegacyClusterClient,
} from '../../../../../src/core/server';
import { initializeClientNotificationConfigs } from './plugin/notification-plugin';

// Reference: https://github.com/opensearch-project/dashboards-notifications/blob/d24660dbec78de4000777884d04e5159501c2b61/server/plugin.ts#L22C43-L70C1
export function notificationSetup(core: CoreSetup) {
const notificationsClient: ILegacyClusterClient =
core.opensearch.legacy.createClient('opensearch_notifications', {
plugins: [initializeClientNotificationConfigs],
});

return notificationsClient;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { initializeClientNotificationConfigs } from './notification-plugin';

// Mock Client constructor
const MockClient = jest.fn();
MockClient.prototype = {};

describe('InitializeClientNotificationConfigs', () => {
let mockComponents: any;
let mockConfig: any;

beforeEach(() => {
jest.clearAllMocks();
MockClient.prototype = {}; // Reset prototype

// Mock components with clientAction
mockComponents = {
clientAction: {
factory: jest.fn(() => jest.fn()),
namespaceFactory: jest.fn(() => {
const namespace = jest.fn();
namespace.prototype = {};
return namespace;
}),
},
};

mockConfig = {};
});

describe('Plugin registration', () => {
it('should register notifications namespace and methods', () => {
// Call the plugin function to register methods
initializeClientNotificationConfigs(
MockClient,
mockConfig,
mockComponents,
);

// Verify that the plugin registered the namespace
expect(mockComponents.clientAction.namespaceFactory).toHaveBeenCalled();

// Verify that clientAction.factory was called for each method
expect(mockComponents.clientAction.factory).toHaveBeenCalledTimes(2); // getConfigs and createConfig

// Verify the methods are added to the prototype
expect(MockClient.prototype.notifications).toBeDefined();
});

it('should configure getConfigs method correctly', () => {
initializeClientNotificationConfigs(
MockClient,
mockConfig,
mockComponents,
);

// Verify getConfigs was configured with correct parameters
expect(mockComponents.clientAction.factory).toHaveBeenCalledWith({
url: {
fmt: '/_plugins/_notifications/configs',
},
method: 'GET',
});
});

it('should configure createConfig method correctly', () => {
initializeClientNotificationConfigs(
MockClient,
mockConfig,
mockComponents,
);

// Verify createConfig was configured with correct parameters
expect(mockComponents.clientAction.factory).toHaveBeenCalledWith({
url: {
fmt: '/_plugins/_notifications/configs',
},
method: 'POST',
needBody: true,
});
});

it('should register both methods on notifications prototype', () => {
const mockGetConfigs = jest.fn();
const mockCreateConfig = jest.fn();
const mockFactory = jest
.fn()
.mockReturnValueOnce(mockGetConfigs)
.mockReturnValueOnce(mockCreateConfig);

const mockNamespace = jest.fn();
mockNamespace.prototype = {};
const mockNamespaceFactory = jest.fn(() => mockNamespace);

const components = {
clientAction: {
factory: mockFactory,
namespaceFactory: mockNamespaceFactory,
},
};

initializeClientNotificationConfigs(MockClient, mockConfig, components);

// Verify both methods are assigned
const notificationsProto = MockClient.prototype.notifications.prototype;
expect(notificationsProto.getConfigs).toBe(mockGetConfigs);
expect(notificationsProto.createConfig).toBe(mockCreateConfig);
});
});

describe('API endpoint configuration', () => {
it('should use correct OpenSearch API path for both methods', () => {
initializeClientNotificationConfigs(
MockClient,
mockConfig,
mockComponents,
);

// Both methods should use the same API endpoint
const expectedUrl = { fmt: '/_plugins/_notifications/configs' };

expect(mockComponents.clientAction.factory).toHaveBeenCalledWith({
url: expectedUrl,
method: 'GET',
});

expect(mockComponents.clientAction.factory).toHaveBeenCalledWith({
url: expectedUrl,
method: 'POST',
needBody: true,
});
});

it('should set needBody for POST method only', () => {
initializeClientNotificationConfigs(
MockClient,
mockConfig,
mockComponents,
);

// Verify GET method doesn't have needBody
expect(mockComponents.clientAction.factory).toHaveBeenCalledWith({
url: { fmt: '/_plugins/_notifications/configs' },
method: 'GET',
});

// Verify POST method has needBody
expect(mockComponents.clientAction.factory).toHaveBeenCalledWith({
url: { fmt: '/_plugins/_notifications/configs' },
method: 'POST',
needBody: true,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { OPENSEARCH_API } from '../common/constants';

export function initializeClientNotificationConfigs(
Client: any,
_config: any,
components: any,
) {
const clientAction = components.clientAction.factory;

Client.prototype.notifications = components.clientAction.namespaceFactory();
const notifications = Client.prototype.notifications.prototype;

notifications.getConfigs = clientAction({
url: {
fmt: OPENSEARCH_API.CONFIGS,
},
method: 'GET',
});

notifications.createConfig = clientAction({
url: {
fmt: OPENSEARCH_API.CONFIGS,
},
method: 'POST',
needBody: true,
});
}
Loading
Loading