From 3cd6dea20857848f38bb70817748ae39ceaf7b04 Mon Sep 17 00:00:00 2001 From: Tal Borenstein Date: Thu, 5 Mar 2026 17:39:56 +0200 Subject: [PATCH 1/4] feat: support triggering workflows for recovered and ongoing alerts Add alertStates configuration to the workflow connector, allowing users to select which alert states (firing, ongoing, recovered) should trigger workflow execution. Defaults to firing-only for full backward compat with existing rules -- no migration needed. Closes elastic/security-team#16239 Made-with: Cursor --- .../common/utils/build_alert_event.test.ts | 94 +++++++++++ .../common/utils/build_alert_event.ts | 6 +- .../public/connectors/workflows/types.ts | 8 + .../workflows/workflows_params.test.tsx | 143 ++++++++++++++++- .../connectors/workflows/workflows_params.tsx | 112 ++++++++++++- .../server/connectors/workflows/index.test.ts | 147 ++++++++++++++++++ .../server/connectors/workflows/index.ts | 24 ++- .../server/connectors/workflows/schema.ts | 14 ++ .../server/connectors/workflows/types.ts | 7 + 9 files changed, 543 insertions(+), 12 deletions(-) create mode 100644 src/platform/plugins/shared/workflows_management/common/utils/build_alert_event.test.ts 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..1fb2396e86f02 --- /dev/null +++ b/src/platform/plugins/shared/workflows_management/common/utils/build_alert_event.test.ts @@ -0,0 +1,94 @@ +/* + * 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'; +import type { AlertEventRule } from '../types/alert_types'; + +const mockRule: AlertEventRule = { + id: 'rule-1', + name: 'Test Rule', + tags: ['test'], + consumer: 'alerts', + producer: 'infrastructure', + ruleTypeId: 'metrics.alert.threshold', +}; + +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 include only new alerts when ongoing and recovered are empty', () => { + const alerts = { + new: makeAlertGroup(['alert-new-1', 'alert-new-2']), + ongoing: makeAlertGroup([]), + recovered: makeAlertGroup([]), + all: makeAlertGroup(['alert-new-1', 'alert-new-2']), + } as unknown as CombinedSummarizedAlerts; + + const result = buildAlertEvent({ alerts, rule: mockRule, spaceId: 'default' }); + + expect(result.alerts).toHaveLength(2); + expect(result.alerts.map((a) => a._id)).toEqual(['alert-new-1', 'alert-new-2']); + }); + + it('should merge alerts from all three states', () => { + const alerts = { + new: makeAlertGroup(['alert-new-1']), + ongoing: makeAlertGroup(['alert-ongoing-1']), + recovered: makeAlertGroup(['alert-recovered-1']), + all: makeAlertGroup(['alert-new-1', 'alert-ongoing-1', 'alert-recovered-1']), + } 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([ + 'alert-new-1', + 'alert-ongoing-1', + 'alert-recovered-1', + ]); + }); + + it('should handle undefined state data gracefully', () => { + const alerts = { + new: { count: 0, data: undefined }, + ongoing: { count: 0, data: undefined }, + recovered: { count: 0, data: undefined }, + all: makeAlertGroup([]), + } as unknown as CombinedSummarizedAlerts; + + const result = buildAlertEvent({ alerts, rule: mockRule, spaceId: 'default' }); + + expect(result.alerts).toEqual([]); + }); + + it('should populate rule information correctly', () => { + const alerts = { + new: makeAlertGroup(['alert-1']), + ongoing: makeAlertGroup([]), + recovered: makeAlertGroup([]), + all: makeAlertGroup(['alert-1']), + } as unknown as CombinedSummarizedAlerts; + + const result = buildAlertEvent({ + alerts, + rule: mockRule, + ruleUrl: 'https://kibana/rule/1', + spaceId: 'my-space', + }); + + expect(result.rule).toEqual(mockRule); + expect(result.ruleUrl).toBe('https://kibana/rule/1'); + expect(result.spaceId).toBe('my-space'); + }); +}); 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..b34bfe2a4a191 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/public/connectors/workflows/types.ts b/src/platform/plugins/shared/workflows_management/public/connectors/workflows/types.ts index f82ed6940fdef..e226011ffcb9d 100644 --- a/src/platform/plugins/shared/workflows_management/public/connectors/workflows/types.ts +++ b/src/platform/plugins/shared/workflows_management/public/connectors/workflows/types.ts @@ -11,10 +11,18 @@ export type WorkflowsConfig = Record; export type WorkflowsSecrets = Record; +export interface AlertStates { + [key: string]: boolean | undefined; + new?: boolean; + ongoing?: boolean; + recovered?: boolean; +} + export interface WorkflowsActionParams { subAction: string; subActionParams: { workflowId: string; summaryMode?: boolean; + alertStates?: AlertStates; }; } diff --git a/src/platform/plugins/shared/workflows_management/public/connectors/workflows/workflows_params.test.tsx b/src/platform/plugins/shared/workflows_management/public/connectors/workflows/workflows_params.test.tsx index 7797d91ca38e5..f31a2c9303259 100644 --- a/src/platform/plugins/shared/workflows_management/public/connectors/workflows/workflows_params.test.tsx +++ b/src/platform/plugins/shared/workflows_management/public/connectors/workflows/workflows_params.test.tsx @@ -123,7 +123,11 @@ describe('WorkflowsParamsFields', () => { expect(mockEditAction).toHaveBeenCalledWith('subAction', 'run', 0); expect(mockEditAction).toHaveBeenCalledWith( 'subActionParams', - { workflowId: '', summaryMode: true }, + { + workflowId: '', + summaryMode: true, + alertStates: { new: true, ongoing: false, recovered: false }, + }, 0 ); }); @@ -919,7 +923,11 @@ describe('WorkflowsParamsFields', () => { await waitFor(() => { expect(mockEditAction).toHaveBeenCalledWith( 'subActionParams', - { workflowId: '', summaryMode: true }, + { + workflowId: '', + summaryMode: true, + alertStates: { new: true, ongoing: false, recovered: false }, + }, 0 ); }); @@ -1094,4 +1102,135 @@ describe('WorkflowsParamsFields', () => { ); }); }); + + describe('Alert states (alertStates parameter)', () => { + test('should render "Run workflow for" section with checkboxes', async () => { + await act(async () => { + renderWithIntl(); + }); + + await waitFor(() => { + expect(screen.getByText('Run workflow for')).toBeInTheDocument(); + expect(screen.getByText('Firing alerts')).toBeInTheDocument(); + expect(screen.getByText('Ongoing alerts')).toBeInTheDocument(); + expect(screen.getByText('Recovered alerts')).toBeInTheDocument(); + }); + }); + + test('should render checkboxes with correct defaults (only firing checked)', async () => { + const props = { + ...defaultProps, + actionParams: { + subAction: 'run', + subActionParams: { + workflowId: 'test-workflow', + summaryMode: true, + alertStates: { new: true, ongoing: false, recovered: false }, + }, + } as WorkflowsActionParams, + }; + + await act(async () => { + renderWithIntl(); + }); + + await waitFor(() => { + const checkboxes = screen.getByTestId('workflow-alert-states-checkboxes'); + expect(checkboxes).toBeInTheDocument(); + + const firingCheckbox = screen.getByLabelText('Firing alerts'); + const ongoingCheckbox = screen.getByLabelText('Ongoing alerts'); + const recoveredCheckbox = screen.getByLabelText('Recovered alerts'); + + expect(firingCheckbox).toBeChecked(); + expect(ongoingCheckbox).not.toBeChecked(); + expect(recoveredCheckbox).not.toBeChecked(); + }); + }); + + test('should toggle recovered checkbox and call editAction', async () => { + const props = { + ...defaultProps, + actionParams: { + subAction: 'run', + subActionParams: { + workflowId: 'test-workflow', + summaryMode: true, + alertStates: { new: true, ongoing: false, recovered: false }, + }, + } as WorkflowsActionParams, + }; + + await act(async () => { + renderWithIntl(); + }); + + const recoveredCheckbox = screen.getByLabelText('Recovered alerts'); + + await act(async () => { + fireEvent.click(recoveredCheckbox); + }); + + await waitFor(() => { + expect(mockEditAction).toHaveBeenCalledWith( + 'subActionParams', + expect.objectContaining({ + alertStates: { new: true, ongoing: false, recovered: true }, + }), + 0 + ); + }); + }); + + test('should initialize alertStates when missing from subActionParams', async () => { + const props = { + ...defaultProps, + actionParams: { + subAction: 'run', + subActionParams: { + workflowId: 'test-workflow', + summaryMode: true, + }, + } as WorkflowsActionParams, + }; + + await act(async () => { + renderWithIntl(); + }); + + await waitFor(() => { + expect(mockEditAction).toHaveBeenCalledWith( + 'subActionParams', + expect.objectContaining({ + alertStates: { new: true, ongoing: false, recovered: false }, + }), + 0 + ); + }); + }); + + test('should show all checkboxes as checked when all states are enabled', async () => { + const props = { + ...defaultProps, + actionParams: { + subAction: 'run', + subActionParams: { + workflowId: 'test-workflow', + summaryMode: true, + alertStates: { new: true, ongoing: true, recovered: true }, + }, + } as WorkflowsActionParams, + }; + + await act(async () => { + renderWithIntl(); + }); + + await waitFor(() => { + expect(screen.getByLabelText('Firing alerts')).toBeChecked(); + expect(screen.getByLabelText('Ongoing alerts')).toBeChecked(); + expect(screen.getByLabelText('Recovered alerts')).toBeChecked(); + }); + }); + }); }); diff --git a/src/platform/plugins/shared/workflows_management/public/connectors/workflows/workflows_params.tsx b/src/platform/plugins/shared/workflows_management/public/connectors/workflows/workflows_params.tsx index 11d96e52db60a..2874cc3797363 100644 --- a/src/platform/plugins/shared/workflows_management/public/connectors/workflows/workflows_params.tsx +++ b/src/platform/plugins/shared/workflows_management/public/connectors/workflows/workflows_params.tsx @@ -8,6 +8,8 @@ */ import { + EuiCheckboxGroup, + type EuiCheckboxGroupOption, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -19,7 +21,7 @@ import React, { useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public'; import { WorkflowSelectorWithProvider } from '@kbn/workflows-ui'; -import type { WorkflowsActionParams } from './types'; +import type { AlertStates, WorkflowsActionParams } from './types'; const RUN_PER_ALERT_LABEL = i18n.translate( 'xpack.stackConnectors.components.workflows.runPerAlert.label', @@ -41,7 +43,11 @@ const WorkflowsParamsFields: React.FunctionComponent { - const { workflowId, summaryMode = true } = actionParams.subActionParams ?? {}; + const { + workflowId, + summaryMode = true, + alertStates = { new: true, ongoing: false, recovered: false }, + } = actionParams.subActionParams ?? {}; const handleWorkflowChange = useCallback( (newWorkflowId: string) => { @@ -67,16 +73,54 @@ const WorkflowsParamsFields: React.FunctionComponent { + const newAlertStates: AlertStates = { + ...alertStates, + [optionId]: !alertStates[optionId as keyof AlertStates], + }; + editAction( + 'subActionParams', + { ...actionParams.subActionParams, alertStates: newAlertStates }, + index + ); + }, + [editAction, index, actionParams.subActionParams, alertStates] + ); + // Ensure proper initialization of action parameters useEffect(() => { if (!actionParams?.subAction) { editAction('subAction', 'run', index); } if (!actionParams?.subActionParams) { - editAction('subActionParams', { workflowId: '', summaryMode: true }, index); - } else if (actionParams.subActionParams.summaryMode === undefined) { - // Ensure summaryMode defaults to true for backward compatibility - editAction('subActionParams', { ...actionParams.subActionParams, summaryMode: true }, index); + editAction( + 'subActionParams', + { + workflowId: '', + summaryMode: true, + alertStates: { new: true, ongoing: false, recovered: false }, + }, + index + ); + } else { + if (actionParams.subActionParams.summaryMode === undefined) { + editAction( + 'subActionParams', + { ...actionParams.subActionParams, summaryMode: true }, + index + ); + } + if (!actionParams.subActionParams.alertStates) { + editAction( + 'subActionParams', + { + ...actionParams.subActionParams, + alertStates: { new: true, ongoing: false, recovered: false }, + }, + index + ); + } } }, [actionParams, editAction, index]); @@ -88,6 +132,27 @@ const WorkflowsParamsFields: React.FunctionComponent + + + {i18n.translate('xpack.stackConnectors.components.workflows.alertStates.label', { + defaultMessage: 'Run workflow for', + })} + + + + + + } + > + + + { expect(result.subActionParams.inputs?.event?.alerts).toHaveLength(1); }); + it('should default to only new alerts when alertStates is not provided', () => { + const adapter = getWorkflowsConnectorAdapter(); + + const mockAlerts = { + all: { data: [], count: 0 }, + new: { data: [{ _id: 'alert-new-1', _index: 'idx' }], count: 1 }, + ongoing: { data: [{ _id: 'alert-ongoing-1', _index: 'idx' }], count: 1 }, + recovered: { data: [{ _id: 'alert-recovered-1', _index: 'idx' }], count: 1 }, + }; + + const mockRule = { + id: 'rule-id', + name: 'test rule', + tags: [], + consumer: 'test-consumer', + producer: 'test-producer', + ruleTypeId: 'test-rule-type', + }; + + const result = adapter.buildActionParams({ + alerts: mockAlerts as any, + rule: mockRule, + params: { subAction: 'run' as const, subActionParams: { workflowId: 'wf-1' } }, + ruleUrl: 'https://example.com/rule', + spaceId: 'default', + }); + + const alertIds = result.subActionParams.inputs?.event?.alerts?.map((a: any) => a._id); + expect(alertIds).toEqual(['alert-new-1']); + expect(result.subActionParams.alertStates).toEqual({ + new: true, + ongoing: false, + recovered: false, + }); + }); + + it('should include recovered alerts when alertStates.recovered is true', () => { + const adapter = getWorkflowsConnectorAdapter(); + + const mockAlerts = { + all: { data: [], count: 0 }, + new: { data: [{ _id: 'alert-new-1', _index: 'idx' }], count: 1 }, + ongoing: { data: [], count: 0 }, + recovered: { data: [{ _id: 'alert-recovered-1', _index: 'idx' }], count: 1 }, + }; + + const mockRule = { + id: 'rule-id', + name: 'test rule', + tags: [], + consumer: 'test-consumer', + producer: 'test-producer', + ruleTypeId: 'test-rule-type', + }; + + const result = adapter.buildActionParams({ + alerts: mockAlerts as any, + rule: mockRule, + params: { + subAction: 'run' as const, + subActionParams: { + workflowId: 'wf-1', + alertStates: { new: true, ongoing: false, recovered: true }, + }, + }, + ruleUrl: 'https://example.com/rule', + spaceId: 'default', + }); + + const alertIds = result.subActionParams.inputs?.event?.alerts?.map((a: any) => a._id); + expect(alertIds).toEqual(['alert-new-1', 'alert-recovered-1']); + }); + + it('should include only recovered alerts when only recovered is selected', () => { + const adapter = getWorkflowsConnectorAdapter(); + + const mockAlerts = { + all: { data: [], count: 0 }, + new: { data: [{ _id: 'alert-new-1', _index: 'idx' }], count: 1 }, + ongoing: { data: [{ _id: 'alert-ongoing-1', _index: 'idx' }], count: 1 }, + recovered: { data: [{ _id: 'alert-recovered-1', _index: 'idx' }], count: 1 }, + }; + + const mockRule = { + id: 'rule-id', + name: 'test rule', + tags: [], + consumer: 'test-consumer', + producer: 'test-producer', + ruleTypeId: 'test-rule-type', + }; + + const result = adapter.buildActionParams({ + alerts: mockAlerts as any, + rule: mockRule, + params: { + subAction: 'run' as const, + subActionParams: { + workflowId: 'wf-1', + alertStates: { new: false, ongoing: false, recovered: true }, + }, + }, + ruleUrl: 'https://example.com/rule', + spaceId: 'default', + }); + + const alertIds = result.subActionParams.inputs?.event?.alerts?.map((a: any) => a._id); + expect(alertIds).toEqual(['alert-recovered-1']); + }); + + it('should include all alert states when all are enabled', () => { + const adapter = getWorkflowsConnectorAdapter(); + + const mockAlerts = { + all: { data: [], count: 0 }, + new: { data: [{ _id: 'new-1', _index: 'idx' }], count: 1 }, + ongoing: { data: [{ _id: 'ongoing-1', _index: 'idx' }], count: 1 }, + recovered: { data: [{ _id: 'recovered-1', _index: 'idx' }], count: 1 }, + }; + + const mockRule = { + id: 'rule-id', + name: 'test rule', + tags: [], + consumer: 'test-consumer', + producer: 'test-producer', + ruleTypeId: 'test-rule-type', + }; + + const result = adapter.buildActionParams({ + alerts: mockAlerts as any, + rule: mockRule, + params: { + subAction: 'run' as const, + subActionParams: { + workflowId: 'wf-1', + alertStates: { new: true, ongoing: true, recovered: true }, + }, + }, + ruleUrl: 'https://example.com/rule', + spaceId: 'default', + }); + + const alertIds = result.subActionParams.inputs?.event?.alerts?.map((a: any) => a._id); + expect(alertIds).toEqual(['new-1', 'ongoing-1', 'recovered-1']); + }); + it('should handle missing workflowId gracefully', () => { const adapter = getWorkflowsConnectorAdapter(); 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..4a35b19ff0db2 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; } @@ -170,16 +172,30 @@ export function getWorkflowsConnectorAdapter(): ConnectorAdapter< throw new Error(`Missing subActionParams. Received: ${JSON.stringify(params)}`); } - const { workflowId, summaryMode = true } = subActionParams; + const { + workflowId, + summaryMode = true, + alertStates = { new: true, ongoing: false, recovered: false }, + } = subActionParams; if (!workflowId) { throw new Error( `Missing required workflowId parameter. Received params: ${JSON.stringify(params)}` ); } - // Build alert event using shared utility function + const emptyAlertGroup = { + count: 0, + data: [], + alert_count: { active: 0, recovered: 0, ignored: 0 }, + }; + const filteredAlerts = { + new: alertStates.new !== false ? alerts.new : emptyAlertGroup, + ongoing: alertStates.ongoing === true ? alerts.ongoing : emptyAlertGroup, + recovered: alertStates.recovered === true ? alerts.recovered : emptyAlertGroup, + }; + const alertEvent = buildAlertEvent({ - alerts, + alerts: filteredAlerts, rule, ruleUrl, spaceId, @@ -192,6 +208,7 @@ export function getWorkflowsConnectorAdapter(): ConnectorAdapter< inputs: { event: alertEvent }, spaceId, summaryMode, + alertStates, }, }; } catch (error) { @@ -201,6 +218,7 @@ export function getWorkflowsConnectorAdapter(): ConnectorAdapter< workflowId: params?.subActionParams?.workflowId || 'unknown', spaceId, summaryMode: params?.subActionParams?.summaryMode ?? true, + alertStates: 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..0af5b32c9cbfe 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,18 @@ import { schema } from '@kbn/config-schema'; import { z } from '@kbn/zod'; +const AlertStatesSchema = z.object({ + new: z.boolean().optional().default(true), + ongoing: z.boolean().optional().default(false), + recovered: z.boolean().optional().default(false), +}); + 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 +31,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..606fc75a91331 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[]; From c37ebb253290e429a39e4c21e7706c8a6a8ccc63 Mon Sep 17 00:00:00 2001 From: Tal Borenstein Date: Sun, 8 Mar 2026 14:47:18 +0200 Subject: [PATCH 2/4] fix: update connector type snapshot and fix type errors - Spread original alerts into filteredAlerts to preserve `all` property - Resolve alertStates to concrete booleans to match Zod-inferred types - Update connector_types.test.ts.snap for new alertStates schema Made-with: Cursor --- .../server/connectors/workflows/index.ts | 20 ++++++++++--------- .../connector_types.test.ts.snap | 18 +++++++++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) 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 4a35b19ff0db2..d6465271a4848 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 @@ -172,11 +172,12 @@ export function getWorkflowsConnectorAdapter(): ConnectorAdapter< throw new Error(`Missing subActionParams. Received: ${JSON.stringify(params)}`); } - const { - workflowId, - summaryMode = true, - alertStates = { new: true, ongoing: false, recovered: false }, - } = subActionParams; + const { workflowId, summaryMode = true } = subActionParams; + const resolvedAlertStates = { + new: subActionParams.alertStates?.new !== false, + ongoing: subActionParams.alertStates?.ongoing === true, + recovered: subActionParams.alertStates?.recovered === true, + }; if (!workflowId) { throw new Error( `Missing required workflowId parameter. Received params: ${JSON.stringify(params)}` @@ -189,9 +190,10 @@ export function getWorkflowsConnectorAdapter(): ConnectorAdapter< alert_count: { active: 0, recovered: 0, ignored: 0 }, }; const filteredAlerts = { - new: alertStates.new !== false ? alerts.new : emptyAlertGroup, - ongoing: alertStates.ongoing === true ? alerts.ongoing : emptyAlertGroup, - recovered: alertStates.recovered === true ? alerts.recovered : emptyAlertGroup, + ...alerts, + new: resolvedAlertStates.new ? alerts.new : emptyAlertGroup, + ongoing: resolvedAlertStates.ongoing ? alerts.ongoing : emptyAlertGroup, + recovered: resolvedAlertStates.recovered ? alerts.recovered : emptyAlertGroup, }; const alertEvent = buildAlertEvent({ @@ -208,7 +210,7 @@ export function getWorkflowsConnectorAdapter(): ConnectorAdapter< inputs: { event: alertEvent }, spaceId, summaryMode, - alertStates, + alertStates: resolvedAlertStates, }, }; } catch (error) { 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 7d45878d9ebac..eea2dcc47c7d8 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 @@ -4929,6 +4929,24 @@ Object { "subActionParams": Object { "additionalProperties": false, "properties": Object { + "alertStates": Object { + "additionalProperties": false, + "properties": Object { + "new": Object { + "default": true, + "type": "boolean", + }, + "ongoing": Object { + "default": false, + "type": "boolean", + }, + "recovered": Object { + "default": false, + "type": "boolean", + }, + }, + "type": "object", + }, "inputs": Object {}, "spaceId": Object { "type": "string", From 44f8bcbce99a2e5565a98a556eeda206e0e23988 Mon Sep 17 00:00:00 2001 From: Tal Borenstein Date: Mon, 9 Mar 2026 12:14:40 +0200 Subject: [PATCH 3/4] fix: resolve alertStates to concrete booleans in error fallback path Made-with: Cursor --- .../server/connectors/workflows/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 d6465271a4848..b5d3ca39ac012 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 @@ -220,7 +220,13 @@ export function getWorkflowsConnectorAdapter(): ConnectorAdapter< workflowId: params?.subActionParams?.workflowId || 'unknown', spaceId, summaryMode: params?.subActionParams?.summaryMode ?? true, - alertStates: params?.subActionParams?.alertStates, + alertStates: params?.subActionParams?.alertStates + ? { + new: params.subActionParams.alertStates.new !== false, + ongoing: params.subActionParams.alertStates.ongoing === true, + recovered: params.subActionParams.alertStates.recovered === true, + } + : undefined, }, }; } From 13e1cb640bc2e8aff772cdd6f9a27c572cbaf28e Mon Sep 17 00:00:00 2001 From: Tal Borenstein Date: Mon, 9 Mar 2026 12:30:11 +0200 Subject: [PATCH 4/4] fix: always return alertStates in error fallback path Made-with: Cursor --- .../server/connectors/workflows/index.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) 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 b5d3ca39ac012..2916b80a9ccc7 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 @@ -214,19 +214,18 @@ export function getWorkflowsConnectorAdapter(): ConnectorAdapter< }, }; } catch (error) { + const fallbackStates = params?.subActionParams?.alertStates; return { subAction: 'run' as const, subActionParams: { workflowId: params?.subActionParams?.workflowId || 'unknown', spaceId, summaryMode: params?.subActionParams?.summaryMode ?? true, - alertStates: params?.subActionParams?.alertStates - ? { - new: params.subActionParams.alertStates.new !== false, - ongoing: params.subActionParams.alertStates.ongoing === true, - recovered: params.subActionParams.alertStates.recovered === true, - } - : undefined, + alertStates: { + new: fallbackStates?.new !== false, + ongoing: fallbackStates?.ongoing === true, + recovered: fallbackStates?.recovered === true, + }, }, }; }