diff --git a/src/platform/plugins/shared/workflows_management/common/utils/build_alert_event.test.ts b/src/platform/plugins/shared/workflows_management/common/utils/build_alert_event.test.ts new file mode 100644 index 0000000000000..a0c0d03e01ddf --- /dev/null +++ b/src/platform/plugins/shared/workflows_management/common/utils/build_alert_event.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { CombinedSummarizedAlerts } from '@kbn/alerting-plugin/server/types'; +import { buildAlertEvent } from './build_alert_event'; + +const mockRule = { + id: 'rule-id', + name: 'test rule', + tags: ['test-tag'], + consumer: 'test-consumer', + producer: 'test-producer', + ruleTypeId: 'test-rule-type', +}; + +const makeAlertGroup = (ids: string[]) => ({ + count: ids.length, + data: ids.map((id) => ({ _id: id, _index: 'test-index' })), + alert_count: { active: ids.length, recovered: 0, ignored: 0 }, +}); + +describe('buildAlertEvent', () => { + it('should merge new, ongoing, and recovered alerts into a flat array', () => { + const alerts = { + all: makeAlertGroup(['a1', 'a2', 'a3']), + new: makeAlertGroup(['a1']), + ongoing: makeAlertGroup(['a2']), + recovered: makeAlertGroup(['a3']), + } as unknown as CombinedSummarizedAlerts; + + const result = buildAlertEvent({ alerts, rule: mockRule, spaceId: 'default' }); + + expect(result.alerts).toHaveLength(3); + expect(result.alerts.map((a) => a._id)).toEqual(['a1', 'a2', 'a3']); + }); + + it('should return only new alerts when ongoing and recovered are empty', () => { + const alerts = { + all: makeAlertGroup(['a1']), + new: makeAlertGroup(['a1']), + ongoing: makeAlertGroup([]), + recovered: makeAlertGroup([]), + } as unknown as CombinedSummarizedAlerts; + + const result = buildAlertEvent({ alerts, rule: mockRule, spaceId: 'default' }); + + expect(result.alerts).toHaveLength(1); + expect(result.alerts[0]._id).toBe('a1'); + }); + + it('should handle undefined data gracefully', () => { + const alerts = { + all: { count: 0, data: [] }, + new: { count: 0, data: undefined }, + ongoing: { count: 0, data: undefined }, + recovered: { count: 0, data: undefined }, + } as unknown as CombinedSummarizedAlerts; + + const result = buildAlertEvent({ alerts, rule: mockRule, spaceId: 'default' }); + + expect(result.alerts).toEqual([]); + }); + + it('should include rule information', () => { + const alerts = { + all: makeAlertGroup([]), + new: makeAlertGroup([]), + ongoing: makeAlertGroup([]), + recovered: makeAlertGroup([]), + } as unknown as CombinedSummarizedAlerts; + + const result = buildAlertEvent({ + alerts, + rule: mockRule, + ruleUrl: 'https://example.com/rule', + spaceId: 'my-space', + }); + + expect(result.rule).toEqual(mockRule); + expect(result.ruleUrl).toBe('https://example.com/rule'); + expect(result.spaceId).toBe('my-space'); + }); + + it('should handle missing ruleUrl', () => { + const alerts = { + all: makeAlertGroup([]), + new: makeAlertGroup([]), + ongoing: makeAlertGroup([]), + recovered: makeAlertGroup([]), + } as unknown as CombinedSummarizedAlerts; + + const result = buildAlertEvent({ alerts, rule: mockRule, spaceId: 'default' }); + + expect(result.ruleUrl).toBeUndefined(); + }); +}); diff --git a/src/platform/plugins/shared/workflows_management/common/utils/build_alert_event.ts b/src/platform/plugins/shared/workflows_management/common/utils/build_alert_event.ts index a75aabd875294..06f410fdab3d6 100644 --- a/src/platform/plugins/shared/workflows_management/common/utils/build_alert_event.ts +++ b/src/platform/plugins/shared/workflows_management/common/utils/build_alert_event.ts @@ -22,7 +22,11 @@ export function buildAlertEvent(params: { spaceId: string; }): AlertEvent { return { - alerts: params.alerts.new.data, + alerts: [ + ...(params.alerts.new?.data ?? []), + ...(params.alerts.ongoing?.data ?? []), + ...(params.alerts.recovered?.data ?? []), + ], rule: { id: params.rule.id, name: params.rule.name, diff --git a/src/platform/plugins/shared/workflows_management/server/connectors/workflows/index.test.ts b/src/platform/plugins/shared/workflows_management/server/connectors/workflows/index.test.ts index 05af0c9fbd9b2..dd2fe6ff0b4ce 100644 --- a/src/platform/plugins/shared/workflows_management/server/connectors/workflows/index.test.ts +++ b/src/platform/plugins/shared/workflows_management/server/connectors/workflows/index.test.ts @@ -16,6 +16,7 @@ import { getConnectorType, getWorkflowsConnectorAdapter, type GetWorkflowsConnectorTypeArgs, + resolveAlertStates, } from '.'; describe('Workflows Connector', () => { @@ -331,5 +332,179 @@ describe('Workflows Connector', () => { expect(result.subActionParams.workflowId).toBe('unknown'); expect(result.subActionParams.inputs).toBeUndefined(); }); + + it('should default alertStates to new=true, ongoing=false, recovered=false when not provided', () => { + const adapter = getWorkflowsConnectorAdapter(); + + const mockAlerts = { + all: { data: [{ _id: 'a1', _index: 'idx' }], count: 1 }, + new: { data: [{ _id: 'a1', _index: 'idx' }], count: 1 }, + ongoing: { data: [{ _id: 'a2', _index: 'idx' }], count: 1 }, + recovered: { data: [{ _id: 'a3', _index: 'idx' }], count: 1 }, + }; + + const mockRule = { + id: 'rule-id', + name: 'test rule', + tags: ['test-tag'], + consumer: 'test-consumer', + producer: 'test-producer', + ruleTypeId: 'test-rule-type', + }; + + const params = { + subAction: 'run' as const, + subActionParams: { workflowId: 'wf-1' }, + }; + + const result = adapter.buildActionParams({ + alerts: mockAlerts as any, + rule: mockRule, + params, + spaceId: 'default', + }); + + expect(result.subActionParams.alertStates).toEqual({ + new: true, + ongoing: false, + recovered: false, + }); + expect(result.subActionParams.inputs?.event?.alerts).toHaveLength(1); + expect(result.subActionParams.inputs?.event?.alerts[0]._id).toBe('a1'); + }); + + it('should include ongoing and recovered alerts when alertStates enables them', () => { + const adapter = getWorkflowsConnectorAdapter(); + + const mockAlerts = { + all: { data: [], count: 3 }, + new: { + data: [{ _id: 'a1', _index: 'idx' }], + count: 1, + alert_count: { active: 1, recovered: 0, ignored: 0 }, + }, + ongoing: { + data: [{ _id: 'a2', _index: 'idx' }], + count: 1, + alert_count: { active: 1, recovered: 0, ignored: 0 }, + }, + recovered: { + data: [{ _id: 'a3', _index: 'idx' }], + count: 1, + alert_count: { active: 0, recovered: 1, ignored: 0 }, + }, + }; + + const mockRule = { + id: 'rule-id', + name: 'test rule', + tags: ['test-tag'], + consumer: 'test-consumer', + producer: 'test-producer', + ruleTypeId: 'test-rule-type', + }; + + const params = { + subAction: 'run' as const, + subActionParams: { + workflowId: 'wf-1', + alertStates: { new: true, ongoing: true, recovered: true }, + }, + }; + + const result = adapter.buildActionParams({ + alerts: mockAlerts as any, + rule: mockRule, + params, + spaceId: 'default', + }); + + expect(result.subActionParams.alertStates).toEqual({ + new: true, + ongoing: true, + recovered: true, + }); + expect(result.subActionParams.inputs?.event?.alerts).toHaveLength(3); + }); + + it('should exclude new alerts when alertStates.new is false', () => { + const adapter = getWorkflowsConnectorAdapter(); + + const mockAlerts = { + all: { data: [], count: 2 }, + new: { + data: [{ _id: 'a1', _index: 'idx' }], + count: 1, + alert_count: { active: 1, recovered: 0, ignored: 0 }, + }, + ongoing: { + data: [], + count: 0, + alert_count: { active: 0, recovered: 0, ignored: 0 }, + }, + recovered: { + data: [{ _id: 'a2', _index: 'idx' }], + count: 1, + alert_count: { active: 0, recovered: 1, ignored: 0 }, + }, + }; + + const mockRule = { + id: 'rule-id', + name: 'test rule', + tags: ['test-tag'], + consumer: 'test-consumer', + producer: 'test-producer', + ruleTypeId: 'test-rule-type', + }; + + const params = { + subAction: 'run' as const, + subActionParams: { + workflowId: 'wf-1', + alertStates: { new: false, ongoing: false, recovered: true }, + }, + }; + + const result = adapter.buildActionParams({ + alerts: mockAlerts as any, + rule: mockRule, + params, + spaceId: 'default', + }); + + expect(result.subActionParams.inputs?.event?.alerts).toHaveLength(1); + expect(result.subActionParams.inputs?.event?.alerts[0]._id).toBe('a2'); + }); + }); + + describe('resolveAlertStates', () => { + it('should return defaults when no alertStates provided', () => { + expect(resolveAlertStates()).toEqual({ new: true, ongoing: false, recovered: false }); + }); + + it('should return defaults when undefined is passed', () => { + expect(resolveAlertStates(undefined)).toEqual({ + new: true, + ongoing: false, + recovered: false, + }); + }); + + it('should respect explicit values', () => { + expect(resolveAlertStates({ new: false, ongoing: true, recovered: true })).toEqual({ + new: false, + ongoing: true, + recovered: true, + }); + }); + + it('should fill in missing fields with defaults', () => { + expect(resolveAlertStates({ recovered: true })).toEqual({ + new: true, + ongoing: false, + recovered: true, + }); + }); }); }); diff --git a/src/platform/plugins/shared/workflows_management/server/connectors/workflows/index.ts b/src/platform/plugins/shared/workflows_management/server/connectors/workflows/index.ts index d55d9aaaaf614..dec090517b0d3 100644 --- a/src/platform/plugins/shared/workflows_management/server/connectors/workflows/index.ts +++ b/src/platform/plugins/shared/workflows_management/server/connectors/workflows/index.ts @@ -25,6 +25,7 @@ import { } from './service'; import * as i18n from './translations'; import type { + AlertStates, ExecutorParams, ExecutorSubActionRunParams, WorkflowsActionParamsType, @@ -41,6 +42,7 @@ export interface WorkflowsRuleActionParams { subActionParams: { workflowId: string; summaryMode?: boolean; + alertStates?: AlertStates; }; [key: string]: unknown; } @@ -155,6 +157,20 @@ export async function executor( return { status: 'ok', data, actionId }; } +const DEFAULT_ALERT_STATES: AlertStates = { + new: true, + ongoing: false, + recovered: false, +}; + +export function resolveAlertStates(alertStates?: Partial): AlertStates { + return { + new: alertStates?.new ?? DEFAULT_ALERT_STATES.new, + ongoing: alertStates?.ongoing ?? DEFAULT_ALERT_STATES.ongoing, + recovered: alertStates?.recovered ?? DEFAULT_ALERT_STATES.recovered, + }; +} + // Connector adapter for system action export function getWorkflowsConnectorAdapter(): ConnectorAdapter< WorkflowsRuleActionParams, @@ -177,9 +193,23 @@ export function getWorkflowsConnectorAdapter(): ConnectorAdapter< ); } - // Build alert event using shared utility function + const resolvedStates = resolveAlertStates(subActionParams.alertStates); + + const emptyAlertGroup = { + count: 0, + data: [], + alert_count: { active: 0, recovered: 0, ignored: 0 }, + }; + + const filteredAlerts = { + ...alerts, + new: resolvedStates.new ? alerts.new : emptyAlertGroup, + ongoing: resolvedStates.ongoing ? alerts.ongoing : emptyAlertGroup, + recovered: resolvedStates.recovered ? alerts.recovered : emptyAlertGroup, + }; + const alertEvent = buildAlertEvent({ - alerts, + alerts: filteredAlerts, rule, ruleUrl, spaceId, @@ -192,6 +222,7 @@ export function getWorkflowsConnectorAdapter(): ConnectorAdapter< inputs: { event: alertEvent }, spaceId, summaryMode, + alertStates: resolvedStates, }, }; } catch (error) { @@ -201,6 +232,7 @@ export function getWorkflowsConnectorAdapter(): ConnectorAdapter< workflowId: params?.subActionParams?.workflowId || 'unknown', spaceId, summaryMode: params?.subActionParams?.summaryMode ?? true, + alertStates: resolveAlertStates(params?.subActionParams?.alertStates), }, }; } diff --git a/src/platform/plugins/shared/workflows_management/server/connectors/workflows/schema.ts b/src/platform/plugins/shared/workflows_management/server/connectors/workflows/schema.ts index dbbfb68e0c74e..7ac63a1b9e8d6 100644 --- a/src/platform/plugins/shared/workflows_management/server/connectors/workflows/schema.ts +++ b/src/platform/plugins/shared/workflows_management/server/connectors/workflows/schema.ts @@ -10,11 +10,20 @@ import { schema } from '@kbn/config-schema'; import { z } from '@kbn/zod'; +const AlertStatesSchema = z + .object({ + new: z.boolean().optional(), + ongoing: z.boolean().optional(), + recovered: z.boolean().optional(), + }) + .strict(); + const RunSubActionParamsSchema = z.object({ workflowId: z.string(), inputs: z.any().optional(), spaceId: z.string(), summaryMode: z.boolean().optional().default(true), + alertStates: AlertStatesSchema.optional(), }); // Schema for rule configuration (what the UI saves) @@ -24,6 +33,13 @@ export const WorkflowsRuleActionParamsSchema = schema.object({ workflowId: schema.string(), inputs: schema.maybe(schema.any()), summaryMode: schema.maybe(schema.boolean()), + alertStates: schema.maybe( + schema.object({ + new: schema.maybe(schema.boolean()), + ongoing: schema.maybe(schema.boolean()), + recovered: schema.maybe(schema.boolean()), + }) + ), }), }); diff --git a/src/platform/plugins/shared/workflows_management/server/connectors/workflows/types.ts b/src/platform/plugins/shared/workflows_management/server/connectors/workflows/types.ts index d3bc4c88f8bf8..9fd8c6acb8f8f 100644 --- a/src/platform/plugins/shared/workflows_management/server/connectors/workflows/types.ts +++ b/src/platform/plugins/shared/workflows_management/server/connectors/workflows/types.ts @@ -16,10 +16,17 @@ import type { ExecutorParamsSchema } from './schema'; export type ExecutorParams = z.infer; export type WorkflowsActionParamsType = ExecutorParams; +export interface AlertStates { + new: boolean; + ongoing: boolean; + recovered: boolean; +} + export interface RunWorkflowParams { workflowId: string; spaceId: string; summaryMode?: boolean; + alertStates?: AlertStates; inputs: { event: { alerts: AlertHit[]; diff --git a/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap b/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap index f7eb91e4b81ba..03bd8ea7b036f 100644 --- a/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap +++ b/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap @@ -4937,6 +4937,21 @@ Object { "subActionParams": Object { "additionalProperties": false, "properties": Object { + "alertStates": Object { + "additionalProperties": false, + "properties": Object { + "new": Object { + "type": "boolean", + }, + "ongoing": Object { + "type": "boolean", + }, + "recovered": Object { + "type": "boolean", + }, + }, + "type": "object", + }, "inputs": Object {}, "spaceId": Object { "type": "string",