From 4a7240c5867d5c36afe1ac306de56ef8c9eebde9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopycin=CC=81ski?= Date: Fri, 11 Sep 2026 21:05:25 +0200 Subject: [PATCH 01/13] [AlertZero] Action catalog: API and Agent Builder tool for category-scoped discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements elastic/security-team#19288. - GET /internal/alertzero/actions — lists installed action workflows (tag: action) with optional categories filter; category is a solution-owned keyword, never validated against an enum - ActionsService: tag-driven discovery, consts.actionMetadata projection, skips invalid metadata with a warning - security.alertzero.actions.listByCategory builtin Agent Builder tool wrapping the same service (tool and API cannot drift) - floor_alert_triage worker: configuration_overrides.tools wiring + managed version bump (fingerprint guard updated) Unit-tested at every layer: service, route param parsing, route handler, tool handler. --- .../alertzero/floor_alert_triage.ts | 2 +- .../alertzero/floor_alert_triage.yaml | 3 + .../managed_workflow_definitions.test.ts | 2 +- .../agent-builder-server/allow_lists.ts | 1 + .../action_catalog_types.ts | 44 ++++ .../kbn-alertzero-common/constants.ts | 7 + .../packages/kbn-alertzero-common/index.ts | 9 + .../list_actions_by_category_tool.test.ts | 86 ++++++++ .../list_actions_by_category_tool.ts | 95 +++++++++ .../plugins/alertzero/server/plugin.test.ts | 3 +- .../plugins/alertzero/server/plugin.ts | 22 ++ .../server/routes/actions/constants.ts | 24 +++ .../routes/actions/list_actions.test.ts | 91 ++++++++ .../server/routes/actions/list_actions.ts | 70 ++++++ .../read_categories_query_param.test.ts | 79 +++++++ .../actions/read_categories_query_param.ts | 53 +++++ .../server/routes/register_routes.ts | 4 + .../services/actions/actions_service.test.ts | 199 ++++++++++++++++++ .../services/actions/actions_service.ts | 125 +++++++++++ 19 files changed, 916 insertions(+), 3 deletions(-) create mode 100644 x-pack/solutions/security/packages/kbn-alertzero-common/action_catalog_types.ts create mode 100644 x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts create mode 100644 x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts create mode 100644 x-pack/solutions/security/plugins/alertzero/server/routes/actions/constants.ts create mode 100644 x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.test.ts create mode 100644 x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.ts create mode 100644 x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.test.ts create mode 100644 x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.ts create mode 100644 x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.test.ts create mode 100644 x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.ts diff --git a/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.ts b/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.ts index 6bb8b10a9b63b..2b9e2bfbbe4d7 100644 --- a/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.ts +++ b/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.ts @@ -19,7 +19,7 @@ export const ALERTZERO_WORKER_FLOOR_ALERT_TRIAGE_WORKFLOW = { id: ALERTZERO_WORKER_FLOOR_ALERT_TRIAGE_WORKFLOW_ID, management: ALERTZERO_WORKER_MANAGEMENT, pluginId: ALERTZERO_MANAGED_WORKFLOW_PLUGIN_ID, - version: 1, + version: 2, yamlTemplate: (values: CommonWorkerTemplateValues): string => renderCommonWorkerYaml(FLOOR_ALERT_TRIAGE_YAML, values), } as const satisfies ManagedWorkflowDefinition; diff --git a/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.yaml b/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.yaml index f8adc6c36f8b4..7cd10a5730e41 100644 --- a/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.yaml +++ b/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.yaml @@ -21,6 +21,9 @@ steps: configuration_overrides: skill_ids: - alert-analysis + tools: + - tool_ids: + - security.alertzero.actions.list_by_category message: > Triage the security alert context below. Prefer recall over precision when unsure. Return a structured diff --git a/src/platform/packages/shared/kbn-workflows/managed/managed_workflow_definitions.test.ts b/src/platform/packages/shared/kbn-workflows/managed/managed_workflow_definitions.test.ts index ac7caa6b54a7c..8eb33a294bb37 100644 --- a/src/platform/packages/shared/kbn-workflows/managed/managed_workflow_definitions.test.ts +++ b/src/platform/packages/shared/kbn-workflows/managed/managed_workflow_definitions.test.ts @@ -146,7 +146,7 @@ function createContentFingerprint(content: string): string { } it.each([ - [ALERTZERO_WORKER_FLOOR_ALERT_TRIAGE_WORKFLOW_ID, FLOOR_ALERT_TRIAGE_YAML, '1:d6a82eff'], + [ALERTZERO_WORKER_FLOOR_ALERT_TRIAGE_WORKFLOW_ID, FLOOR_ALERT_TRIAGE_YAML, '2:faf316b1'], [ALERTZERO_WORKER_FLOOR_ATTACK_DISCOVERY_WORKFLOW_ID, FLOOR_ATTACK_DISCOVERY_YAML, '2:d13818a0'], [ ALERTZERO_WORKER_DARK_CONTINUOUS_THREAT_HUNT_WORKFLOW_ID, diff --git a/x-pack/platform/packages/shared/agent-builder/agent-builder-server/allow_lists.ts b/x-pack/platform/packages/shared/agent-builder/agent-builder-server/allow_lists.ts index 5ba28f67fe07e..4df6cb5394795 100644 --- a/x-pack/platform/packages/shared/agent-builder/agent-builder-server/allow_lists.ts +++ b/x-pack/platform/packages/shared/agent-builder/agent-builder-server/allow_lists.ts @@ -56,6 +56,7 @@ export const AGENT_BUILDER_BUILTIN_TOOLS = [ `${internalNamespaces.ml}.query_anomalies`, // Security Solution + `${internalNamespaces.security}.alertzero.actions.list_by_category`, `${internalNamespaces.security}.entity_risk_score`, `${internalNamespaces.security}.create_detection_rule`, `${internalNamespaces.security}.run_rule_preview`, diff --git a/x-pack/solutions/security/packages/kbn-alertzero-common/action_catalog_types.ts b/x-pack/solutions/security/packages/kbn-alertzero-common/action_catalog_types.ts new file mode 100644 index 0000000000000..2709b218f26cf --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-alertzero-common/action_catalog_types.ts @@ -0,0 +1,44 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * 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"; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ActionApprovalPolicy, ActionCategory, ActionImpact } from '@kbn/workflows'; + +/** + * One entry of the action catalog: the lightweight, agent-facing projection of + * an installed action workflow. Mirrors `consts.actionMetadata` on the + * workflow definition plus the workflow id, so an agent can propose the action + * without reading the full YAML. + */ +export interface ActionCatalogEntry { + workflowId: string; + name: string; + description?: string; + category?: ActionCategory; + impact?: ActionImpact; + approvalPolicy?: ActionApprovalPolicy; +} + +/** Response of `GET /internal/alertzero/actions`. */ +export interface ListActionsResponse { + actions: ActionCatalogEntry[]; + total: number; +} diff --git a/x-pack/solutions/security/packages/kbn-alertzero-common/constants.ts b/x-pack/solutions/security/packages/kbn-alertzero-common/constants.ts index b0e4babcedb71..f224517f4227f 100644 --- a/x-pack/solutions/security/packages/kbn-alertzero-common/constants.ts +++ b/x-pack/solutions/security/packages/kbn-alertzero-common/constants.ts @@ -44,6 +44,13 @@ export const buildInvestigationUrl = (id: string) => /** Proposals grouped by category — AlertZero landing page. */ export const ALERTZERO_PROPOSALS_URL = `${ALERTZERO_INTERNAL_URL}/proposals` as const; +/** Action catalog — category-scoped discovery of installed action workflows. */ +export const ALERTZERO_ACTIONS_URL = `${ALERTZERO_INTERNAL_URL}/actions` as const; + +/** Agent Builder builtin tool wrapping the action catalog API. */ +export const ALERTZERO_ACTIONS_LIST_BY_CATEGORY_TOOL_ID = + 'security.alertzero.actions.list_by_category' as const; + /** * Shared thin AlertZero agent for all Worker `ai.agent` steps. * Can expand this to multiple scoped thin agents in the future if needed. diff --git a/x-pack/solutions/security/packages/kbn-alertzero-common/index.ts b/x-pack/solutions/security/packages/kbn-alertzero-common/index.ts index 4a460499e7c87..ee7bc18104b70 100644 --- a/x-pack/solutions/security/packages/kbn-alertzero-common/index.ts +++ b/x-pack/solutions/security/packages/kbn-alertzero-common/index.ts @@ -24,6 +24,8 @@ export { ALERTZERO_INVESTIGATIONS_URL, ALERTZERO_INVESTIGATION_URL_TEMPLATE, ALERTZERO_PLUGIN_NAME, + ALERTZERO_ACTIONS_URL, + ALERTZERO_ACTIONS_LIST_BY_CATEGORY_TOOL_ID, ALERTZERO_PROPOSALS_URL, ALERTZERO_THIN_AGENT_ID, ALERTZERO_SKILLS_URL, @@ -64,6 +66,13 @@ export { buildWorkerUrl, } from './constants'; +export type { + ActionApprovalPolicy, + ActionCategory, + ActionCatalogEntry, + ActionImpact, + ListActionsResponse, +} from './action_catalog_types'; export { CONVERSATION_QUEUE_CATEGORIES, CONVERSATION_QUEUE_LABELS } from './translations'; export { diff --git a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts new file mode 100644 index 0000000000000..985b613c7fad0 --- /dev/null +++ b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts @@ -0,0 +1,86 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * 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"; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { listActionsByCategoryTool } from './list_actions_by_category_tool'; +import type { ActionsService } from '../services/actions/actions_service'; +import { ToolResultType } from '@kbn/agent-builder-common/tools/tool_result'; + +const logger = () => ({ error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn() }); + +const serviceWith = (list: jest.Mock) => ({ list } as unknown as ActionsService); + +const run = async (service: ActionsService, input: { categories?: string[] } = {}) => { + const tool = listActionsByCategoryTool(() => service); + const result = await tool.handler(input, { logger: logger() } as never); + if (!('results' in result)) { + throw new Error('expected a standard tool result'); + } + return result; +}; + +const ACTION = (over: Partial> = {}) => ({ + workflowId: 'system-alertzero-action-create-rule', + name: 'Create detection rule', + category: 'tune', + ...over, +}); + +describe('listActionsByCategoryTool', () => { + it('lists all actions when called without categories', async () => { + const list = jest.fn().mockResolvedValue({ + actions: [ACTION(), ACTION({ workflowId: 'a2', name: 'Isolate host', category: 'contain' })], + total: 2, + }); + const result = await run(serviceWith(list)); + expect(list).toHaveBeenCalledWith('default', undefined); + expect(result.results[0].type).toBe(ToolResultType.other); + expect(result.results[0].data).toMatchObject({ total: 2 }); + }); + + it('forwards categories to the service and reports empty results explicitly', async () => { + const list = jest.fn().mockResolvedValue({ actions: [], total: 0 }); + const result = await run(serviceWith(list), { categories: ['escalate'] }); + expect(list).toHaveBeenCalledWith('default', ['escalate']); + expect(result.results[0].data).toMatchObject({ + total: 0, + message: 'No actions found in categories: escalate.', + }); + }); + + it('returns an error result instead of throwing when the service fails', async () => { + const list = jest.fn().mockRejectedValue(new Error('workflows management down')); + const result = await run(serviceWith(list)); + expect(result.results[0].type).not.toBe(ToolResultType.other); + expect(JSON.stringify(result.results[0])).toContain('workflows management down'); + }); + + it('declares the documented tool id and read-only annotations', () => { + const tool = listActionsByCategoryTool(() => serviceWith(jest.fn())); + expect(tool.id).toBe('security.alertzero.actions.list_by_category'); + expect(tool.annotations).toMatchObject({ + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + }); + expect(tool.type).toBe('builtin'); + }); +}); diff --git a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts new file mode 100644 index 0000000000000..a8ec1aad69b1b --- /dev/null +++ b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts @@ -0,0 +1,95 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * 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"; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '@kbn/zod/v4'; +import { createErrorResult } from '@kbn/agent-builder-server'; +import { ToolResultType } from '@kbn/agent-builder-common/tools/tool_result'; +import type { BuiltinToolDefinition } from '@kbn/agent-builder-server/tools'; +import { ToolType } from '@kbn/agent-builder-common'; +import { actionCategorySchema } from '@kbn/workflows/managed'; +import { ALERTZERO_ACTIONS_LIST_BY_CATEGORY_TOOL_ID } from '@kbn/alertzero-common'; +import type { ActionsService } from '../services/actions/actions_service'; + +const listByCategorySchema = z.object({ + categories: z + .array(actionCategorySchema) + .max(20) + .optional() + .describe( + 'Category keywords to filter on (e.g. ["contain", "escalate"]). An action is returned when its declared category matches ANY of these. Omit to list every available action.' + ), +}); + +/** + * `security.alertzero.actions.listByCategory` — lets an agent discover the + * installed action workflows at runtime instead of hard-coding workflow ids. + * + * Registered by the AlertZero plugin (setup), reads the catalog through + * {@link ActionsService} — the same service backing the HTTP API — so the tool + * and the API can never drift. + */ +export const listActionsByCategoryTool = ( + getActionsService: () => ActionsService +): BuiltinToolDefinition => ({ + id: ALERTZERO_ACTIONS_LIST_BY_CATEGORY_TOOL_ID, + type: ToolType.builtin, + description: + 'List available AlertZero actions, optionally filtered by category. Each result includes the workflowId to reference when proposing the action, plus its name, description, category, impact (low/medium/high/critical) and approvalPolicy (always-gate/autonomy-dependent). Call this before proposing an action so the proposal references a real, installed workflow.', + annotations: { + title: 'List AlertZero Actions', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + schema: listByCategorySchema, + tags: ['alertzero'], + handler: async ({ categories }, { logger }) => { + try { + const { actions, total } = await getActionsService().list('default', categories); + const message = + total === 0 + ? categories + ? `No actions found in categories: ${categories.join(', ')}.` + : 'No actions are installed.' + : undefined; + return { + results: [ + { + type: ToolResultType.other, + data: { + total, + actions, + ...(message && { message }), + }, + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`[List Actions Tool] Error listing actions: ${errorMessage}`); + return { + results: [createErrorResult(`Error listing actions: ${errorMessage}`)], + }; + } + }, +}); diff --git a/x-pack/solutions/security/plugins/alertzero/server/plugin.test.ts b/x-pack/solutions/security/plugins/alertzero/server/plugin.test.ts index 11936cc4c5b38..58c987d435ee2 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/plugin.test.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/plugin.test.ts @@ -103,6 +103,7 @@ describe('AlertZeroPlugin feature-flag gating', () => { features, workflowsExtensions, workflowsManagement: { management: {} }, + agentBuilder: { tools: { register: jest.fn() } }, } as never ); @@ -130,7 +131,7 @@ describe('AlertZeroPlugin feature-flag gating', () => { const coreSetup = coreMock.createSetup(); const features = { registerKibanaFeature: jest.fn() }; const workflowsExtensions = { registerManagedWorkflowOwner: jest.fn() }; - const agentBuilder = { agents: { registerType: jest.fn() } }; + const agentBuilder = { agents: { registerType: jest.fn() }, tools: { register: jest.fn() } }; plugin.setup( coreSetup as never, diff --git a/x-pack/solutions/security/plugins/alertzero/server/plugin.ts b/x-pack/solutions/security/plugins/alertzero/server/plugin.ts index a9ebb625d6f12..02c8566d182ea 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/plugin.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/plugin.ts @@ -36,6 +36,8 @@ import { WatchesService } from './services/watches/watches_service'; import { WorkersService } from './services/workers/workers_service'; import { ConversationProposalsService } from './services/conversation_proposals/conversation_proposals_service'; import { WatchWorkflowsManagementClientImpl } from './services/watches/watch_workflows_management_client'; +import { ActionsService } from './services/actions/actions_service'; +import { listActionsByCategoryTool } from './agent_builder_tools/list_actions_by_category_tool'; import { agentType, ensureAgent, ensureAgentSafe, registerAgentType } from './agent'; export class AlertZeroPlugin @@ -54,6 +56,7 @@ export class AlertZeroPlugin /** Created during `start`; routes resolve them lazily after managed-workflow initialization. */ private watchesService?: WatchesService; + private actionsService?: ActionsService; private workersService?: WorkersService; private conversationProposalsService?: ConversationProposalsService; @@ -83,6 +86,11 @@ export class AlertZeroPlugin registerOwner({ workflowsExtensions }); registerAgentType(agentBuilder); + // Registered in setup so the builtin tool is available to Agent Builder before + // the first agent run; the handler resolves the service lazily like the routes do. + agentBuilder.tools.register({ + ...listActionsByCategoryTool(() => this.requireActionsService()), + }); features.registerKibanaFeature({ id: ALERTZERO_FEATURE_ID, @@ -116,6 +124,7 @@ export class AlertZeroPlugin getWatchesService: () => this.requireWatchesService(), getWorkersService: () => this.requireWorkersService(), getConversationProposalsService: () => this.requireConversationProposalsService(), + getActionsService: () => this.requireActionsService(), }); return {}; @@ -160,6 +169,13 @@ export class AlertZeroPlugin // Mock mode changes presentation data only; durable Worker settings and enablement still use Workflows. this.watchesService = new WatchesService(); + this.actionsService = new ActionsService( + () => + this.workflowsManagementApi + ? new WatchWorkflowsManagementClientImpl(this.workflowsManagementApi) + : undefined, + this.logger + ); this.workersService = new WorkersService(management, managedWorkflows, this.logger, { ensureAgentForSpace: plugins.agentBuilder ? (spaceId) => @@ -179,6 +195,12 @@ export class AlertZeroPlugin return this.watchesService; } + private requireActionsService(): ActionsService { + if (!this.actionsService) { + throw new Error('Actions service is not available until the AlertZero plugin has started'); + } + return this.actionsService; + } private requireWorkersService(): WorkersService { if (!this.workersService) { throw new Error('Workers service is not available until the AlertZero plugin has started'); diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/constants.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/constants.ts new file mode 100644 index 0000000000000..2181e47f9f8de --- /dev/null +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/constants.ts @@ -0,0 +1,24 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * 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"; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Max number of values accepted in the `categories` query param (each also length-capped upstream by actionCategorySchema). */ +export const ACTION_CATEGORIES_QUERY_PARAM_MAX_ITEMS = 20; diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.test.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.test.ts new file mode 100644 index 0000000000000..ec512ddc1e7ca --- /dev/null +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.test.ts @@ -0,0 +1,91 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * 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"; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { httpServerMock } from '@kbn/core-http-server-mocks'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import type { RouteDependencies } from '../register_routes'; +import { registerListActionsRoute } from './list_actions'; + +const makeDeps = (actionsService: unknown) => { + const addVersion = jest.fn(); + const router = { + versioned: { + get: jest.fn().mockReturnValue({ addVersion }), + }, + }; + registerListActionsRoute({ + router: router as unknown as RouteDependencies['router'], + logger: loggingSystemMock.createLogger(), + getSpaceId: () => 'default', + getActionsService: () => actionsService, + } as unknown as RouteDependencies); + const handler = addVersion.mock.calls[0][1] as ( + context: unknown, + request: ReturnType, + response: ReturnType + ) => Promise; + return { handler }; +}; + +const requestWithCategories = (categories?: string[]) => + httpServerMock.createKibanaRequest({ + path: '/internal/alertzero/actions', + query: categories ? { categories } : undefined, + }); + +describe('registerListActionsRoute', () => { + it('passes categories through to the service', async () => { + const list = jest.fn().mockResolvedValue({ actions: [], total: 0 }); + const { handler } = makeDeps({ list }); + const response = httpServerMock.createResponseFactory(); + await handler( + {}, + httpServerMock.createKibanaRequest({ + path: '/internal/alertzero/actions', + // simulate the router-parsed multi-valued query param + query: { categories: ['contain', 'escalate'] }, + }), + response + ); + expect(list).toHaveBeenCalledWith('default', ['contain', 'escalate']); + expect(response.ok).toHaveBeenCalled(); + }); + + it('returns the full catalog when no categories are given', async () => { + const list = jest.fn().mockResolvedValue({ actions: [], total: 0 }); + const { handler } = makeDeps({ list }); + const response = httpServerMock.createResponseFactory(); + await handler({}, requestWithCategories(), response); + expect(list).toHaveBeenCalledWith('default', undefined); + }); + + it('maps service errors to 500', async () => { + const list = jest.fn().mockRejectedValue(new Error('boom')); + const { handler } = makeDeps({ list }); + const response = httpServerMock.createResponseFactory(); + await handler({}, requestWithCategories(), response); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 500, + body: { message: 'Failed to list actions' }, + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.ts new file mode 100644 index 0000000000000..64133f3b0d720 --- /dev/null +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.ts @@ -0,0 +1,70 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * 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"; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { API_VERSIONS, INTERNAL_API_ACCESS, ALERTZERO_ACTIONS_URL } from '@kbn/alertzero-common'; +import type { ListActionsResponse } from '@kbn/alertzero-common'; +import { ALERTZERO_API_PRIVILEGE_READ } from '../../../common/constants'; +import type { RouteDependencies } from '../register_routes'; +import { readActionCategoriesQueryParam } from './read_categories_query_param'; + +export const registerListActionsRoute = ({ + router, + logger, + getSpaceId, + getActionsService, +}: RouteDependencies) => { + router.versioned + .get({ + path: ALERTZERO_ACTIONS_URL, + access: INTERNAL_API_ACCESS, + security: { + authz: { + requiredPrivileges: [ALERTZERO_API_PRIVILEGE_READ], + }, + }, + summary: 'List AlertZero action workflows, optionally filtered by category', + }) + .addVersion( + { + version: API_VERSIONS.internal.v1, + validate: { + request: {}, + }, + }, + async (_context, request, response) => { + try { + const categories = readActionCategoriesQueryParam(request); + const body: ListActionsResponse = await getActionsService().list( + getSpaceId(request), + categories + ); + return response.ok({ body }); + } catch (error) { + logger.error(`Failed to list actions: ${error}`); + return response.customError({ + statusCode: 500, + body: { message: 'Failed to list actions' }, + }); + } + } + ); +}; diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.test.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.test.ts new file mode 100644 index 0000000000000..cfe5228e2b866 --- /dev/null +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.test.ts @@ -0,0 +1,79 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * 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"; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { httpServerMock } from '@kbn/core-http-server-mocks'; +import { readActionCategoriesQueryParam } from './read_categories_query_param'; +import { ACTION_CATEGORIES_QUERY_PARAM_MAX_ITEMS } from './constants'; + +const requestWithQuery = (query: Record | undefined) => + httpServerMock.createKibanaRequest({ path: '/internal/alertzero/actions', query }); + +describe('readActionCategoriesQueryParam', () => { + it('returns undefined when the param is absent', () => { + expect(readActionCategoriesQueryParam(requestWithQuery(undefined))).toBeUndefined(); + }); + + it('returns undefined for empty / blank values', () => { + expect(readActionCategoriesQueryParam(requestWithQuery({ categories: '' }))).toBeUndefined(); + expect(readActionCategoriesQueryParam(requestWithQuery({ categories: ' ' }))).toBeUndefined(); + }); + + it('returns the single category as a one-element array', () => { + expect(readActionCategoriesQueryParam(requestWithQuery({ categories: 'contain' }))).toEqual([ + 'contain', + ]); + }); + + it('returns the multi-valued param as an OR-set', () => { + expect( + readActionCategoriesQueryParam(requestWithQuery({ categories: ['contain', 'escalate'] })) + ).toEqual(['contain', 'escalate']); + }); + + it('splits a comma-joined single value', () => { + expect(readActionCategoriesQueryParam(requestWithQuery({ categories: 'contain,tune' }))).toEqual( + ['contain', 'tune'] + ); + }); + + it('rejects more than the max values even when comma-joined', () => { + expect(() => + readActionCategoriesQueryParam(requestWithQuery({ categories: Array(21).fill('c').join(',') })) + ).toThrow(/at most 20/); + }); + + it('trims whitespace around values', () => { + expect(readActionCategoriesQueryParam(requestWithQuery({ categories: ' contain ' }))).toEqual([ + 'contain', + ]); + }); + + it('rejects more than the max values', () => { + const tooMany = Array.from( + { length: ACTION_CATEGORIES_QUERY_PARAM_MAX_ITEMS + 1 }, + (_, i) => `c${i}` + ); + expect(() => readActionCategoriesQueryParam(requestWithQuery({ categories: tooMany }))).toThrow( + /at most \d+ values/ + ); + }); +}); diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.ts new file mode 100644 index 0000000000000..bfeefdbff904c --- /dev/null +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.ts @@ -0,0 +1,53 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * 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"; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { KibanaRequest } from '@kbn/core-http-server'; +import { ACTION_CATEGORIES_QUERY_PARAM_MAX_ITEMS } from './constants'; + +/** + * Reads the optional `categories` query param from the list-actions request. + * + * `categories` is a multi-valued query param (`?categories=contain&categories=escalate`); + * a comma-joined single value (`?categories=contain,escalate`) is accepted and split. + * The returned array is the OR-set the API filters on: an action is returned + * when its declared category matches ANY of the requested categories. + * Absent / empty param → `undefined`, meaning "no category filter, return all". + * + * The category vocabulary is solution-owned (alertzero, nightshift, …), so + * nothing is validated against a fixed enum here — unknown categories simply + * match nothing. + */ +export const readActionCategoriesQueryParam = (request: KibanaRequest): string[] | undefined => { + const raw = request.url.searchParams + .getAll('categories') + .flatMap((value) => value.split(',')); + const trimmed = raw.map((value) => value.trim()).filter((value) => value.length > 0); + if (trimmed.length === 0) { + return undefined; + } + if (trimmed.length > ACTION_CATEGORIES_QUERY_PARAM_MAX_ITEMS) { + throw new Error( + `The categories query param accepts at most ${ACTION_CATEGORIES_QUERY_PARAM_MAX_ITEMS} values` + ); + } + return trimmed; +}; diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/register_routes.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/register_routes.ts index b3643efed2c72..4b7aee3ba1246 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/routes/register_routes.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/register_routes.ts @@ -11,6 +11,7 @@ import type { AlertZeroSpaceIdResolver } from '../types'; import type { WatchesService } from '../services/watches/watches_service'; import type { WorkersService } from '../services/workers/workers_service'; import type { ConversationProposalsService } from '../services/conversation_proposals/conversation_proposals_service'; +import type { ActionsService } from '../services/actions/actions_service'; import { registerListWatchesRoute } from './watches/list_watches'; import { registerGetWatchRoute } from './watches/get_watch'; import { registerListWorkersRoute } from './workers/list_workers'; @@ -19,6 +20,7 @@ import { registerListInvestigationsRoute } from './investigations/list_investiga import { registerGetInvestigationRoute } from './investigations/get_investigation'; import { registerListInvestigationProposalsRoute } from './investigations/list_proposals'; import { registerGetProposalsRoute } from './proposals/get_proposals'; +import { registerListActionsRoute } from './actions/list_actions'; export interface RouteDependencies { router: IRouter; @@ -28,6 +30,7 @@ export interface RouteDependencies { getWatchesService: () => WatchesService; getWorkersService: () => WorkersService; getConversationProposalsService: () => ConversationProposalsService; + getActionsService: () => ActionsService; } export const registerRoutes = (deps: RouteDependencies): void => { @@ -39,4 +42,5 @@ export const registerRoutes = (deps: RouteDependencies): void => { registerGetInvestigationRoute(deps); registerListInvestigationProposalsRoute(deps); registerGetProposalsRoute(deps); + registerListActionsRoute(deps); }; diff --git a/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.test.ts b/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.test.ts new file mode 100644 index 0000000000000..1107c684e6277 --- /dev/null +++ b/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.test.ts @@ -0,0 +1,199 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * 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"; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import type { WorkflowListDto, WorkflowListItemDto } from '@kbn/workflows'; +import { ActionsService } from './actions_service'; +import type { WatchWorkflowsManagementClient } from '../watches/watch_workflows_management_client'; + +const logger = loggingSystemMock.create().get('alertzero'); + +const makeManagement = ( + pages: WorkflowListDto[] +): { client: WatchWorkflowsManagementClient; getWorkflows: jest.Mock } => { + const getWorkflows = jest.fn(); + pages.forEach((page, index) => { + getWorkflows.mockResolvedValueOnce(page); + // Any further call falls through with the last page (defensive: service must stop paging). + if (index === pages.length - 1) { + getWorkflows.mockResolvedValue(page); + } + }); + return { + getWorkflows, + client: { getWorkflows } as unknown as WatchWorkflowsManagementClient, + }; +}; + +const workflowItem = (id: string, actionMetadata: unknown, tags: string[] = ['action']) => ({ + id, + name: id, + description: '', + enabled: true, + managed: true, + managedBy: 'alertzero', + definition: (actionMetadata === null + ? null + : { consts: { actionMetadata } }) as WorkflowListItemDto['definition'], + createdAt: '2026-01-01T00:00:00.000Z', + tags, + valid: true, +}); + +const page = (results: WorkflowListDto['results'], total = results.length): WorkflowListDto => ({ + page: 1, + size: 100, + total, + results, +}); + +describe('ActionsService', () => { + it('queries managed workflows tagged action (managed installs, not unmanaged)', async () => { + const { getWorkflows, client } = makeManagement([page([])]); + const service = new ActionsService(() => client, logger); + await service.list('default'); + expect(getWorkflows).toHaveBeenCalledWith( + expect.objectContaining({ tags: ['action'], managedFilter: 'managed' }), + 'default' + ); + }); + + it('projects action workflows to catalog entries', async () => { + const { client } = makeManagement([ + page([ + workflowItem('action-create-rule', { + name: 'Create detection rule', + description: 'Creates a new, disabled custom query detection rule.', + category: 'tune', + impact: 'low', + reversible: true, + approvalPolicy: 'always-gate', + }), + ]), + ]); + const service = new ActionsService(() => client, logger); + const result = await service.list('default'); + expect(result).toEqual({ + total: 1, + actions: [ + { + workflowId: 'action-create-rule', + name: 'Create detection rule', + description: 'Creates a new, disabled custom query detection rule.', + category: 'tune', + impact: 'low', + approvalPolicy: 'always-gate', + }, + ], + }); + }); + + it('filters by category (OR semantics) and omits entries without a category', async () => { + const { client, getWorkflows } = makeManagement([ + page([ + workflowItem('a-contain', { name: 'A', category: 'contain' }), + workflowItem('b-escalate', { name: 'B', category: 'escalate' }), + workflowItem('c-tune', { name: 'C', category: 'tune' }), + workflowItem('d-uncategorized', { name: 'D' }), + ]), + ]); + const service = new ActionsService(() => client, logger); + const result = await service.list('default', ['contain', 'escalate']); + expect(result.actions.map((a) => a.workflowId)).toEqual(['a-contain', 'b-escalate']); + expect(result.total).toBe(2); + // the filter is applied AFTER fetching, so the API still queries by tag only + expect(getWorkflows).toHaveBeenCalledWith( + expect.objectContaining({ tags: ['action'] }), + 'default' + ); + }); + + it('does not validate categories against an enum — unknown categories just match nothing', async () => { + const { client } = makeManagement([ + page([workflowItem('a-contain', { name: 'A', category: 'contain' })]), + ]); + const service = new ActionsService(() => client, logger); + const result = await service.list('default', ['nightshift-specific-category']); + expect(result).toEqual({ actions: [], total: 0 }); + }); + + it('skips workflows with invalid actionMetadata, keeping the rest of the catalog', async () => { + const { client } = makeManagement([ + page([ + workflowItem('invalid', { name: 'x', impact: 'nuclear' }), + workflowItem('valid', { name: 'Valid', category: 'contain' }), + ]), + ]); + const service = new ActionsService(() => client, logger); + const result = await service.list('default'); + expect(result.actions.map((a) => a.workflowId)).toEqual(['valid']); + }); + + it('skips workflows without actionMetadata', async () => { + const { client } = makeManagement([ + page([ + workflowItem('no-metadata', undefined), + workflowItem('null-definition', null), + workflowItem('valid', { name: 'Valid' }), + ]), + ]); + const service = new ActionsService(() => client, logger); + const result = await service.list('default'); + expect(result.actions.map((a) => a.workflowId)).toEqual(['valid']); + }); + + it('pages until all tagged workflows are read', async () => { + const first = Array.from({ length: 100 }, (_, i) => + workflowItem(`wf-${i}`, { name: `wf-${i}` }) + ); + const second = [workflowItem('wf-100', { name: 'wf-100' })]; + const { client, getWorkflows } = makeManagement([ + { ...page(first, 101), page: 1 }, + { ...page(second, 101), page: 2 }, + ]); + const service = new ActionsService(() => client, logger); + const result = await service.list('default'); + expect(result.total).toBe(101); + expect(getWorkflows).toHaveBeenCalledTimes(2); + expect(getWorkflows).toHaveBeenLastCalledWith( + expect.objectContaining({ page: 2, size: 100 }), + 'default' + ); + }); + + it('throws when workflows management is unavailable', async () => { + const service = new ActionsService(() => undefined, logger); + await expect(service.list('default')).rejects.toThrow('Workflows management is not available'); + }); + + it('sorts entries by name', async () => { + const { client } = makeManagement([ + page([ + workflowItem('zeta', { name: 'Zeta action' }), + workflowItem('alpha', { name: 'Alpha action' }), + ]), + ]); + const service = new ActionsService(() => client, logger); + const result = await service.list('default'); + expect(result.actions.map((a) => a.name)).toEqual(['Alpha action', 'Zeta action']); + }); +}); diff --git a/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.ts b/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.ts new file mode 100644 index 0000000000000..e332526741650 --- /dev/null +++ b/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.ts @@ -0,0 +1,125 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * 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"; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ACTION_WORKFLOW_TAG, actionMetadataSchema } from '@kbn/workflows/managed'; +import type { WorkflowListDto } from '@kbn/workflows'; +import type { Logger } from '@kbn/logging'; +import type { ActionCatalogEntry, ListActionsResponse } from '@kbn/alertzero-common'; +import type { WatchWorkflowsManagementClient } from '../watches/watch_workflows_management_client'; + +/** Structural subset of WorkflowListItemDto.definition the catalog reads. */ +interface ActionWorkflowDefinition { + consts?: { actionMetadata?: unknown }; +} + +const PAGE_SIZE = 100; + +/** + * Action catalog service: lists installed action workflows and projects them to + * the lightweight {@link ActionCatalogEntry} shape. + * + * Discovery is tag-driven (`action`), never a hardcoded workflow-id list, and + * category is a solution-owned keyword — never validated against an enum. + * Definitions declaring invalid `consts.actionMetadata` are skipped with a + * warning rather than failing the whole catalog. + */ +export class ActionsService { + constructor( + private readonly getManagement: () => WatchWorkflowsManagementClient | undefined, + private readonly logger: Logger + ) {} + + async list(spaceId: string, categories?: string[]): Promise { + const management = this.getManagement(); + if (!management) { + throw new Error('Workflows management is not available'); + } + + const filter = categories && categories.length > 0 ? new Set(categories) : undefined; + + const actions: ActionCatalogEntry[] = []; + let page = 1; + // Page until exhausted. The catalog is small (installed action workflows), + // so the loop is bounded by the number of action workflows, not all workflows: + // the tag filter runs server-side inside getWorkflows. + for (;;) { + const response: WorkflowListDto = await management.getWorkflows( + { + tags: [ACTION_WORKFLOW_TAG], + page, + size: PAGE_SIZE, + // Action workflows are managed installs (global space); the search + // service's `unmanaged` default would filter them all out. + managedFilter: 'managed', + }, + spaceId + ); + for (const item of response.results) { + const entry = this.toEntry(item.id, item.definition as ActionWorkflowDefinition | null); + if (!entry) { + continue; + } + if (filter && (entry.category === undefined || !filter.has(entry.category))) { + continue; + } + actions.push(entry); + } + if (page * PAGE_SIZE >= response.total) { + break; + } + page += 1; + } + + actions.sort((a, b) => a.name.localeCompare(b.name)); + return { actions, total: actions.length }; + } + + /** + * Projects a workflow definition to a catalog entry, or `undefined` when the + * definition carries no parseable `consts.actionMetadata`. + */ + private toEntry( + workflowId: string, + definition: ActionWorkflowDefinition | null + ): ActionCatalogEntry | undefined { + const candidate = definition?.consts?.actionMetadata; + if (candidate === undefined) { + return undefined; + } + const parsed = actionMetadataSchema.safeParse(candidate); + if (!parsed.success) { + this.logger.warn( + `Action workflow [${workflowId}] declares invalid consts.actionMetadata: ${parsed.error.message}` + ); + return undefined; + } + const { name, description, category, impact, approvalPolicy } = parsed.data; + return { + workflowId, + name, + ...(description !== undefined && { description }), + ...(category !== undefined && { category }), + ...(impact !== undefined && { impact }), + ...(approvalPolicy !== undefined && { approvalPolicy }), + }; + } +} From 5ae826f24fff770968d2d233aebe1750a26368dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopycin=CC=81ski?= Date: Sat, 12 Sep 2026 10:53:31 +0200 Subject: [PATCH 02/13] fix(alertzero): map invalid categories param to 400 and fix stale tool-id doc comment - readActionCategoriesQueryParam now throws InvalidCategoriesError; the route maps it to 400 badRequest with the param message instead of a generic 500 - doc comment on list_actions_by_category_tool referenced the pre-rename camelCase id; corrected to list_by_category - adds route test for the 400 path --- .../list_actions_by_category_tool.ts | 2 +- .../server/routes/actions/list_actions.test.ts | 15 +++++++++++++++ .../server/routes/actions/list_actions.ts | 8 +++++++- .../actions/read_categories_query_param.test.ts | 10 ++++++---- .../routes/actions/read_categories_query_param.ts | 9 +++++---- 5 files changed, 34 insertions(+), 10 deletions(-) diff --git a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts index a8ec1aad69b1b..c3e235c18a270 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts @@ -40,7 +40,7 @@ const listByCategorySchema = z.object({ }); /** - * `security.alertzero.actions.listByCategory` — lets an agent discover the + * `security.alertzero.actions.list_by_category` — lets an agent discover the * installed action workflows at runtime instead of hard-coding workflow ids. * * Registered by the AlertZero plugin (setup), reads the catalog through diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.test.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.test.ts index ec512ddc1e7ca..feb47fed26309 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.test.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.test.ts @@ -78,6 +78,21 @@ describe('registerListActionsRoute', () => { expect(list).toHaveBeenCalledWith('default', undefined); }); + it('maps an invalid categories param to 400 with the param message', async () => { + const { handler } = makeDeps({ list: jest.fn() }); + const response = httpServerMock.createResponseFactory(); + const request = httpServerMock.createKibanaRequest({ + path: '/internal/alertzero/actions', + query: { categories: Array(21).fill('c') }, + }); + await handler({}, request, response); + expect(response.badRequest).toHaveBeenCalledWith({ + body: { + message: expect.stringContaining('at most 20'), + }, + }); + }); + it('maps service errors to 500', async () => { const list = jest.fn().mockRejectedValue(new Error('boom')); const { handler } = makeDeps({ list }); diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.ts index 64133f3b0d720..c2087d88cd941 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.ts @@ -24,7 +24,10 @@ import { API_VERSIONS, INTERNAL_API_ACCESS, ALERTZERO_ACTIONS_URL } from '@kbn/a import type { ListActionsResponse } from '@kbn/alertzero-common'; import { ALERTZERO_API_PRIVILEGE_READ } from '../../../common/constants'; import type { RouteDependencies } from '../register_routes'; -import { readActionCategoriesQueryParam } from './read_categories_query_param'; +import { + InvalidCategoriesError, + readActionCategoriesQueryParam, +} from './read_categories_query_param'; export const registerListActionsRoute = ({ router, @@ -59,6 +62,9 @@ export const registerListActionsRoute = ({ ); return response.ok({ body }); } catch (error) { + if (error instanceof InvalidCategoriesError) { + return response.badRequest({ body: { message: error.message } }); + } logger.error(`Failed to list actions: ${error}`); return response.customError({ statusCode: 500, diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.test.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.test.ts index cfe5228e2b866..a924c951edae4 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.test.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.test.ts @@ -50,14 +50,16 @@ describe('readActionCategoriesQueryParam', () => { }); it('splits a comma-joined single value', () => { - expect(readActionCategoriesQueryParam(requestWithQuery({ categories: 'contain,tune' }))).toEqual( - ['contain', 'tune'] - ); + expect( + readActionCategoriesQueryParam(requestWithQuery({ categories: 'contain,tune' })) + ).toEqual(['contain', 'tune']); }); it('rejects more than the max values even when comma-joined', () => { expect(() => - readActionCategoriesQueryParam(requestWithQuery({ categories: Array(21).fill('c').join(',') })) + readActionCategoriesQueryParam( + requestWithQuery({ categories: Array(21).fill('c').join(',') }) + ) ).toThrow(/at most 20/); }); diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.ts index bfeefdbff904c..9b435e5ee6e56 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.ts @@ -36,16 +36,17 @@ import { ACTION_CATEGORIES_QUERY_PARAM_MAX_ITEMS } from './constants'; * nothing is validated against a fixed enum here — unknown categories simply * match nothing. */ +/** Thrown when the `categories` query param is structurally invalid (too many values). */ +export class InvalidCategoriesError extends Error {} + export const readActionCategoriesQueryParam = (request: KibanaRequest): string[] | undefined => { - const raw = request.url.searchParams - .getAll('categories') - .flatMap((value) => value.split(',')); + const raw = request.url.searchParams.getAll('categories').flatMap((value) => value.split(',')); const trimmed = raw.map((value) => value.trim()).filter((value) => value.length > 0); if (trimmed.length === 0) { return undefined; } if (trimmed.length > ACTION_CATEGORIES_QUERY_PARAM_MAX_ITEMS) { - throw new Error( + throw new InvalidCategoriesError( `The categories query param accepts at most ${ACTION_CATEGORIES_QUERY_PARAM_MAX_ITEMS} values` ); } From 3ee5a09df5278a4d18f03d34e7a97c4be8fdec44 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:09:14 +0000 Subject: [PATCH 03/13] Changes from node scripts/check --- .../solutions/security/packages/kbn-alertzero-common/moon.yml | 1 + .../security/packages/kbn-alertzero-common/tsconfig.json | 1 + x-pack/solutions/security/plugins/alertzero/moon.yml | 2 ++ x-pack/solutions/security/plugins/alertzero/tsconfig.json | 2 ++ 4 files changed, 6 insertions(+) diff --git a/x-pack/solutions/security/packages/kbn-alertzero-common/moon.yml b/x-pack/solutions/security/packages/kbn-alertzero-common/moon.yml index 51fa088950842..f62b9f1250e50 100644 --- a/x-pack/solutions/security/packages/kbn-alertzero-common/moon.yml +++ b/x-pack/solutions/security/packages/kbn-alertzero-common/moon.yml @@ -20,6 +20,7 @@ dependsOn: - '@kbn/zod' - '@kbn/i18n' - '@kbn/deeplinks-security' + - '@kbn/workflows' tags: - shared-common - package diff --git a/x-pack/solutions/security/packages/kbn-alertzero-common/tsconfig.json b/x-pack/solutions/security/packages/kbn-alertzero-common/tsconfig.json index d1ee06880a4ce..e205a3053175d 100644 --- a/x-pack/solutions/security/packages/kbn-alertzero-common/tsconfig.json +++ b/x-pack/solutions/security/packages/kbn-alertzero-common/tsconfig.json @@ -18,5 +18,6 @@ "@kbn/zod", "@kbn/i18n", "@kbn/deeplinks-security", + "@kbn/workflows", ] } diff --git a/x-pack/solutions/security/plugins/alertzero/moon.yml b/x-pack/solutions/security/plugins/alertzero/moon.yml index bd0110d713477..ab9c1326e91ec 100644 --- a/x-pack/solutions/security/plugins/alertzero/moon.yml +++ b/x-pack/solutions/security/plugins/alertzero/moon.yml @@ -48,6 +48,8 @@ dependsOn: - '@kbn/core-http-server-mocks' - '@kbn/std' - '@kbn/core-security-server' + - '@kbn/core-logging-server-mocks' + - '@kbn/core-http-server' tags: - plugin - prod diff --git a/x-pack/solutions/security/plugins/alertzero/tsconfig.json b/x-pack/solutions/security/plugins/alertzero/tsconfig.json index fded166b0570e..c004610168c71 100644 --- a/x-pack/solutions/security/plugins/alertzero/tsconfig.json +++ b/x-pack/solutions/security/plugins/alertzero/tsconfig.json @@ -37,5 +37,7 @@ "@kbn/core-http-server-mocks", "@kbn/std", "@kbn/core-security-server", + "@kbn/core-logging-server-mocks", + "@kbn/core-http-server", ] } From e1333af5152d53a3a2a9804e41386e40da46052f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopycin=CC=81ski?= Date: Mon, 14 Sep 2026 12:20:20 +0200 Subject: [PATCH 04/13] fix(alertzero-common): re-export ActionApprovalPolicy/ActionCategory/ActionImpact --- .../packages/kbn-alertzero-common/action_catalog_types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/solutions/security/packages/kbn-alertzero-common/action_catalog_types.ts b/x-pack/solutions/security/packages/kbn-alertzero-common/action_catalog_types.ts index 2709b218f26cf..e5ef12b88d86f 100644 --- a/x-pack/solutions/security/packages/kbn-alertzero-common/action_catalog_types.ts +++ b/x-pack/solutions/security/packages/kbn-alertzero-common/action_catalog_types.ts @@ -22,6 +22,8 @@ import type { ActionApprovalPolicy, ActionCategory, ActionImpact } from '@kbn/workflows'; +export type { ActionApprovalPolicy, ActionCategory, ActionImpact }; + /** * One entry of the action catalog: the lightweight, agent-facing projection of * an installed action workflow. Mirrors `consts.actionMetadata` on the From 8c8730bc51f6beedf2c2ffd7f180dc53010d951e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopycin=CC=81ski?= Date: Mon, 14 Sep 2026 15:11:48 +0200 Subject: [PATCH 05/13] chore: dedupe license headers and narrow tool dependency type --- .../action_catalog_types.ts | 15 --------------- .../list_actions_by_category_tool.test.ts | 17 +---------------- .../list_actions_by_category_tool.ts | 17 +---------------- 3 files changed, 2 insertions(+), 47 deletions(-) diff --git a/x-pack/solutions/security/packages/kbn-alertzero-common/action_catalog_types.ts b/x-pack/solutions/security/packages/kbn-alertzero-common/action_catalog_types.ts index e5ef12b88d86f..83581c090ec59 100644 --- a/x-pack/solutions/security/packages/kbn-alertzero-common/action_catalog_types.ts +++ b/x-pack/solutions/security/packages/kbn-alertzero-common/action_catalog_types.ts @@ -5,21 +5,6 @@ * 2.0. */ -/* - * 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"; you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import type { ActionApprovalPolicy, ActionCategory, ActionImpact } from '@kbn/workflows'; export type { ActionApprovalPolicy, ActionCategory, ActionImpact }; diff --git a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts index 985b613c7fad0..5d234f6cc8f66 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts @@ -5,28 +5,13 @@ * 2.0. */ -/* - * 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"; you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import { listActionsByCategoryTool } from './list_actions_by_category_tool'; import type { ActionsService } from '../services/actions/actions_service'; import { ToolResultType } from '@kbn/agent-builder-common/tools/tool_result'; const logger = () => ({ error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn() }); -const serviceWith = (list: jest.Mock) => ({ list } as unknown as ActionsService); +const serviceWith = (list: jest.Mock) => ({ list } as Pick); const run = async (service: ActionsService, input: { categories?: string[] } = {}) => { const tool = listActionsByCategoryTool(() => service); diff --git a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts index c3e235c18a270..aec68d3d175db 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts @@ -5,21 +5,6 @@ * 2.0. */ -/* - * 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"; you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import { z } from '@kbn/zod/v4'; import { createErrorResult } from '@kbn/agent-builder-server'; import { ToolResultType } from '@kbn/agent-builder-common/tools/tool_result'; @@ -48,7 +33,7 @@ const listByCategorySchema = z.object({ * and the API can never drift. */ export const listActionsByCategoryTool = ( - getActionsService: () => ActionsService + getActionsService: () => Pick ): BuiltinToolDefinition => ({ id: ALERTZERO_ACTIONS_LIST_BY_CATEGORY_TOOL_ID, type: ToolType.builtin, From 33ed9be5720323f42f0fa2634e30a8d2e44ac895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopycin=CC=81ski?= Date: Mon, 14 Sep 2026 15:53:37 +0200 Subject: [PATCH 06/13] fix(alertzero): quote the Pick type argument and narrow the test helper param --- .../agent_builder_tools/list_actions_by_category_tool.test.ts | 4 ++-- .../agent_builder_tools/list_actions_by_category_tool.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts index 5d234f6cc8f66..84bf55415fa0a 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts @@ -11,9 +11,9 @@ import { ToolResultType } from '@kbn/agent-builder-common/tools/tool_result'; const logger = () => ({ error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn() }); -const serviceWith = (list: jest.Mock) => ({ list } as Pick); +const serviceWith = (list: jest.Mock) => ({ list } as Pick); -const run = async (service: ActionsService, input: { categories?: string[] } = {}) => { +const run = async (service: Pick, input: { categories?: string[] } = {}) => { const tool = listActionsByCategoryTool(() => service); const result = await tool.handler(input, { logger: logger() } as never); if (!('results' in result)) { diff --git a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts index aec68d3d175db..a948a9d58f3fc 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts @@ -33,7 +33,7 @@ const listByCategorySchema = z.object({ * and the API can never drift. */ export const listActionsByCategoryTool = ( - getActionsService: () => Pick + getActionsService: () => Pick ): BuiltinToolDefinition => ({ id: ALERTZERO_ACTIONS_LIST_BY_CATEGORY_TOOL_ID, type: ToolType.builtin, From d389f09bfaf185a3a3d83a8d6588cc1e7f3b8684 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:06:07 +0000 Subject: [PATCH 07/13] Changes from node scripts/check --- .../list_actions_by_category_tool.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts index 84bf55415fa0a..cf4b09a5ee4d4 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts @@ -13,7 +13,10 @@ const logger = () => ({ error: jest.fn(), warn: jest.fn(), info: jest.fn(), debu const serviceWith = (list: jest.Mock) => ({ list } as Pick); -const run = async (service: Pick, input: { categories?: string[] } = {}) => { +const run = async ( + service: Pick, + input: { categories?: string[] } = {} +) => { const tool = listActionsByCategoryTool(() => service); const result = await tool.handler(input, { logger: logger() } as never); if (!('results' in result)) { From 810f238260dfed7e5016b6b5bf3747abbbe67da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopycin=CC=81ski?= Date: Mon, 14 Sep 2026 16:40:43 +0200 Subject: [PATCH 08/13] chore: dedupe license headers in actions routes and service files --- .../alertzero/server/routes/actions/constants.ts | 15 --------------- .../server/routes/actions/list_actions.test.ts | 15 --------------- .../server/routes/actions/list_actions.ts | 15 --------------- .../actions/read_categories_query_param.test.ts | 15 --------------- .../routes/actions/read_categories_query_param.ts | 15 --------------- .../services/actions/actions_service.test.ts | 15 --------------- .../server/services/actions/actions_service.ts | 15 --------------- 7 files changed, 105 deletions(-) diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/constants.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/constants.ts index 2181e47f9f8de..bb18d373ceea4 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/constants.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/constants.ts @@ -5,20 +5,5 @@ * 2.0. */ -/* - * 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"; you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - /** Max number of values accepted in the `categories` query param (each also length-capped upstream by actionCategorySchema). */ export const ACTION_CATEGORIES_QUERY_PARAM_MAX_ITEMS = 20; diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.test.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.test.ts index feb47fed26309..f4e68b355f78e 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.test.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.test.ts @@ -5,21 +5,6 @@ * 2.0. */ -/* - * 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"; you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import { httpServerMock } from '@kbn/core-http-server-mocks'; import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import type { RouteDependencies } from '../register_routes'; diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.ts index c2087d88cd941..adc746fa3c4ca 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/list_actions.ts @@ -5,21 +5,6 @@ * 2.0. */ -/* - * 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"; you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import { API_VERSIONS, INTERNAL_API_ACCESS, ALERTZERO_ACTIONS_URL } from '@kbn/alertzero-common'; import type { ListActionsResponse } from '@kbn/alertzero-common'; import { ALERTZERO_API_PRIVILEGE_READ } from '../../../common/constants'; diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.test.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.test.ts index a924c951edae4..586639d313587 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.test.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.test.ts @@ -5,21 +5,6 @@ * 2.0. */ -/* - * 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"; you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import { httpServerMock } from '@kbn/core-http-server-mocks'; import { readActionCategoriesQueryParam } from './read_categories_query_param'; import { ACTION_CATEGORIES_QUERY_PARAM_MAX_ITEMS } from './constants'; diff --git a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.ts b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.ts index 9b435e5ee6e56..dac473f4642e8 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/routes/actions/read_categories_query_param.ts @@ -5,21 +5,6 @@ * 2.0. */ -/* - * 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"; you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import type { KibanaRequest } from '@kbn/core-http-server'; import { ACTION_CATEGORIES_QUERY_PARAM_MAX_ITEMS } from './constants'; diff --git a/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.test.ts b/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.test.ts index 1107c684e6277..0563f615b9fb5 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.test.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.test.ts @@ -5,21 +5,6 @@ * 2.0. */ -/* - * 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"; you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import type { WorkflowListDto, WorkflowListItemDto } from '@kbn/workflows'; import { ActionsService } from './actions_service'; diff --git a/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.ts b/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.ts index e332526741650..19497556200d2 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/services/actions/actions_service.ts @@ -5,21 +5,6 @@ * 2.0. */ -/* - * 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"; you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import { ACTION_WORKFLOW_TAG, actionMetadataSchema } from '@kbn/workflows/managed'; import type { WorkflowListDto } from '@kbn/workflows'; import type { Logger } from '@kbn/logging'; From dee2d6316559e890af6ab12de7284bfd6bc32316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopycin=CC=81ski?= Date: Mon, 14 Sep 2026 22:11:04 +0200 Subject: [PATCH 09/13] refactor(alertzero): rename tool to security.alertzero.actions.list per review --- .../definitions/alertzero/floor_alert_triage.ts | 2 +- .../definitions/alertzero/floor_alert_triage.yaml | 2 +- .../agent-builder/agent-builder-server/allow_lists.ts | 2 +- .../packages/kbn-alertzero-common/constants.ts | 3 +-- .../security/packages/kbn-alertzero-common/index.ts | 2 +- ...category_tool.test.ts => list_actions_tool.test.ts} | 10 +++++----- ...ctions_by_category_tool.ts => list_actions_tool.ts} | 8 ++++---- .../security/plugins/alertzero/server/plugin.ts | 4 ++-- 8 files changed, 16 insertions(+), 17 deletions(-) rename x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/{list_actions_by_category_tool.test.ts => list_actions_tool.test.ts} (89%) rename x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/{list_actions_by_category_tool.ts => list_actions_tool.ts} (91%) diff --git a/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.ts b/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.ts index 2b9e2bfbbe4d7..b5dad4b610e2b 100644 --- a/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.ts +++ b/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.ts @@ -19,7 +19,7 @@ export const ALERTZERO_WORKER_FLOOR_ALERT_TRIAGE_WORKFLOW = { id: ALERTZERO_WORKER_FLOOR_ALERT_TRIAGE_WORKFLOW_ID, management: ALERTZERO_WORKER_MANAGEMENT, pluginId: ALERTZERO_MANAGED_WORKFLOW_PLUGIN_ID, - version: 2, + version: 3, yamlTemplate: (values: CommonWorkerTemplateValues): string => renderCommonWorkerYaml(FLOOR_ALERT_TRIAGE_YAML, values), } as const satisfies ManagedWorkflowDefinition; diff --git a/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.yaml b/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.yaml index 7cd10a5730e41..49b9359410c37 100644 --- a/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.yaml +++ b/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.yaml @@ -23,7 +23,7 @@ steps: - alert-analysis tools: - tool_ids: - - security.alertzero.actions.list_by_category + - security.alertzero.actions.list message: > Triage the security alert context below. Prefer recall over precision when unsure. Return a structured diff --git a/x-pack/platform/packages/shared/agent-builder/agent-builder-server/allow_lists.ts b/x-pack/platform/packages/shared/agent-builder/agent-builder-server/allow_lists.ts index ea2b446e3d8f8..b179f5df477f7 100644 --- a/x-pack/platform/packages/shared/agent-builder/agent-builder-server/allow_lists.ts +++ b/x-pack/platform/packages/shared/agent-builder/agent-builder-server/allow_lists.ts @@ -56,7 +56,7 @@ export const AGENT_BUILDER_BUILTIN_TOOLS = [ `${internalNamespaces.ml}.query_anomalies`, // Security Solution - `${internalNamespaces.security}.alertzero.actions.list_by_category`, + `${internalNamespaces.security}.alertzero.actions.list`, `${internalNamespaces.security}.entity_risk_score`, `${internalNamespaces.security}.create_detection_rule`, `${internalNamespaces.security}.run_rule_preview`, diff --git a/x-pack/solutions/security/packages/kbn-alertzero-common/constants.ts b/x-pack/solutions/security/packages/kbn-alertzero-common/constants.ts index f224517f4227f..9dafdd1797e41 100644 --- a/x-pack/solutions/security/packages/kbn-alertzero-common/constants.ts +++ b/x-pack/solutions/security/packages/kbn-alertzero-common/constants.ts @@ -48,8 +48,7 @@ export const ALERTZERO_PROPOSALS_URL = `${ALERTZERO_INTERNAL_URL}/proposals` as export const ALERTZERO_ACTIONS_URL = `${ALERTZERO_INTERNAL_URL}/actions` as const; /** Agent Builder builtin tool wrapping the action catalog API. */ -export const ALERTZERO_ACTIONS_LIST_BY_CATEGORY_TOOL_ID = - 'security.alertzero.actions.list_by_category' as const; +export const ALERTZERO_ACTIONS_LIST_TOOL_ID = 'security.alertzero.actions.list' as const; /** * Shared thin AlertZero agent for all Worker `ai.agent` steps. diff --git a/x-pack/solutions/security/packages/kbn-alertzero-common/index.ts b/x-pack/solutions/security/packages/kbn-alertzero-common/index.ts index ee7bc18104b70..5b2c25741990f 100644 --- a/x-pack/solutions/security/packages/kbn-alertzero-common/index.ts +++ b/x-pack/solutions/security/packages/kbn-alertzero-common/index.ts @@ -25,7 +25,7 @@ export { ALERTZERO_INVESTIGATION_URL_TEMPLATE, ALERTZERO_PLUGIN_NAME, ALERTZERO_ACTIONS_URL, - ALERTZERO_ACTIONS_LIST_BY_CATEGORY_TOOL_ID, + ALERTZERO_ACTIONS_LIST_TOOL_ID, ALERTZERO_PROPOSALS_URL, ALERTZERO_THIN_AGENT_ID, ALERTZERO_SKILLS_URL, diff --git a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_tool.test.ts similarity index 89% rename from x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts rename to x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_tool.test.ts index cf4b09a5ee4d4..851dac7c04ac2 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.test.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_tool.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { listActionsByCategoryTool } from './list_actions_by_category_tool'; +import { listActionsTool } from './list_actions_tool'; import type { ActionsService } from '../services/actions/actions_service'; import { ToolResultType } from '@kbn/agent-builder-common/tools/tool_result'; @@ -17,7 +17,7 @@ const run = async ( service: Pick, input: { categories?: string[] } = {} ) => { - const tool = listActionsByCategoryTool(() => service); + const tool = listActionsTool(() => service); const result = await tool.handler(input, { logger: logger() } as never); if (!('results' in result)) { throw new Error('expected a standard tool result'); @@ -32,7 +32,7 @@ const ACTION = (over: Partial> = {}) => ({ ...over, }); -describe('listActionsByCategoryTool', () => { +describe('listActionsTool', () => { it('lists all actions when called without categories', async () => { const list = jest.fn().mockResolvedValue({ actions: [ACTION(), ACTION({ workflowId: 'a2', name: 'Isolate host', category: 'contain' })], @@ -62,8 +62,8 @@ describe('listActionsByCategoryTool', () => { }); it('declares the documented tool id and read-only annotations', () => { - const tool = listActionsByCategoryTool(() => serviceWith(jest.fn())); - expect(tool.id).toBe('security.alertzero.actions.list_by_category'); + const tool = listActionsTool(() => serviceWith(jest.fn())); + expect(tool.id).toBe('security.alertzero.actions.list'); expect(tool.annotations).toMatchObject({ readOnlyHint: true, destructiveHint: false, diff --git a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_tool.ts similarity index 91% rename from x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts rename to x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_tool.ts index a948a9d58f3fc..2107b0ed4a1a0 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_by_category_tool.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/agent_builder_tools/list_actions_tool.ts @@ -11,7 +11,7 @@ import { ToolResultType } from '@kbn/agent-builder-common/tools/tool_result'; import type { BuiltinToolDefinition } from '@kbn/agent-builder-server/tools'; import { ToolType } from '@kbn/agent-builder-common'; import { actionCategorySchema } from '@kbn/workflows/managed'; -import { ALERTZERO_ACTIONS_LIST_BY_CATEGORY_TOOL_ID } from '@kbn/alertzero-common'; +import { ALERTZERO_ACTIONS_LIST_TOOL_ID } from '@kbn/alertzero-common'; import type { ActionsService } from '../services/actions/actions_service'; const listByCategorySchema = z.object({ @@ -25,17 +25,17 @@ const listByCategorySchema = z.object({ }); /** - * `security.alertzero.actions.list_by_category` — lets an agent discover the + * `security.alertzero.actions.list` — lets an agent discover the * installed action workflows at runtime instead of hard-coding workflow ids. * * Registered by the AlertZero plugin (setup), reads the catalog through * {@link ActionsService} — the same service backing the HTTP API — so the tool * and the API can never drift. */ -export const listActionsByCategoryTool = ( +export const listActionsTool = ( getActionsService: () => Pick ): BuiltinToolDefinition => ({ - id: ALERTZERO_ACTIONS_LIST_BY_CATEGORY_TOOL_ID, + id: ALERTZERO_ACTIONS_LIST_TOOL_ID, type: ToolType.builtin, description: 'List available AlertZero actions, optionally filtered by category. Each result includes the workflowId to reference when proposing the action, plus its name, description, category, impact (low/medium/high/critical) and approvalPolicy (always-gate/autonomy-dependent). Call this before proposing an action so the proposal references a real, installed workflow.', diff --git a/x-pack/solutions/security/plugins/alertzero/server/plugin.ts b/x-pack/solutions/security/plugins/alertzero/server/plugin.ts index 02c8566d182ea..0f572573e7aea 100644 --- a/x-pack/solutions/security/plugins/alertzero/server/plugin.ts +++ b/x-pack/solutions/security/plugins/alertzero/server/plugin.ts @@ -37,7 +37,7 @@ import { WorkersService } from './services/workers/workers_service'; import { ConversationProposalsService } from './services/conversation_proposals/conversation_proposals_service'; import { WatchWorkflowsManagementClientImpl } from './services/watches/watch_workflows_management_client'; import { ActionsService } from './services/actions/actions_service'; -import { listActionsByCategoryTool } from './agent_builder_tools/list_actions_by_category_tool'; +import { listActionsTool } from './agent_builder_tools/list_actions_tool'; import { agentType, ensureAgent, ensureAgentSafe, registerAgentType } from './agent'; export class AlertZeroPlugin @@ -89,7 +89,7 @@ export class AlertZeroPlugin // Registered in setup so the builtin tool is available to Agent Builder before // the first agent run; the handler resolves the service lazily like the routes do. agentBuilder.tools.register({ - ...listActionsByCategoryTool(() => this.requireActionsService()), + ...listActionsTool(() => this.requireActionsService()), }); features.registerKibanaFeature({ From ba36b0f8278237157681b9f985d4154449bf795e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopycin=CC=81ski?= Date: Mon, 14 Sep 2026 22:40:26 +0200 Subject: [PATCH 10/13] [AO] fix CI failure on #290705 --- docs/changelog/290705.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/changelog/290705.yaml diff --git a/docs/changelog/290705.yaml b/docs/changelog/290705.yaml new file mode 100644 index 0000000000000..1db096bfcea92 --- /dev/null +++ b/docs/changelog/290705.yaml @@ -0,0 +1,8 @@ +prs: + - https://github.com/elastic/kibana/pull/290705 +type: feature +products: + - product: cloud-serverless + - product: kibana +title: Add AlertZero action catalog API and agent builder tool +description: Adds an internal AlertZero API for listing actions and action categories, plus the `security.alertzero.actions.list` agent builder tool that surfaces the catalog to AI agents. From eddf11f256d3d92ae8a33ed74ad1f141f83f2a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopycin=CC=81ski?= Date: Mon, 14 Sep 2026 22:48:37 +0200 Subject: [PATCH 11/13] [AO] address review feedback on #290705 --- .../managed/definitions/alertzero/floor_alert_triage.ts | 2 +- .../kbn-workflows/managed/managed_workflow_definitions.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.ts b/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.ts index b5dad4b610e2b..2b9e2bfbbe4d7 100644 --- a/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.ts +++ b/src/platform/packages/shared/kbn-workflows/managed/definitions/alertzero/floor_alert_triage.ts @@ -19,7 +19,7 @@ export const ALERTZERO_WORKER_FLOOR_ALERT_TRIAGE_WORKFLOW = { id: ALERTZERO_WORKER_FLOOR_ALERT_TRIAGE_WORKFLOW_ID, management: ALERTZERO_WORKER_MANAGEMENT, pluginId: ALERTZERO_MANAGED_WORKFLOW_PLUGIN_ID, - version: 3, + version: 2, yamlTemplate: (values: CommonWorkerTemplateValues): string => renderCommonWorkerYaml(FLOOR_ALERT_TRIAGE_YAML, values), } as const satisfies ManagedWorkflowDefinition; diff --git a/src/platform/packages/shared/kbn-workflows/managed/managed_workflow_definitions.test.ts b/src/platform/packages/shared/kbn-workflows/managed/managed_workflow_definitions.test.ts index d3ddb238cb6ea..5f07ad96fc750 100644 --- a/src/platform/packages/shared/kbn-workflows/managed/managed_workflow_definitions.test.ts +++ b/src/platform/packages/shared/kbn-workflows/managed/managed_workflow_definitions.test.ts @@ -151,7 +151,7 @@ function createContentFingerprint(content: string): string { } it.each([ - [ALERTZERO_WORKER_FLOOR_ALERT_TRIAGE_WORKFLOW_ID, FLOOR_ALERT_TRIAGE_YAML, '2:faf316b1'], + [ALERTZERO_WORKER_FLOOR_ALERT_TRIAGE_WORKFLOW_ID, FLOOR_ALERT_TRIAGE_YAML, '2:275b444e'], [ALERTZERO_WORKER_FLOOR_ATTACK_DISCOVERY_WORKFLOW_ID, FLOOR_ATTACK_DISCOVERY_YAML, '2:d13818a0'], [ ALERTZERO_WORKER_DARK_CONTINUOUS_THREAT_HUNT_WORKFLOW_ID, From b6604e23f36a8f11a145ef39e424890d4a2f850a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopycin=CC=81ski?= Date: Mon, 14 Sep 2026 22:58:51 +0200 Subject: [PATCH 12/13] [AO] fix CI failure on #290705 --- docs/changelog/290705.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changelog/290705.yaml b/docs/changelog/290705.yaml index 1db096bfcea92..c9aa50cc31a57 100644 --- a/docs/changelog/290705.yaml +++ b/docs/changelog/290705.yaml @@ -1,8 +1,10 @@ prs: - https://github.com/elastic/kibana/pull/290705 +issues: + - https://github.com/elastic/security-team/issues/19288 type: feature products: - product: cloud-serverless - product: kibana title: Add AlertZero action catalog API and agent builder tool -description: Adds an internal AlertZero API for listing actions and action categories, plus the `security.alertzero.actions.list` agent builder tool that surfaces the catalog to AI agents. +description: Adds the internal AlertZero action catalog API (`/internal/alertzero/actions`) for category-scoped discovery of installed action workflows, plus the `security.alertzero.actions.list` agent builder tool that surfaces the catalog to AI agents at runtime. From 3edc16b1ee253487390a24d6a5d3a04fefc8a89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopycin=CC=81ski?= Date: Tue, 15 Sep 2026 03:40:09 +0200 Subject: [PATCH 13/13] chore: remove changelog entry (release_note:skip) --- docs/changelog/290705.yaml | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 docs/changelog/290705.yaml diff --git a/docs/changelog/290705.yaml b/docs/changelog/290705.yaml deleted file mode 100644 index c9aa50cc31a57..0000000000000 --- a/docs/changelog/290705.yaml +++ /dev/null @@ -1,10 +0,0 @@ -prs: - - https://github.com/elastic/kibana/pull/290705 -issues: - - https://github.com/elastic/security-team/issues/19288 -type: feature -products: - - product: cloud-serverless - - product: kibana -title: Add AlertZero action catalog API and agent builder tool -description: Adds the internal AlertZero action catalog API (`/internal/alertzero/actions`) for category-scoped discovery of installed action workflows, plus the `security.alertzero.actions.list` agent builder tool that surfaces the catalog to AI agents at runtime.